Skip to content

Commit df3bb76

Browse files
authored
Merge pull request #125 from zardoy/develop
2 parents f436958 + 62f1cd5 commit df3bb76

19 files changed

+258
-102
lines changed

README.MD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ function Foo() {
315315
}
316316
```
317317

318+
`tsEssentialPlugins.methodSnippetsInsertText`:
319+
320+
Optionally resolve insertText of all completion at suggest trigger:
321+
322+
![method-snippets-insert-text](media/method-snippets-insert-text.png)
323+
318324
### Ambiguous Suggestions
319325

320326
Some variables like `Object` or `lodash` are common to access properties as well as call directly:

media/method-snippets-insert-text.png

23.4 KB
Loading

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
"command": "insertNameOfCompletion",
5656
"title": "Insert Name of Completion",
5757
"category": "TS Essentials"
58+
},
59+
{
60+
"command": "copyFullType",
61+
"title": "Copy Full Type"
5862
}
5963
],
6064
"keybindings": [

src/configurationType.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,15 @@ export type Configuration = {
358358
* @default true
359359
*/
360360
enableMethodSnippets: boolean
361+
/**
362+
* Wether add insert text and detail to every function completion on each suggest trigger (instead of expanding method snippet after completion accept).
363+
* This way you can enable support for method snippets in Vue files.
364+
* `methodSnippets.replaceArguments` isn't supported for now.
365+
* This is not enabled by default as it might be really slow in some cases.
366+
* Recommended to try!
367+
* @default disable
368+
*/
369+
methodSnippetsInsertText: 'disable' | 'only-local' | 'all'
361370
/**
362371
* ```ts
363372
* const example = ({ a }, b?, c = 5, ...d) => { }
@@ -540,6 +549,12 @@ export type Configuration = {
540549
* @default false
541550
*/
542551
'experiments.changeKindToFunction': boolean
552+
/**
553+
* Use workaround method for inserting name of TypeScript suggestion.
554+
* If you move to next suggestion and then to previous, and then run *insert name of completion* via keybinding, name of **last resolved** completion will be inserted, so you might prefer to enable this setting. Also it makes this feature work in Vue.
555+
* @default false
556+
*/
557+
'experiments.enableInsertNameOfSuggestionFix': boolean
543558
/**
544559
* Map *symbol - array of modules* to change sorting of imports - first available takes precedence in auto import code fixes (+ import all action)
545560
*

src/onCompletionAccepted.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ import { getActiveRegularEditor } from '@zardoy/vscode-utils'
33
import { expandPosition } from '@zardoy/vscode-utils/build/position'
44
import { getExtensionSetting, registerExtensionCommand } from 'vscode-framework'
55
import { oneOf } from '@zardoy/utils'
6-
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
7-
import { sendCommand } from './sendCommand'
6+
7+
export const onCompletionAcceptedOverride: { value: ((item: any) => void) | undefined } = { value: undefined }
88

99
export default (tsApi: { onCompletionAccepted }) => {
1010
let inFlightMethodSnippetOperation: undefined | AbortController
1111
let justAcceptedReturnKeywordSuggestion = false
1212
let lastAcceptedAmbiguousMethodSnippetSuggestion: string | undefined
13-
let onCompletionAcceptedOverride: ((item: any) => void) | undefined
1413

1514
// eslint-disable-next-line complexity
1615
tsApi.onCompletionAccepted(async (item: vscode.CompletionItem & { document: vscode.TextDocument; tsEntry }) => {
17-
if (onCompletionAcceptedOverride) {
18-
onCompletionAcceptedOverride(item)
16+
if (onCompletionAcceptedOverride.value) {
17+
onCompletionAcceptedOverride.value(item)
18+
onCompletionAcceptedOverride.value = undefined
1919
return
2020
}
2121

@@ -38,10 +38,18 @@ export default (tsApi: { onCompletionAccepted }) => {
3838

3939
if (/* snippet is by vscode or by us to ignore pos */ typeof insertText !== 'object') {
4040
const editor = getActiveRegularEditor()!
41-
if (item.tsEntry.source) {
41+
42+
const documentation = typeof item.documentation === 'object' ? item.documentation.value : item.documentation
43+
const dataMarker = '<!--tep '
44+
if (!documentation?.startsWith(dataMarker)) return
45+
const parsed = JSON.parse(documentation.slice(dataMarker.length, documentation.indexOf('e-->')))
46+
const { methodSnippet: params, isAmbiguous, wordStartOffset } = parsed
47+
const startPos = editor.selection.start
48+
const acceptedWordStartOffset = wordStartOffset !== undefined && editor.document.getWordRangeAtPosition(startPos, /[\w\d]+/i)?.start
49+
if (!oneOf(acceptedWordStartOffset, false, undefined) && wordStartOffset === editor.document.offsetAt(acceptedWordStartOffset)) {
4250
await new Promise<void>(resolve => {
43-
vscode.workspace.onDidChangeTextDocument(({ document }) => {
44-
if (editor.document !== document) return
51+
vscode.workspace.onDidChangeTextDocument(({ document, contentChanges }) => {
52+
if (document !== editor.document || contentChanges.length === 0) return
4553
resolve()
4654
})
4755
})
@@ -50,12 +58,9 @@ export default (tsApi: { onCompletionAccepted }) => {
5058
})
5159
}
5260

53-
const documentation = typeof item.documentation === 'object' ? item.documentation.value : item.documentation
54-
const dataMarker = '<!--tep '
55-
if (!documentation?.startsWith(dataMarker)) return
56-
const parsed = JSON.parse(documentation.slice(dataMarker.length, documentation.indexOf('e-->')))
57-
const { methodSnippet: params, isAmbiguous } = parsed
58-
if (!params) return
61+
// nextChar check also duplicated in completionEntryDetails for perf, but we need to run this check again with correct position
62+
const nextChar = editor.document.getText(new vscode.Range(startPos, startPos.translate(0, 1)))
63+
if (!params || ['(', '.', '`'].includes(nextChar)) return
5964

6065
if (isAmbiguous && lastAcceptedAmbiguousMethodSnippetSuggestion !== suggestionName) {
6166
lastAcceptedAmbiguousMethodSnippetSuggestion = suggestionName
@@ -100,10 +105,9 @@ export default (tsApi: { onCompletionAccepted }) => {
100105
async (_progress, token) => {
101106
const accepted = await new Promise<boolean>(resolve => {
102107
token.onCancellationRequested(() => {
103-
onCompletionAcceptedOverride = undefined
104108
resolve(false)
105109
})
106-
onCompletionAcceptedOverride = item => {
110+
onCompletionAcceptedOverride.value = item => {
107111
console.dir(item, { depth: 4 })
108112
resolve(true)
109113
}

src/specialCommands.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as vscode from 'vscode'
22
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
3-
import { getExtensionCommandId, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
3+
import { getExtensionCommandId, getExtensionSetting, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
44
import { showQuickPick } from '@zardoy/vscode-utils/build/quickPick'
55
import _ from 'lodash'
66
import { compact } from '@zardoy/utils'
7+
import { offsetPosition } from '@zardoy/vscode-utils/build/position'
78
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
89
import { sendCommand } from './sendCommand'
910
import { tsRangeToVscode, tsRangeToVscodeSelection } from './util'
11+
import { onCompletionAcceptedOverride } from './onCompletionAccepted'
1012

1113
export default () => {
1214
registerExtensionCommand('removeFunctionArgumentsTypesInSelection', async () => {
@@ -238,11 +240,52 @@ export default () => {
238240
await vscode.commands.executeCommand(preview ? 'acceptRenameInputWithPreview' : 'acceptRenameInput')
239241
})
240242

241-
registerExtensionCommand('insertNameOfCompletion', async () => {
243+
registerExtensionCommand('insertNameOfCompletion', async (_, { insertMode } = {}) => {
242244
const editor = vscode.window.activeTextEditor
243245
if (!editor) return
244-
const result = await sendCommand<RequestResponseTypes['getLastResolvedCompletion']>('getLastResolvedCompletion')
245-
if (!result) return
246-
await editor.insertSnippet(new vscode.SnippetString().appendText(result.name))
246+
if (!getExtensionSetting('experiments.enableInsertNameOfSuggestionFix')) {
247+
const result = await sendCommand<RequestResponseTypes['getLastResolvedCompletion']>('getLastResolvedCompletion')
248+
if (!result) return
249+
const position = editor.selection.active
250+
const range = result.range ? tsRangeToVscode(editor.document, result.range) : editor.document.getWordRangeAtPosition(position)
251+
await editor.insertSnippet(
252+
new vscode.SnippetString().appendText(result.name),
253+
(insertMode || vscode.workspace.getConfiguration().get('editor.suggest.insertMode')) === 'replace' ? range : range?.with(undefined, position),
254+
)
255+
return
256+
}
257+
258+
onCompletionAcceptedOverride.value = () => {}
259+
const { ranges, text } = await new Promise<{ text: string; ranges: vscode.Range[] }>(resolve => {
260+
vscode.workspace.onDidChangeTextDocument(({ document, contentChanges }) => {
261+
if (document !== editor.document || contentChanges.length === 0) return
262+
const ranges = contentChanges.map(
263+
change => new vscode.Range(change.range.start, offsetPosition(document, change.range.start, change.text.length)),
264+
)
265+
resolve({ ranges, text: contentChanges[0]!.text })
266+
})
267+
void vscode.commands.executeCommand('acceptSelectedSuggestion')
268+
})
269+
const needle = ['(', ': '].find(needle => text.includes(needle))
270+
if (!needle) return
271+
const cleanedText = text.slice(0, text.indexOf(needle))
272+
await editor.edit(
273+
e => {
274+
for (const range of ranges) {
275+
e.replace(range, cleanedText)
276+
}
277+
},
278+
{
279+
undoStopBefore: false,
280+
undoStopAfter: false,
281+
},
282+
)
283+
})
284+
285+
registerExtensionCommand('copyFullType', async () => {
286+
const response = await sendCommand<RequestResponseTypes['getFullType']>('getFullType')
287+
if (!response) return
288+
const { text } = response
289+
await vscode.env.clipboard.writeText(text)
247290
})
248291
}

typescript/src/completionEntryDetails.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import constructMethodSnippet from './constructMethodSnippet'
33
import { RequestResponseTypes } from './ipcTypes'
44
import namespaceAutoImports from './namespaceAutoImports'
55
import { GetConfig } from './types'
6+
import { wordStartAtPos } from './utils'
67

78
export const lastResolvedCompletion = {
89
value: undefined as undefined | RequestResponseTypes['getLastResolvedCompletion'],
@@ -16,7 +17,7 @@ export default function completionEntryDetails(
1617
{ enableMethodCompletion, completionsSymbolMap }: PrevCompletionsAdditionalData,
1718
): ts.CompletionEntryDetails | undefined {
1819
const [fileName, position, entryName, formatOptions, source, preferences, data] = inputArgs
19-
lastResolvedCompletion.value = { name: entryName }
20+
lastResolvedCompletion.value = { name: entryName, range: prevCompletionsMap[entryName]?.range }
2021
const program = languageService.getProgram()
2122
const sourceFile = program?.getSourceFile(fileName)
2223
if (!program || !sourceFile) return
@@ -49,6 +50,7 @@ export default function completionEntryDetails(
4950
prior.displayParts = [{ kind: 'text', text: detailPrepend }, ...prior.displayParts]
5051
}
5152
if (!prior) return
53+
// might be incorrect: write [].entries() -> []|.entries|() -> []./*position*/e
5254
const nextChar = sourceFile.getFullText().slice(position, position + 1)
5355

5456
if (enableMethodCompletion && c('enableMethodSnippets') && !['(', '.', '`'].includes(nextChar)) {
@@ -59,7 +61,12 @@ export default function completionEntryDetails(
5961
}
6062
const methodSnippet = constructMethodSnippet(languageService, sourceFile, position, symbol, c, resolveData)
6163
if (methodSnippet) {
62-
const data = JSON.stringify({ methodSnippet, isAmbiguous: resolveData.isAmbiguous })
64+
const wordStartOffset = source ? wordStartAtPos(sourceFile.getFullText(), position) : undefined
65+
const data = JSON.stringify({
66+
methodSnippet,
67+
isAmbiguous: resolveData.isAmbiguous,
68+
wordStartOffset,
69+
})
6370
prior.documentation = [{ kind: 'text', text: `<!--tep ${data} e-->` }, ...(prior.documentation ?? [])]
6471
}
6572
}

typescript/src/completions/changeKindToFunction.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { oneOf } from '@zardoy/utils'
2+
import constructMethodSnippet from '../constructMethodSnippet'
3+
import { insertTextAfterEntry } from '../utils'
4+
import { sharedCompletionContext } from './sharedContext'
5+
6+
export default (entries: ts.CompletionEntry[]) => {
7+
const { languageService, c, sourceFile, position } = sharedCompletionContext
8+
9+
const methodSnippetInsertTextMode = c('methodSnippetsInsertText')
10+
const enableResolvingInsertText = c('enableMethodSnippets') && methodSnippetInsertTextMode !== 'disable'
11+
const changeKindToFunction = c('experiments.changeKindToFunction')
12+
13+
if (!enableResolvingInsertText && !changeKindToFunction) return
14+
15+
const typeChecker = languageService.getProgram()!.getTypeChecker()!
16+
// let timeSpend = 0
17+
const newEntries = entries.map(entry => {
18+
const patch = (): ts.CompletionEntry | undefined => {
19+
const { kind, symbol } = entry
20+
if (
21+
!enableResolvingInsertText &&
22+
!oneOf(
23+
kind,
24+
ts.ScriptElementKind.alias,
25+
ts.ScriptElementKind.memberVariableElement,
26+
ts.ScriptElementKind.variableElement,
27+
ts.ScriptElementKind.localVariableElement,
28+
ts.ScriptElementKind.constElement,
29+
ts.ScriptElementKind.variableElement,
30+
)
31+
) {
32+
return
33+
}
34+
if (methodSnippetInsertTextMode === 'only-local' && entry.source) return
35+
if (!symbol) return
36+
const { valueDeclaration = symbol.declarations?.[0] } = symbol
37+
if (!valueDeclaration) return
38+
39+
// const dateNow = Date.now()
40+
if (enableResolvingInsertText) {
41+
const resolveData = {} as { isAmbiguous: boolean }
42+
const methodSnippet = constructMethodSnippet(languageService, sourceFile, position, symbol, c, resolveData)
43+
if (!methodSnippet || resolveData.isAmbiguous) return
44+
return {
45+
...entry,
46+
insertText: insertTextAfterEntry(entry, `(${methodSnippet.map((x, i) => `$\{${i + 1}:${x}}`).join(', ')})`),
47+
labelDetails: {
48+
detail: `(${methodSnippet.join(', ')})`,
49+
description: ts.displayPartsToString(entry.sourceDisplay),
50+
},
51+
kind: changeKindToFunction ? ts.ScriptElementKind.functionElement : entry.kind,
52+
isSnippet: true,
53+
}
54+
}
55+
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, valueDeclaration)
56+
const signatures = typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call)
57+
// timeSpend += Date.now() - dateNow
58+
if (signatures.length === 0) return
59+
60+
return { ...entry, kind: ts.ScriptElementKind.functionElement }
61+
}
62+
63+
return patch() ?? entry
64+
})
65+
66+
// remove logging once stable
67+
// console.log('changeKindToFunction time:', timeSpend)
68+
69+
return newEntries
70+
}

typescript/src/completions/localityBonus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default (entries: ts.CompletionEntry[]) => {
99
// eslint-disable-next-line prefer-destructuring
1010
const symbol: ts.Symbol | undefined = entry['symbol']
1111
if (!symbol) return
12-
const { valueDeclaration } = symbol
12+
const { valueDeclaration = symbol.declarations?.[0] } = symbol
1313
if (!valueDeclaration) return
1414
if (valueDeclaration.getSourceFile().fileName !== sourceFile.fileName) return -1
1515
return valueDeclaration.pos

0 commit comments

Comments
 (0)