diff --git a/extensions/vscode-vue-language-features/package.json b/extensions/vscode-vue-language-features/package.json index 9bc1ecb34..c80a4849f 100644 --- a/extensions/vscode-vue-language-features/package.json +++ b/extensions/vscode-vue-language-features/package.json @@ -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", diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index da8cea6e9..124642e0a 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -21,7 +21,8 @@ export interface PositionCapabilities { apply?(newName: string): string, }, completion?: boolean | { - additional: boolean, + additional?: boolean, + autoImportOnly?: boolean, }, diagnostic?: boolean, semanticTokens?: boolean, diff --git a/packages/language-service/src/languageFeatures/complete.ts b/packages/language-service/src/languageFeatures/complete.ts index 7c197c61b..7a138c025 100644 --- a/packages/language-service/src/languageFeatures/complete.ts +++ b/packages/language-service/src/languageFeatures/complete.ts @@ -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 }; } diff --git a/plugins/typescript/src/services/completions/resolve.ts b/plugins/typescript/src/services/completions/resolve.ts index a0d0352bb..ef32fb8ff 100644 --- a/plugins/typescript/src/services/completions/resolve.ts +++ b/plugins/typescript/src/services/completions/resolve.ts @@ -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) diff --git a/vue-language-tools/vue-language-core/src/generators/template.ts b/vue-language-tools/vue-language-core/src/generators/template.ts index 70bdeb9b2..34b33cdc0 100644 --- a/vue-language-tools/vue-language-core/src/generators/template.ts +++ b/vue-language-tools/vue-language-core/src/generators/template.ts @@ -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`); @@ -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`); } diff --git a/vue-language-tools/vue-language-service/src/languageService.ts b/vue-language-tools/vue-language-service/src/languageService.ts index d1bca4f9e..a51be3744 100644 --- a/vue-language-tools/vue-language-service/src/languageService.ts +++ b/vue-language-tools/vue-language-service/src/languageService.ts @@ -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() { @@ -44,25 +45,104 @@ export function getLanguageServicePlugins( const _tsPlugin = useTsPlugin(); const tsPlugin: embeddedLS.LanguageServicePlugin = (() => { let context: embeddedLS.LanguageServicePluginContext; + const autoImportPositions = new WeakSet(); 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('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('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, + }; + 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, + }; + 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; }, }, diff --git a/vue-language-tools/vue-language-service/src/plugins/vue-template.ts b/vue-language-tools/vue-language-service/src/plugins/vue-template.ts index b285b0432..3f1f36fb9 100644 --- a/vue-language-tools/vue-language-service/src/plugins/vue-template.ts +++ b/vue-language-tools/vue-language-service/src/plugins/vue-template.ts @@ -1,12 +1,7 @@ import useHtmlPlugin from '@volar-plugins/html'; -import { LanguageServicePlugin, LanguageServiceRuntimeContext, LanguageServicePluginContext, SourceFileDocument } from '@volar/language-service'; -import * as shared from '@volar/shared'; -import { getFormatCodeSettings } from '@volar-plugins/typescript/out/configs/getFormatCodeSettings'; -import { getUserPreferences } from '@volar-plugins/typescript/out/configs/getUserPreferences'; +import { LanguageServicePlugin, LanguageServicePluginContext, LanguageServiceRuntimeContext, SourceFileDocument } from '@volar/language-service'; import * as vue from '@volar/vue-language-core'; -import { camelize, capitalize, hyphenate } from '@vue/shared'; -import type * as ts from 'typescript/lib/tsserverlibrary'; -import { posix as path } from 'path'; +import { hyphenate } from '@vue/shared'; import * as html from 'vscode-html-languageservice'; import * as vscode from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -42,12 +37,6 @@ const eventModifiers: Record = { passive: 'attaches a DOM event with { passive: true }.', }; -interface AutoImportCompletionData { - mode: 'autoImport', - vueDocumentUri: string, - importUri: string, -} - export default function useVueTemplateLanguagePlugin>(options: { getSemanticTokenLegend(): vscode.SemanticTokensLegend, getScanner(document: TextDocument): html.Scanner | undefined, @@ -57,7 +46,6 @@ export default function useVueTemplateLanguagePlugin(); const tokenTypes = new Map(options.getSemanticTokenLegend().tokenTypes.map((t, i) => [t, i])); const runtimeMode = vue.resolveVueCompilerOptions(options.vueLsHost.getVueCompilationSettings()).experimentalRuntimeMode; @@ -101,17 +89,6 @@ export default function useVueTemplateLanguagePlugin` / `