Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(language-service): auto insert const props = with props completion #4942

Merged
merged 10 commits into from
Oct 30, 2024
5 changes: 5 additions & 0 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions packages/language-core/lib/parsers/scriptSetupRanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export function parseScriptSetupRanges(
const slots: {
name?: string;
isObjectBindingPattern?: boolean;
define?: ReturnType<typeof parseDefineFunction>;
define?: ReturnType<typeof parseDefineFunction> & {
statement: TextRange;
};
} = {};
const emits: {
name?: string;
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions packages/language-server/tests/completions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,44 @@ describe('Completions', async () => {
`);
});

it('Auto insert defines', async () => {
expect(
(await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', `
<script lang="ts" setup>
defineProps<{
foo: string;
}>();
props|
</script>
`, '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 () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/language-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -197,6 +198,7 @@ function getCommonLanguageServicePlugins(
createVueTwoslashQueriesPlugin(getTsPluginClient),
createVueDocumentLinksPlugin(),
createVueDocumentDropPlugin(ts, getTsPluginClient),
createVueAutoDefineAssignmentPlugin(),
createVueAutoDotValuePlugin(ts, getTsPluginClient),
createVueAutoAddSpacePlugin(),
createVueInlayHintsPlugin(ts),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' ||
Expand Down
Original file line number Diff line number Diff line change
@@ -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?.<boolean>('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
}
}]
});
}
},
};
},
};
}