From 24eb2bf730498c24a117384874daef7455f81059 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 00:45:58 +0800 Subject: [PATCH 01/10] wip --- extensions/vscode/package.json | 4 + extensions/vscode/src/common.ts | 1 + extensions/vscode/src/config.ts | 1 + packages/component-meta/lib/base.ts | 5 +- packages/language-core/lib/plugins.ts | 6 +- packages/language-core/lib/utils/ts.ts | 16 +- packages/language-plugin-pug/package.json | 2 +- packages/language-server/lib/types.ts | 5 +- packages/language-server/node.ts | 33 ++-- packages/language-service/index.ts | 32 +++- .../language-service/lib/plugins/vue-sfc.ts | 11 +- packages/language-service/package.json | 17 ++- packages/language-service/tests/complete.ts | 2 +- packages/language-service/tests/inlayHint.ts | 2 +- .../tests/utils/createTester.ts | 7 +- packages/tsc/index.ts | 12 +- packages/tsc/tests/dts.spec.ts | 4 +- packages/typescript-plugin/index.ts | 141 +----------------- packages/typescript-plugin/lib/common.ts | 134 +++++++++++++++++ pnpm-lock.yaml | 78 +++++----- 20 files changed, 275 insertions(+), 238 deletions(-) create mode 100644 packages/typescript-plugin/lib/common.ts 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 From b16d5562e681584a14895e713521db7c2f1b0144 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 04:43:56 +0800 Subject: [PATCH 02/10] wip [skip ci] --- extensions/vscode/src/features/doctor.ts | 65 ++++----- packages/language-server/node.ts | 15 +- packages/language-service/index.ts | 108 ++++++++++---- .../lib/plugins/vue-autoinsert-dotvalue.ts | 5 +- .../lib/plugins/vue-extract-file.ts | 5 +- .../lib/plugins/vue-template.ts | 57 ++++++-- .../lib/plugins/vue-twoslash-queries.ts | 5 +- .../tests/utils/createTester.ts | 5 +- packages/typescript-plugin/index.ts | 9 +- packages/typescript-plugin/lib/client.ts | 64 +-------- packages/typescript-plugin/lib/common.ts | 133 +++++++++++------- .../lib/requests/collectExtractProps.ts | 22 +-- .../lib/requests/componentInfos.ts | 102 ++++++++------ .../lib/requests/containsFile.ts | 5 - .../lib/requests/getPropertiesAtLocation.ts | 23 +-- .../lib/requests/getQuickInfoAtPosition.ts | 21 ++- packages/typescript-plugin/lib/server.ts | 113 +++++++++------ packages/typescript-plugin/lib/utils.ts | 75 +++++++--- 18 files changed, 482 insertions(+), 350 deletions(-) delete mode 100644 packages/typescript-plugin/lib/requests/containsFile.ts diff --git a/extensions/vscode/src/features/doctor.ts b/extensions/vscode/src/features/doctor.ts index 9008942fcc..1cab87c456 100644 --- a/extensions/vscode/src/features/doctor.ts +++ b/extensions/vscode/src/features/doctor.ts @@ -134,30 +134,6 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan }); } - // check should use @volar-plugins/vetur instead of vetur - const vetur = vscode.extensions.getExtension('octref.vetur'); - if (vetur?.isActive) { - problems.push({ - title: 'Use volar-service-vetur instead of Vetur', - message: 'Detected Vetur enabled. Consider disabling Vetur and use [volar-service-vetur](https://github.com/volarjs/services/tree/master/packages/vetur) instead.', - }); - } - - // #3942, https://github.com/microsoft/TypeScript/issues/57633 - for (const extId of ['svelte.svelte-vscode', 'styled-components.vscode-styled-components']) { - const ext = vscode.extensions.getExtension(extId); - if (ext) { - problems.push({ - title: `Recommended to disable "${ext.packageJSON.displayName || extId}" in Vue workspace`, - message: [ - `This extension's TypeScript Plugin and Vue's TypeScript Plugin are known to cause some conflicts. Until the problem is resolved, it is recommended that you temporarily disable the this extension in the Vue workspace.`, - '', - 'Issues: https://github.com/vuejs/language-tools/issues/3942, https://github.com/microsoft/TypeScript/issues/57633', - ].join('\n'), - }); - } - } - // check using pug but don't install @vue/language-plugin-pug if ( sfc?.descriptor.template?.lang === 'pug' @@ -236,18 +212,35 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan }); } - // #3942 - const namedPipe = await client.sendRequest(GetConnectedNamedPipeServerRequest.type, fileUri.fsPath.replace(/\\/g, '/')); - if (namedPipe?.serverKind === 0) { - problems.push({ - title: 'Missing jsconfig/tsconfig', - message: [ - 'The current file does not have a matching tsconfig/jsconfig, and extension version 2.0 will not work properly for this at the moment.', - 'To avoid this problem, you can create a jsconfig in the project root, or downgrade to 1.8.27.', - '', - 'Issue: https://github.com/vuejs/language-tools/issues/3942', - ].join('\n'), - }); + if (config.server.hybridMode) { + // #3942 + const namedPipe = await client.sendRequest(GetConnectedNamedPipeServerRequest.type, fileUri.fsPath.replace(/\\/g, '/')); + if (namedPipe?.serverKind === 0) { + problems.push({ + title: 'Missing jsconfig/tsconfig', + message: [ + 'The current file does not have a matching tsconfig/jsconfig, and extension version 2.0 will not work properly for this at the moment.', + 'To avoid this problem, you can create a jsconfig in the project root, or downgrade to 1.8.27.', + '', + 'Issue: https://github.com/vuejs/language-tools/issues/3942', + ].join('\n'), + }); + } + + // #3942, https://github.com/microsoft/TypeScript/issues/57633 + for (const extId of ['svelte.svelte-vscode', 'styled-components.vscode-styled-components']) { + const ext = vscode.extensions.getExtension(extId); + if (ext) { + problems.push({ + title: `Recommended to disable "${ext.packageJSON.displayName || extId}" in Vue workspace`, + message: [ + `This extension's TypeScript Plugin and Vue's TypeScript Plugin are known to cause some conflicts. Until the problem is resolved, it is recommended that you temporarily disable the this extension in the Vue workspace.`, + '', + 'Issues: https://github.com/vuejs/language-tools/issues/3942, https://github.com/microsoft/TypeScript/issues/57633', + ].join('\n'), + }); + } + } } // check outdated vue language plugins diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index e93fb92a8f..6035e53168 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -5,6 +5,7 @@ import { ServiceEnvironment, convertAttrName, convertTagName, createVueServicePl import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest } from './lib/protocol'; import type { VueInitializationOptions } from './lib/types'; import * as tsPluginClient from '@vue/typescript-plugin/lib/client'; +import { searchNamedPipeServerForFile } from '@vue/typescript-plugin/lib/utils'; import { GetConnectedNamedPipeServerRequest } from './lib/protocol'; export const connection: Connection = createConnection(); @@ -39,7 +40,11 @@ connection.onInitialize(async params => { { watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions], getServicePlugins() { - return createVueServicePlugins(tsdk.typescript, env => envToVueOptions.get(env)!, options.vue.hybridMode, tsPluginClient); + return createVueServicePlugins( + tsdk.typescript, + env => envToVueOptions.get(env)!, + options.vue.hybridMode ? () => tsPluginClient : undefined, + ); }, async getLanguagePlugins(serviceEnv, projectContext) { const commandLine = await parseCommandLine(); @@ -101,8 +106,10 @@ connection.onInitialize(async params => { }, ); - // handle by tsserver + @vue/typescript-plugin - result.capabilities.semanticTokensProvider = undefined; + if (options.vue.hybridMode) { + // handle by tsserver + @vue/typescript-plugin + result.capabilities.semanticTokensProvider = undefined; + } return result; }); @@ -141,7 +148,7 @@ connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { }); connection.onRequest(GetConnectedNamedPipeServerRequest.type, async fileName => { - const server = await tsPluginClient.searchNamedPipeServerForFile(fileName); + const server = await searchNamedPipeServerForFile(fileName); if (server) { return server; } diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 5e90ad346f..ef4a049fd7 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -3,14 +3,13 @@ export * from '@vue/language-core'; export * from './lib/ideFeatures/nameCasing'; export * from './lib/types'; -import type { ServiceEnvironment, ServicePlugin } from '@volar/language-service'; +import type { ServiceContext, 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 createTypeScriptServicePlugins } 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'; @@ -28,34 +27,21 @@ import { create as createVueToggleVBindServicePlugin } from './lib/plugins/vue-t import { create as createVueTwoslashQueriesServicePlugin } from './lib/plugins/vue-twoslash-queries'; import { create as createVueVisualizeHiddenCallbackParamServicePlugin } from './lib/plugins/vue-visualize-hidden-callback-param'; +import { decorateLanguageServiceForVue } from '@vue/typescript-plugin/lib/common'; +import { collectExtractProps } from '@vue/typescript-plugin/lib/requests/collectExtractProps'; +import { getComponentEvents, getComponentNames, getComponentProps, getElementAttrs, getTemplateContextProps } from '@vue/typescript-plugin/lib/requests/componentInfos'; +import { getPropertiesAtLocation } from '@vue/typescript-plugin/lib/requests/getPropertiesAtLocation'; +import { getQuickInfoAtPosition } from '@vue/typescript-plugin/lib/requests/getQuickInfoAtPosition'; + export function createVueServicePlugins( ts: typeof import('typescript'), getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, - hybridMode = true, - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin[] { - const plugins: ServicePlugin[] = [ - createTypeScriptTwoslashQueriesServicePlugin(ts), - createCssServicePlugin(), - createPugFormatServicePlugin(), - createJsonServicePlugin(), - createVueTemplateServicePlugin('html', ts, getVueOptions, tsPluginClient), - createVueTemplateServicePlugin('pug', ts, getVueOptions, tsPluginClient), - createVueSfcServicePlugin(), - createVueTwoslashQueriesServicePlugin(ts, tsPluginClient), - createVueReferencesCodeLensServicePlugin(), - createVueDocumentDropServicePlugin(ts), - createVueAutoDotValueServicePlugin(ts, tsPluginClient), - createVueAutoWrapParenthesesServicePlugin(ts), - createVueAutoAddSpaceServicePlugin(), - createVueVisualizeHiddenCallbackParamServicePlugin(), - createVueDirectiveCommentsServicePlugin(), - createVueExtractFileServicePlugin(ts, tsPluginClient), - createVueToggleVBindServicePlugin(ts), - createEmmetServicePlugin(), - ]; + const plugins: ServicePlugin[] = []; + const hybridMode = !!getTsPluginClient; if (!hybridMode) { - plugins.push(...createTypeScriptServicePlugin(ts)); + plugins.push(...createTypeScriptServicePlugins(ts)); for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; if (plugin.name === 'typescript-semantic') { @@ -63,18 +49,82 @@ export function createVueServicePlugins( ...plugin, create(context) { const created = plugin.create(context); + if (!context.language.typescript) { + return created; + } const languageService = (created.provide as import('volar-service-typescript').Provide)['typescript/languageService'](); const vueOptions = getVueOptions(context.env); - decorateLanguageServiceForVue(context.language.files, languageService, vueOptions, ts); + decorateLanguageServiceForVue(context.language.files, languageService, vueOptions, ts, false); return created; }, }; + break; } } + getTsPluginClient = context => { + if (!context.language.typescript) { + return; + } + const requestContext = { + typescript: ts, + files: context.language.files, + languageService: context.inject<(import('volar-service-typescript').Provide), 'typescript/languageService'>('typescript/languageService'), + vueOptions: getVueOptions(context.env), + isTsPlugin: false, + }; + return { + async collectExtractProps(...args) { + return await collectExtractProps.apply(requestContext, args); + }, + async getPropertiesAtLocation(...args) { + return await getPropertiesAtLocation.apply(requestContext, args); + }, + async getComponentEvents(...args) { + return await getComponentEvents.apply(requestContext, args); + }, + async getComponentNames(...args) { + return await getComponentNames.apply(requestContext, args); + }, + async getComponentProps(...args) { + return await getComponentProps.apply(requestContext, args); + }, + async getElementAttrs(...args) { + return await getElementAttrs.apply(requestContext, args); + }, + async getTemplateContextProps(...args) { + return await getTemplateContextProps.apply(requestContext, args); + }, + async getQuickInfoAtPosition(...args) { + return await getQuickInfoAtPosition.apply(requestContext, args); + }, + }; + }; } else { - plugins.push(createTypeScriptSyntacticServicePlugin(ts)); - plugins.push(createTypeScriptDocCommentTemplateServicePlugin(ts)); + plugins.push( + createTypeScriptSyntacticServicePlugin(ts), + createTypeScriptDocCommentTemplateServicePlugin(ts), + ); } + plugins.push( + createTypeScriptTwoslashQueriesServicePlugin(ts), + createCssServicePlugin(), + createPugFormatServicePlugin(), + createJsonServicePlugin(), + createVueTemplateServicePlugin('html', ts, getVueOptions, getTsPluginClient), + createVueTemplateServicePlugin('pug', ts, getVueOptions, getTsPluginClient), + createVueSfcServicePlugin(), + createVueTwoslashQueriesServicePlugin(ts, getTsPluginClient), + createVueReferencesCodeLensServicePlugin(), + createVueDocumentDropServicePlugin(ts), + createVueAutoDotValueServicePlugin(ts, getTsPluginClient), + createVueAutoWrapParenthesesServicePlugin(ts), + createVueAutoAddSpaceServicePlugin(), + createVueVisualizeHiddenCallbackParamServicePlugin(), + createVueDirectiveCommentsServicePlugin(), + createVueExtractFileServicePlugin(ts, getTsPluginClient), + createVueToggleVBindServicePlugin(ts), + createEmmetServicePlugin(), + ); return plugins; } diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index 3bf3a917c0..c748388856 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -1,4 +1,4 @@ -import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +import type { ServiceContext, ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { hyphenateAttr } from '@vue/language-core'; import type * as ts from 'typescript'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -17,11 +17,12 @@ function getAst(ts: typeof import('typescript'), fileName: string, snapshot: ts. export function create( ts: typeof import('typescript'), - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { return { name: 'vue-autoinsert-dotvalue', create(context): ServicePluginInstance { + const tsPluginClient = getTsPluginClient?.(context); let currentReq = 0; return { async provideAutoInsertionEdit(document, position, lastChange) { diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index ff1e2abf2c..20fa131474 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -1,4 +1,4 @@ -import type { CreateFile, ServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service'; +import type { CreateFile, ServiceContext, ServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service'; import type { ExpressionNode, TemplateChildNode } from '@vue/compiler-dom'; import { Sfc, VueGeneratedCode, scriptRanges } from '@vue/language-core'; import type * as ts from 'typescript'; @@ -14,11 +14,12 @@ const unicodeReg = /\\u/g; export function create( ts: typeof import('typescript'), - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { return { name: 'vue-extract-file', create(context) { + const tsPluginClient = getTsPluginClient?.(context); return { async provideCodeActions(document, range, _context) { diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 1f206f00ef..58cd5673f4 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -1,4 +1,4 @@ -import type { Disposable, ServiceEnvironment, ServicePluginInstance } from '@volar/language-service'; +import type { Disposable, ServiceContext, ServiceEnvironment, ServicePluginInstance } from '@volar/language-service'; import { VueGeneratedCode, hyphenateAttr, hyphenateTag, parseScriptSetupRanges, tsCodegen } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import { create as createHtmlService } from 'volar-service-html'; @@ -10,6 +10,7 @@ import { getNameCasing } from '../ideFeatures/nameCasing'; import { AttrNameCasing, ServicePlugin, TagNameCasing, VueCompilerOptions } from '../types'; import { loadModelModifiersData, loadTemplateData } from './data'; import { URI, Utils } from 'vscode-uri'; +import { getComponentSpans } from '@vue/typescript-plugin/lib/common'; let builtInData: html.HTMLDataV1; let modelData: html.HTMLDataV1; @@ -18,7 +19,7 @@ export function create( mode: 'html' | 'pug', ts: typeof import('typescript'), getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { let customData: html.IHTMLDataProvider[] = []; @@ -51,7 +52,7 @@ export function create( '@', // vue event shorthand ], create(context): ServicePluginInstance { - + const tsPluginClient = getTsPluginClient?.(context); const baseServiceInstance = baseService.create(context); const vueCompilerOptions = getVueOptions(context.env); @@ -327,6 +328,45 @@ export function create( ]; } }, + + provideDocumentSemanticTokens(document, range, legend) { + if (!isSupportedDocument(document)) { + return; + } + const [_virtualCode, sourceFile] = context.documents.getVirtualCodeByUri(document.uri); + if ( + !sourceFile + || !(sourceFile.generated?.code instanceof VueGeneratedCode) + || !sourceFile.generated.code.sfc.template + ) { + return []; + } + const { template } = sourceFile.generated.code.sfc; + const spans = getComponentSpans.call( + { + files: context.language.files, + languageService: context.inject<(import('volar-service-typescript').Provide), 'typescript/languageService'>('typescript/languageService'), + typescript: ts, + vueOptions: getVueOptions(context.env), + }, + sourceFile.generated.code, + template, + { + start: document.offsetAt(range.start), + length: document.offsetAt(range.end) - document.offsetAt(range.start), + }); + const classTokenIndex = legend.tokenTypes.indexOf('class'); + return spans.map(span => { + const start = document.positionAt(span.start); + return [ + start.line, + start.character, + span.length, + classTokenIndex, + 0, + ]; + }); + }, }; async function provideHtmlData(sourceDocumentUri: string, vueCode: VueGeneratedCode) { @@ -419,12 +459,8 @@ export function create( return tags; }, provideAttributes: tag => { + const tagInfo = tagInfos.get(tag); - - - let failed = false; - - let tagInfo = tagInfos.get(tag); if (!tagInfo) { promises.push((async () => { const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? []; @@ -440,11 +476,6 @@ export function create( return []; } - - if (failed) { - return []; - } - const { attrs, props, events } = tagInfo; const attributes: html.IAttributeData[] = []; const _tsCodegen = tsCodegen.get(vueCode.sfc); diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index 5059463785..6c74e2c206 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -1,4 +1,4 @@ -import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +import type { ServiceContext, ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import * as vue from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -6,11 +6,12 @@ const twoslashReg = //g; export function create( ts: typeof import('typescript'), - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { return { name: 'vue-twoslash-queries', create(context): ServicePluginInstance { + const tsPluginClient = getTsPluginClient?.(context); return { async provideInlayHints(document, range) { diff --git a/packages/language-service/tests/utils/createTester.ts b/packages/language-service/tests/utils/createTester.ts index 4ea9ac6391..a0b7acb950 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -1,7 +1,7 @@ import { TypeScriptProjectHost, createLanguageService, resolveCommonLanguageId } from '@volar/language-service'; import { createLanguage } from '@volar/typescript'; import * as path from 'path'; -import type * as ts from 'typescript'; +import * as ts from 'typescript'; import { URI } from 'vscode-uri'; import { createParsedCommandLine, createVueLanguagePlugin, createVueServicePlugins } from '../..'; import { createMockServiceEnv } from './mockEnv'; @@ -11,7 +11,6 @@ export const tester = createTester(rootUri); function createTester(rootUri: string) { - const ts = require('typescript') as typeof import('typescript'); const serviceEnv = createMockServiceEnv(rootUri, () => currentVSCodeSettings ?? defaultVSCodeSettings); const rootPath = serviceEnv.typescript!.uriToFileName(rootUri.toString()); const realTsConfig = path.join(rootPath, 'tsconfig.json').replace(/\\/g, '/'); @@ -46,7 +45,7 @@ function createTester(rootUri: string) { parsedCommandLine.options, parsedCommandLine.vueOptions, ); - const vueServicePlugins = createVueServicePlugins(ts, () => parsedCommandLine.vueOptions, false); + const vueServicePlugins = createVueServicePlugins(ts, () => parsedCommandLine.vueOptions); const defaultVSCodeSettings: any = { 'typescript.preferences.quoteStyle': 'single', 'javascript.preferences.quoteStyle': 'single', diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index ed4c3ec0f7..727b0cd7f7 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -4,8 +4,7 @@ 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 { projects } from './lib/utils'; +import { startNamedPipeServer, projects } from './lib/server'; const windowsPathReg = /\\/g; const externalFiles = new WeakMap>(); @@ -69,12 +68,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, vueOptions }); decorateLanguageService(files, info.languageService); - decorateLanguageServiceForVue(files, info.languageService, vueOptions, ts); + decorateLanguageServiceForVue(files, info.languageService, vueOptions, ts, true); decorateLanguageServiceHost(files, info.languageServiceHost, ts); - startNamedPipeServer(info.project.projectKind, info.project.getCurrentDirectory()); + startNamedPipeServer(ts, info.project.projectKind, info.project.getCurrentDirectory()); } return info.languageService; diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index 4090d0cd94..884ec1a4e7 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -1,10 +1,5 @@ -import * as fs from 'fs'; -import type * as net from 'net'; -import * as path from 'path'; -import type * as ts from 'typescript'; import type { Request } from './server'; -import type { NamedPipeServer } from './utils'; -import { connect, pipeTable } from './utils'; +import { connect, searchNamedPipeServerForFile, sendRequestWorker } from './utils'; export function collectExtractProps( ...args: Parameters @@ -93,60 +88,3 @@ async function sendRequest(request: Request) { } return await sendRequestWorker(request, client); } - -export async function searchNamedPipeServerForFile(fileName: string) { - if (!fs.existsSync(pipeTable)) { - return; - } - const servers: NamedPipeServer[] = JSON.parse(fs.readFileSync(pipeTable, 'utf8')); - const configuredServers = servers - .filter(item => item.serverKind === 1 satisfies ts.server.ProjectKind.Configured); - const inferredServers = servers - .filter(item => item.serverKind === 0 satisfies ts.server.ProjectKind.Inferred) - .sort((a, b) => b.currentDirectory.length - a.currentDirectory.length); - for (const server of configuredServers) { - const client = await connect(server.path); - if (client) { - const response = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, client); - if (response) { - return server; - } - } - } - for (const server of inferredServers) { - if (!path.relative(server.currentDirectory, fileName).startsWith('..')) { - const client = await connect(server.path); - if (client) { - return server; - } - } - } -} - -function sendRequestWorker(request: Request, client: net.Socket) { - return new Promise(resolve => { - let dataChunks: Buffer[] = []; - client.on('data', chunk => { - dataChunks.push(chunk); - }); - client.on('end', () => { - if (!dataChunks.length) { - console.warn('[Vue Named Pipe Client] No response from server for request:', request.type); - resolve(undefined); - return; - } - const data = Buffer.concat(dataChunks); - const text = data.toString(); - let json = null; - try { - json = JSON.parse(text); - } catch (e) { - console.error('[Vue Named Pipe Client] Failed to parse response:', text); - resolve(undefined); - return; - } - resolve(json); - }); - client.write(JSON.stringify(request)); - }); -} diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 1f9ca7e059..c75ea86354 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -8,12 +8,14 @@ export function decorateLanguageServiceForVue( languageService: ts.LanguageService, vueOptions: vue.VueCompilerOptions, ts: typeof import('typescript'), + isTsPlugin: boolean, ) { - - const getCompletionsAtPosition = languageService.getCompletionsAtPosition; - const getCompletionEntryDetails = languageService.getCompletionEntryDetails; - const getCodeFixesAtPosition = languageService.getCodeFixesAtPosition; - const getEncodedSemanticClassifications = languageService.getEncodedSemanticClassifications; + const { + getCompletionsAtPosition, + getCompletionEntryDetails, + getCodeFixesAtPosition, + getEncodedSemanticClassifications, + } = languageService; languageService.getCompletionsAtPosition = (fileName, position, options) => { const result = getCompletionsAtPosition(fileName, position, options); @@ -75,60 +77,85 @@ export function decorateLanguageServiceForVue( 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)) { + if (isTsPlugin) { + languageService.getEncodedSemanticClassifications = (fileName, span, format) => { + const result = getEncodedSemanticClassifications(fileName, span, format); + const file = files.get(fileName); + if (file?.generated?.code instanceof vue.VueGeneratedCode) { + const { template } = file.generated.code.sfc; + if (template) { + for (const componentSpan of getComponentSpans.call( + { typescript: ts, languageService, vueOptions }, + file.generated.code, + template, + { + start: span.start - template.startTagEnd, + length: span.length, + }, + )) { result.spans.push( - node.loc.start.offset + node.loc.source.indexOf(node.tag) + template.startTagEnd, - node.tag.length, + componentSpan.start + template.startTagEnd, + componentSpan.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); - } - } + } + return result; + }; + } +} + +export function getComponentSpans( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + vueOptions: vue.VueCompilerOptions; + }, + vueCode: vue.VueGeneratedCode, + template: NonNullable, + spanTemplateRange: ts.TextSpan, +) { + const { typescript: ts, languageService, vueOptions } = this; + const result: ts.TextSpan[] = []; + const validComponentNames = _getComponentNames(ts, languageService, vueCode, vueOptions); + const components = new Set([ + ...validComponentNames, + ...validComponentNames.map(vue.hyphenateTag), + ]); + template.ast?.children.forEach(function visit(node) { + if (node.loc.end.offset <= spanTemplateRange.start || node.loc.start.offset >= (spanTemplateRange.start + spanTemplateRange.length)) { + return; + } + if (node.type === 1 satisfies vue.CompilerDOM.NodeTypes.ELEMENT) { + if (components.has(node.tag)) { + result.push({ + start: node.loc.start.offset + node.loc.source.indexOf(node.tag), + length: node.tag.length, + }); + if (template.lang === 'html' && !node.isSelfClosing) { + result.push({ + start: node.loc.start.offset + node.loc.source.lastIndexOf(node.tag), + length: node.tag.length, + }); } - else if (node.type === 11 satisfies vue.CompilerDOM.NodeTypes.FOR) { - for (const child of node.children) { - visit(child); - } + } + 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); } - }); + } } - return result; - }; + else if (node.type === 11 satisfies vue.CompilerDOM.NodeTypes.FOR) { + for (const child of node.children) { + visit(child); + } + } + }); + return result; } diff --git a/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index e2655a49e5..9ef7b01317 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -1,15 +1,18 @@ -import { VueGeneratedCode, isSemanticTokensEnabled } from '@vue/language-core'; -import { getProject } from '../utils'; +import { FileRegistry, VueGeneratedCode, isSemanticTokensEnabled } from '@vue/language-core'; import type * as ts from 'typescript'; -export function collectExtractProps(fileName: string, templateCodeRange: [number, number], isTsPlugin: boolean = true) { +export function collectExtractProps( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: FileRegistry; + isTsPlugin: boolean, + }, + fileName: string, + templateCodeRange: [number, number], +) { + const { typescript: ts, languageService, files, isTsPlugin } = this; - const match = getProject(fileName); - if (!match) { - return; - } - - const { info, files, ts } = match; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof VueGeneratedCode)) { return; @@ -20,7 +23,6 @@ export function collectExtractProps(fileName: string, templateCodeRange: [number type: string; model: boolean; }>(); - const languageService = info.languageService; const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index a8c4ac7f8c..d69f174326 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -1,27 +1,31 @@ import * as vue from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import type * as ts from 'typescript'; -import { getProject } from '../utils'; -export function getComponentProps(fileName: string, tag: string, requiredOnly = false) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files, vueOptions } = match; +export function getComponentProps( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + }, + fileName: string, + tag: string, + requiredOnly = false, +) { + const { typescript: ts, files, vueOptions, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } const vueCode = volarFile.generated.code; - const tsLs = match.info.languageService; - const program: ts.Program = (tsLs as any).getCurrentProgram(); + const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; } const checker = program.getTypeChecker(); - const components = getVariableType(ts, tsLs, vueCode, '__VLS_components'); + const components = getVariableType(ts, languageService, vueCode, '__VLS_components'); if (!components) { return []; } @@ -86,25 +90,29 @@ export function getComponentProps(fileName: string, tag: string, requiredOnly = return [...result]; } -export function getComponentEvents(fileName: string, tag: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files, vueOptions } = match; +export function getComponentEvents( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + }, + fileName: string, + tag: string, +) { + const { typescript: ts, files, vueOptions, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; const vueCode = volarFile.generated.code; - const program: ts.Program = (tsLs as any).getCurrentProgram(); + const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; } const checker = program.getTypeChecker(); - const components = getVariableType(ts, tsLs, vueCode, '__VLS_components'); + const components = getVariableType(ts, languageService, vueCode, '__VLS_components'); if (!components) { return []; } @@ -163,39 +171,44 @@ export function getComponentEvents(fileName: string, tag: string) { return [...result]; } -export function getTemplateContextProps(fileName: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files } = match; +export function getTemplateContextProps( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + }, + fileName: string, +) { + const { typescript: ts, files, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; const vueCode = volarFile.generated.code; - return getVariableType(ts, tsLs, vueCode, '__VLS_ctx') + return getVariableType(ts, languageService, vueCode, '__VLS_ctx') ?.type ?.getProperties() .map(c => c.name); } -export function getComponentNames(fileName: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files, vueOptions } = match; +export function getComponentNames( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + }, + fileName: string, +) { + const { typescript: ts, files, vueOptions, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; const vueCode = volarFile.generated.code; - return getVariableType(ts, tsLs, vueCode, '__VLS_components') + return getVariableType(ts, languageService, vueCode, '__VLS_components') ?.type ?.getProperties() .map(c => c.name) @@ -219,18 +232,21 @@ export function _getComponentNames( ?? []; } -export function getElementAttrs(fileName: string, tagName: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files } = match; +export function getElementAttrs( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + }, + fileName: string, + tagName: string, +) { + const { typescript: ts, files, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; - const program: ts.Program = (tsLs as any).getCurrentProgram(); + const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; } diff --git a/packages/typescript-plugin/lib/requests/containsFile.ts b/packages/typescript-plugin/lib/requests/containsFile.ts deleted file mode 100644 index d191f258ab..0000000000 --- a/packages/typescript-plugin/lib/requests/containsFile.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { getProject } from '../utils'; - -export function containsFile(fileName: string) { - return !!getProject(fileName); -} diff --git a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts index fd39eded66..43a2eba3c8 100644 --- a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts +++ b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts @@ -1,16 +1,17 @@ -import { isCompletionEnabled } from '@vue/language-core'; -import { getProject } from '../utils'; +import { FileRegistry, isCompletionEnabled } from '@vue/language-core'; import type * as ts from 'typescript'; -export function getPropertiesAtLocation(fileName: string, position: number, isTsPlugin: boolean = true) { - - const match = getProject(fileName); - if (!match) { - return; - } - - const { info, files, ts } = match; - const languageService = info.languageService; +export function getPropertiesAtLocation( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: FileRegistry; + isTsPlugin: boolean, + }, + fileName: string, + position: number, +) { + const { languageService, files, typescript: ts, isTsPlugin } = this; // mapping const file = files.get(fileName); diff --git a/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts b/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts index 7a1015f066..d4c3162bbc 100644 --- a/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts +++ b/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts @@ -1,14 +1,11 @@ -import { getProject } from '../utils'; - -export function getQuickInfoAtPosition(fileName: string, position: number) { - - const match = getProject(fileName); - if (!match) { - return; - } - - const { info } = match; - const languageService = info.languageService; - +import type * as ts from 'typescript'; +export function getQuickInfoAtPosition( + this: { + languageService: ts.LanguageService; + }, + fileName: string, + position: number, +) { + const { languageService } = this; return languageService.getQuickInfoAtPosition(fileName, position); } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index d288684b29..521a84efa3 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -3,10 +3,10 @@ import * as net from 'net'; import type * as ts from 'typescript'; import { collectExtractProps } from './requests/collectExtractProps'; import { getComponentEvents, getComponentNames, getComponentProps, getElementAttrs, getTemplateContextProps } from './requests/componentInfos'; -import { containsFile } from './requests/containsFile'; import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation'; import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition'; import { NamedPipeServer, connect, pipeTable } from './utils'; +import type { FileRegistry, VueCompilerOptions } from '@vue/language-core'; export interface Request { type: 'containsFile' @@ -19,13 +19,16 @@ export interface Request { | 'getTemplateContextProps' | 'getComponentNames' | 'getElementAttrs'; - args: any; + args: [fileName: string, ...rest: any]; } let started = false; -export function startNamedPipeServer(serverKind: ts.server.ProjectKind, currentDirectory: string) { - +export function startNamedPipeServer( + ts: typeof import('typescript'), + serverKind: ts.server.ProjectKind, + currentDirectory: string, +) { if (started) { return; } @@ -38,45 +41,59 @@ export function startNamedPipeServer(serverKind: ts.server.ProjectKind, currentD connection.on('data', data => { const text = data.toString(); const request: Request = JSON.parse(text); - if (request.type === 'containsFile') { - const result = containsFile.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'collectExtractProps') { - const result = collectExtractProps.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getPropertiesAtLocation') { - const result = getPropertiesAtLocation.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getQuickInfoAtPosition') { - const result = getQuickInfoAtPosition.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - // Component Infos - else if (request.type === 'getComponentProps') { - const result = getComponentProps.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getComponentEvents') { - const result = getComponentEvents.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getTemplateContextProps') { - const result = getTemplateContextProps.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getComponentNames') { - const result = getComponentNames.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getElementAttrs') { - const result = getElementAttrs.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); + const fileName = request.args[0]; + const project = getProject(fileName); + if (project) { + const requestContext = { + typescript: ts, + languageService: project.info.languageService, + files: project.files, + vueOptions: project.vueOptions, + isTsPlugin: true, + }; + if (request.type === 'containsFile') { + const result = !!getProject(fileName); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'collectExtractProps') { + const result = collectExtractProps.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getPropertiesAtLocation') { + const result = getPropertiesAtLocation.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getQuickInfoAtPosition') { + const result = getQuickInfoAtPosition.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + // Component Infos + else if (request.type === 'getComponentProps') { + const result = getComponentProps.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getComponentEvents') { + const result = getComponentEvents.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getTemplateContextProps') { + const result = getTemplateContextProps.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getComponentNames') { + const result = getComponentNames.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getElementAttrs') { + const result = getElementAttrs.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else { + console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); + } } else { - console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); + console.warn('[Vue Named Pipe Server] No project found for:', fileName); } connection.end(); }); @@ -120,3 +137,17 @@ function cleanupPipeTable() { }); } } + +export const projects = new Map(); + +function getProject(fileName: string) { + for (const [project, data] of projects) { + if (project.containsFile(fileName as ts.server.NormalizedPath)) { + return data; + } + } +} diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 9d9c79915b..746f3cadc7 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -1,8 +1,9 @@ -import type { FileRegistry, VueCompilerOptions } from '@vue/language-core'; import * as os from 'os'; import * as net from 'net'; import * as path from 'path'; import type * as ts from 'typescript'; +import * as fs from 'fs'; +import type { Request } from './server'; export interface NamedPipeServer { path: string; @@ -14,21 +15,6 @@ const { version } = require('../package.json'); export const pipeTable = path.join(os.tmpdir(), `vue-tsp-table-${version}.json`); -export const projects = new Map(); - -export function getProject(fileName: string) { - for (const [project, data] of projects) { - if (project.containsFile(fileName as ts.server.NormalizedPath)) { - return data; - } - } -} - export function connect(path: string) { return new Promise(resolve => { const client = net.connect(path); @@ -40,3 +26,60 @@ export function connect(path: string) { }); }); } + +export async function searchNamedPipeServerForFile(fileName: string) { + if (!fs.existsSync(pipeTable)) { + return; + } + const servers: NamedPipeServer[] = JSON.parse(fs.readFileSync(pipeTable, 'utf8')); + const configuredServers = servers + .filter(item => item.serverKind === 1 satisfies ts.server.ProjectKind.Configured); + const inferredServers = servers + .filter(item => item.serverKind === 0 satisfies ts.server.ProjectKind.Inferred) + .sort((a, b) => b.currentDirectory.length - a.currentDirectory.length); + for (const server of configuredServers) { + const client = await connect(server.path); + if (client) { + const response = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, client); + if (response) { + return server; + } + } + } + for (const server of inferredServers) { + if (!path.relative(server.currentDirectory, fileName).startsWith('..')) { + const client = await connect(server.path); + if (client) { + return server; + } + } + } +} + +export function sendRequestWorker(request: Request, client: net.Socket) { + return new Promise(resolve => { + let dataChunks: Buffer[] = []; + client.on('data', chunk => { + dataChunks.push(chunk); + }); + client.on('end', () => { + if (!dataChunks.length) { + console.warn('[Vue Named Pipe Client] No response from server for request:', request.type); + resolve(undefined); + return; + } + const data = Buffer.concat(dataChunks); + const text = data.toString(); + let json = null; + try { + json = JSON.parse(text); + } catch (e) { + console.error('[Vue Named Pipe Client] Failed to parse response:', text); + resolve(undefined); + return; + } + resolve(json); + }); + client.write(JSON.stringify(request)); + }); +} From 0c28079b719bba3ce6f3670b0f5a465a84e87e93 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 04:52:49 +0800 Subject: [PATCH 03/10] wip [skip ci] --- packages/language-server/lib/types.ts | 4 ++-- packages/language-server/node.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/language-server/lib/types.ts b/packages/language-server/lib/types.ts index 7d1ca802bd..0148645ca0 100644 --- a/packages/language-server/lib/types.ts +++ b/packages/language-server/lib/types.ts @@ -4,8 +4,8 @@ export type VueInitializationOptions = InitializationOptions & { typescript: { tsdk: string; }; - vue: { - hybridMode: boolean; + vue?: { + hybridMode?: boolean; /** * @example ['vue1', 'vue2'] */ diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index 6035e53168..f28c6304a0 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -21,6 +21,7 @@ connection.listen(); connection.onInitialize(async params => { const options: VueInitializationOptions = params.initializationOptions; + const hybridMode = options.vue?.hybridMode ?? true; tsdk = loadTsdkByPath(options.typescript.tsdk, params.locale); @@ -34,7 +35,7 @@ connection.onInitialize(async params => { const result = await server.initialize( params, - options.vue.hybridMode + hybridMode ? createSimpleProjectProviderFactory() : createTypeScriptProjectProviderFactory(tsdk.typescript, tsdk.diagnosticMessages), { @@ -43,7 +44,7 @@ connection.onInitialize(async params => { return createVueServicePlugins( tsdk.typescript, env => envToVueOptions.get(env)!, - options.vue.hybridMode ? () => tsPluginClient : undefined, + hybridMode ? () => tsPluginClient : undefined, ); }, async getLanguagePlugins(serviceEnv, projectContext) { @@ -106,7 +107,7 @@ connection.onInitialize(async params => { }, ); - if (options.vue.hybridMode) { + if (hybridMode) { // handle by tsserver + @vue/typescript-plugin result.capabilities.semanticTokensProvider = undefined; } From ab05f2e141b756b599d1ec0b60d1284143ce0ad9 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 05:43:34 +0800 Subject: [PATCH 04/10] wip [skip ci] --- .vscode/settings.json | 1 - extensions/vscode/package.json | 17 -------- extensions/vscode/src/common.ts | 45 ++++++++----------- extensions/vscode/src/config.ts | 2 - extensions/vscode/src/nodeClientMain.ts | 58 ++++++------------------- 5 files changed, 32 insertions(+), 91 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b91a99dae0..da50bdc4ff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,6 @@ "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" }, - "vue.server.path": "./extensions/vscode/server.js", "files.exclude": { "packages/*/*.d.ts": true, "packages/*/*.js": true, diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index fbd9007626..84f958a62f 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -239,23 +239,6 @@ "type": "boolean", "default": false }, - "vue.server.path": { - "type": [ - "string", - "null" - ], - "default": null, - "description": "Path to node_modules/vue-language-server/bin/vue-language-server.js." - }, - "vue.server.runtime": { - "type": "string", - "enum": [ - "node", - "bun" - ], - "default": "node", - "description": "Vue Language Server runtime." - }, "vue.server.maxFileSize": { "type": "number", "default": 20971520, diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 5084f77bed..2e9d4e06a0 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -24,22 +24,19 @@ type CreateLanguageClient = ( outputChannel: vscode.OutputChannel, ) => lsp.BaseLanguageClient; +const beginHybridMode = config.server.hybridMode; + export async function activate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { const stopCheck = vscode.window.onDidChangeActiveTextEditor(tryActivate); tryActivate(); function tryActivate() { - - if (!vscode.window.activeTextEditor) { - // onWebviewPanel:preview - doActivate(context, createLc); - stopCheck.dispose(); - return; - } - - const currentLangId = vscode.window.activeTextEditor.document.languageId; - if (currentLangId === 'vue' || (currentLangId === 'markdown' && config.server.vitePress.supportMdFile) || (currentLangId === 'html' && config.server.petiteVue.supportHtmlFile)) { + if ( + vscode.window.visibleTextEditors.some(editor => editor.document.languageId === 'vue') + || (config.server.vitePress.supportMdFile && vscode.window.visibleTextEditors.some(editor => editor.document.languageId === 'vue')) + || (config.server.petiteVue.supportHtmlFile && vscode.window.visibleTextEditors.some(editor => editor.document.languageId === 'html')) + ) { doActivate(context, createLc); stopCheck.dispose(); } @@ -61,7 +58,7 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang outputChannel ); - activateServerMaxOldSpaceSizeChange(); + activateConfigWatcher(); activateRestartRequest(); activateClientRequests(); @@ -82,21 +79,22 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); activateServerSys(client); - async function requestReloadVscode() { - const reload = await vscode.window.showInformationMessage( - 'Please reload VSCode to restart language servers.', - 'Reload Window' - ); + async function requestReloadVscode(msg: string) { + const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); if (reload === undefined) return; // cancel vscode.commands.executeCommand('workbench.action.reloadWindow'); } - function activateServerMaxOldSpaceSizeChange() { + function activateConfigWatcher() { context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('vue.server.runtime') || e.affectsConfiguration('vue.server.path')) { - requestReloadVscode(); + if (e.affectsConfiguration('vue.server.hybridMode') && config.server.hybridMode !== beginHybridMode) { + requestReloadVscode( + config.server.hybridMode + ? 'Please reload VSCode to enable Hybrid Mode.' + : 'Please reload VSCode to disable Hybrid Mode.' + ); } - if (e.affectsConfiguration('vue')) { + else if (e.affectsConfiguration('vue')) { vscode.commands.executeCommand('vue.action.restartServer'); } })); @@ -104,15 +102,10 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang async function activateRestartRequest() { context.subscriptions.push(vscode.commands.registerCommand('vue.action.restartServer', async () => { - await client.stop(); - outputChannel.clear(); - client.clientOptions.initializationOptions = await getInitializationOptions(context); - await client.start(); - activateClientRequests(); })); } @@ -151,7 +144,7 @@ async function getInitializationOptions( tokenModifiers: [], }, vue: { - hybridMode: config.server.hybridMode, + hybridMode: beginHybridMode, additionalExtensions: [ ...config.server.additionalExtensions, ...!config.server.petiteVue.supportHtmlFile ? [] : ['html'], diff --git a/extensions/vscode/src/config.ts b/extensions/vscode/src/config.ts index 1393d239b7..d2276b24b2 100644 --- a/extensions/vscode/src/config.ts +++ b/extensions/vscode/src/config.ts @@ -17,8 +17,6 @@ export const config = { }, get server(): Readonly<{ hybridMode: boolean; - path: null | string; - runtime: 'node' | 'bun'; maxOldSpaceSize: number; maxFileSize: number; diagnosticModel: 'push' | 'pull'; diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 6f53366142..3fa4fd001a 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -9,8 +9,6 @@ import { middleware } from './middleware'; export async function activate(context: vscode.ExtensionContext) { - let serverPathStatusItem: vscode.StatusBarItem | undefined; - const volarLabs = createLabsInfo(serverLib); volarLabs.extensionExports.volarLabs.codegenStackSupport = true; @@ -32,27 +30,6 @@ export async function activate(context: vscode.ExtensionContext) { let serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); - if (config.server.path) { - try { - const roots = (vscode.workspace.workspaceFolders ?? []).map(folder => folder.uri.fsPath); - const serverPath = require.resolve(config.server.path, { paths: roots }); - serverModule = vscode.Uri.file(serverPath); - - if (!serverPathStatusItem) { - serverPathStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); - serverPathStatusItem.text = '[vue] configured server path'; - serverPathStatusItem.command = 'vue.action.gotoServerFile'; - serverPathStatusItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); - serverPathStatusItem.show(); - vscode.commands.registerCommand(serverPathStatusItem.command, () => { - vscode.window.showTextDocument(serverModule); - }); - } - } catch (err) { - vscode.window.showWarningMessage(`Cannot find vue language server path: ${config.server.path}`); - } - } - const runOptions: lsp.ForkOptions = {}; if (config.server.maxOldSpaceSize) { runOptions.execArgv ??= []; @@ -71,28 +48,6 @@ export async function activate(context: vscode.ExtensionContext) { options: debugOptions }, }; - if (config.server.runtime === 'bun') { - serverOptions = { - run: { - transport: { - kind: lsp.TransportKind.socket, - port, - }, - options: runOptions, - command: 'bun', - args: ['--bun', 'run', serverModule.fsPath], - }, - debug: { - transport: { - kind: lsp.TransportKind.socket, - port, - }, - options: debugOptions, - command: 'bun', - args: ['--bun', 'run', serverModule.fsPath], - }, - }; - } const clientOptions: lsp.LanguageClientOptions = { middleware, documentSelector: documentSelector, @@ -178,6 +133,7 @@ try { const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')!; const readFileSync = fs.readFileSync; const extensionJsPath = require.resolve('./dist/extension.js', { paths: [tsExtension.extensionPath] }); + const { hybridMode } = config.server; // @ts-expect-error fs.readFileSync = (...args) => { @@ -193,6 +149,18 @@ try { text = text.replace('t.jsTsLanguageModes=[t.javascript,t.javascriptreact,t.typescript,t.typescriptreact]', s => s + '.concat("vue")'); // patch jsTsLanguageModes text = text.replace('.languages.match([t.typescript,t.typescriptreact,t.javascript,t.javascriptreact]', s => s + '.concat("vue")'); // patch isSupportedLanguageMode + if (!hybridMode) { + // patch readPlugins + text = text.replace( + 'languages:Array.isArray(e.languages)', + [ + 'languages:', + `e.name==='typescript-vue-plugin-bundle'?[]:`, + 'Array.isArray(e.languages)', + ].join(''), + ); + } + return text; } // @ts-expect-error From 793569712edbab168f0eb4c856335739b6c48bb9 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 06:51:27 +0800 Subject: [PATCH 05/10] wip [skip ci] --- extensions/vscode/package.json | 5 --- extensions/vscode/src/common.ts | 37 +++++++++++++------- extensions/vscode/src/config.ts | 1 - extensions/vscode/src/features/nameCasing.ts | 22 +++++------- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 84f958a62f..2aabd3787b 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -355,11 +355,6 @@ "default": "autoKebab", "description": "Preferred attr name case." }, - "vue.complete.casing.status": { - "type": "boolean", - "default": true, - "description": "Show name casing in status bar." - }, "vue.autoInsert.parentheses": { "type": "boolean", "default": true, diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 2e9d4e06a0..3636704541 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -3,6 +3,8 @@ import { activateDocumentDropEdit, activateServerSys, activateWriteVirtualFiles, + activateTsConfigStatusItem, + activateTsVersionStatusItem, getTsdk, } from '@volar/vscode'; import { DiagnosticModel, VueInitializationOptions } from '@vue/language-server'; @@ -58,13 +60,6 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang outputChannel ); - activateConfigWatcher(); - activateRestartRequest(); - activateClientRequests(); - - splitEditors.register(context, client); - doctor.register(context, client); - const selectors: vscode.DocumentFilter[] = [{ language: 'vue' }]; if (config.server.petiteVue.supportHtmlFile) { @@ -74,11 +69,34 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang selectors.push({ language: 'markdown' }); } + activateConfigWatcher(); + activateRestartRequest(); + + nameCasing.activate(context, client, selectors); + splitEditors.register(context, client); + doctor.register(context, client); + activateAutoInsertion(selectors, client); activateDocumentDropEdit(selectors, client); activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); activateServerSys(client); + if (!config.server.hybridMode) { + activateTsConfigStatusItem(selectors, 'vue.tsconfig', client); + activateTsVersionStatusItem(selectors, 'vue.tsversion', context, client, text => 'TS ' + text); + } + + const hybridModeStatus = vscode.languages.createLanguageStatusItem('vue-hybrid-mode', selectors); + hybridModeStatus.text = config.server.hybridMode ? 'Hybrid Mode: Enabled' : 'Hybrid Mode: Disabled'; + hybridModeStatus.command = { + title: 'Open Setting', + command: 'workbench.action.openSettings', + arguments: ['vue.server.hybridMode'], + }; + if (!config.server.hybridMode) { + hybridModeStatus.severity = vscode.LanguageStatusSeverity.Warning; + } + async function requestReloadVscode(msg: string) { const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); if (reload === undefined) return; // cancel @@ -106,13 +124,8 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang outputChannel.clear(); client.clientOptions.initializationOptions = await getInitializationOptions(context); await client.start(); - activateClientRequests(); })); } - - function activateClientRequests() { - nameCasing.activate(context, client); - } } export function deactivate(): Thenable | undefined { diff --git a/extensions/vscode/src/config.ts b/extensions/vscode/src/config.ts index d2276b24b2..826bcf4a84 100644 --- a/extensions/vscode/src/config.ts +++ b/extensions/vscode/src/config.ts @@ -47,7 +47,6 @@ export const config = { }, get complete(): Readonly<{ casing: { - status: boolean; props: 'autoKebab' | 'autoCamel' | 'kebab' | 'camel'; tags: 'autoKebab' | 'autoPascal' | 'kebab' | 'pascal'; }; diff --git a/extensions/vscode/src/features/nameCasing.ts b/extensions/vscode/src/features/nameCasing.ts index 3fa27fe1bc..6ab166dc3d 100644 --- a/extensions/vscode/src/features/nameCasing.ts +++ b/extensions/vscode/src/features/nameCasing.ts @@ -7,13 +7,16 @@ import { config } from '../config'; export const attrNameCasings = new Map(); export const tagNameCasings = new Map(); -export async function activate(_context: vscode.ExtensionContext, client: BaseLanguageClient) { +export async function activate(_context: vscode.ExtensionContext, client: BaseLanguageClient, selector: vscode.DocumentSelector) { await client.start(); const disposes: vscode.Disposable[] = []; - const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); - statusBar.command = 'vue.action.nameCasing'; + const statusBar = vscode.languages.createLanguageStatusItem('vue-name-casing', selector); + statusBar.command = { + title: 'Open Menu', + command: 'vue.action.nameCasing', + }; update(vscode.window.activeTextEditor?.document); @@ -132,12 +135,9 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa async function update(document: vscode.TextDocument | undefined) { if ( - config.complete.casing.status - && ( - document?.languageId === 'vue' - || (config.server.vitePress.supportMdFile && document?.languageId === 'markdown') - || (config.server.petiteVue.supportHtmlFile && document?.languageId === 'html') - ) + document?.languageId === 'vue' + || (config.server.vitePress.supportMdFile && document?.languageId === 'markdown') + || (config.server.petiteVue.supportHtmlFile && document?.languageId === 'html') ) { let detected: Awaited> | undefined; let attrNameCasing = attrNameCasings.get(document.uri.toString()); @@ -187,10 +187,6 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa } updateStatusBarText(); - statusBar.show(); - } - else { - statusBar.hide(); } } From c1b137a3ebd279e0966532e371025f03b90ad4ff Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 07:49:39 +0800 Subject: [PATCH 06/10] Update package.json [skip ci] --- extensions/vscode/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 2aabd3787b..0f96b769c3 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -237,7 +237,8 @@ }, "vue.server.hybridMode": { "type": "boolean", - "default": false + "default": false, + "description": "Vue language server only handles CSS and HTML language support, and tsserver takes over TS language support via TS plugin." }, "vue.server.maxFileSize": { "type": "number", From c51f915fd5be92a24275ba0bfc5a7c785af8f29a Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 08:47:28 +0800 Subject: [PATCH 07/10] fix tests --- packages/language-service/index.ts | 1 + packages/typescript-plugin/lib/common.ts | 2 +- .../lib/requests/collectExtractProps.ts | 5 ++-- .../lib/requests/componentInfos.ts | 25 +++++++++++-------- .../lib/requests/getPropertiesAtLocation.ts | 5 ++-- packages/typescript-plugin/lib/server.ts | 1 + .../complete/#2511/input/entry.vue | 2 +- .../complete/#2511/output/entry.vue | 2 +- .../component-auto-import/input/entry.vue | 2 +- .../component-auto-import/output/entry.vue | 2 +- 10 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index ef4a049fd7..9e8e9ae624 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -71,6 +71,7 @@ export function createVueServicePlugins( languageService: context.inject<(import('volar-service-typescript').Provide), 'typescript/languageService'>('typescript/languageService'), vueOptions: getVueOptions(context.env), isTsPlugin: false, + getFileId: context.env.typescript!.fileNameToUri, }; return { async collectExtractProps(...args) { diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index c75ea86354..15567245f8 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -32,7 +32,7 @@ export function decorateLanguageServiceForVue( 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); + item.name = capitalize(item.name.slice(0, -suffix.length)); if (item.insertText) { // #2286 item.insertText = item.insertText.replace(`${suffix}$1`, '$1'); diff --git a/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index 9ef7b01317..96bf039764 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -7,13 +7,14 @@ export function collectExtractProps( languageService: ts.LanguageService; files: FileRegistry; isTsPlugin: boolean, + getFileId: (fileName: string) => string, }, fileName: string, templateCodeRange: [number, number], ) { - const { typescript: ts, languageService, files, isTsPlugin } = this; + const { typescript: ts, languageService, files, isTsPlugin, getFileId } = this; - const volarFile = files.get(fileName); + const volarFile = files.get(getFileId(fileName)); if (!(volarFile?.generated?.code instanceof VueGeneratedCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index d69f174326..e7eccff4dc 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -8,13 +8,14 @@ export function getComponentProps( languageService: ts.LanguageService; files: vue.FileRegistry; vueOptions: vue.VueCompilerOptions, + getFileId: (fileName: string) => string, }, fileName: string, tag: string, requiredOnly = false, ) { - const { typescript: ts, files, vueOptions, languageService } = this; - const volarFile = files.get(fileName); + const { typescript: ts, files, vueOptions, languageService, getFileId } = this; + const volarFile = files.get(getFileId(fileName)); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } @@ -96,12 +97,13 @@ export function getComponentEvents( languageService: ts.LanguageService; files: vue.FileRegistry; vueOptions: vue.VueCompilerOptions, + getFileId: (fileName: string) => string, }, fileName: string, tag: string, ) { - const { typescript: ts, files, vueOptions, languageService } = this; - const volarFile = files.get(fileName); + const { typescript: ts, files, vueOptions, languageService, getFileId } = this; + const volarFile = files.get(getFileId(fileName)); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } @@ -176,11 +178,12 @@ export function getTemplateContextProps( typescript: typeof import('typescript'); languageService: ts.LanguageService; files: vue.FileRegistry; + getFileId: (fileName: string) => string, }, fileName: string, ) { - const { typescript: ts, files, languageService } = this; - const volarFile = files.get(fileName); + const { typescript: ts, files, languageService, getFileId } = this; + const volarFile = files.get(getFileId(fileName)); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } @@ -198,11 +201,12 @@ export function getComponentNames( languageService: ts.LanguageService; files: vue.FileRegistry; vueOptions: vue.VueCompilerOptions, + getFileId: (fileName: string) => string, }, fileName: string, ) { - const { typescript: ts, files, vueOptions, languageService } = this; - const volarFile = files.get(fileName); + const { typescript: ts, files, vueOptions, languageService, getFileId } = this; + const volarFile = files.get(getFileId(fileName)); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } @@ -237,12 +241,13 @@ export function getElementAttrs( typescript: typeof import('typescript'); languageService: ts.LanguageService; files: vue.FileRegistry; + getFileId: (fileName: string) => string, }, fileName: string, tagName: string, ) { - const { typescript: ts, files, languageService } = this; - const volarFile = files.get(fileName); + const { typescript: ts, files, languageService, getFileId } = this; + const volarFile = files.get(getFileId(fileName)); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts index 43a2eba3c8..a034c71d56 100644 --- a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts +++ b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts @@ -7,14 +7,15 @@ export function getPropertiesAtLocation( languageService: ts.LanguageService; files: FileRegistry; isTsPlugin: boolean, + getFileId: (fileName: string) => string, }, fileName: string, position: number, ) { - const { languageService, files, typescript: ts, isTsPlugin } = this; + const { languageService, files, typescript: ts, isTsPlugin, getFileId } = this; // mapping - const file = files.get(fileName); + const file = files.get(getFileId(fileName)); if (file?.generated) { const virtualScript = file.generated.languagePlugin.typescript?.getScript(file.generated.code); if (!virtualScript) { diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index 521a84efa3..a5bb025653 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -50,6 +50,7 @@ export function startNamedPipeServer( files: project.files, vueOptions: project.vueOptions, isTsPlugin: true, + getFileId: (fileName: string) => fileName, }; if (request.type === 'containsFile') { const result = !!getProject(fileName); diff --git a/test-workspace/language-service/complete/#2511/input/entry.vue b/test-workspace/language-service/complete/#2511/input/entry.vue index 3720634d8c..8f093eed73 100644 --- a/test-workspace/language-service/complete/#2511/input/entry.vue +++ b/test-workspace/language-service/complete/#2511/input/entry.vue @@ -1,4 +1,4 @@ diff --git a/test-workspace/language-service/complete/#2511/output/entry.vue b/test-workspace/language-service/complete/#2511/output/entry.vue index f8ba73815f..223cf4517a 100644 --- a/test-workspace/language-service/complete/#2511/output/entry.vue +++ b/test-workspace/language-service/complete/#2511/output/entry.vue @@ -1,4 +1,4 @@ diff --git a/test-workspace/language-service/complete/component-auto-import/input/entry.vue b/test-workspace/language-service/complete/component-auto-import/input/entry.vue index c60258d8c7..dac432aed0 100644 --- a/test-workspace/language-service/complete/component-auto-import/input/entry.vue +++ b/test-workspace/language-service/complete/component-auto-import/input/entry.vue @@ -3,5 +3,5 @@ diff --git a/test-workspace/language-service/complete/component-auto-import/output/entry.vue b/test-workspace/language-service/complete/component-auto-import/output/entry.vue index 25cf2f4ef7..1072e574b6 100644 --- a/test-workspace/language-service/complete/component-auto-import/output/entry.vue +++ b/test-workspace/language-service/complete/component-auto-import/output/entry.vue @@ -5,5 +5,5 @@ import ComponentForAutoImport from './component-for-auto-import.vue'; From 780719a9ac71a5c886abaed156e80910b5f287ef Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 08:51:10 +0800 Subject: [PATCH 08/10] Update package.json --- packages/language-service/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 4b78a4f791..a3cab7ca48 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -41,7 +41,6 @@ "@types/node": "latest", "@types/path-browserify": "latest", "@volar/kit": "~2.1.2", - "@vue/typescript-plugin": "2.0.6", "vscode-languageserver-protocol": "^3.17.5" } } From 5b07c8e350482af69a9b68be032860e0a0130668 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 09:33:58 +0800 Subject: [PATCH 09/10] Update nodeClientMain.ts --- extensions/vscode/src/nodeClientMain.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 3fa4fd001a..e57af0734f 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -81,7 +81,7 @@ export async function activate(context: vscode.ExtensionContext) { } else { vscode.window.showWarningMessage( - 'Takeover mode is no longer needed in version 2.0. Please enable the "TypeScript and JavaScript Language Features" extension.', + 'Takeover mode is no longer needed since v2. Please enable the "TypeScript and JavaScript Language Features" extension.', 'Show Extension' ).then((selected) => { if (selected) { @@ -92,7 +92,7 @@ export async function activate(context: vscode.ExtensionContext) { if (vueTsPluginExtension) { vscode.window.showWarningMessage( - `The "${vueTsPluginExtension.packageJSON.displayName}" extension is no longer needed in version 2.0. Please uninstall it.`, + `The "${vueTsPluginExtension.packageJSON.displayName}" extension is no longer needed since v2. Please uninstall it.`, 'Show Extension' ).then((selected) => { if (selected) { From 681bf139fba484607409f21515ad1c5b7b1bc22d Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 09:37:29 +0800 Subject: [PATCH 10/10] Update doctor.ts --- extensions/vscode/src/features/doctor.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/vscode/src/features/doctor.ts b/extensions/vscode/src/features/doctor.ts index 1cab87c456..c447535b3e 100644 --- a/extensions/vscode/src/features/doctor.ts +++ b/extensions/vscode/src/features/doctor.ts @@ -228,7 +228,11 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan } // #3942, https://github.com/microsoft/TypeScript/issues/57633 - for (const extId of ['svelte.svelte-vscode', 'styled-components.vscode-styled-components']) { + for (const extId of [ + 'svelte.svelte-vscode', + 'styled-components.vscode-styled-components', + 'Divlo.vscode-styled-jsx-languageserver', + ]) { const ext = vscode.extensions.getExtension(extId); if (ext) { problems.push({