Skip to content

Commit

Permalink
feat: support auto import component from TS
Browse files Browse the repository at this point in the history
close #1643
  • Loading branch information
johnsoncodehk committed Oct 23, 2022
1 parent c489658 commit 0779ae2
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 277 deletions.
9 changes: 2 additions & 7 deletions extensions/vscode-vue-language-features/package.json
Expand Up @@ -487,15 +487,10 @@
"default": "auto-kebab",
"description": "Preferred attr name case."
},
"volar.completion.autoImportComponent": {
"volar.completion.normalizeComponentAutoImportName": {
"type": "boolean",
"default": true,
"description": "Enabled auto-import for component with tag completion."
},
"volar.completion.trimVueFromImportName": {
"type": "boolean",
"default": true,
"description": "Trim \"Vue\" from import name from auto import."
"description": "Normalize import name for auto import. (\"myCompVue\" -> \"MyComp\")"
},
"volar.preview.script.vite": {
"type": "string",
Expand Down
3 changes: 2 additions & 1 deletion packages/language-core/src/types.ts
Expand Up @@ -21,7 +21,8 @@ export interface PositionCapabilities {
apply?(newName: string): string,
},
completion?: boolean | {
additional: boolean,
additional?: boolean,
autoImportOnly?: boolean,
},
diagnostic?: boolean,
semanticTokens?: boolean,
Expand Down
4 changes: 4 additions & 0 deletions packages/language-service/src/languageFeatures/complete.ts
Expand Up @@ -156,6 +156,10 @@ export function register(context: LanguageServiceRuntimeContext) {
if (!embeddedCompletionList || !embeddedCompletionList.items.length)
continue;

if (typeof _data?.completion === 'object' && _data.completion.autoImportOnly) {
embeddedCompletionList.items = embeddedCompletionList.items.filter(item => !!item.labelDetails);
}

if (!isAdditional) {
cache!.mainCompletion = { documentUri: sourceMap.mappedDocument.uri };
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/typescript/src/services/completions/resolve.ts
Expand Up @@ -44,7 +44,7 @@ export function register(
details = languageService.getCompletionEntryDetails(fileName, offset, data.originalItem.name, formatOptions, data.originalItem.source, preferences, data.originalItem.data);
}
catch (err) {
item.detail = `[TS Error] ${err}`;
item.detail = `[TS Error] ${JSON.stringify(err)}`;
}

if (!details)
Expand Down
16 changes: 14 additions & 2 deletions vue-language-tools/vue-language-core/src/generators/template.ts
Expand Up @@ -539,7 +539,13 @@ export function generate(
tagText,
'template',
[startTagOffset, startTagOffset + node.tag.length],
capabilitiesSet.tagHover,
{
...capabilitiesSet.tagHover,
completion: {
additional: true,
autoImportOnly: true,
},
},
]);
codeGen.push(`>;\n`);

Expand All @@ -548,7 +554,13 @@ export function generate(
tagText,
'template',
[endTagOffset, endTagOffset + node.tag.length],
capabilitiesSet.tagHover,
{
...capabilitiesSet.tagHover,
completion: {
additional: true,
autoImportOnly: true,
},
},
]);
codeGen.push(`;\n`);
}
Expand Down
92 changes: 86 additions & 6 deletions vue-language-tools/vue-language-service/src/languageService.ts
Expand Up @@ -19,6 +19,7 @@ import useRefSugarConversionsPlugin from './plugins/vue-convert-refsugar';
import useScriptSetupConversionsPlugin from './plugins/vue-convert-scriptsetup';
import useTwoslashQueries from './plugins/vue-twoslash-queries';
import useVueTemplateLanguagePlugin, { semanticTokenTypes as vueTemplateSemanticTokenTypes } from './plugins/vue-template';
import type { Data } from '@volar-plugins/typescript/src/services/completions/basic';

export function getSemanticTokenLegend() {

Expand All @@ -44,25 +45,104 @@ export function getLanguageServicePlugins(
const _tsPlugin = useTsPlugin();
const tsPlugin: embeddedLS.LanguageServicePlugin = (() => {
let context: embeddedLS.LanguageServicePluginContext;
const autoImportPositions = new WeakSet<vscode.Position>();
return {
..._tsPlugin,
setup(_context) {
_tsPlugin.setup?.(_context);
context = _context;
},
resolveEmbeddedRange(range) {
if (autoImportPositions.has(range.start) && autoImportPositions.has(range.end))
return range;
},
complete: {
..._tsPlugin.complete,
async resolve(item) {
item = await _tsPlugin.complete!.resolve!(item);

if (
/\w*Vue$/.test(item.label)
&& item.textEdit?.newText && /\w*Vue$/.test(item.textEdit.newText)
&& item.additionalTextEdits?.length === 1 && item.additionalTextEdits[0].newText.indexOf('Vue from ') >= 0
&& (await context.env.configurationHost?.getConfiguration<boolean>('volar.completion.trimVueFromImportName') ?? true)
item.textEdit?.newText && /\w*Vue$/.test(item.textEdit.newText)
&& item.additionalTextEdits?.length === 1 && item.additionalTextEdits[0].newText.indexOf('import ' + item.textEdit.newText + ' from ') >= 0
&& (await context.env.configurationHost?.getConfiguration<boolean>('volar.completion.normalizeComponentAutoImportName') ?? true)
) {
item.textEdit.newText = item.textEdit.newText.slice(0, -'Vue'.length);
item.additionalTextEdits[0].newText = item.additionalTextEdits[0].newText.replace('Vue from ', ' from ');
let newName = item.textEdit.newText.slice(0, -'Vue'.length);
newName = newName[0].toUpperCase() + newName.substring(1);
item.additionalTextEdits[0].newText = item.additionalTextEdits[0].newText.replace(
'import ' + item.textEdit.newText + ' from ',
'import ' + newName + ' from ',
);
item.textEdit.newText = newName;
}

const data: Data = item.data;
if (data && item.additionalTextEdits?.length && item.textEdit) {
const map = apis.context.documents.sourceMapFromEmbeddedDocumentUri(data.uri);
const doc = map ? apis.context.documents.get(map.sourceDocument.uri) : undefined;
if (map && doc?.file instanceof vue.VueSourceFile) {
let isComponentAutoImport = false;
for (const [_, mapping] of map.toSourceOffsets(data.offset)) {
if (typeof mapping.data.completion === 'object' && mapping.data.completion.autoImportOnly) {
isComponentAutoImport = true;
break;
}
}

const sfc = doc.file.sfc;
const componentName = item.textEdit.newText;
const textDoc = doc.getDocument();
if (isComponentAutoImport && sfc.scriptAst && sfc.script) {
const ts = context.typescript.module;
const _scriptRanges = vue.scriptRanges.parseScriptRanges(ts, sfc.scriptAst, !!sfc.scriptSetup, true);
const exportDefault = _scriptRanges.exportDefault;
if (exportDefault) {
// https://github.com/microsoft/TypeScript/issues/36174
const printer = ts.createPrinter();
if (exportDefault.componentsOption && exportDefault.componentsOptionNode) {
const newNode: typeof exportDefault.componentsOptionNode = {
...exportDefault.componentsOptionNode,
properties: [
...exportDefault.componentsOptionNode.properties,
ts.factory.createShorthandPropertyAssignment(componentName),
] as any as ts.NodeArray<ts.ObjectLiteralElementLike>,
};
const printText = printer.printNode(ts.EmitHint.Expression, newNode, sfc.scriptAst);
const editRange = vscode.Range.create(
textDoc.positionAt(sfc.script.startTagEnd + exportDefault.componentsOption.start),
textDoc.positionAt(sfc.script.startTagEnd + exportDefault.componentsOption.end),
);
autoImportPositions.add(editRange.start);
autoImportPositions.add(editRange.end);
item.additionalTextEdits.push(vscode.TextEdit.replace(
editRange,
unescape(printText.replace(/\\u/g, '%u')),
));
}
else if (exportDefault.args && exportDefault.argsNode) {
const newNode: typeof exportDefault.argsNode = {
...exportDefault.argsNode,
properties: [
...exportDefault.argsNode.properties,
ts.factory.createShorthandPropertyAssignment(`components: { ${componentName} }`),
] as any as ts.NodeArray<ts.ObjectLiteralElementLike>,
};
const printText = printer.printNode(ts.EmitHint.Expression, newNode, sfc.scriptAst);
const editRange = vscode.Range.create(
textDoc.positionAt(sfc.script.startTagEnd + exportDefault.args.start),
textDoc.positionAt(sfc.script.startTagEnd + exportDefault.args.end),
);
autoImportPositions.add(editRange.start);
autoImportPositions.add(editRange.end);
item.additionalTextEdits.push(vscode.TextEdit.replace(
editRange,
unescape(printText.replace(/\\u/g, '%u')),
));
}
}
}
}
}

return item;
},
},
Expand Down

0 comments on commit 0779ae2

Please sign in to comment.