Skip to content

Commit 0ef516a

Browse files
committed
feat: Rewrote enableMethodSnippets from the ground, fixing old bugs and introducing absolutely new modes (settings). Now enableMethodSnippets works on absolutely any variable that actually has signature. Finally!
1 parent 6493a3a commit 0ef516a

File tree

8 files changed

+202
-182
lines changed

8 files changed

+202
-182
lines changed

README.MD

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ TOC:
77

88
- [Top Features](#top-features)
99
- [Minor Useful Features](#minor-useful-features)
10+
- [Method Snippets](#method-snippets)
1011
- [Auto Imports](#auto-imports)
1112
- [Rename Features](#rename-features)
1213
- [Special Commands List](#special-commands-list)
@@ -35,19 +36,6 @@ Also is not supported in the web.
3536

3637
90% work done in this extension highly improves completions experience!
3738

38-
### Method Snippets
39-
40-
(*enabled by default*)
41-
42-
Expands arrow callbacks with signature snippet with adding additional undo stack!
43-
44-
Example:
45-
46-
```ts
47-
const callback = (arg) => {}
48-
callback -> callback(arg)
49-
```
50-
5139
### Strict Emmet
5240

5341
(*enabled by default*) when react langs are in `emmet.excludeLanguages`
@@ -205,6 +193,47 @@ const a = 5
205193
a = 6
206194
```
207195

196+
### Method Snippets
197+
198+
(*enabled by default*)
199+
200+
Expands arrow callback completions with signature snippet. Also adds additional undo stack!
201+
202+
Example:
203+
204+
```ts
205+
const callback = (arg) => {}
206+
callback -> callback(arg)
207+
```
208+
209+
#### Configuration
210+
211+
There are value descriptions for two settings:
212+
213+
`tsEssentialPlugins.methodSnippets.insertText`:
214+
215+
```ts
216+
const example = ({ a }, b?, c = 5, ...d) => { }
217+
// prefer-name (default)
218+
example({ a }, b, c, ...d)
219+
// always-declaration (recommended)
220+
example({ a }, b?, c = 5, ...d)
221+
// always-name
222+
example(__0, b, c, d)
223+
```
224+
225+
`tsEssentialPlugins.methodSnippets.skip`:
226+
227+
```ts
228+
const example = ({ a }, b?, c = 5, ...d) => { }
229+
// only-rest
230+
example({ a }, b, c)
231+
// optional-and-rest
232+
example({ a })
233+
// no-skip (default)
234+
example({ a }, b, c, ...d)
235+
```
236+
208237
## Auto Imports
209238
210239
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: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,45 @@ export type Configuration = {
311311
*/
312312
changeDtsFileDefinitionToJs: boolean
313313
/**
314-
* Experimental. Also includes optional args
314+
* Wether to enable method call snippets after completion (suggestion) accept eg:
315+
* ```ts
316+
* const simple = (foo: boolean) => {}
317+
* const complex = ({ a }, b?, c = 5) => { }
318+
*
319+
* simple(foo)
320+
* test({ a }, b, c)
321+
* ```
315322
* @default true
316323
*/
317324
enableMethodSnippets: boolean
325+
/**
326+
* ```ts
327+
* const example = ({ a }, b?, c = 5, ...d) => { }
328+
*
329+
* // prefer-name (default)
330+
* example({ a }, b, c, ...d)
331+
* // always-declaration (popular)
332+
* example({ a }, b?, c = 5, ...d)
333+
* // always-name
334+
* example(__0, b, c, d)
335+
* ```
336+
* @default prefer-name
337+
*/
338+
'methodSnippets.insertText': 'prefer-name' | 'always-declaration' | 'always-name'
339+
/**
340+
* ```ts
341+
* const example = ({ a }, b?, c = 5, ...d) => { }
342+
*
343+
* // only-rest
344+
* example({ a }, b, c)
345+
* // optional-and-rest (popular)
346+
* example({ a })
347+
* // no-skip (default)
348+
* example({ a }, b, c, ...d)
349+
* ```
350+
* @default no-skip
351+
*/
352+
'methodSnippets.skip': 'only-rest' | 'optional-and-rest' | 'no-skip'
318353
/**
319354
* Wether to disable our and builtin method snippets within jsx attributes
320355
* @default true

src/onCompletionAccepted.ts

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import { conditionallyRegister } from '@zardoy/vscode-utils/build/settings'
44
import { expandPosition } from '@zardoy/vscode-utils/build/position'
55
import { getExtensionSetting, registerExtensionCommand } from 'vscode-framework'
66
import { oneOf } from '@zardoy/utils'
7+
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
8+
import { sendCommand } from './sendCommand'
79

810
export default (tsApi: { onCompletionAccepted }) => {
911
let justAcceptedReturnKeywordSuggestion = false
1012
let onCompletionAcceptedOverride: ((item: any) => void) | undefined
1113

12-
tsApi.onCompletionAccepted((item: vscode.CompletionItem & { document: vscode.TextDocument }) => {
14+
// eslint-disable-next-line complexity
15+
tsApi.onCompletionAccepted(async (item: vscode.CompletionItem & { document: vscode.TextDocument }) => {
1316
if (onCompletionAcceptedOverride) {
1417
onCompletionAcceptedOverride(item)
1518
return
@@ -32,32 +35,59 @@ export default (tsApi: { onCompletionAccepted }) => {
3235
}
3336

3437
const enableMethodSnippets = vscode.workspace.getConfiguration(process.env.IDS_PREFIX, item.document).get('enableMethodSnippets')
35-
const documentationString = documentation instanceof vscode.MarkdownString ? documentation.value : documentation
36-
const insertFuncArgs = /<!-- insert-func: (.*)-->/.exec(documentationString)?.[1]
37-
console.debug('insertFuncArgs', insertFuncArgs)
38-
if (enableMethodSnippets && insertFuncArgs !== undefined) {
38+
39+
if (enableMethodSnippets && (typeof insertText !== 'object' || !insertText.value.endsWith(')$0'))) {
3940
const editor = getActiveRegularEditor()!
4041
const startPos = editor.selection.start
4142
const nextSymbol = editor.document.getText(new vscode.Range(startPos, startPos.translate(0, 1)))
4243
if (!['(', '.'].includes(nextSymbol)) {
43-
const snippet = new vscode.SnippetString('')
44-
snippet.appendText('(')
45-
const args = insertFuncArgs.split(',')
46-
for (let [i, arg] of args.entries()) {
47-
if (!arg) continue
48-
// skip empty, but add tabstops if we explicitly want it!
49-
if (arg === ' ') arg = ''
50-
snippet.appendPlaceholder(arg)
51-
if (i !== args.length - 1) snippet.appendText(', ')
52-
}
53-
54-
snippet.appendText(')')
55-
void editor.insertSnippet(snippet, undefined, {
56-
undoStopAfter: false,
57-
undoStopBefore: false,
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'],
5850
})
59-
if (vscode.workspace.getConfiguration('editor.parameterHints').get('enabled')) {
60-
void vscode.commands.executeCommand('editor.action.triggerParameterHints')
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+
59+
const snippet = new vscode.SnippetString('')
60+
snippet.appendText('(')
61+
// 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, @typescript-eslint/switch-exhaustiveness-check
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(', ')
81+
}
82+
83+
snippet.appendText(')')
84+
void editor.insertSnippet(snippet, undefined, {
85+
undoStopAfter: false,
86+
undoStopBefore: false,
87+
})
88+
if (vscode.workspace.getConfiguration('editor.parameterHints').get('enabled')) {
89+
void vscode.commands.executeCommand('editor.action.triggerParameterHints')
90+
}
6191
}
6292
}
6393
}

src/sendCommand.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { getExtensionSetting } from 'vscode-framework'
44
import { passthroughExposedApiCommands, TriggerCharacterCommand } from '../typescript/src/ipcTypes'
55

66
type SendCommandData<K> = {
7-
position: vscode.Position
8-
document: vscode.TextDocument
7+
position?: vscode.Position
8+
document?: vscode.TextDocument
99
inputOptions?: K
1010
}
1111
export const sendCommand = async <T, K = any>(command: TriggerCharacterCommand, sendCommandDataArg?: SendCommandData<K>): Promise<T | undefined> => {
@@ -27,21 +27,12 @@ export const sendCommand = async <T, K = any>(command: TriggerCharacterCommand,
2727
return
2828
}
2929

30-
if (sendCommandDataArg?.inputOptions) {
31-
command = `${command}?${JSON.stringify(sendCommandDataArg.inputOptions)}` as any
32-
}
30+
const _editor = getActiveRegularEditor()!
31+
const { document: { uri } = _editor.document, position = _editor.selection.active, inputOptions } = sendCommandDataArg ?? {}
3332

34-
const {
35-
document: { uri },
36-
position,
37-
} = ((): SendCommandData<any> => {
38-
if (sendCommandDataArg) return sendCommandDataArg
39-
const editor = getActiveRegularEditor()!
40-
return {
41-
document: editor.document,
42-
position: editor.selection.active,
43-
}
44-
})()
33+
if (inputOptions) {
34+
command = `${command}?${JSON.stringify(inputOptions)}` as any
35+
}
4536

4637
if (process.env.NODE_ENV === 'development') console.time(`request ${command}`)
4738
let requestFile = uri.fsPath

typescript/src/completionEntryDetails.ts

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,7 @@
1-
import { oneOf } from '@zardoy/utils'
2-
import { getParameterListParts } from './completions/snippetForFunctionCall'
31
import { PrevCompletionMap, PrevCompletionsAdditionalData } from './completionsAtPosition'
42
import namespaceAutoImports from './namespaceAutoImports'
53
import { GetConfig } from './types'
64

7-
const handleMethodSnippets = (prior: ts.CompletionEntryDetails, c: GetConfig, { enableMethodCompletion }: PrevCompletionsAdditionalData) => {
8-
if (
9-
c('enableMethodSnippets') &&
10-
oneOf(
11-
prior.kind,
12-
ts.ScriptElementKind.constElement,
13-
ts.ScriptElementKind.letElement,
14-
ts.ScriptElementKind.alias,
15-
ts.ScriptElementKind.variableElement,
16-
ts.ScriptElementKind.memberVariableElement,
17-
)
18-
) {
19-
// - 1 to look for possibly previous completing item
20-
let rawPartsOverride: ts.SymbolDisplayPart[] | undefined
21-
if (enableMethodCompletion && prior.kind === ts.ScriptElementKind.alias) {
22-
enableMethodCompletion =
23-
prior.displayParts[5]?.text === 'method' || (prior.displayParts[4]?.kind === 'keyword' && prior.displayParts[4].text === 'function')
24-
const { parts, gotMethodHit, hasOptionalParameters } = getParameterListParts(prior.displayParts)
25-
if (gotMethodHit) rawPartsOverride = hasOptionalParameters ? [...parts, { kind: '', text: ' ' }] : parts
26-
}
27-
const punctuationIndex = prior.displayParts.findIndex(({ kind, text }) => kind === 'punctuation' && text === ':')
28-
if (enableMethodCompletion && punctuationIndex !== 1) {
29-
const isParsableMethod = prior.displayParts
30-
// next is space
31-
.slice(punctuationIndex + 2)
32-
.map(({ text }) => text)
33-
.join('')
34-
.match(/^\((.*)\) => /)
35-
if (rawPartsOverride || isParsableMethod) {
36-
let firstArgMeet = false
37-
const args = (
38-
rawPartsOverride ||
39-
prior.displayParts.filter(({ kind }, index, array) => {
40-
if (kind !== 'parameterName') return false
41-
if (array[index - 1]!.text === '(') {
42-
if (!firstArgMeet) {
43-
// bad parsing, as it doesn't take second and more args
44-
firstArgMeet = true
45-
return true
46-
}
47-
return false
48-
}
49-
return true
50-
})
51-
).map(({ text }) => text)
52-
prior = {
53-
...prior,
54-
documentation: [...(prior.documentation ?? []), { kind: 'text', text: `<!-- insert-func: ${args.join(',')}-->` }],
55-
}
56-
}
57-
}
58-
}
59-
return prior
60-
}
61-
625
export default function completionEntryDetails(
636
inputArgs: Parameters<ts.LanguageService['getCompletionEntryDetails']>,
647
languageService: ts.LanguageService,
@@ -125,5 +68,5 @@ export default function completionEntryDetails(
12568
if (documentationAppend) {
12669
prior.documentation = [...(prior.documentation ?? []), { kind: 'text', text: documentationAppend }]
12770
}
128-
return handleMethodSnippets(prior, c, prevCompletionsAdittionalData)
71+
return prior
12972
}

0 commit comments

Comments
 (0)