diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 82b908aaaa..0430f3d58e 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -259,6 +259,10 @@ "default": "off", "description": "Traces the communication between VS Code and the language server." }, + "vue.server.hybridMode": { + "type": "boolean", + "default": false + }, "vue.server.path": { "type": [ "string", diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 4746b8cb3b..5084f77bed 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -151,6 +151,7 @@ async function getInitializationOptions( tokenModifiers: [], }, vue: { + hybridMode: config.server.hybridMode, additionalExtensions: [ ...config.server.additionalExtensions, ...!config.server.petiteVue.supportHtmlFile ? [] : ['html'], diff --git a/extensions/vscode/src/config.ts b/extensions/vscode/src/config.ts index 84b8feea07..1393d239b7 100644 --- a/extensions/vscode/src/config.ts +++ b/extensions/vscode/src/config.ts @@ -16,6 +16,7 @@ export const config = { return _config().get('doctor')!; }, get server(): Readonly<{ + hybridMode: boolean; path: null | string; runtime: 'node' | 'bun'; maxOldSpaceSize: number; diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 0e10f78e6e..3486005cfb 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -71,7 +71,6 @@ function createCheckerWorker( let projectVersion = 0; const scriptSnapshots = new Map(); - const resolvedVueOptions = vue.resolveVueCompilerOptions(parsedCommandLine.vueOptions); const _host: vue.TypeScriptProjectHost = { getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), @@ -88,7 +87,7 @@ function createCheckerWorker( return scriptSnapshots.get(fileName); }, getLanguageId: fileName => { - if (resolvedVueOptions.extensions.some(ext => fileName.endsWith(ext))) { + if (parsedCommandLine.vueOptions.extensions.some(ext => fileName.endsWith(ext))) { return 'vue'; } return vue.resolveCommonLanguageId(fileName); @@ -96,7 +95,7 @@ function createCheckerWorker( }; return { - ...baseCreate(ts, configFileName, _host, resolvedVueOptions, checkerOptions, globalComponentName), + ...baseCreate(ts, configFileName, _host, parsedCommandLine.vueOptions, checkerOptions, globalComponentName), updateFile(fileName: string, text: string) { fileName = fileName.replace(windowsPathReg, '/'); scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(text)); diff --git a/packages/language-core/lib/plugins.ts b/packages/language-core/lib/plugins.ts index 04efc46234..8bfc741375 100644 --- a/packages/language-core/lib/plugins.ts +++ b/packages/language-core/lib/plugins.ts @@ -27,7 +27,9 @@ export function getDefaultVueLanguagePlugins(pluginContext: Parameters { try { - return plugin(pluginContext); + const instance = plugin(pluginContext); + instance.name ??= (plugin as any).__moduleName; + return instance; } catch (err) { console.warn('[Vue] Failed to create plugin', err); } @@ -42,7 +44,7 @@ export function getDefaultVueLanguagePlugins(pluginContext: Parameters { const valid = plugin.version === pluginVersion; if (!valid) { - console.warn(`[Vue] Plugin ${JSON.stringify(plugin.name)} API version incompatible, expected ${JSON.stringify(pluginVersion)} but got ${JSON.stringify(plugin.version)}`); + console.warn(`[Vue] Plugin ${JSON.stringify(plugin.name)} API version incompatible, expected "${pluginVersion}" but got "${plugin.version}".`); } return valid; }); diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index b912f4347e..0766fc84d1 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -3,7 +3,7 @@ import * as path from 'path-browserify'; import type { RawVueCompilerOptions, VueCompilerOptions, VueLanguagePlugin } from '../types'; export type ParsedCommandLine = ts.ParsedCommandLine & { - vueOptions: Partial; + vueOptions: VueCompilerOptions; }; export function createParsedCommandLineByJson( @@ -28,6 +28,7 @@ export function createParsedCommandLineByJson( } catch (err) { } } + const resolvedVueOptions = resolveVueCompilerOptions(vueOptions); const parsed = ts.parseJsonConfigFileContent( json, proxyHost.host, @@ -35,7 +36,7 @@ export function createParsedCommandLineByJson( {}, configFileName, undefined, - (vueOptions.extensions ?? ['.vue']).map(extension => ({ + resolvedVueOptions.extensions.map(extension => ({ extension: extension.slice(1), isMixedContent: true, scriptKind: ts.ScriptKind.Deferred, @@ -49,7 +50,7 @@ export function createParsedCommandLineByJson( return { ...parsed, - vueOptions, + vueOptions: resolvedVueOptions, }; } @@ -74,6 +75,7 @@ export function createParsedCommandLine( } catch (err) { } } + const resolvedVueOptions = resolveVueCompilerOptions(vueOptions); const parsed = ts.parseJsonSourceFileConfigFileContent( config, proxyHost.host, @@ -81,7 +83,7 @@ export function createParsedCommandLine( {}, tsConfigPath, undefined, - (vueOptions.extensions ?? ['.vue']).map(extension => ({ + resolvedVueOptions.extensions.map(extension => ({ extension: extension.slice(1), isMixedContent: true, scriptKind: ts.ScriptKind.Deferred, @@ -95,7 +97,7 @@ export function createParsedCommandLine( return { ...parsed, - vueOptions, + vueOptions: resolvedVueOptions, }; } catch (err) { @@ -163,7 +165,9 @@ function getPartialVueCompilerOptions( try { const resolvedPath = resolvePath(pluginPath); if (resolvedPath) { - return require(resolvedPath); + const plugin = require(resolvedPath); + plugin.__moduleName = pluginPath; + return plugin; } else { console.warn('[Vue] Load plugin failed:', pluginPath); diff --git a/packages/language-plugin-pug/package.json b/packages/language-plugin-pug/package.json index 3e7ce98045..941ccbb2d9 100644 --- a/packages/language-plugin-pug/package.json +++ b/packages/language-plugin-pug/package.json @@ -17,6 +17,6 @@ }, "dependencies": { "@volar/source-map": "~2.1.2", - "volar-service-pug": "0.0.31" + "volar-service-pug": "0.0.34" } } diff --git a/packages/language-server/lib/types.ts b/packages/language-server/lib/types.ts index 664da3891a..7d1ca802bd 100644 --- a/packages/language-server/lib/types.ts +++ b/packages/language-server/lib/types.ts @@ -3,8 +3,9 @@ import type { InitializationOptions } from "@volar/language-server"; export type VueInitializationOptions = InitializationOptions & { typescript: { tsdk: string; - } - vue?: { + }; + vue: { + hybridMode: boolean; /** * @example ['vue1', 'vue2'] */ diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index 3bae46ac43..e93fb92a8f 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -1,5 +1,5 @@ import type { Connection } from '@volar/language-server'; -import { createConnection, createServer, createSimpleProjectProviderFactory, loadTsdkByPath } from '@volar/language-server/node'; +import { createConnection, createServer, createSimpleProjectProviderFactory, createTypeScriptProjectProviderFactory, loadTsdkByPath } from '@volar/language-server/node'; import { ParsedCommandLine, VueCompilerOptions, createParsedCommandLine, createVueLanguagePlugin, parse, resolveVueCompilerOptions } from '@vue/language-core'; import { ServiceEnvironment, convertAttrName, convertTagName, createVueServicePlugins, detect } from '@vue/language-service'; import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest } from './lib/protocol'; @@ -33,15 +33,22 @@ connection.onInitialize(async params => { const result = await server.initialize( params, - createSimpleProjectProviderFactory(), + options.vue.hybridMode + ? createSimpleProjectProviderFactory() + : createTypeScriptProjectProviderFactory(tsdk.typescript, tsdk.diagnosticMessages), { watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions], getServicePlugins() { - return createVueServicePlugins(tsdk.typescript, env => envToVueOptions.get(env)!, tsPluginClient); + return createVueServicePlugins(tsdk.typescript, env => envToVueOptions.get(env)!, options.vue.hybridMode, tsPluginClient); }, async getLanguagePlugins(serviceEnv, projectContext) { - const [commandLine, vueOptions] = await parseCommandLine(); - const resolvedVueOptions = resolveVueCompilerOptions(vueOptions); + const commandLine = await parseCommandLine(); + const vueOptions = commandLine?.vueOptions ?? resolveVueCompilerOptions({}); + for (const ext of vueFileExtensions) { + if (vueOptions.extensions.includes(`.${ext}`)) { + vueOptions.extensions.push(`.${ext}`); + } + } const vueLanguagePlugin = createVueLanguagePlugin( tsdk.typescript, serviceEnv.typescript!.uriToFileName, @@ -60,18 +67,17 @@ connection.onInitialize(async params => { } }, commandLine?.options ?? {}, - resolvedVueOptions, + vueOptions, options.codegenStack, ); - envToVueOptions.set(serviceEnv, resolvedVueOptions); + envToVueOptions.set(serviceEnv, vueOptions); return [vueLanguagePlugin]; async function parseCommandLine() { let commandLine: ParsedCommandLine | undefined; - let vueOptions: Partial = {}; if (projectContext.typescript) { @@ -89,16 +95,7 @@ connection.onInitialize(async params => { } } - if (commandLine) { - vueOptions = commandLine.vueOptions; - } - vueOptions.extensions = [ - ...vueOptions.extensions ?? ['.vue'], - ...vueFileExtensions.map(ext => '.' + ext), - ]; - vueOptions.extensions = [...new Set(vueOptions.extensions)]; - - return [commandLine, vueOptions] as const; + return commandLine; } }, }, diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 1a6ef91412..5e90ad346f 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -6,11 +6,14 @@ export * from './lib/types'; import type { ServiceEnvironment, ServicePlugin } from '@volar/language-service'; import type { VueCompilerOptions } from './lib/types'; +import { decorateLanguageServiceForVue } from '@vue/typescript-plugin/lib/common'; import { create as createEmmetServicePlugin } from 'volar-service-emmet'; import { create as createJsonServicePlugin } from 'volar-service-json'; import { create as createPugFormatServicePlugin } from 'volar-service-pug-beautify'; import { create as createTypeScriptServicePlugin } from 'volar-service-typescript'; import { create as createTypeScriptTwoslashQueriesServicePlugin } from 'volar-service-typescript-twoslash-queries'; +import { create as createTypeScriptDocCommentTemplateServicePlugin } from 'volar-service-typescript/lib/plugins/docCommentTemplate'; +import { create as createTypeScriptSyntacticServicePlugin } from 'volar-service-typescript/lib/plugins/syntactic'; import { create as createCssServicePlugin } from './lib/plugins/css'; import { create as createVueAutoDotValueServicePlugin } from './lib/plugins/vue-autoinsert-dotvalue'; import { create as createVueAutoWrapParenthesesServicePlugin } from './lib/plugins/vue-autoinsert-parentheses'; @@ -28,11 +31,11 @@ import { create as createVueVisualizeHiddenCallbackParamServicePlugin } from './ export function createVueServicePlugins( ts: typeof import('typescript'), getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, + hybridMode = true, tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), ): ServicePlugin[] { - return [ - createTypeScriptServicePlugin(ts), - createTypeScriptTwoslashQueriesServicePlugin(), + const plugins: ServicePlugin[] = [ + createTypeScriptTwoslashQueriesServicePlugin(ts), createCssServicePlugin(), createPugFormatServicePlugin(), createJsonServicePlugin(), @@ -51,4 +54,27 @@ export function createVueServicePlugins( createVueToggleVBindServicePlugin(ts), createEmmetServicePlugin(), ]; + if (!hybridMode) { + plugins.push(...createTypeScriptServicePlugin(ts)); + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.name === 'typescript-semantic') { + plugins[i] = { + ...plugin, + create(context) { + const created = plugin.create(context); + const languageService = (created.provide as import('volar-service-typescript').Provide)['typescript/languageService'](); + const vueOptions = getVueOptions(context.env); + decorateLanguageServiceForVue(context.language.files, languageService, vueOptions, ts); + return created; + }, + }; + } + } + } + else { + plugins.push(createTypeScriptSyntacticServicePlugin(ts)); + plugins.push(createTypeScriptDocCommentTemplateServicePlugin(ts)); + } + return plugins; } diff --git a/packages/language-service/lib/plugins/vue-sfc.ts b/packages/language-service/lib/plugins/vue-sfc.ts index f7b62ac781..5ac0f71674 100644 --- a/packages/language-service/lib/plugins/vue-sfc.ts +++ b/packages/language-service/lib/plugins/vue-sfc.ts @@ -16,17 +16,16 @@ export function create(): ServicePlugin { return { name: 'vue-sfc', create(context): ServicePluginInstance { - const htmlPlugin = createHtmlService({ documentSelector: ['vue'], - useCustomDataProviders: false, + useDefaultDataProvider: false, + getCustomData(context) { + sfcDataProvider ??= html.newHTMLDataProvider('vue', loadLanguageBlocks(context.env.locale ?? 'en')); + return [sfcDataProvider]; + }, }).create(context); const htmlLanguageService: html.LanguageService = htmlPlugin.provide['html/languageService'](); - sfcDataProvider ??= html.newHTMLDataProvider('vue', loadLanguageBlocks(context.env.locale ?? 'en')); - - htmlLanguageService.setDataProviders(false, [sfcDataProvider]); - return { ...htmlPlugin, diff --git a/packages/language-service/package.json b/packages/language-service/package.json index ba1784f6ae..4b78a4f791 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -21,17 +21,18 @@ "@volar/typescript": "~2.1.2", "@vue/compiler-dom": "^3.4.0", "@vue/language-core": "2.0.6", + "@vue/typescript-plugin": "2.0.6", "@vue/shared": "^3.4.0", "computeds": "^0.0.1", "path-browserify": "^1.0.1", - "volar-service-css": "0.0.31", - "volar-service-emmet": "0.0.31", - "volar-service-html": "0.0.31", - "volar-service-json": "0.0.31", - "volar-service-pug": "0.0.31", - "volar-service-pug-beautify": "0.0.31", - "volar-service-typescript": "0.0.31-patch.1", - "volar-service-typescript-twoslash-queries": "0.0.31", + "volar-service-css": "0.0.34", + "volar-service-emmet": "0.0.34", + "volar-service-html": "0.0.34", + "volar-service-json": "0.0.34", + "volar-service-pug": "0.0.34", + "volar-service-pug-beautify": "0.0.34", + "volar-service-typescript": "0.0.34", + "volar-service-typescript-twoslash-queries": "0.0.34", "vscode-html-languageservice": "^5.1.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" diff --git a/packages/language-service/tests/complete.ts b/packages/language-service/tests/complete.ts index 41c20c95c5..27d80999e3 100644 --- a/packages/language-service/tests/complete.ts +++ b/packages/language-service/tests/complete.ts @@ -11,7 +11,7 @@ const normalizeNewline = (text: string) => text.replace(/\r\n/g, '\n'); for (const dirName of testDirs) { - describe.skipIf(dirName === 'core#8811' || dirName === '#2511' || dirName === 'component-auto-import')(`complete: ${dirName}`, async () => { + describe(`complete: ${dirName}`, async () => { const dir = path.join(baseDir, dirName); const inputFiles = readFiles(path.join(dir, 'input')); diff --git a/packages/language-service/tests/inlayHint.ts b/packages/language-service/tests/inlayHint.ts index 1b6bf9ca09..4d760eded6 100644 --- a/packages/language-service/tests/inlayHint.ts +++ b/packages/language-service/tests/inlayHint.ts @@ -10,7 +10,7 @@ const testDirs = fs.readdirSync(baseDir); for (const dirName of testDirs) { - describe.skipIf(dirName === 'missing-props')(`inlay hint: ${dirName}`, async () => { + describe(`inlay hint: ${dirName}`, async () => { const dir = path.join(baseDir, dirName); const inputFiles = readFiles(dir); diff --git a/packages/language-service/tests/utils/createTester.ts b/packages/language-service/tests/utils/createTester.ts index 07fc9ab6fe..4ea9ac6391 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -3,7 +3,7 @@ import { createLanguage } from '@volar/typescript'; import * as path from 'path'; import type * as ts from 'typescript'; import { URI } from 'vscode-uri'; -import { createParsedCommandLine, createVueLanguagePlugin, createVueServicePlugins, resolveVueCompilerOptions } from '../..'; +import { createParsedCommandLine, createVueLanguagePlugin, createVueServicePlugins } from '../..'; import { createMockServiceEnv } from './mockEnv'; export const rootUri = URI.file(path.resolve(__dirname, '../../../../test-workspace/language-service')).toString(); @@ -26,7 +26,6 @@ function createTester(rootUri: string) { getScriptSnapshot, getLanguageId: resolveCommonLanguageId, }; - const resolvedVueOptions = resolveVueCompilerOptions(parsedCommandLine.vueOptions); const vueLanguagePlugin = createVueLanguagePlugin( ts, serviceEnv.typescript!.uriToFileName, @@ -45,9 +44,9 @@ function createTester(rootUri: string) { } }, parsedCommandLine.options, - resolvedVueOptions, + parsedCommandLine.vueOptions, ); - const vueServicePlugins = createVueServicePlugins(ts, () => resolvedVueOptions); + const vueServicePlugins = createVueServicePlugins(ts, () => parsedCommandLine.vueOptions, false); const defaultVSCodeSettings: any = { 'typescript.preferences.quoteStyle': 'single', 'javascript.preferences.quoteStyle': 'single', diff --git a/packages/tsc/index.ts b/packages/tsc/index.ts index 3b498fc9e6..55db48d7e0 100644 --- a/packages/tsc/index.ts +++ b/packages/tsc/index.ts @@ -16,26 +16,24 @@ export function run() { const { configFilePath } = options.options; const vueOptions = typeof configFilePath === 'string' ? vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions - : {}; - const resolvedVueOptions = vue.resolveVueCompilerOptions(vueOptions); - const { extensions } = resolvedVueOptions; + : vue.resolveVueCompilerOptions({}); const fakeGlobalTypesHolder = createFakeGlobalTypesHolder(options); if ( - runExtensions.length === extensions.length - && runExtensions.every(ext => extensions.includes(ext)) + runExtensions.length === vueOptions.extensions.length + && runExtensions.every(ext => vueOptions.extensions.includes(ext)) ) { const vueLanguagePlugin = vue.createVueLanguagePlugin( ts, id => id, fileName => fileName === fakeGlobalTypesHolder, options.options, - resolvedVueOptions, + vueOptions, false, ); return [vueLanguagePlugin]; } else { - runExtensions = extensions; + runExtensions = vueOptions.extensions; throw extensionsChangedException; } }, diff --git a/packages/tsc/tests/dts.spec.ts b/packages/tsc/tests/dts.spec.ts index 3cf31c02e7..caee8a4a23 100644 --- a/packages/tsc/tests/dts.spec.ts +++ b/packages/tsc/tests/dts.spec.ts @@ -29,13 +29,13 @@ describe('vue-tsc-dts', () => { const { configFilePath } = options.options; const vueOptions = typeof configFilePath === 'string' ? vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions - : {}; + : vue.resolveVueCompilerOptions({}); const vueLanguagePlugin = vue.createVueLanguagePlugin( ts, id => id, fileName => fileName === fakeGlobalTypesHolder, options.options, - vue.resolveVueCompilerOptions(vueOptions), + vueOptions, false, ); return [vueLanguagePlugin]; diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index d4a846e5c1..ed4c3ec0f7 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -1,12 +1,11 @@ -import type * as ts from 'typescript'; import { decorateLanguageService } from '@volar/typescript/lib/node/decorateLanguageService'; import { decorateLanguageServiceHost, searchExternalFiles } from '@volar/typescript/lib/node/decorateLanguageServiceHost'; -import { createFileRegistry, resolveCommonLanguageId } from '@vue/language-core'; -import { projects } from './lib/utils'; import * as vue from '@vue/language-core'; +import { createFileRegistry, resolveCommonLanguageId } from '@vue/language-core'; +import type * as ts from 'typescript'; +import { decorateLanguageServiceForVue } from './lib/common'; import { startNamedPipeServer } from './lib/server'; -import { _getComponentNames } from './lib/requests/componentInfos'; -import { capitalize } from '@vue/shared'; +import { projects } from './lib/utils'; const windowsPathReg = /\\/g; const externalFiles = new WeakMap>(); @@ -28,7 +27,7 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory { decoratedLanguageServices.add(info.languageService); decoratedLanguageServiceHosts.add(info.languageServiceHost); - const vueOptions = vue.resolveVueCompilerOptions(getVueCompilerOptions()); + const vueOptions = getVueCompilerOptions(); const languagePlugin = vue.createVueLanguagePlugin( ts, id => id, @@ -70,138 +69,12 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory { ); projectExternalFileExtensions.set(info.project, extensions); - projects.set(info.project, { - info, - files, - ts, - vueOptions, - }); + projects.set(info.project, { info, files, ts, vueOptions }); decorateLanguageService(files, info.languageService); + decorateLanguageServiceForVue(files, info.languageService, vueOptions, ts); decorateLanguageServiceHost(files, info.languageServiceHost, ts); startNamedPipeServer(info.project.projectKind, info.project.getCurrentDirectory()); - - const getCompletionsAtPosition = info.languageService.getCompletionsAtPosition; - const getCompletionEntryDetails = info.languageService.getCompletionEntryDetails; - const getCodeFixesAtPosition = info.languageService.getCodeFixesAtPosition; - const getEncodedSemanticClassifications = info.languageService.getEncodedSemanticClassifications; - - info.languageService.getCompletionsAtPosition = (fileName, position, options) => { - const result = getCompletionsAtPosition(fileName, position, options); - if (result) { - // filter __VLS_ - result.entries = result.entries.filter( - entry => entry.name.indexOf('__VLS_') === -1 - && (!entry.labelDetails?.description || entry.labelDetails.description.indexOf('__VLS_') === -1) - ); - // modify label - for (const item of result.entries) { - if (item.source) { - const originalName = item.name; - for (const ext of vueOptions.extensions) { - const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue - if (item.source.endsWith(ext) && item.name.endsWith(suffix)) { - item.name = item.name.slice(0, -suffix.length); - if (item.insertText) { - // #2286 - item.insertText = item.insertText.replace(`${suffix}$1`, '$1'); - } - if (item.data) { - // @ts-expect-error - item.data.__isComponentAutoImport = { - ext, - suffix, - originalName, - newName: item.insertText, - }; - } - break; - } - } - } - } - } - return result; - }; - info.languageService.getCompletionEntryDetails = (...args) => { - const details = getCompletionEntryDetails(...args); - // modify import statement - // @ts-expect-error - if (args[6]?.__isComponentAutoImport) { - // @ts-expect-error - const { ext, suffix, originalName, newName } = args[6]?.__isComponentAutoImport; - for (const codeAction of details?.codeActions ?? []) { - for (const change of codeAction.changes) { - for (const textChange of change.textChanges) { - textChange.newText = textChange.newText.replace('import ' + originalName + ' from ', 'import ' + newName + ' from '); - } - } - } - } - return details; - }; - info.languageService.getCodeFixesAtPosition = (...args) => { - let result = getCodeFixesAtPosition(...args); - // filter __VLS_ - result = result.filter(entry => entry.description.indexOf('__VLS_') === -1); - return result; - }; - info.languageService.getEncodedSemanticClassifications = (fileName, span, format) => { - const result = getEncodedSemanticClassifications(fileName, span, format); - const file = files.get(fileName); - if ( - file?.generated?.code instanceof vue.VueGeneratedCode - && file.generated.code.sfc.template - ) { - const validComponentNames = _getComponentNames(ts, info.languageService, file.generated.code, vueOptions); - const components = new Set([ - ...validComponentNames, - ...validComponentNames.map(vue.hyphenateTag), - ]); - const { template } = file.generated.code.sfc; - const spanTemplateRange = [ - span.start - template.startTagEnd, - span.start + span.length - template.startTagEnd, - ] as const; - template.ast?.children.forEach(function visit(node) { - if (node.loc.end.offset <= spanTemplateRange[0] || node.loc.start.offset >= spanTemplateRange[1]) { - return; - } - if (node.type === 1 satisfies vue.CompilerDOM.NodeTypes.ELEMENT) { - if (components.has(node.tag)) { - result.spans.push( - node.loc.start.offset + node.loc.source.indexOf(node.tag) + template.startTagEnd, - node.tag.length, - 256, // class - ); - if (template.lang === 'html' && !node.isSelfClosing) { - result.spans.push( - node.loc.start.offset + node.loc.source.lastIndexOf(node.tag) + template.startTagEnd, - node.tag.length, - 256, // class - ); - } - } - for (const child of node.children) { - visit(child); - } - } - else if (node.type === 9 satisfies vue.CompilerDOM.NodeTypes.IF) { - for (const branch of node.branches) { - for (const child of branch.children) { - visit(child); - } - } - } - else if (node.type === 11 satisfies vue.CompilerDOM.NodeTypes.FOR) { - for (const child of node.children) { - visit(child); - } - } - }); - } - return result; - }; } return info.languageService; diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts new file mode 100644 index 0000000000..1f9ca7e059 --- /dev/null +++ b/packages/typescript-plugin/lib/common.ts @@ -0,0 +1,134 @@ +import * as vue from '@vue/language-core'; +import type * as ts from 'typescript'; +import { capitalize } from '@vue/shared'; +import { _getComponentNames } from './requests/componentInfos'; + +export function decorateLanguageServiceForVue( + files: vue.FileRegistry, + languageService: ts.LanguageService, + vueOptions: vue.VueCompilerOptions, + ts: typeof import('typescript'), +) { + + const getCompletionsAtPosition = languageService.getCompletionsAtPosition; + const getCompletionEntryDetails = languageService.getCompletionEntryDetails; + const getCodeFixesAtPosition = languageService.getCodeFixesAtPosition; + const getEncodedSemanticClassifications = languageService.getEncodedSemanticClassifications; + + languageService.getCompletionsAtPosition = (fileName, position, options) => { + const result = getCompletionsAtPosition(fileName, position, options); + if (result) { + // filter __VLS_ + result.entries = result.entries.filter( + entry => entry.name.indexOf('__VLS_') === -1 + && (!entry.labelDetails?.description || entry.labelDetails.description.indexOf('__VLS_') === -1) + ); + // modify label + for (const item of result.entries) { + if (item.source) { + const originalName = item.name; + for (const ext of vueOptions.extensions) { + const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue + if (item.source.endsWith(ext) && item.name.endsWith(suffix)) { + item.name = item.name.slice(0, -suffix.length); + if (item.insertText) { + // #2286 + item.insertText = item.insertText.replace(`${suffix}$1`, '$1'); + } + if (item.data) { + // @ts-expect-error + item.data.__isComponentAutoImport = { + ext, + suffix, + originalName, + newName: item.insertText, + }; + } + break; + } + } + } + } + } + return result; + }; + languageService.getCompletionEntryDetails = (...args) => { + const details = getCompletionEntryDetails(...args); + // modify import statement + // @ts-expect-error + if (args[6]?.__isComponentAutoImport) { + // @ts-expect-error + const { ext, suffix, originalName, newName } = args[6]?.__isComponentAutoImport; + for (const codeAction of details?.codeActions ?? []) { + for (const change of codeAction.changes) { + for (const textChange of change.textChanges) { + textChange.newText = textChange.newText.replace('import ' + originalName + ' from ', 'import ' + newName + ' from '); + } + } + } + } + return details; + }; + languageService.getCodeFixesAtPosition = (...args) => { + let result = getCodeFixesAtPosition(...args); + // filter __VLS_ + result = result.filter(entry => entry.description.indexOf('__VLS_') === -1); + return result; + }; + languageService.getEncodedSemanticClassifications = (fileName, span, format) => { + const result = getEncodedSemanticClassifications(fileName, span, format); + const file = files.get(fileName); + if ( + file?.generated?.code instanceof vue.VueGeneratedCode + && file.generated.code.sfc.template + ) { + const validComponentNames = _getComponentNames(ts, languageService, file.generated.code, vueOptions); + const components = new Set([ + ...validComponentNames, + ...validComponentNames.map(vue.hyphenateTag), + ]); + const { template } = file.generated.code.sfc; + const spanTemplateRange = [ + span.start - template.startTagEnd, + span.start + span.length - template.startTagEnd, + ] as const; + template.ast?.children.forEach(function visit(node) { + if (node.loc.end.offset <= spanTemplateRange[0] || node.loc.start.offset >= spanTemplateRange[1]) { + return; + } + if (node.type === 1 satisfies vue.CompilerDOM.NodeTypes.ELEMENT) { + if (components.has(node.tag)) { + result.spans.push( + node.loc.start.offset + node.loc.source.indexOf(node.tag) + template.startTagEnd, + node.tag.length, + 256, // class + ); + if (template.lang === 'html' && !node.isSelfClosing) { + result.spans.push( + node.loc.start.offset + node.loc.source.lastIndexOf(node.tag) + template.startTagEnd, + node.tag.length, + 256, // class + ); + } + } + for (const child of node.children) { + visit(child); + } + } + else if (node.type === 9 satisfies vue.CompilerDOM.NodeTypes.IF) { + for (const branch of node.branches) { + for (const child of branch.children) { + visit(child); + } + } + } + else if (node.type === 11 satisfies vue.CompilerDOM.NodeTypes.FOR) { + for (const child of node.children) { + visit(child); + } + } + }); + } + return result; + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50f6b40d90..26f3cbb936 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,8 +145,8 @@ importers: specifier: ~2.1.2 version: 2.1.2 volar-service-pug: - specifier: 0.0.31 - version: 0.0.31 + specifier: 0.0.34 + version: 0.0.34 devDependencies: '@types/node': specifier: latest @@ -196,6 +196,9 @@ importers: '@vue/shared': specifier: ^3.4.0 version: 3.4.20 + '@vue/typescript-plugin': + specifier: 2.0.6 + version: link:../typescript-plugin computeds: specifier: ^0.0.1 version: 0.0.1 @@ -203,29 +206,29 @@ importers: specifier: ^1.0.1 version: 1.0.1 volar-service-css: - specifier: 0.0.31 - version: 0.0.31(@volar/language-service@2.1.2) + specifier: 0.0.34 + version: 0.0.34(@volar/language-service@2.1.2) volar-service-emmet: - specifier: 0.0.31 - version: 0.0.31(@volar/language-service@2.1.2) + specifier: 0.0.34 + version: 0.0.34(@volar/language-service@2.1.2) volar-service-html: - specifier: 0.0.31 - version: 0.0.31(@volar/language-service@2.1.2) + specifier: 0.0.34 + version: 0.0.34(@volar/language-service@2.1.2) volar-service-json: - specifier: 0.0.31 - version: 0.0.31(@volar/language-service@2.1.2) + specifier: 0.0.34 + version: 0.0.34(@volar/language-service@2.1.2) volar-service-pug: - specifier: 0.0.31 - version: 0.0.31 + specifier: 0.0.34 + version: 0.0.34 volar-service-pug-beautify: - specifier: 0.0.31 - version: 0.0.31(@volar/language-service@2.1.2) + specifier: 0.0.34 + version: 0.0.34(@volar/language-service@2.1.2) volar-service-typescript: - specifier: 0.0.31-patch.1 - version: 0.0.31-patch.1(@volar/language-service@2.1.2)(@volar/typescript@2.1.2) + specifier: 0.0.34 + version: 0.0.34(@volar/language-service@2.1.2) volar-service-typescript-twoslash-queries: - specifier: 0.0.31 - version: 0.0.31(@volar/language-service@2.1.2) + specifier: 0.0.34 + version: 0.0.34(@volar/language-service@2.1.2) vscode-html-languageservice: specifier: ^5.1.0 version: 5.1.2 @@ -245,9 +248,6 @@ importers: '@volar/kit': specifier: ~2.1.2 version: 2.1.2(typescript@5.4.2) - '@vue/typescript-plugin': - specifier: 2.0.6 - version: link:../typescript-plugin vscode-languageserver-protocol: specifier: ^3.17.5 version: 3.17.5 @@ -5227,8 +5227,8 @@ packages: - terser dev: true - /volar-service-css@0.0.31(@volar/language-service@2.1.2): - resolution: {integrity: sha512-YDY+qwqYipkXVwh63f9Lk7x/48j9lsxVeXj9lsj5Fp1VAwpPoVpWQhAq3oNp3my9gyS8lEbdIPl0rJzBcJCuUA==} + /volar-service-css@0.0.34(@volar/language-service@2.1.2): + resolution: {integrity: sha512-C7ua0j80ZD7bsgALAz/cA1bykPehoIa5n+3+Ccr+YLpj0fypqw9iLUmGLX11CqzqNCO2XFGe/1eXB/c+SWrF/g==} peerDependencies: '@volar/language-service': ~2.1.0 peerDependenciesMeta: @@ -5241,8 +5241,8 @@ packages: vscode-uri: 3.0.8 dev: false - /volar-service-emmet@0.0.31(@volar/language-service@2.1.2): - resolution: {integrity: sha512-d+KfC0axTB6Ku4v70So3GEqsEzrE9zifDvwnqHUrg+Bts05kCFlRgDCLziXmddKhtaaJJ6oSizHr7WcFUyesww==} + /volar-service-emmet@0.0.34(@volar/language-service@2.1.2): + resolution: {integrity: sha512-ubQvMCmHPp8Ic82LMPkgrp9ot+u2p/RDd0RyT0EykRkZpWsagHUF5HWkVheLfiMyx2rFuWx/+7qZPOgypx6h6g==} peerDependencies: '@volar/language-service': ~2.1.0 peerDependenciesMeta: @@ -5254,8 +5254,8 @@ packages: vscode-html-languageservice: 5.1.2 dev: false - /volar-service-html@0.0.31(@volar/language-service@2.1.2): - resolution: {integrity: sha512-duMjl/VLvPWtmYsIAUtwYw/esFY3FWnVmH7537UpnfY9ncYTX/G43xmoVd+oQJPWh7xi8zwFeUQgZAA6T45Bhg==} + /volar-service-html@0.0.34(@volar/language-service@2.1.2): + resolution: {integrity: sha512-kMEneea1tQbiRcyKavqdrSVt8zV06t+0/3pGkjO3gV6sikXTNShIDkdtB4Tq9vE2cQdM50TuS7utVV7iysUxHw==} peerDependencies: '@volar/language-service': ~2.1.0 peerDependenciesMeta: @@ -5268,8 +5268,8 @@ packages: vscode-uri: 3.0.8 dev: false - /volar-service-json@0.0.31(@volar/language-service@2.1.2): - resolution: {integrity: sha512-LdADOPbO1+toDP/0oG6plOnzE34tA8oB/aJqdOJFv8OIyMtxn0kCprtyhzVWLMCpz3TgpkBSiAI3BuMMYXcDlQ==} + /volar-service-json@0.0.34(@volar/language-service@2.1.2): + resolution: {integrity: sha512-ZK5DUL9Tod8mv3YnplKbNt5+dAL52JvKDVqMVuB2lbCaR/anGd1uGh4rzEf7fXxE0olvbDOXVDDiZR1rKuTbaA==} peerDependencies: '@volar/language-service': ~2.1.0 peerDependenciesMeta: @@ -5281,8 +5281,8 @@ packages: vscode-uri: 3.0.8 dev: false - /volar-service-pug-beautify@0.0.31(@volar/language-service@2.1.2): - resolution: {integrity: sha512-Y1Dhiipn/+2GNYFxgToSS4DGxDE7rAU5S9rkbleASCksAKFFWknxLF0aBmcvhnDqcVHyvIjoeIqGtQw2xx3wrw==} + /volar-service-pug-beautify@0.0.34(@volar/language-service@2.1.2): + resolution: {integrity: sha512-1fuZG3EEFOHofgrY2IdcPR1tI2UvBPKqQP1LxeV0ma5EUAVN6yayd0JU3dDBd0zolgLV0JFv5GZP2z2Xlpj4mw==} peerDependencies: '@volar/language-service': ~2.1.0 peerDependenciesMeta: @@ -5293,19 +5293,19 @@ packages: '@volar/language-service': 2.1.2 dev: false - /volar-service-pug@0.0.31: - resolution: {integrity: sha512-hnzdMb9lq74FgKy3LI3nNW4SARWbPy+FwMr6VLaII0R8F3IOvx5w+2nJSzboivPDJ0F5xHASPTWO53G5mXK+vQ==} + /volar-service-pug@0.0.34: + resolution: {integrity: sha512-h0DSnQXkvweXKaBmCYJaDbmmsatp9KIxsTxZD0SVKFyVixHSUjrVJP6eu9o3pGuDNIy2135XBNryUP/Lv7/3oA==} dependencies: '@volar/language-service': 2.1.2 pug-lexer: 5.0.1 pug-parser: 6.0.0 - volar-service-html: 0.0.31(@volar/language-service@2.1.2) + volar-service-html: 0.0.34(@volar/language-service@2.1.2) vscode-html-languageservice: 5.1.2 vscode-languageserver-textdocument: 1.0.11 dev: false - /volar-service-typescript-twoslash-queries@0.0.31(@volar/language-service@2.1.2): - resolution: {integrity: sha512-NsI1izFST7H6GN7WQow/GEPykPLGt0zlIJl+05bX9W6pXY8kD6PUSz7U+v5TSbUMMmjFFn8IkAAHopbH11OWrA==} + /volar-service-typescript-twoslash-queries@0.0.34(@volar/language-service@2.1.2): + resolution: {integrity: sha512-XAY2YtWKUp6ht89gxt3L5Dr46LU45d/VlBkj1KXUwNlinpoWiGN4Nm3B6DRF3VoBThAnQgm4c7WD0S+5yTzh+w==} peerDependencies: '@volar/language-service': ~2.1.0 peerDependenciesMeta: @@ -5315,17 +5315,15 @@ packages: '@volar/language-service': 2.1.2 dev: false - /volar-service-typescript@0.0.31-patch.1(@volar/language-service@2.1.2)(@volar/typescript@2.1.2): - resolution: {integrity: sha512-q9Dv9lg3fyLopMgXll4Xal862YLVHw4PShFcllHqIQXUMiPzQndZ7dA7B/3OldVFYeJLWP44w/M+90tjdxtl7w==} + /volar-service-typescript@0.0.34(@volar/language-service@2.1.2): + resolution: {integrity: sha512-NbAry0w8ZXFgGsflvMwmPDCzgJGx3C+eYxFEbldaumkpTAJiywECWiUbPIOfmEHgpOllUKSnhwtLlWFK4YnfQg==} peerDependencies: '@volar/language-service': ~2.1.0 - '@volar/typescript': ~2.1.0 peerDependenciesMeta: '@volar/language-service': optional: true dependencies: '@volar/language-service': 2.1.2 - '@volar/typescript': 2.1.2 path-browserify: 1.0.1 semver: 7.6.0 typescript-auto-import-cache: 0.3.2