Skip to content

Commit 82d27ce

Browse files
feat(language-service): auto insert const props = with props completion (#4942)
Co-authored-by: Johnson Chu <johnsoncodehk@gmail.com>
1 parent 06166e2 commit 82d27ce

File tree

6 files changed

+153
-3
lines changed

6 files changed

+153
-3
lines changed

extensions/vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@
376376
"default": "autoKebab",
377377
"description": "Preferred attr name case."
378378
},
379+
"vue.complete.defineAssignment": {
380+
"type": "boolean",
381+
"default": true,
382+
"description": "Auto add `const props = ` before `defineProps` when selecting the completion item `props`. (also `emit` and `slots`)"
383+
},
379384
"vue.autoInsert.dotValue": {
380385
"type": "boolean",
381386
"default": false,

packages/language-core/lib/parsers/scriptSetupRanges.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export function parseScriptSetupRanges(
2727
const slots: {
2828
name?: string;
2929
isObjectBindingPattern?: boolean;
30-
define?: ReturnType<typeof parseDefineFunction>;
30+
define?: ReturnType<typeof parseDefineFunction> & {
31+
statement: TextRange;
32+
};
3133
} = {};
3234
const emits: {
3335
name?: string;
@@ -281,7 +283,10 @@ export function parseScriptSetupRanges(
281283
});
282284
}
283285
else if (vueCompilerOptions.macros.defineSlots.includes(callText)) {
284-
slots.define = parseDefineFunction(node);
286+
slots.define = {
287+
...parseDefineFunction(node),
288+
statement: getStatementRange(ts, parents, node, ast)
289+
};
285290
if (ts.isVariableDeclaration(parent)) {
286291
if (ts.isIdentifier(parent.name)) {
287292
slots.name = getNodeText(ts, parent.name, ast);

packages/language-server/tests/completions.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,44 @@ describe('Completions', async () => {
456456
`);
457457
});
458458
459+
it('Auto insert defines', async () => {
460+
expect(
461+
(await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', `
462+
<script lang="ts" setup>
463+
defineProps<{
464+
foo: string;
465+
}>();
466+
props|
467+
</script>
468+
`, 'props'))
469+
).toMatchInlineSnapshot(`
470+
{
471+
"additionalTextEdits": [
472+
{
473+
"newText": "const props = ",
474+
"range": {
475+
"end": {
476+
"character": 4,
477+
"line": 2,
478+
},
479+
"start": {
480+
"character": 4,
481+
"line": 2,
482+
},
483+
},
484+
},
485+
],
486+
"commitCharacters": [
487+
".",
488+
",",
489+
";",
490+
],
491+
"kind": 6,
492+
"label": "props",
493+
}
494+
`);
495+
});
496+
459497
const openedDocuments: TextDocument[] = [];
460498
461499
afterEach(async () => {

packages/language-service/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { create as createTypeScriptTwoslashQueriesPlugin } from 'volar-service-t
1616
import { create as createTypeScriptDocCommentTemplatePlugin } from 'volar-service-typescript/lib/plugins/docCommentTemplate';
1717
import { create as createTypeScriptSyntacticPlugin } from 'volar-service-typescript/lib/plugins/syntactic';
1818
import { create as createCssPlugin } from './lib/plugins/css';
19+
import { create as createVueAutoDefineAssignmentPlugin } from './lib/plugins/vue-complete-define-assignment';
1920
import { create as createVueAutoDotValuePlugin } from './lib/plugins/vue-autoinsert-dotvalue';
2021
import { create as createVueAutoAddSpacePlugin } from './lib/plugins/vue-autoinsert-space';
2122
import { create as createVueDirectiveCommentsPlugin } from './lib/plugins/vue-directive-comments';
@@ -197,6 +198,7 @@ function getCommonLanguageServicePlugins(
197198
createVueTwoslashQueriesPlugin(getTsPluginClient),
198199
createVueDocumentLinksPlugin(),
199200
createVueDocumentDropPlugin(ts, getTsPluginClient),
201+
createVueAutoDefineAssignmentPlugin(),
200202
createVueAutoDotValuePlugin(ts, getTsPluginClient),
201203
createVueAutoAddSpacePlugin(),
202204
createVueInlayHintsPlugin(ts),

packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ function sleep(ms: number) {
113113
return new Promise(resolve => setTimeout(resolve, ms));
114114
}
115115

116-
function isTsDocument(document: TextDocument) {
116+
export function isTsDocument(document: TextDocument) {
117117
return document.languageId === 'javascript' ||
118118
document.languageId === 'typescript' ||
119119
document.languageId === 'javascriptreact' ||
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service';
2+
import { TextRange, tsCodegen, VueVirtualCode } from '@vue/language-core';
3+
import type * as vscode from 'vscode-languageserver-protocol';
4+
import { URI } from 'vscode-uri';
5+
import { isTsDocument } from './vue-autoinsert-dotvalue';
6+
7+
export function create(): LanguageServicePlugin {
8+
return {
9+
name: 'vue-complete-define-assignment',
10+
capabilities: {
11+
completionProvider: {},
12+
},
13+
create(context): LanguageServicePluginInstance {
14+
return {
15+
isAdditionalCompletion: true,
16+
async provideCompletionItems(document) {
17+
if (!isTsDocument(document)) {
18+
return;
19+
}
20+
21+
const enabled = await context.env.getConfiguration?.<boolean>('vue.complete.defineAssignment') ?? true;
22+
if (!enabled) {
23+
return;
24+
}
25+
26+
const result: vscode.CompletionItem[] = [];
27+
const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri));
28+
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
29+
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
30+
if (!sourceScript || !virtualCode) {
31+
return;
32+
}
33+
34+
const root = sourceScript?.generated?.root;
35+
if (!(root instanceof VueVirtualCode)) {
36+
return;
37+
}
38+
39+
const codegen = tsCodegen.get(root._sfc);
40+
const scriptSetup = root._sfc.scriptSetup;
41+
const scriptSetupRanges = codegen?.scriptSetupRanges.get();
42+
if (!scriptSetup || !scriptSetupRanges) {
43+
return;
44+
}
45+
46+
const mappings = [...context.language.maps.forEach(virtualCode)];
47+
48+
addDefineCompletionItem(scriptSetupRanges.props.define && {
49+
exp: scriptSetupRanges.props.withDefaults ?? scriptSetupRanges.props.define.exp,
50+
statement: scriptSetupRanges.props.define.statement
51+
}, 'props');
52+
addDefineCompletionItem(scriptSetupRanges.emits.define, 'emit');
53+
addDefineCompletionItem(scriptSetupRanges.slots.define, 'slots');
54+
55+
return {
56+
isIncomplete: false,
57+
items: result
58+
};
59+
60+
function addDefineCompletionItem(
61+
define: {
62+
exp: TextRange,
63+
statement: TextRange;
64+
} | undefined,
65+
name: string
66+
) {
67+
if (!define || define.exp.start !== define.statement.start) {
68+
return;
69+
}
70+
71+
let offset;
72+
for (const [, map] of mappings) {
73+
for (const [generatedOffset] of map.toGeneratedLocation(scriptSetup!.startTagEnd + define.exp.start)) {
74+
offset = generatedOffset;
75+
break;
76+
}
77+
}
78+
if (offset === undefined) {
79+
return;
80+
}
81+
82+
const pos = document.positionAt(offset);
83+
result.push({
84+
label: name,
85+
kind: 6 satisfies typeof vscode.CompletionItemKind.Variable,
86+
commitCharacters: ['.', ',', ';'],
87+
additionalTextEdits: [{
88+
newText: `const ${name} = `,
89+
range: {
90+
start: pos,
91+
end: pos
92+
}
93+
}]
94+
});
95+
}
96+
},
97+
};
98+
},
99+
};
100+
}

0 commit comments

Comments
 (0)