Skip to content

Commit db09aca

Browse files
committed
finializing method snippets by replacing public api and adding tests!
also refactor test utils in its own file
1 parent bdca55e commit db09aca

File tree

11 files changed

+430
-255
lines changed

11 files changed

+430
-255
lines changed

README.MD

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,9 @@ There are value descriptions for two settings:
244244
245245
```ts
246246
const example = ({ a }, b?, c = 5, ...d) => { }
247-
// prefer-name (default)
247+
// binding-name (default)
248248
example({ a }, b, c, ...d)
249-
// always-declaration (recommended)
249+
// always-declaration (also popular)
250250
example({ a }, b?, c = 5, ...d)
251251
// always-name
252252
example(__0, b, c, d)
@@ -264,6 +264,30 @@ example({ a })
264264
example({ a }, b, c, ...d)
265265
```
266266
267+
`tsEssentialPlugins.methodSnippets.multipleSignatures`:
268+
269+
```ts
270+
// overload 1
271+
function foo(this: {}, a)
272+
// overload 2
273+
function foo(this: {}, b)
274+
function foo(this: {}) {}
275+
276+
// pick-first (default)
277+
foo(a)
278+
// empty
279+
foo(|)
280+
```
281+
282+
`disableMethodSnippets.jsxAttributes`:
283+
284+
```tsx
285+
const callback = (arg) => {}
286+
function Foo() {
287+
return <div onClick={callback/* when true (default) - no expansion here */} />
288+
}
289+
```
290+
267291
## Auto Imports
268292

269293
With this plugin you have total (almost) control over auto imports that appear in completions, quick fixes and import all quick fix. Some examples of what you can do:

src/configurationType.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,16 +326,16 @@ export type Configuration = {
326326
* ```ts
327327
* const example = ({ a }, b?, c = 5, ...d) => { }
328328
*
329-
* // prefer-name (default)
329+
* // binding-name (default)
330330
* example({ a }, b, c, ...d)
331-
* // always-declaration (popular)
331+
* // always-declaration (also popular)
332332
* example({ a }, b?, c = 5, ...d)
333333
* // always-name
334334
* example(__0, b, c, d)
335335
* ```
336-
* @default prefer-name
336+
* @default binding-name
337337
*/
338-
'methodSnippets.insertText': 'prefer-name' | 'always-declaration' | 'always-name'
338+
'methodSnippets.insertText': 'binding-name' | 'always-declaration' | 'always-name'
339339
/**
340340
* ```ts
341341
* const example = ({ a }, b?, c = 5, ...d) => { }
@@ -350,6 +350,10 @@ export type Configuration = {
350350
* @default no-skip
351351
*/
352352
'methodSnippets.skip': 'only-rest' | 'optional-and-rest' | 'no-skip'
353+
/**
354+
* @default pick-first
355+
*/
356+
'methodSnippets.multipleSignatures': 'pick-first' | 'empty' /* DON'T SEE A NEED TO IMPLEMENT: | 'pick-longest' | 'pick-shortest' */
353357
/**
354358
* Wether to disable our and builtin method snippets within jsx attributes
355359
* @default true

src/onCompletionAccepted.ts

Lines changed: 45 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipc
88
import { sendCommand } from './sendCommand'
99

1010
export default (tsApi: { onCompletionAccepted }) => {
11+
let inFlightMethodSnippetOperation: undefined | AbortController
1112
let justAcceptedReturnKeywordSuggestion = false
1213
let onCompletionAcceptedOverride: ((item: any) => void) | undefined
1314

14-
// eslint-disable-next-line complexity
1515
tsApi.onCompletionAccepted(async (item: vscode.CompletionItem & { document: vscode.TextDocument }) => {
1616
if (onCompletionAcceptedOverride) {
1717
onCompletionAcceptedOverride(item)
@@ -41,55 +41,24 @@ export default (tsApi: { onCompletionAccepted }) => {
4141
const startPos = editor.selection.start
4242
const nextSymbol = editor.document.getText(new vscode.Range(startPos, startPos.translate(0, 1)))
4343
if (!['(', '.'].includes(nextSymbol)) {
44-
const insertMode = getExtensionSetting('methodSnippets.insertText')
45-
const skipMode = getExtensionSetting('methodSnippets.skip')
46-
const data: RequestResponseTypes['getSignatureInfo'] | undefined = await sendCommand('getSignatureInfo', {
47-
inputOptions: {
48-
includeInitializer: insertMode === 'always-declaration',
49-
} satisfies RequestOptionsTypes['getSignatureInfo'],
50-
})
51-
if (data) {
52-
const parameters = data.parameters.filter(({ insertText, isOptional }) => {
53-
const isRest = insertText.startsWith('...')
54-
if (skipMode === 'only-rest' && isRest) return false
55-
if (skipMode === 'optional-and-rest' && isOptional) return false
56-
return true
57-
})
58-
44+
const controller = new AbortController()
45+
inFlightMethodSnippetOperation = controller
46+
const params: RequestResponseTypes['getFullMethodSnippet'] | undefined = await sendCommand('getFullMethodSnippet')
47+
if (!controller.signal.aborted && params) {
5948
const snippet = new vscode.SnippetString('')
6049
snippet.appendText('(')
6150
// todo maybe when have skipped, add a way to leave trailing , (previous behavior)
62-
for (const [i, { insertText, name }] of parameters.entries()) {
63-
const isRest = insertText.startsWith('...')
64-
let text: string
65-
// eslint-disable-next-line default-case
66-
switch (insertMode) {
67-
case 'always-name':
68-
text = name
69-
break
70-
case 'prefer-name':
71-
// prefer name, but only if identifier and not binding pattern & rest
72-
text = oneOf(insertText[0], '[', '{') ? insertText : isRest ? insertText : name
73-
break
74-
case 'always-declaration':
75-
text = insertText
76-
break
77-
}
78-
79-
snippet.appendPlaceholder(text)
80-
if (i !== parameters.length - 1) snippet.appendText(', ')
51+
for (const [i, param] of params.entries()) {
52+
snippet.appendPlaceholder(param)
53+
if (i !== params.length - 1) snippet.appendText(', ')
8154
}
8255

83-
const allFiltered = data.parameters.length > parameters.length
84-
// TODO when many, but at least one not empty
85-
if (allFiltered || data.hasManySignatures) snippet.appendTabstop()
86-
8756
snippet.appendText(')')
8857
void editor.insertSnippet(snippet, undefined, {
8958
undoStopAfter: false,
9059
undoStopBefore: false,
9160
})
92-
if (vscode.workspace.getConfiguration('editor.parameterHints').get('enabled')) {
61+
if (vscode.workspace.getConfiguration('editor.parameterHints').get('enabled') && params.length > 0) {
9362
void vscode.commands.executeCommand('editor.action.triggerParameterHints')
9463
}
9564
}
@@ -120,40 +89,42 @@ export default (tsApi: { onCompletionAccepted }) => {
12089
)
12190
})
12291

123-
conditionallyRegister(
124-
'suggestions.keywordsInsertText',
125-
() =>
126-
vscode.workspace.onDidChangeTextDocument(({ document, contentChanges, reason }) => {
127-
if (!justAcceptedReturnKeywordSuggestion) return
128-
if (document !== vscode.window.activeTextEditor?.document) return
129-
try {
130-
if (oneOf(reason, vscode.TextDocumentChangeReason.Redo, vscode.TextDocumentChangeReason.Undo)) {
131-
return
132-
}
92+
vscode.workspace.onDidChangeTextDocument(({ document, contentChanges, reason }) => {
93+
if (document !== vscode.window.activeTextEditor?.document) return
94+
// do the same for position change?
95+
if (inFlightMethodSnippetOperation) {
96+
inFlightMethodSnippetOperation.abort()
97+
inFlightMethodSnippetOperation = undefined
98+
}
13399

134-
const char = contentChanges[0]?.text
135-
if (char?.length !== 1 || contentChanges.some(({ text }) => text !== char)) {
136-
return
137-
}
100+
if (!justAcceptedReturnKeywordSuggestion) return
138101

139-
if (char === ';' || char === '\n') {
140-
void vscode.window.activeTextEditor.edit(
141-
builder => {
142-
for (const { range } of contentChanges) {
143-
const pos = range.start
144-
builder.delete(expandPosition(document, pos, -1))
145-
}
146-
},
147-
{
148-
undoStopAfter: false,
149-
undoStopBefore: false,
150-
},
151-
)
152-
}
153-
} finally {
154-
justAcceptedReturnKeywordSuggestion = false
155-
}
156-
}),
157-
() => getExtensionSetting('suggestions.keywordsInsertText') !== 'none',
158-
)
102+
try {
103+
if (oneOf(reason, vscode.TextDocumentChangeReason.Redo, vscode.TextDocumentChangeReason.Undo)) {
104+
return
105+
}
106+
107+
const char = contentChanges[0]?.text
108+
if (char?.length !== 1 || contentChanges.some(({ text }) => text !== char)) {
109+
return
110+
}
111+
112+
if (char === ';' || char === '\n') {
113+
void vscode.window.activeTextEditor.edit(
114+
builder => {
115+
for (const { range } of contentChanges) {
116+
const pos = range.start
117+
builder.delete(expandPosition(document, pos, -1))
118+
}
119+
},
120+
{
121+
undoStopAfter: false,
122+
undoStopBefore: false,
123+
},
124+
)
125+
}
126+
} finally {
127+
justAcceptedReturnKeywordSuggestion = false
128+
}
129+
})
159130
}

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"lib": [
55
"ESNext",
66
"WebWorker"
7-
]
7+
],
8+
"noFallthroughCasesInSwitch": true
89
},
910
// type-check sources
1011
"include": [

typescript/src/codeActions/getCodeActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { compact } from '@zardoy/utils'
22
import { findChildContainingPosition } from '../utils'
33
import objectSwapKeysAndValues from './custom/objectSwapKeysAndValues'
44
import changeStringReplaceToRegex from './custom/changeStringReplaceToRegex'
5+
import splitDeclarationAndInitialization from './custom/splitDeclarationAndInitialization'
56

67
type SimplifiedRefactorInfo =
78
| {
@@ -30,7 +31,7 @@ export type CodeAction = {
3031
tryToApply: ApplyCodeAction
3132
}
3233

33-
const codeActions: CodeAction[] = [/* toggleBraces */ objectSwapKeysAndValues, changeStringReplaceToRegex]
34+
const codeActions: CodeAction[] = [/* toggleBraces */ objectSwapKeysAndValues, changeStringReplaceToRegex, splitDeclarationAndInitialization]
3435

3536
export const REFACTORS_CATEGORY = 'essential-refactors'
3637

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { compact } from '@zardoy/utils'
2+
import { isTypeNode } from './completions/keywordsSpace'
3+
import { GetConfig } from './types'
4+
import { findChildContainingExactPosition } from './utils'
5+
6+
export default (languageService: ts.LanguageService, sourceFile: ts.SourceFile, position: number, c: GetConfig) => {
7+
const node = findChildContainingExactPosition(sourceFile, position)
8+
if (!node || isTypeNode(node)) return
9+
10+
const typeChecker = languageService.getProgram()!.getTypeChecker()!
11+
const type = typeChecker.getContextualType(node as any) ?? typeChecker.getTypeAtLocation(node)
12+
const signatures = typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call)
13+
if (signatures.length === 0) return
14+
const signature = signatures[0]
15+
if (signatures.length > 1 && c('methodSnippets.multipleSignatures') === 'empty') {
16+
return ['']
17+
}
18+
if (!signature) return
19+
20+
const insertMode = c('methodSnippets.insertText')
21+
const skipMode = c('methodSnippets.skip')
22+
23+
// Investigate merging signatures
24+
const { parameters } = signatures[0]!
25+
const printer = ts.createPrinter()
26+
const paramsToInsert = compact(
27+
parameters.map(param => {
28+
const valueDeclaration = param.valueDeclaration as ts.ParameterDeclaration | undefined
29+
const isOptional =
30+
valueDeclaration && (valueDeclaration.questionToken || valueDeclaration.initializer || valueDeclaration.dotDotDotToken) ? true : false
31+
switch (skipMode) {
32+
case 'only-rest':
33+
if (valueDeclaration?.dotDotDotToken) return undefined
34+
break
35+
case 'optional-and-rest':
36+
if (isOptional) return undefined
37+
break
38+
}
39+
const insertName = insertMode === 'always-name' || !valueDeclaration
40+
const insertText = insertName
41+
? param.name
42+
: printer.printNode(
43+
ts.EmitHint.Unspecified,
44+
ts.factory.createParameterDeclaration(
45+
undefined,
46+
valueDeclaration.dotDotDotToken,
47+
!ts.isIdentifier(valueDeclaration.name) && insertMode !== 'always-declaration'
48+
? cloneBindingName(valueDeclaration.name)
49+
: valueDeclaration.name,
50+
valueDeclaration.questionToken,
51+
undefined,
52+
insertMode === 'always-declaration' ? valueDeclaration.initializer : undefined,
53+
),
54+
valueDeclaration.getSourceFile(),
55+
)
56+
return insertText
57+
}),
58+
)
59+
60+
const allFiltered = paramsToInsert.length === 0 && parameters.length > paramsToInsert.length
61+
if (allFiltered) return ['']
62+
63+
return paramsToInsert
64+
// return `(${paramsToInsert.map((param, i) => `\${${i + 1}:${param.replaceAll}}`).join(', ')})`
65+
66+
function cloneBindingName(node: ts.BindingName): ts.BindingName {
67+
return elideInitializerAndSetEmitFlags(node) as ts.BindingName
68+
function elideInitializerAndSetEmitFlags(node: ts.Node): ts.Node {
69+
let visited = ts.visitEachChild(
70+
node,
71+
elideInitializerAndSetEmitFlags,
72+
tsFull.nullTransformationContext as any,
73+
/*nodesVisitor*/ undefined,
74+
elideInitializerAndSetEmitFlags,
75+
)!
76+
if (ts.isBindingElement(visited)) {
77+
visited = ts.factory.updateBindingElement(visited, visited.dotDotDotToken, visited.propertyName, visited.name, /*initializer*/ undefined)
78+
}
79+
// if (!tsFull.nodeIsSynthesized(visited)) {
80+
// visited = ts.factory.cloneNode(visited);
81+
// }
82+
return ts.setEmitFlags(visited, ts.EmitFlags.SingleLine | ts.EmitFlags.NoAsciiEscaping)
83+
}
84+
}
85+
}

typescript/src/decorateProxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const decorateLanguageService = (
6363
position,
6464
options.triggerCharacter as TriggerCharacterCommand,
6565
languageService,
66-
config.config,
66+
config.config && c,
6767
options,
6868
formatOptions,
6969
)

typescript/src/ipcTypes.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const passthroughExposedApiCommands = ['getNodePath', 'getSpanOfEnclosingComment', 'getNodeAtPosition', 'getSignatureInfo'] as const
1+
export const passthroughExposedApiCommands = ['getNodePath', 'getSpanOfEnclosingComment', 'getNodeAtPosition'] as const
22

33
export const triggerCharacterCommands = [
44
...passthroughExposedApiCommands,
@@ -10,6 +10,7 @@ export const triggerCharacterCommands = [
1010
'turnArrayIntoObject',
1111
'getFixAllEdits',
1212
'acceptRenameWithParams',
13+
'getFullMethodSnippet',
1314
] as const
1415

1516
export type TriggerCharacterCommand = (typeof triggerCharacterCommands)[number]
@@ -54,12 +55,7 @@ export type RequestResponseTypes = {
5455
}
5556
turnArrayIntoObjectEdit: ts.TextChange[]
5657
getFixAllEdits: ts.TextChange[]
57-
getSignatureInfo: {
58-
// stable
59-
parameters: GetSignatureInfoParameter[]
60-
// unstable
61-
hasManySignatures: boolean
62-
}
58+
getFullMethodSnippet: string[] | undefined
6359
}
6460

6561
// INPUT
@@ -76,9 +72,6 @@ export type RequestOptionsTypes = {
7672
strings: boolean
7773
alias: boolean
7874
}
79-
getSignatureInfo: {
80-
includeInitializer?: boolean
81-
}
8275
}
8376

8477
// export type EmmetResult = {

0 commit comments

Comments
 (0)