From 3758474d8f03f941c885526d53cb296f3d5b7a73 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 09:41:50 +0800 Subject: [PATCH] feat(language-server): reintroducing full TS support (#4119) --- .vscode/settings.json | 1 - extensions/vscode/package.json | 25 +-- extensions/vscode/src/common.ts | 79 +++++---- extensions/vscode/src/config.ts | 4 +- extensions/vscode/src/features/doctor.ts | 69 ++++---- extensions/vscode/src/features/nameCasing.ts | 22 +-- extensions/vscode/src/nodeClientMain.ts | 62 ++----- 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 | 3 +- packages/language-server/node.ts | 47 ++--- packages/language-service/index.ts | 101 +++++++++-- .../lib/plugins/vue-autoinsert-dotvalue.ts | 5 +- .../lib/plugins/vue-extract-file.ts | 5 +- .../language-service/lib/plugins/vue-sfc.ts | 11 +- .../lib/plugins/vue-template.ts | 57 +++++-- .../lib/plugins/vue-twoslash-queries.ts | 5 +- packages/language-service/package.json | 18 +- packages/language-service/tests/complete.ts | 2 +- packages/language-service/tests/inlayHint.ts | 2 +- .../tests/utils/createTester.ts | 10 +- packages/tsc/index.ts | 12 +- packages/tsc/tests/dts.spec.ts | 4 +- packages/typescript-plugin/index.ts | 144 +--------------- packages/typescript-plugin/lib/client.ts | 64 +------ packages/typescript-plugin/lib/common.ts | 161 ++++++++++++++++++ .../lib/requests/collectExtractProps.ts | 25 +-- .../lib/requests/componentInfos.ts | 117 +++++++------ .../lib/requests/containsFile.ts | 5 - .../lib/requests/getPropertiesAtLocation.ts | 26 +-- .../lib/requests/getQuickInfoAtPosition.ts | 21 +-- packages/typescript-plugin/lib/server.ts | 114 ++++++++----- packages/typescript-plugin/lib/utils.ts | 75 ++++++-- pnpm-lock.yaml | 78 +++++---- .../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 +- 40 files changed, 769 insertions(+), 642 deletions(-) create mode 100644 packages/typescript-plugin/lib/common.ts delete mode 100644 packages/typescript-plugin/lib/requests/containsFile.ts 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 c67e8a209c..0f96b769c3 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -235,22 +235,10 @@ "default": "off", "description": "Traces the communication between VS Code and the language server." }, - "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.hybridMode": { + "type": "boolean", + "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", @@ -368,11 +356,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 4746b8cb3b..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'; @@ -24,22 +26,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,13 +60,6 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang outputChannel ); - activateServerMaxOldSpaceSizeChange(); - activateRestartRequest(); - activateClientRequests(); - - splitEditors.register(context, client); - doctor.register(context, client); - const selectors: vscode.DocumentFilter[] = [{ language: 'vue' }]; if (config.server.petiteVue.supportHtmlFile) { @@ -77,26 +69,50 @@ 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); - async function requestReloadVscode() { - const reload = await vscode.window.showInformationMessage( - 'Please reload VSCode to restart language servers.', - 'Reload Window' - ); + 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 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,22 +120,12 @@ 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(); })); } - - function activateClientRequests() { - nameCasing.activate(context, client); - } } export function deactivate(): Thenable | undefined { @@ -151,6 +157,7 @@ async function getInitializationOptions( tokenModifiers: [], }, vue: { + 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 84b8feea07..826bcf4a84 100644 --- a/extensions/vscode/src/config.ts +++ b/extensions/vscode/src/config.ts @@ -16,8 +16,7 @@ export const config = { return _config().get('doctor')!; }, get server(): Readonly<{ - path: null | string; - runtime: 'node' | 'bun'; + hybridMode: boolean; maxOldSpaceSize: number; maxFileSize: number; diagnosticModel: 'push' | 'pull'; @@ -48,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/doctor.ts b/extensions/vscode/src/features/doctor.ts index 9008942fcc..c447535b3e 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,39 @@ 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', + 'Divlo.vscode-styled-jsx-languageserver', + ]) { + 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/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(); } } diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 6f53366142..e57af0734f 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, @@ -126,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) { @@ -137,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) { @@ -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 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..0148645ca0 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?: { + hybridMode?: boolean; /** * @example ['vue1', 'vue2'] */ diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index 3bae46ac43..f28c6304a0 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -1,10 +1,11 @@ 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'; 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(); @@ -20,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); @@ -33,15 +35,26 @@ connection.onInitialize(async params => { const result = await server.initialize( params, - createSimpleProjectProviderFactory(), + 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)!, + hybridMode ? () => tsPluginClient : undefined, + ); }, 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 +73,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,23 +101,16 @@ 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; } }, }, ); - // handle by tsserver + @vue/typescript-plugin - result.capabilities.semanticTokensProvider = undefined; + if (hybridMode) { + // handle by tsserver + @vue/typescript-plugin + result.capabilities.semanticTokensProvider = undefined; + } return result; }); @@ -144,7 +149,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 1a6ef91412..9e8e9ae624 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -3,14 +3,16 @@ 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 { 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'; 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'; @@ -25,30 +27,105 @@ 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, - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin[] { - return [ - createTypeScriptServicePlugin(ts), - createTypeScriptTwoslashQueriesServicePlugin(), + const plugins: ServicePlugin[] = []; + const hybridMode = !!getTsPluginClient; + if (!hybridMode) { + plugins.push(...createTypeScriptServicePlugins(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); + 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, 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, + getFileId: context.env.typescript!.fileNameToUri, + }; + 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), + createTypeScriptDocCommentTemplateServicePlugin(ts), + ); + } + plugins.push( + createTypeScriptTwoslashQueriesServicePlugin(ts), createCssServicePlugin(), createPugFormatServicePlugin(), createJsonServicePlugin(), - createVueTemplateServicePlugin('html', ts, getVueOptions, tsPluginClient), - createVueTemplateServicePlugin('pug', ts, getVueOptions, tsPluginClient), + createVueTemplateServicePlugin('html', ts, getVueOptions, getTsPluginClient), + createVueTemplateServicePlugin('pug', ts, getVueOptions, getTsPluginClient), createVueSfcServicePlugin(), - createVueTwoslashQueriesServicePlugin(ts, tsPluginClient), + createVueTwoslashQueriesServicePlugin(ts, getTsPluginClient), createVueReferencesCodeLensServicePlugin(), createVueDocumentDropServicePlugin(ts), - createVueAutoDotValueServicePlugin(ts, tsPluginClient), + createVueAutoDotValueServicePlugin(ts, getTsPluginClient), createVueAutoWrapParenthesesServicePlugin(ts), createVueAutoAddSpaceServicePlugin(), createVueVisualizeHiddenCallbackParamServicePlugin(), createVueDirectiveCommentsServicePlugin(), - createVueExtractFileServicePlugin(ts, tsPluginClient), + 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-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/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/package.json b/packages/language-service/package.json index ba1784f6ae..a3cab7ca48 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" @@ -40,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" } } 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..a0b7acb950 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -1,9 +1,9 @@ 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, resolveVueCompilerOptions } from '../..'; +import { createParsedCommandLine, createVueLanguagePlugin, createVueServicePlugins } from '../..'; import { createMockServiceEnv } from './mockEnv'; export const rootUri = URI.file(path.resolve(__dirname, '../../../../test-workspace/language-service')).toString(); @@ -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, '/'); @@ -26,7 +25,6 @@ function createTester(rootUri: string) { getScriptSnapshot, getLanguageId: resolveCommonLanguageId, }; - const resolvedVueOptions = resolveVueCompilerOptions(parsedCommandLine.vueOptions); const vueLanguagePlugin = createVueLanguagePlugin( ts, serviceEnv.typescript!.uriToFileName, @@ -45,9 +43,9 @@ function createTester(rootUri: string) { } }, parsedCommandLine.options, - resolvedVueOptions, + parsedCommandLine.vueOptions, ); - const vueServicePlugins = createVueServicePlugins(ts, () => resolvedVueOptions); + const vueServicePlugins = createVueServicePlugins(ts, () => parsedCommandLine.vueOptions); 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..727b0cd7f7 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -1,12 +1,10 @@ -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 { startNamedPipeServer } from './lib/server'; -import { _getComponentNames } from './lib/requests/componentInfos'; -import { capitalize } from '@vue/shared'; +import { createFileRegistry, resolveCommonLanguageId } from '@vue/language-core'; +import type * as ts from 'typescript'; +import { decorateLanguageServiceForVue } from './lib/common'; +import { startNamedPipeServer, projects } from './lib/server'; const windowsPathReg = /\\/g; const externalFiles = new WeakMap>(); @@ -28,7 +26,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 +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, true); 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; - }; + 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 new file mode 100644 index 0000000000..15567245f8 --- /dev/null +++ b/packages/typescript-plugin/lib/common.ts @@ -0,0 +1,161 @@ +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'), + isTsPlugin: boolean, +) { + const { + getCompletionsAtPosition, + getCompletionEntryDetails, + getCodeFixesAtPosition, + getEncodedSemanticClassifications, + } = languageService; + + 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 = capitalize(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; + }; + 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( + componentSpan.start + template.startTagEnd, + componentSpan.length, + 256, // class + ); + } + } + } + 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, + }); + } + } + 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/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index e2655a49e5..96bf039764 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -1,16 +1,20 @@ -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, + getFileId: (fileName: string) => string, + }, + fileName: string, + templateCodeRange: [number, number], +) { + const { typescript: ts, languageService, files, isTsPlugin, getFileId } = this; - const match = getProject(fileName); - if (!match) { - return; - } - - const { info, files, ts } = match; - const volarFile = files.get(fileName); + const volarFile = files.get(getFileId(fileName)); if (!(volarFile?.generated?.code instanceof VueGeneratedCode)) { return; } @@ -20,7 +24,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..e7eccff4dc 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -1,27 +1,32 @@ 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; - const volarFile = files.get(fileName); +export function getComponentProps( + this: { + typescript: typeof import('typescript'); + 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, getFileId } = this; + const volarFile = files.get(getFileId(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 +91,30 @@ 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; - const volarFile = files.get(fileName); +export function getComponentEvents( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + getFileId: (fileName: string) => string, + }, + fileName: string, + tag: string, +) { + const { typescript: ts, files, vueOptions, languageService, getFileId } = this; + const volarFile = files.get(getFileId(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 +173,46 @@ 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; - const volarFile = files.get(fileName); +export function getTemplateContextProps( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + getFileId: (fileName: string) => string, + }, + fileName: string, +) { + const { typescript: ts, files, languageService, getFileId } = this; + const volarFile = files.get(getFileId(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; - const volarFile = files.get(fileName); +export function getComponentNames( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + getFileId: (fileName: string) => string, + }, + fileName: string, +) { + const { typescript: ts, files, vueOptions, languageService, getFileId } = this; + const volarFile = files.get(getFileId(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 +236,22 @@ export function _getComponentNames( ?? []; } -export function getElementAttrs(fileName: string, tagName: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files } = match; - const volarFile = files.get(fileName); +export function getElementAttrs( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + getFileId: (fileName: string) => string, + }, + fileName: string, + tagName: string, +) { + const { typescript: ts, files, languageService, getFileId } = this; + const volarFile = files.get(getFileId(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..a034c71d56 100644 --- a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts +++ b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts @@ -1,19 +1,21 @@ -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, + getFileId: (fileName: string) => string, + }, + fileName: string, + position: number, +) { + 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/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..a5bb025653 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,60 @@ 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, + getFileId: (fileName: string) => fileName, + }; + 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 +138,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)); + }); +} 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 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';