From 82d27ceae0fcd034e8081b327b246b240542713a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Thu, 31 Oct 2024 02:37:01 +0800 Subject: [PATCH] feat(language-service): auto insert `const props =` with `props` completion (#4942) Co-authored-by: Johnson Chu --- extensions/vscode/package.json | 5 + .../lib/parsers/scriptSetupRanges.ts | 9 +- .../language-server/tests/completions.spec.ts | 38 +++++++ packages/language-service/index.ts | 2 + .../lib/plugins/vue-autoinsert-dotvalue.ts | 2 +- .../plugins/vue-complete-define-assignment.ts | 100 ++++++++++++++++++ 6 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 packages/language-service/lib/plugins/vue-complete-define-assignment.ts diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 6d3e2ae858..853d2462ca 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -376,6 +376,11 @@ "default": "autoKebab", "description": "Preferred attr name case." }, + "vue.complete.defineAssignment": { + "type": "boolean", + "default": true, + "description": "Auto add `const props = ` before `defineProps` when selecting the completion item `props`. (also `emit` and `slots`)" + }, "vue.autoInsert.dotValue": { "type": "boolean", "default": false, diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index 5b0accc2a6..da978db002 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -27,7 +27,9 @@ export function parseScriptSetupRanges( const slots: { name?: string; isObjectBindingPattern?: boolean; - define?: ReturnType; + define?: ReturnType & { + statement: TextRange; + }; } = {}; const emits: { name?: string; @@ -281,7 +283,10 @@ export function parseScriptSetupRanges( }); } else if (vueCompilerOptions.macros.defineSlots.includes(callText)) { - slots.define = parseDefineFunction(node); + slots.define = { + ...parseDefineFunction(node), + statement: getStatementRange(ts, parents, node, ast) + }; if (ts.isVariableDeclaration(parent)) { if (ts.isIdentifier(parent.name)) { slots.name = getNodeText(ts, parent.name, ast); diff --git a/packages/language-server/tests/completions.spec.ts b/packages/language-server/tests/completions.spec.ts index 10ab2efa5c..dcc67b3a06 100644 --- a/packages/language-server/tests/completions.spec.ts +++ b/packages/language-server/tests/completions.spec.ts @@ -456,6 +456,44 @@ describe('Completions', async () => { `); }); + it('Auto insert defines', async () => { + expect( + (await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` + + `, 'props')) + ).toMatchInlineSnapshot(` + { + "additionalTextEdits": [ + { + "newText": "const props = ", + "range": { + "end": { + "character": 4, + "line": 2, + }, + "start": { + "character": 4, + "line": 2, + }, + }, + }, + ], + "commitCharacters": [ + ".", + ",", + ";", + ], + "kind": 6, + "label": "props", + } + `); + }); + const openedDocuments: TextDocument[] = []; afterEach(async () => { diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 122442fa92..bf4ed50965 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -16,6 +16,7 @@ import { create as createTypeScriptTwoslashQueriesPlugin } from 'volar-service-t import { create as createTypeScriptDocCommentTemplatePlugin } from 'volar-service-typescript/lib/plugins/docCommentTemplate'; import { create as createTypeScriptSyntacticPlugin } from 'volar-service-typescript/lib/plugins/syntactic'; import { create as createCssPlugin } from './lib/plugins/css'; +import { create as createVueAutoDefineAssignmentPlugin } from './lib/plugins/vue-complete-define-assignment'; import { create as createVueAutoDotValuePlugin } from './lib/plugins/vue-autoinsert-dotvalue'; import { create as createVueAutoAddSpacePlugin } from './lib/plugins/vue-autoinsert-space'; import { create as createVueDirectiveCommentsPlugin } from './lib/plugins/vue-directive-comments'; @@ -197,6 +198,7 @@ function getCommonLanguageServicePlugins( createVueTwoslashQueriesPlugin(getTsPluginClient), createVueDocumentLinksPlugin(), createVueDocumentDropPlugin(ts, getTsPluginClient), + createVueAutoDefineAssignmentPlugin(), createVueAutoDotValuePlugin(ts, getTsPluginClient), createVueAutoAddSpacePlugin(), createVueInlayHintsPlugin(ts), diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index 6a347c6c68..43e0bf854c 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -113,7 +113,7 @@ function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -function isTsDocument(document: TextDocument) { +export function isTsDocument(document: TextDocument) { return document.languageId === 'javascript' || document.languageId === 'typescript' || document.languageId === 'javascriptreact' || diff --git a/packages/language-service/lib/plugins/vue-complete-define-assignment.ts b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts new file mode 100644 index 0000000000..20853a0bc2 --- /dev/null +++ b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts @@ -0,0 +1,100 @@ +import type { LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service'; +import { TextRange, tsCodegen, VueVirtualCode } from '@vue/language-core'; +import type * as vscode from 'vscode-languageserver-protocol'; +import { URI } from 'vscode-uri'; +import { isTsDocument } from './vue-autoinsert-dotvalue'; + +export function create(): LanguageServicePlugin { + return { + name: 'vue-complete-define-assignment', + capabilities: { + completionProvider: {}, + }, + create(context): LanguageServicePluginInstance { + return { + isAdditionalCompletion: true, + async provideCompletionItems(document) { + if (!isTsDocument(document)) { + return; + } + + const enabled = await context.env.getConfiguration?.('vue.complete.defineAssignment') ?? true; + if (!enabled) { + return; + } + + const result: vscode.CompletionItem[] = []; + const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!sourceScript || !virtualCode) { + return; + } + + const root = sourceScript?.generated?.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + + const codegen = tsCodegen.get(root._sfc); + const scriptSetup = root._sfc.scriptSetup; + const scriptSetupRanges = codegen?.scriptSetupRanges.get(); + if (!scriptSetup || !scriptSetupRanges) { + return; + } + + const mappings = [...context.language.maps.forEach(virtualCode)]; + + addDefineCompletionItem(scriptSetupRanges.props.define && { + exp: scriptSetupRanges.props.withDefaults ?? scriptSetupRanges.props.define.exp, + statement: scriptSetupRanges.props.define.statement + }, 'props'); + addDefineCompletionItem(scriptSetupRanges.emits.define, 'emit'); + addDefineCompletionItem(scriptSetupRanges.slots.define, 'slots'); + + return { + isIncomplete: false, + items: result + }; + + function addDefineCompletionItem( + define: { + exp: TextRange, + statement: TextRange; + } | undefined, + name: string + ) { + if (!define || define.exp.start !== define.statement.start) { + return; + } + + let offset; + for (const [, map] of mappings) { + for (const [generatedOffset] of map.toGeneratedLocation(scriptSetup!.startTagEnd + define.exp.start)) { + offset = generatedOffset; + break; + } + } + if (offset === undefined) { + return; + } + + const pos = document.positionAt(offset); + result.push({ + label: name, + kind: 6 satisfies typeof vscode.CompletionItemKind.Variable, + commitCharacters: ['.', ',', ';'], + additionalTextEdits: [{ + newText: `const ${name} = `, + range: { + start: pos, + end: pos + } + }] + }); + } + }, + }; + }, + }; +} \ No newline at end of file