Skip to content

Commit

Permalink
feat(language-service): auto insert const props = with props comp…
Browse files Browse the repository at this point in the history
…letion (#4942)

Co-authored-by: Johnson Chu <[email protected]>
  • Loading branch information
KazariEX and johnsoncodehk authored Oct 30, 2024
1 parent 06166e2 commit 82d27ce
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 3 deletions.
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
}
}]
});
}
},
};
},
};
}

0 comments on commit 82d27ce

Please sign in to comment.