diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb93ed380..b0d82b3680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,97 @@ # Changelog +## 2.0.4 (2024/3/4) + +### Features + +- **vscode:** report requires TSDK version in doctor + +### Fixes + +- **typescript-plugin:** JSON parsing error when server data length > 8192 (#3961) + +## 2.0.3 (2024/3/3) + +### Features + +- **vscode:** identify #3942 in doctor + +### Fixes + +- **vscode:** compatible with VSCode 1.87.0 +- **vscode:** search "TypeScript and JavaScript Language Features" with id (#3932) +- **typescript-plugin:** more reliable connection to named pipe server (#3941) + +### Refactors + +- **language-service:** dependency injection typescript plugin (#3994) + +## 2.0.2 (2024/3/2) + +### Fixes + +- **vscode:** fix random `Cannot access 'i' before initialization` errors +- **typescript-plugin:** `vue-tsp-table.json` path is invalid in windows + +## 2.0.1 (2024/3/2) + +### Fixes + +- npm release does not include files (#3919) + +## 2.0.0 (2024/3/2) + +### Features + +- Hybrid Mode + - Takeover Mode has been deprecated. The extension now has the same performance as Takeover Mode by default. + - TypeScript language support has been moved from Vue language server to TypeScript plugin (#3788) + - Integrated all TypeScript editor features + - Warn when internal TypeScript extension is disabled or "TypeScript Vue Plugin" extension is installed + - Migrated to named pipe server using TypeScript LanguageService (#3908, #3916) + - `typescript.tsdk` duplicate registration errors are no longer reported + - **language-service:** reimplemented component tag semantic tokens in TypeScript plugin (#3915) + - **language-service:** reimplemented auto-import patching in TypeScript plugin (#3917) + - **language-service:** ensured tsserver readiness when requesting auto insert `.value` (#3914) +- Upgraded to Volar 2.0 and 2.1 (#3736, #3906) + - **vscode:** extension now compatible with [Volar Labs](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volarjs-labs) v2 + - **vscode:** removed `volar.format.initialIndent` option, replaced with 3 new options: + - `vue.format.template.initialIndent` + - `vue.format.script.initialIndent` + - `vue.format.style.initialIndent` + - **language-server:** `ignoreTriggerCharacters`, `reverseConfigFilePriority` and `fullCompletionList` options are no longer supported +- Supported Component Drag and Drop Import (#3692) +- **tsc:** supported `vueCompilerOptions.extensions` option (#3800) +- **language-core:** achieved compatibility with Vue 3.4 type changes (#3860) + +### Fixes + +- **vscode:** prevented reading undefined properties in non-VS Code editors (#3836) +- **vscode:** prevented extension activation with TS files +- **vscode:** corrected trace server ID +- **language-core:** implemented emit codegen for defineModel (#3895) +- **language-core:** addressed transition type incompatibility with Vue 2.7.16 (#3882) +- **language-core:** excluded vue directive syntax injection in Angular bindings (#3891) +- **component-type-helpers:** resolved inference issue for Vue 3.4.20 functional component + +### Refactors + +- Renamed "Volar Language Features (Volar)" extension to "Vue - Official" +- "TypeScript Vue Plugin" extension has been deprecated +- Relocated source scripts from `src` to `lib` (#3913) +- Replaced `typescript/lib/tsserverlibrary` imports with `typescript` +- **language-core:** implemented codegen based on Generator (#3778) +- **language-core:** generated global types in a single virtual file (#3803) +- **language-core:** implemented plugin API v2 (#3918) +- **language-core:** ignored nested codeblocks in markdown file (#3839) +- **language-core:** removed `experimentalAdditionalLanguageModules` and deprecated APIs (#3907) +- **language-service:** made service plugins independent of project context +- **language-server:** `volar.config.js` is no longer supported +- **component-meta:** renamed APIs +- **typescript-plugin:** renamed package to `@vue/typescript-plugin` (#3910) +- **tsc:** rewritten based on first-party TS API and no longer relies on TypeScript module (#3795) +- **tsc:** deprecated hooks API (#3793) + ## 1.8.27 (2023/12/26) - fix(language-core): remove misuse of `JSX.Element` for compatible with vue 3.4 (https://github.com/vuejs/core/issues/9923) diff --git a/README.md b/README.md index 16b6e7a6c4..1e27c38b02 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ *Component props, events, slots types information extract tool* - [vite-plugin-vue-component-preview](https://github.com/johnsoncodehk/vite-plugin-vue-component-preview) \ *Vite plugin for support Vue component preview view with `Vue Language Features`* +- [`@vue/language-server`](/packages/language-server/) \ +*The language server itself*. +- [`@vue/typescript-plugin`](/packages/typescript-plugin/) \ +*Typescript plugin for the language server*. ## Community Integration diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 69a183c49f..6526cbbd80 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "volar", - "version": "1.8.27", + "version": "2.0.4", "repository": { "type": "git", "url": "https://github.com/vuejs/language-tools.git", @@ -14,7 +14,7 @@ "url": "https://github.com/sponsors/johnsoncodehk" }, "icon": "images/icon.png", - "displayName": "Vue Language Support - Official", + "displayName": "Vue - Official", "description": "Language Support for Vue", "author": "johnsoncodehk", "publisher": "Vue", @@ -85,32 +85,6 @@ "url": "./dist/schemas/vue-tsconfig.deprecated.schema.json" } ], - "semanticTokenScopes": [ - { - "language": "vue", - "scopes": { - "component": [ - "support.class.component.vue" - ] - } - }, - { - "language": "markdown", - "scopes": { - "component": [ - "support.class.component.vue" - ] - } - }, - { - "language": "html", - "scopes": { - "component": [ - "support.class.component.vue" - ] - } - } - ], "languages": [ { "id": "vue", @@ -341,7 +315,7 @@ "items": { "type": "string" }, - "default": [ ], + "default": [], "description": "List any additional file extensions that should be processed as Vue files (requires restart)." }, "vue.doctor.status": { @@ -423,11 +397,6 @@ "default": true, "description": "Show name casing in status bar." }, - "vue.complete.normalizeComponentImportName": { - "type": "boolean", - "default": true, - "description": "Normalize import name for auto import. (\"myCompVue\" -> \"MyComp\")" - }, "vue.autoInsert.parentheses": { "type": "boolean", "default": true, @@ -576,9 +545,9 @@ "@types/semver": "^7.5.3", "@types/vscode": "^1.82.0", "@volar/vscode": "~2.1.0", - "@vue/language-core": "1.8.27", - "@vue/language-server": "1.8.27", - "@vue/typescript-plugin": "1.8.27", + "@vue/language-core": "2.0.4", + "@vue/language-server": "2.0.4", + "@vue/typescript-plugin": "2.0.4", "esbuild": "latest", "esbuild-plugin-copy": "latest", "esbuild-visualizer": "latest", diff --git a/extensions/vscode/src/features/doctor.ts b/extensions/vscode/src/features/doctor.ts index e4f0a88a7e..61aaac1c84 100644 --- a/extensions/vscode/src/features/doctor.ts +++ b/extensions/vscode/src/features/doctor.ts @@ -1,5 +1,5 @@ import { getTsdk } from '@volar/vscode'; -import { ParseSFCRequest } from '@vue/language-server'; +import { GetConnectedNamedPipeServerRequest, ParseSFCRequest } from '@vue/language-server'; import * as semver from 'semver'; import * as vscode from 'vscode'; import type { BaseLanguageClient } from 'vscode-languageclient'; @@ -244,15 +244,28 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan } */ - // check tsdk version should not be 4.9 + // check tsdk version should be higher than 5.0.0 const tsdk = await getTsdk(context); - if (tsdk?.version?.startsWith('4.9')) { + if (tsdk.version && !semver.gte(tsdk.version, '5.0.0')) { problems.push({ - title: 'Bad TypeScript version', + title: 'Requires TSDK 5.0 or higher', message: [ - 'TS 4.9 has a bug that will cause auto import to fail. Please downgrade to TS 4.8 or upgrade to TS 5.0+.', + `Extension >= 2.0 requires TSDK 5.0+. You are currently using TSDK ${tsdk.version}, please upgrade to TSDK.`, + 'If you need to use TSDK 4.x, please downgrade the extension to v1.', + ].join('\n'), + }); + } + + // #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/2190', + 'Issue: https://github.com/vuejs/language-tools/issues/3942', ].join('\n'), }); } diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index b5eb95f713..6f53366142 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -11,6 +11,9 @@ export async function activate(context: vscode.ExtensionContext) { let serverPathStatusItem: vscode.StatusBarItem | undefined; + const volarLabs = createLabsInfo(serverLib); + volarLabs.extensionExports.volarLabs.codegenStackSupport = true; + await commonActivate(context, ( id, name, @@ -106,31 +109,7 @@ export async function activate(context: vscode.ExtensionContext) { serverOptions, clientOptions, ); - client.start().then(() => { - const legend = client.initializeResult?.capabilities.semanticTokensProvider?.legend; - if (!legend) { - console.error('Server does not support semantic tokens'); - return; - } - // When tsserver has provided semantic tokens for the .vue file, VSCode will no longer request semantic tokens from the Vue language server, so it needs to be provided here again. - vscode.languages.registerDocumentSemanticTokensProvider(documentSelector, { - async provideDocumentSemanticTokens(document) { - const tokens = await client.sendRequest(lsp.SemanticTokensRequest.type, { - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), - } satisfies lsp.SemanticTokensParams); - return client.protocol2CodeConverter.asSemanticTokens(tokens); - }, - }, legend); - vscode.languages.registerDocumentRangeSemanticTokensProvider(documentSelector, { - async provideDocumentRangeSemanticTokens(document, range) { - const tokens = await client.sendRequest(lsp.SemanticTokensRangeRequest.type, { - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), - range: client.code2ProtocolConverter.asRange(range), - } satisfies lsp.SemanticTokensRangeParams); - return client.protocol2CodeConverter.asSemanticTokens(tokens); - }, - }, legend); - }); + client.start(); volarLabs.addLanguageClient(client); @@ -151,7 +130,7 @@ export async function activate(context: vscode.ExtensionContext) { 'Show Extension' ).then((selected) => { if (selected) { - vscode.commands.executeCommand('workbench.extensions.search', '@builtin TypeScript and JavaScript Language Features'); + vscode.commands.executeCommand('workbench.extensions.search', '@builtin typescript-language-features'); } }); } @@ -167,8 +146,6 @@ export async function activate(context: vscode.ExtensionContext) { }); } - const volarLabs = createLabsInfo(serverLib); - volarLabs.extensionExports.volarLabs.codegenStackSupport = true; return volarLabs.extensionExports; } @@ -193,9 +170,6 @@ function updateProviders(client: lsp.LanguageClient) { capabilities.workspace.fileOperations.willRename = undefined; } - // TODO: disalbe for now because this break ts plugin semantic tokens - capabilities.semanticTokensProvider = undefined; - return initializeFeatures.call(client, ...args); }; } @@ -211,11 +185,13 @@ try { // @ts-expect-error let text = readFileSync(...args) as string; - // patch jsTsLanguageModes - text = text.replace('t.$u=[t.$r,t.$s,t.$p,t.$q]', s => s + '.concat("vue")'); + // VSCode < 1.87.0 + text = text.replace('t.$u=[t.$r,t.$s,t.$p,t.$q]', s => s + '.concat("vue")'); // patch jsTsLanguageModes + text = text.replace('.languages.match([t.$p,t.$q,t.$r,t.$s]', s => s + '.concat("vue")'); // patch isSupportedLanguageMode - // patch isSupportedLanguageMode - text = text.replace('.languages.match([t.$p,t.$q,t.$r,t.$s]', s => s + '.concat("vue")'); + // VSCode >= 1.87.0 + 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 return text; } diff --git a/lerna.json b/lerna.json index 27866ceab8..ce0a3ca69e 100644 --- a/lerna.json +++ b/lerna.json @@ -6,5 +6,5 @@ "packages/*", "test-workspace" ], - "version": "1.8.27" + "version": "2.0.4" } diff --git a/packages/component-meta/package.json b/packages/component-meta/package.json index f12fe47c5a..74ff943b2c 100644 --- a/packages/component-meta/package.json +++ b/packages/component-meta/package.json @@ -1,10 +1,10 @@ { "name": "vue-component-meta", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ - "out/**/*.js", - "out/**/*.d.ts" + "**/*.js", + "**/*.d.ts" ], "repository": { "type": "git", @@ -13,9 +13,9 @@ }, "dependencies": { "@volar/typescript": "~2.1.0", - "@vue/language-core": "1.8.27", + "@vue/language-core": "2.0.4", "path-browserify": "^1.0.1", - "vue-component-type-helpers": "1.8.27" + "vue-component-type-helpers": "2.0.4" }, "peerDependencies": { "typescript": "*" diff --git a/packages/component-type-helpers/package.json b/packages/component-type-helpers/package.json index 9c9fa8f570..afa338dd70 100644 --- a/packages/component-type-helpers/package.json +++ b/packages/component-type-helpers/package.json @@ -1,10 +1,10 @@ { "name": "vue-component-type-helpers", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ - "*.js", - "*.d.ts" + "**/*.js", + "**/*.d.ts" ], "repository": { "type": "git", diff --git a/packages/language-core/lib/plugins.ts b/packages/language-core/lib/plugins.ts index a9aaf0e0b1..272af3bb9a 100644 --- a/packages/language-core/lib/plugins.ts +++ b/packages/language-core/lib/plugins.ts @@ -9,7 +9,7 @@ import useVueSfcStyles from './plugins/vue-sfc-styles'; import useVueSfcTemplate from './plugins/vue-sfc-template'; import useHtmlTemplatePlugin from './plugins/vue-template-html'; import useVueTsx from './plugins/vue-tsx'; -import type { VueCompilerOptions, VueLanguagePlugin } from './types'; +import { pluginVersion, type VueCompilerOptions, type VueLanguagePlugin } from './types'; import * as CompilerVue2 from './utils/vue2TemplateCompiler'; export function createPluginContext( @@ -61,9 +61,9 @@ export function getDefaultVueLanguagePlugins(pluginContext: Parameters { - const valid = plugin.version >= 2 && plugin.version < 3; + const valid = plugin.version === pluginVersion; if (!valid) { - console.warn(`Plugin ${JSON.stringify(plugin.name)} API version incompatible, expected 2.x but got ${JSON.stringify(plugin.version)}`); + console.warn(`Plugin ${JSON.stringify(plugin.name)} API version incompatible, expected ${JSON.stringify(pluginVersion)} but got ${JSON.stringify(plugin.version)}`); } return valid; }); diff --git a/packages/language-core/lib/plugins/vue-sfc-customblocks.ts b/packages/language-core/lib/plugins/vue-sfc-customblocks.ts index fc38bdeab5..de08db5b78 100644 --- a/packages/language-core/lib/plugins/vue-sfc-customblocks.ts +++ b/packages/language-core/lib/plugins/vue-sfc-customblocks.ts @@ -7,14 +7,14 @@ const plugin: VueLanguagePlugin = () => { version: 2, - getEmbeddedFiles(_fileName, sfc) { + getEmbeddedCodes(_fileName, sfc) { return sfc.customBlocks.map((customBlock, i) => ({ id: 'customBlock_' + i, lang: customBlock.lang, })); }, - resolveEmbeddedFile(_fileName, sfc, embeddedFile) { + resolveEmbeddedCode(_fileName, sfc, embeddedFile) { if (embeddedFile.id.startsWith('customBlock_')) { const index = parseInt(embeddedFile.id.slice('customBlock_'.length)); const customBlock = sfc.customBlocks[index]; diff --git a/packages/language-core/lib/plugins/vue-sfc-scripts.ts b/packages/language-core/lib/plugins/vue-sfc-scripts.ts index 3133d599d8..655bffa8b7 100644 --- a/packages/language-core/lib/plugins/vue-sfc-scripts.ts +++ b/packages/language-core/lib/plugins/vue-sfc-scripts.ts @@ -7,7 +7,7 @@ const plugin: VueLanguagePlugin = () => { version: 2, - getEmbeddedFiles(_fileName, sfc) { + getEmbeddedCodes(_fileName, sfc) { const names: { id: string; lang: string; @@ -21,7 +21,7 @@ const plugin: VueLanguagePlugin = () => { return names; }, - resolveEmbeddedFile(_fileName, sfc, embeddedFile) { + resolveEmbeddedCode(_fileName, sfc, embeddedFile) { const script = embeddedFile.id === 'scriptFormat' ? sfc.script : embeddedFile.id === 'scriptSetupFormat' ? sfc.scriptSetup : undefined; diff --git a/packages/language-core/lib/plugins/vue-sfc-styles.ts b/packages/language-core/lib/plugins/vue-sfc-styles.ts index 9f78e50c04..1b1ba20bf3 100644 --- a/packages/language-core/lib/plugins/vue-sfc-styles.ts +++ b/packages/language-core/lib/plugins/vue-sfc-styles.ts @@ -7,14 +7,14 @@ const plugin: VueLanguagePlugin = () => { version: 2, - getEmbeddedFiles(_fileName, sfc) { + getEmbeddedCodes(_fileName, sfc) { return sfc.styles.map((style, i) => ({ id: 'style_' + i, lang: style.lang, })); }, - resolveEmbeddedFile(_fileName, sfc, embeddedFile) { + resolveEmbeddedCode(_fileName, sfc, embeddedFile) { if (embeddedFile.id.startsWith('style_')) { const index = parseInt(embeddedFile.id.slice('style_'.length)); const style = sfc.styles[index]; diff --git a/packages/language-core/lib/plugins/vue-sfc-template.ts b/packages/language-core/lib/plugins/vue-sfc-template.ts index f0fe04e37d..3686f6fa34 100644 --- a/packages/language-core/lib/plugins/vue-sfc-template.ts +++ b/packages/language-core/lib/plugins/vue-sfc-template.ts @@ -7,7 +7,7 @@ const plugin: VueLanguagePlugin = () => { version: 2, - getEmbeddedFiles(_fileName, sfc) { + getEmbeddedCodes(_fileName, sfc) { if (sfc.template) { return [{ id: 'template', @@ -17,7 +17,7 @@ const plugin: VueLanguagePlugin = () => { return []; }, - resolveEmbeddedFile(_fileName, sfc, embeddedFile) { + resolveEmbeddedCode(_fileName, sfc, embeddedFile) { if (embeddedFile.id === 'template' && sfc.template) { embeddedFile.content.push([ sfc.template.content, diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index af5f7b2574..21036abadb 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -20,7 +20,7 @@ const plugin: VueLanguagePlugin = (ctx) => { 'exactOptionalPropertyTypes', ], - getEmbeddedFiles(fileName, sfc) { + getEmbeddedCodes(fileName, sfc) { const tsx = useTsx(fileName, sfc); const files: { @@ -40,7 +40,7 @@ const plugin: VueLanguagePlugin = (ctx) => { return files; }, - resolveEmbeddedFile(fileName, sfc, embeddedFile) { + resolveEmbeddedCode(fileName, sfc, embeddedFile) { const _tsx = useTsx(fileName, sfc); @@ -61,7 +61,7 @@ const plugin: VueLanguagePlugin = (ctx) => { } else if (embeddedFile.id === 'template_format') { - embeddedFile.parentFileId = 'template'; + embeddedFile.parentCodeId = 'template'; const template = _tsx.generatedTemplate(); if (template) { @@ -88,7 +88,7 @@ const plugin: VueLanguagePlugin = (ctx) => { } else if (embeddedFile.id === 'template_style') { - embeddedFile.parentFileId = 'template'; + embeddedFile.parentCodeId = 'template'; const template = _tsx.generatedTemplate(); if (template) { diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 96cdf23b3c..d42a57fabd 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -1,7 +1,7 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { SFCParseResult } from '@vue/compiler-sfc'; import type * as ts from 'typescript'; -import type { VueEmbeddedFile } from './virtualFile/embeddedFile'; +import type { VueEmbeddedCode } from './virtualFile/embeddedFile'; import type { CodeInformation, Segment } from '@volar/language-core'; export type { SFCParseResult } from '@vue/compiler-sfc'; @@ -57,6 +57,8 @@ export interface VueCompilerOptions { experimentalUseElementAccessInTemplate: boolean; } +export const pluginVersion = 2; + export type VueLanguagePlugin = (ctx: { modules: { typescript: typeof import('typescript'); @@ -67,7 +69,7 @@ export type VueLanguagePlugin = (ctx: { codegenStack: boolean; globalTypesHolder: string | undefined; }) => { - version: 2; + version: typeof pluginVersion; name?: string; order?: number; requiredCompilerOptions?: string[]; @@ -76,8 +78,8 @@ export type VueLanguagePlugin = (ctx: { resolveTemplateCompilerOptions?(options: CompilerDOM.CompilerOptions): CompilerDOM.CompilerOptions; compileSFCTemplate?(lang: string, template: string, options: CompilerDOM.CompilerOptions): CompilerDOM.CodegenResult | undefined; updateSFCTemplate?(oldResult: CompilerDOM.CodegenResult, textChange: { start: number, end: number, newText: string; }): CompilerDOM.CodegenResult | undefined; - getEmbeddedFiles?(fileName: string, sfc: Sfc): { id: string; lang: string; }[]; - resolveEmbeddedFile?(fileName: string, sfc: Sfc, embeddedFile: VueEmbeddedFile): void; + getEmbeddedCodes?(fileName: string, sfc: Sfc): { id: string; lang: string; }[]; + resolveEmbeddedCode?(fileName: string, sfc: Sfc, embeddedFile: VueEmbeddedCode): void; }; export interface SfcBlock { diff --git a/packages/language-core/lib/virtualFile/computedFiles.ts b/packages/language-core/lib/virtualFile/computedFiles.ts index b6f4decaa5..1a85937da1 100644 --- a/packages/language-core/lib/virtualFile/computedFiles.ts +++ b/packages/language-core/lib/virtualFile/computedFiles.ts @@ -2,7 +2,7 @@ import { VirtualCode, buildMappings, buildStacks, resolveCommonLanguageId, toStr import { computed } from 'computeds'; import type * as ts from 'typescript'; import type { Sfc, SfcBlock, VueLanguagePlugin } from '../types'; -import { VueEmbeddedFile } from './embeddedFile'; +import { VueEmbeddedCode } from './embeddedFile'; export function computedFiles( plugins: ReturnType[], @@ -56,7 +56,7 @@ export function computedFiles( codegenStacks, embeddedCodes: [], }); - console.error('Unable to resolve embedded: ' + file.parentFileId + ' -> ' + file.id); + console.error('Unable to resolve embedded: ' + file.parentCodeId + ' -> ' + file.id); } return embeddedCodes; @@ -64,7 +64,7 @@ export function computedFiles( function consumeRemain() { for (let i = remain.length - 1; i >= 0; i--) { const { file, snapshot, mappings, codegenStacks } = remain[i]; - if (!file.parentFileId) { + if (!file.parentCodeId) { embeddedCodes.push({ id: file.id, languageId: resolveCommonLanguageId(`/dummy.${file.lang}`), @@ -77,7 +77,7 @@ export function computedFiles( remain.splice(i, 1); } else { - const parent = findParentStructure(file.parentFileId, embeddedCodes); + const parent = findParentStructure(file.parentCodeId, embeddedCodes); if (parent) { parent.embeddedCodes ??= []; parent.embeddedCodes.push({ @@ -118,13 +118,13 @@ function computedPluginFiles( nameToBlock: () => Record, codegenStack: boolean ) { - const embeddedFiles: Record { file: VueEmbeddedFile; snapshot: ts.IScriptSnapshot; }> = {}; + const embeddedFiles: Record { file: VueEmbeddedCode; snapshot: ts.IScriptSnapshot; }> = {}; const files = computed(() => { try { - if (!plugin.getEmbeddedFiles) { + if (!plugin.getEmbeddedCodes) { return Object.values(embeddedFiles); } - const fileInfos = plugin.getEmbeddedFiles(fileName, sfc); + const fileInfos = plugin.getEmbeddedCodes(fileName, sfc); for (const oldId of Object.keys(embeddedFiles)) { if (!fileInfos.some(file => file.id === oldId)) { delete embeddedFiles[oldId]; @@ -134,13 +134,13 @@ function computedPluginFiles( if (!embeddedFiles[fileInfo.id]) { embeddedFiles[fileInfo.id] = computed(() => { const [content, stacks] = codegenStack ? track([]) : [[], []]; - const file = new VueEmbeddedFile(fileInfo.id, fileInfo.lang, content, stacks); + const file = new VueEmbeddedCode(fileInfo.id, fileInfo.lang, content, stacks); for (const plugin of plugins) { - if (!plugin.resolveEmbeddedFile) { + if (!plugin.resolveEmbeddedCode) { continue; } try { - plugin.resolveEmbeddedFile(fileName, sfc, file); + plugin.resolveEmbeddedCode(fileName, sfc, file); } catch (e) { console.error(e); diff --git a/packages/language-core/lib/virtualFile/embeddedFile.ts b/packages/language-core/lib/virtualFile/embeddedFile.ts index 506159a30d..4307ce6f29 100644 --- a/packages/language-core/lib/virtualFile/embeddedFile.ts +++ b/packages/language-core/lib/virtualFile/embeddedFile.ts @@ -1,11 +1,11 @@ import type { Mapping, StackNode } from '@volar/language-core'; import type { Code } from '../types'; -export class VueEmbeddedFile { +export class VueEmbeddedCode { - public parentFileId?: string; + public parentCodeId?: string; public linkedCodeMappings: Mapping[] = []; - public embeddedFiles: VueEmbeddedFile[] = []; + public embeddedCodes: VueEmbeddedCode[] = []; constructor( public id: string, diff --git a/packages/language-core/package.json b/packages/language-core/package.json index a19f66c42b..6421ee160c 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -1,10 +1,10 @@ { "name": "@vue/language-core", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ - "out/**/*.js", - "out/**/*.d.ts" + "**/*.js", + "**/*.d.ts" ], "repository": { "type": "git", diff --git a/packages/language-plugin-pug/package.json b/packages/language-plugin-pug/package.json index ded0d97d1b..63eaf32557 100644 --- a/packages/language-plugin-pug/package.json +++ b/packages/language-plugin-pug/package.json @@ -1,10 +1,10 @@ { "name": "@vue/language-plugin-pug", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ - "out/**/*.js", - "out/**/*.d.ts" + "**/*.js", + "**/*.d.ts" ], "repository": { "type": "git", @@ -13,7 +13,7 @@ }, "devDependencies": { "@types/node": "latest", - "@vue/language-core": "1.8.27" + "@vue/language-core": "2.0.4" }, "dependencies": { "@volar/source-map": "~2.1.0", diff --git a/packages/language-server/lib/protocol.ts b/packages/language-server/lib/protocol.ts index 3ab50a9fa7..d85d71f2a8 100644 --- a/packages/language-server/lib/protocol.ts +++ b/packages/language-server/lib/protocol.ts @@ -39,3 +39,13 @@ export namespace ParseSFCRequest { export type ErrorType = never; export const type = new vscode.RequestType('vue/parseSfc'); } + +export namespace GetConnectedNamedPipeServerRequest { + export type ParamsType = string; + export type ResponseType = { + path: string, + serverKind: number, + } | undefined; + export type ErrorType = never; + export const type = new vscode.RequestType('vue/namedPipeServer'); +} diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index 8af7655ef4..4c8ff8a98f 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -4,6 +4,8 @@ import { ParsedCommandLine, VueCompilerOptions, createParsedCommandLine, createV 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 { GetConnectedNamedPipeServerRequest } from './lib/protocol'; export const connection: Connection = createConnection(); @@ -15,7 +17,7 @@ let tsdk: ReturnType; connection.listen(); -connection.onInitialize(params => { +connection.onInitialize(async params => { const options: VueInitializationOptions = params.initializationOptions; @@ -29,13 +31,13 @@ connection.onInitialize(params => { } } - return server.initialize( + const result = await server.initialize( params, createSimpleProjectProviderFactory(), { watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions], getServicePlugins() { - return createVueServicePlugins(tsdk.typescript, env => envToVueOptions.get(env)!); + return createVueServicePlugins(tsdk.typescript, env => envToVueOptions.get(env)!, tsPluginClient); }, async getLanguagePlugins(serviceEnv, projectContext) { const [commandLine, vueOptions] = await parseCommandLine(); @@ -81,6 +83,11 @@ connection.onInitialize(params => { }, }, ); + + // handle by tsserver + @vue/typescript-plugin + result.capabilities.semanticTokensProvider = undefined; + + return result; }); connection.onInitialized(() => { @@ -98,21 +105,28 @@ connection.onRequest(ParseSFCRequest.type, params => { connection.onRequest(DetectNameCasingRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - return await detect(languageService.context, params.textDocument.uri); + return await detect(languageService.context, params.textDocument.uri, tsPluginClient); } }); connection.onRequest(GetConvertTagCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - return await convertTagName(languageService.context, params.textDocument.uri, params.casing); + return await convertTagName(languageService.context, params.textDocument.uri, params.casing, tsPluginClient); } }); connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - return await convertAttrName(languageService.context, params.textDocument.uri, params.casing); + return await convertAttrName(languageService.context, params.textDocument.uri, params.casing, tsPluginClient); + } +}); + +connection.onRequest(GetConnectedNamedPipeServerRequest.type, async fileName => { + const server = await tsPluginClient.searchNamedPipeServerForFile(fileName); + if (server) { + return server; } }); diff --git a/packages/language-server/package.json b/packages/language-server/package.json index f2026e8e93..edcadea70b 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -1,10 +1,10 @@ { "name": "@vue/language-server", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ - "out/**/*.js", - "out/**/*.d.ts" + "**/*.js", + "**/*.d.ts" ], "bin": { "vue-language-server": "./bin/vue-language-server.js" @@ -17,8 +17,9 @@ "dependencies": { "@volar/language-core": "~2.1.0", "@volar/language-server": "~2.1.0", - "@vue/language-core": "1.8.27", - "@vue/language-service": "1.8.27", + "@vue/language-core": "2.0.4", + "@vue/language-service": "2.0.4", + "@vue/typescript-plugin": "2.0.4", "vscode-languageserver-protocol": "^3.17.5" } } diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 9bd5b356fb..1a6ef91412 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -9,9 +9,9 @@ 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 createTypeScriptTwoslashQueriesServicePlugin } from 'volar-service-typescript-twoslash-queries'; import { create as createCssServicePlugin } from './lib/plugins/css'; -import { create as createTypeScriptServicePlugin } from './lib/plugins/typescript'; import { create as createVueAutoDotValueServicePlugin } from './lib/plugins/vue-autoinsert-dotvalue'; import { create as createVueAutoWrapParenthesesServicePlugin } from './lib/plugins/vue-autoinsert-parentheses'; import { create as createVueAutoAddSpaceServicePlugin } from './lib/plugins/vue-autoinsert-space'; @@ -28,25 +28,26 @@ import { create as createVueVisualizeHiddenCallbackParamServicePlugin } from './ export function createVueServicePlugins( ts: typeof import('typescript'), getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, + tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), ): ServicePlugin[] { return [ - createTypeScriptServicePlugin(ts, getVueOptions), + createTypeScriptServicePlugin(ts), createTypeScriptTwoslashQueriesServicePlugin(), createCssServicePlugin(), createPugFormatServicePlugin(), createJsonServicePlugin(), - createVueTemplateServicePlugin('html', ts, getVueOptions), - createVueTemplateServicePlugin('pug', ts, getVueOptions), + createVueTemplateServicePlugin('html', ts, getVueOptions, tsPluginClient), + createVueTemplateServicePlugin('pug', ts, getVueOptions, tsPluginClient), createVueSfcServicePlugin(), - createVueTwoslashQueriesServicePlugin(ts), + createVueTwoslashQueriesServicePlugin(ts, tsPluginClient), createVueReferencesCodeLensServicePlugin(), createVueDocumentDropServicePlugin(ts), - createVueAutoDotValueServicePlugin(ts), + createVueAutoDotValueServicePlugin(ts, tsPluginClient), createVueAutoWrapParenthesesServicePlugin(ts), createVueAutoAddSpaceServicePlugin(), createVueVisualizeHiddenCallbackParamServicePlugin(), createVueDirectiveCommentsServicePlugin(), - createVueExtractFileServicePlugin(ts), + createVueExtractFileServicePlugin(ts, tsPluginClient), createVueToggleVBindServicePlugin(ts), createEmmetServicePlugin(), ]; diff --git a/packages/language-service/lib/ideFeatures/nameCasing.ts b/packages/language-service/lib/ideFeatures/nameCasing.ts index f0fbc7bd3b..8e7d509a6f 100644 --- a/packages/language-service/lib/ideFeatures/nameCasing.ts +++ b/packages/language-service/lib/ideFeatures/nameCasing.ts @@ -2,12 +2,16 @@ import type { ServiceContext, VirtualCode } from '@volar/language-service'; import type { CompilerDOM } from '@vue/language-core'; import * as vue from '@vue/language-core'; import { VueGeneratedCode, hyphenateAttr, hyphenateTag } from '@vue/language-core'; -import * as namedPipeClient from '@vue/typescript-plugin/lib/client'; import { computed } from 'computeds'; import type * as vscode from 'vscode-languageserver-protocol'; import { AttrNameCasing, TagNameCasing } from '../types'; -export async function convertTagName(context: ServiceContext, uri: string, casing: TagNameCasing) { +export async function convertTagName( + context: ServiceContext, + uri: string, + casing: TagNameCasing, + tsPluginClient: typeof import('@vue/typescript-plugin/lib/client'), +) { const sourceFile = context.language.files.get(uri); if (!sourceFile) @@ -24,7 +28,7 @@ export async function convertTagName(context: ServiceContext, uri: string, casin const template = desc.template; const document = context.documents.get(sourceFile.id, sourceFile.languageId, sourceFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = await namedPipeClient.getComponentNames(rootCode.fileName) ?? []; + const components = await tsPluginClient.getComponentNames(rootCode.fileName) ?? []; const tags = getTemplateTagsAndAttrs(rootCode); for (const [tagName, { offsets }] of tags) { @@ -47,7 +51,12 @@ export async function convertTagName(context: ServiceContext, uri: string, casin return edits; } -export async function convertAttrName(context: ServiceContext, uri: string, casing: AttrNameCasing) { +export async function convertAttrName( + context: ServiceContext, + uri: string, + casing: AttrNameCasing, + tsPluginClient: typeof import('@vue/typescript-plugin/lib/client'), +) { const sourceFile = context.language.files.get(uri); if (!sourceFile) @@ -64,13 +73,13 @@ export async function convertAttrName(context: ServiceContext, uri: string, casi const template = desc.template; const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = await namedPipeClient.getComponentNames(rootCode.fileName) ?? []; + const components = await tsPluginClient.getComponentNames(rootCode.fileName) ?? []; const tags = getTemplateTagsAndAttrs(rootCode); for (const [tagName, { attrs }] of tags) { const componentName = components.find(component => component === tagName || hyphenateTag(component) === tagName); if (componentName) { - const props = await namedPipeClient.getComponentProps(rootCode.fileName, componentName) ?? []; + const props = await tsPluginClient.getComponentProps(rootCode.fileName, componentName) ?? []; for (const [attrName, { offsets }] of attrs) { const propName = props.find(prop => prop === attrName || hyphenateAttr(prop) === attrName); if (propName) { @@ -93,9 +102,13 @@ export async function convertAttrName(context: ServiceContext, uri: string, casi return edits; } -export async function getNameCasing(context: ServiceContext, uri: string) { +export async function getNameCasing( + context: ServiceContext, + uri: string, + tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), +) { - const detected = await detect(context, uri); + const detected = await detect(context, uri, tsPluginClient); const [attr, tag] = await Promise.all([ context.env.getConfiguration?.<'autoKebab' | 'autoCamel' | 'kebab' | 'camel'>('vue.complete.casing.props', uri), context.env.getConfiguration?.<'autoKebab' | 'autoPascal' | 'kebab' | 'pascal'>('vue.complete.casing.tags', uri), @@ -109,7 +122,11 @@ export async function getNameCasing(context: ServiceContext, uri: string) { }; } -export async function detect(context: ServiceContext, uri: string): Promise<{ +export async function detect( + context: ServiceContext, + uri: string, + tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), +): Promise<{ tag: TagNameCasing[], attr: AttrNameCasing[], }> { @@ -153,7 +170,7 @@ export async function detect(context: ServiceContext, uri: string): Promise<{ } async function getTagNameCase(file: VueGeneratedCode): Promise { - const components = await namedPipeClient.getComponentNames(file.fileName) ?? []; + const components = await tsPluginClient?.getComponentNames(file.fileName) ?? []; const tagNames = getTemplateTagsAndAttrs(file); const result: TagNameCasing[] = []; diff --git a/packages/language-service/lib/plugins/typescript.ts b/packages/language-service/lib/plugins/typescript.ts deleted file mode 100644 index 8d32627fdd..0000000000 --- a/packages/language-service/lib/plugins/typescript.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { ServiceEnvironment, ServicePlugin, ServicePluginInstance } from '@volar/language-service'; -import { VueCompilerOptions, VueGeneratedCode, hyphenateTag, scriptRanges } from '@vue/language-core'; -import { capitalize } from '@vue/shared'; -import * as ts from 'typescript'; -import { create as baseCreate } from 'volar-service-typescript'; -import type { Data } from 'volar-service-typescript/lib/features/completions/basic'; -import { getNameCasing } from '../ideFeatures/nameCasing'; -import { TagNameCasing } from '../types'; -import { createAddComponentToOptionEdit } from './vue-extract-file'; - -// TODO: migrate patchs to ts plugin - -const asts = new WeakMap(); - -export function getAst(fileName: string, snapshot: ts.IScriptSnapshot, scriptKind?: ts.ScriptKind) { - let ast = asts.get(snapshot); - if (!ast) { - ast = ts.createSourceFile(fileName, snapshot.getText(0, snapshot.getLength()), ts.ScriptTarget.Latest, undefined, scriptKind); - asts.set(snapshot, ast); - } - return ast; -} - -export function create( - ts: typeof import('typescript'), - getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, -): ServicePlugin { - - const base = baseCreate(ts); - - return { - ...base, - create(context): ServicePluginInstance { - const baseInstance = base.create(context); - return { - ...baseInstance, - async provideCompletionItems(document, position, completeContext, item) { - const result = await baseInstance.provideCompletionItems?.(document, position, completeContext, item); - if (result) { - - // filter __VLS_ - result.items = result.items.filter(item => - item.label.indexOf('__VLS_') === -1 - && (!item.labelDetails?.description || item.labelDetails.description.indexOf('__VLS_') === -1) - ); - - // handle component auto-import patch - let casing: Awaited> | undefined; - - const [virtualCode, sourceFile] = context.documents.getVirtualCodeByUri(document.uri); - - if (virtualCode && sourceFile) { - - for (const map of context.documents.getMaps(virtualCode)) { - - const sourceVirtualFile = context.language.files.get(map.sourceDocument.uri)?.generated?.code; - - if (sourceVirtualFile instanceof VueGeneratedCode) { - - const isAutoImport = !!map.getSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.onlyImport); - if (isAutoImport) { - - for (const item of result.items) { - item.data.__isComponentAutoImport = true; - } - - // fix #2458 - casing ??= await getNameCasing(context, sourceFile.id); - - if (casing.tag === TagNameCasing.Kebab) { - for (const item of result.items) { - item.filterText = hyphenateTag(item.filterText ?? item.label); - } - } - } - } - } - } - } - return result; - }, - async resolveCompletionItem(item, token) { - - item = await baseInstance.resolveCompletionItem?.(item, token) ?? item; - - const itemData = item.data as { uri?: string; } | undefined; - - let newName: string | undefined; - - for (const ext of getVueOptions(context.env).extensions) { - const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue - if ( - itemData?.uri - && item.textEdit?.newText.endsWith(suffix) - && item.additionalTextEdits?.length === 1 && item.additionalTextEdits[0].newText.indexOf('import ' + item.textEdit.newText + ' from ') >= 0 - && (await context.env.getConfiguration?.('vue.complete.normalizeComponentImportName') ?? true) - ) { - newName = item.textEdit.newText.slice(0, -suffix.length); - newName = newName[0].toUpperCase() + newName.substring(1); - if (newName === 'Index') { - const tsItem = (item.data as Data).originalItem; - if (tsItem.source) { - const dirs = tsItem.source.split('/'); - if (dirs.length >= 3) { - newName = dirs[dirs.length - 2]; - newName = newName[0].toUpperCase() + newName.substring(1); - } - } - } - item.additionalTextEdits[0].newText = item.additionalTextEdits[0].newText.replace( - 'import ' + item.textEdit.newText + ' from ', - 'import ' + newName + ' from ', - ); - item.textEdit.newText = newName; - const [_, sourceFile] = context.documents.getVirtualCodeByUri(itemData.uri); - if (sourceFile) { - const casing = await getNameCasing(context, sourceFile.id); - if (casing.tag === TagNameCasing.Kebab) { - item.textEdit.newText = hyphenateTag(item.textEdit.newText); - } - } - } - else if (item.textEdit?.newText && new RegExp(`import \\w*${suffix}\\$1 from [\\S\\s]*`).test(item.textEdit.newText)) { - // https://github.com/vuejs/language-tools/issues/2286 - item.textEdit.newText = item.textEdit.newText.replace(`${suffix}$1`, '$1'); - } - } - - const data: Data = item.data; - if (item.data?.__isComponentAutoImport && data && item.additionalTextEdits?.length && item.textEdit && itemData?.uri) { - const [virtualCode, sourceFile] = context.documents.getVirtualCodeByUri(itemData.uri); - if (virtualCode && (sourceFile.generated?.code instanceof VueGeneratedCode)) { - const script = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (script) { - const ast = getAst(sourceFile.generated.code.fileName, script.code.snapshot, script.scriptKind); - const exportDefault = scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault; - if (exportDefault) { - const componentName = newName ?? item.textEdit.newText; - const optionEdit = createAddComponentToOptionEdit(ts, ast, componentName); - if (optionEdit) { - const textDoc = context.documents.get(context.documents.getVirtualCodeUri(sourceFile.id, virtualCode.id), virtualCode.languageId, virtualCode.snapshot); - item.additionalTextEdits.push({ - range: { - start: textDoc.positionAt(optionEdit.range.start), - end: textDoc.positionAt(optionEdit.range.end), - }, - newText: optionEdit.newText, - }); - } - } - } - } - } - - return item; - }, - async provideCodeActions(document, range, context, token) { - const result = await baseInstance.provideCodeActions?.(document, range, context, token); - return result?.filter(codeAction => codeAction.title.indexOf('__VLS_') === -1); - }, - async provideSemanticDiagnostics(document, token) { - const result = await baseInstance.provideSemanticDiagnostics?.(document, token); - return result?.map(diagnostic => { - if ( - diagnostic.source === 'ts' - && diagnostic.code === 2578 /* Unused '@ts-expect-error' directive. */ - && document.getText(diagnostic.range) === '// @ts-expect-error __VLS_TS_EXPECT_ERROR' - ) { - diagnostic.source = 'vue'; - diagnostic.code = 'ts-2578'; - diagnostic.message = diagnostic.message.replace(/@ts-expect-error/g, '@vue-expect-error'); - } - return diagnostic; - }); - }, - }; - }, - }; -} diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index a733659707..d869600941 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -1,15 +1,28 @@ import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { hyphenateAttr } from '@vue/language-core'; -import * as namedPipeClient from '@vue/typescript-plugin/lib/client'; import type * as ts from 'typescript'; import type * as vscode from 'vscode-languageserver-protocol'; import type { TextDocument } from 'vscode-languageserver-textdocument'; -import { getAst } from './typescript'; -export function create(ts: typeof import('typescript')): ServicePlugin { +const asts = new WeakMap(); + +function getAst(ts: typeof import('typescript'), fileName: string, snapshot: ts.IScriptSnapshot, scriptKind?: ts.ScriptKind) { + let ast = asts.get(snapshot); + if (!ast) { + ast = ts.createSourceFile(fileName, snapshot.getText(0, snapshot.getLength()), ts.ScriptTarget.Latest, undefined, scriptKind); + asts.set(snapshot, ast); + } + return ast; +} + +export function create( + ts: typeof import('typescript'), + tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), +): ServicePlugin { return { name: 'vue-autoinsert-dotvalue', create(context): ServicePluginInstance { + let currentReq = 0; return { async provideAutoInsertionEdit(document, position, lastChange) { @@ -19,34 +32,52 @@ export function create(ts: typeof import('typescript')): ServicePlugin { if (!isCharacterTyping(document, lastChange)) return; + const req = ++currentReq; + // Wait for tsserver to sync + await sleep(250); + if (req !== currentReq) + return; + const enabled = await context.env.getConfiguration?.('vue.autoInsert.dotValue') ?? true; if (!enabled) return; - const [_, file] = context.documents.getVirtualCodeByUri(document.uri); + const [code, file] = context.documents.getVirtualCodeByUri(document.uri); + if (!file) + return; - let fileName: string | undefined; let ast: ts.SourceFile | undefined; + let sourceCodeOffset = document.offsetAt(position); + + const fileName = context.env.typescript!.uriToFileName(file.id); if (file?.generated) { const script = file.generated.languagePlugin.typescript?.getScript(file.generated.code); - if (script) { - fileName = context.env.typescript!.uriToFileName(file.id); - ast = getAst(fileName, script.code.snapshot, script.scriptKind); + if (script?.code !== code) { + return; + } + ast = getAst(ts, fileName, script.code.snapshot, script.scriptKind); + let mapped = false; + for (const [_1, [_2, map]] of context.language.files.getMaps(code)) { + const sourceOffset = map.getSourceOffset(document.offsetAt(position)); + if (sourceOffset !== undefined) { + sourceCodeOffset = sourceOffset[0]; + mapped = true; + break; + } + } + if (!mapped) { + return; } } - else if (file) { - fileName = context.env.typescript!.uriToFileName(file.id); - ast = getAst(fileName, file.snapshot); + else { + ast = getAst(ts, fileName, file.snapshot); } - if (!ast || !fileName) - return; - if (isBlacklistNode(ts, ast, document.offsetAt(position), false)) return; - const props = await namedPipeClient.getPropertiesAtLocation(fileName, document.offsetAt(position)) ?? []; + const props = await tsPluginClient?.getPropertiesAtLocation(fileName, sourceCodeOffset) ?? []; if (props.some(prop => prop === 'value')) { return '${1:.value}'; } @@ -56,6 +87,10 @@ export function create(ts: typeof import('typescript')): ServicePlugin { }; } +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + function isTsDocument(document: TextDocument) { return document.languageId === 'javascript' || document.languageId === 'typescript' || diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index c85f81f34f..19e92ce1c4 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -1,7 +1,6 @@ import type { CreateFile, ServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service'; import type { ExpressionNode, TemplateChildNode } from '@vue/compiler-dom'; import { Sfc, VueGeneratedCode, scriptRanges } from '@vue/language-core'; -import { collectExtractProps } from '@vue/typescript-plugin/lib/client'; import type * as ts from 'typescript'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -13,7 +12,10 @@ interface ActionData { const unicodeReg = /\\u/g; -export function create(ts: typeof import('typescript')): ServicePlugin { +export function create( + ts: typeof import('typescript'), + tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), +): ServicePlugin { return { name: 'vue-extract-file', create(context) { @@ -73,7 +75,7 @@ export function create(ts: typeof import('typescript')): ServicePlugin { if (!templateCodeRange) return codeAction; - const toExtract = await collectExtractProps(sourceFile.generated.code.fileName, templateCodeRange) ?? []; + const toExtract = await tsPluginClient?.collectExtractProps(sourceFile.generated.code.fileName, templateCodeRange) ?? []; if (!toExtract) return codeAction; diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index e57bf1ffa8..223566a3ca 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -1,7 +1,6 @@ import type { Disposable, ServiceEnvironment, ServicePluginInstance } from '@volar/language-service'; import { VueGeneratedCode, hyphenateAttr, hyphenateTag, parseScriptSetupRanges, tsCodegen } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; -import * as namedPipeClient from '@vue/typescript-plugin/lib/client'; import { create as createHtmlService } from 'volar-service-html'; import { create as createPugService } from 'volar-service-pug'; import * as html from 'vscode-html-languageservice'; @@ -18,6 +17,7 @@ export function create( mode: 'html' | 'pug', ts: typeof import('typescript'), getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, + tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), ): ServicePlugin { let customData: html.IHTMLDataProvider[] = []; @@ -141,8 +141,8 @@ export function create( if (code instanceof VueGeneratedCode && scanner) { // visualize missing required props - const casing = await getNameCasing(context, map.sourceDocument.uri); - const components = await namedPipeClient.getComponentNames(code.fileName) ?? []; + const casing = await getNameCasing(context, map.sourceDocument.uri, tsPluginClient); + const components = await tsPluginClient?.getComponentNames(code.fileName) ?? []; const componentProps: Record = {}; let token: html.TokenType; let current: { @@ -159,7 +159,7 @@ export function create( : components.find(component => component === tagName || hyphenateTag(component) === tagName); const checkTag = tagName.indexOf('.') >= 0 ? tagName : component; if (checkTag) { - componentProps[checkTag] ??= await namedPipeClient.getComponentProps(code.fileName, checkTag, true) ?? []; + componentProps[checkTag] ??= await tsPluginClient?.getComponentProps(code.fileName, checkTag, true) ?? []; current = { unburnedRequiredProps: [...componentProps[checkTag]], labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(), @@ -303,76 +303,11 @@ export function create( ]; } }, - - async provideDocumentSemanticTokens(document, range, legend, token) { - - if (!isSupportedDocument(document)) - return; - - const result = await baseServiceInstance.provideDocumentSemanticTokens?.(document, range, legend, token) ?? []; - const scanner = getScanner(baseServiceInstance, document); - if (!scanner) - return; - - const [virtualCode] = context.documents.getVirtualCodeByUri(document.uri); - if (!virtualCode) - return; - - for (const map of context.documents.getMaps(virtualCode)) { - - const code = context.language.files.get(map.sourceDocument.uri)?.generated?.code; - if (!(code instanceof VueGeneratedCode)) - continue; - - const templateScriptData = await namedPipeClient.getComponentNames(code.fileName) ?? []; - const components = new Set([ - ...templateScriptData, - ...templateScriptData.map(hyphenateTag), - ]); - const offsetRange = { - start: document.offsetAt(range.start), - end: document.offsetAt(range.end), - }; - - let token = scanner.scan(); - - while (token !== html.TokenType.EOS) { - - const tokenOffset = scanner.getTokenOffset(); - - // TODO: fix source map perf and break in while condition - if (tokenOffset > offsetRange.end) - break; - - if (tokenOffset >= offsetRange.start && (token === html.TokenType.StartTag || token === html.TokenType.EndTag)) { - - const tokenText = scanner.getTokenText(); - - if (components.has(tokenText) || tokenText.indexOf('.') >= 0) { - - const tokenLength = scanner.getTokenLength(); - const tokenPosition = document.positionAt(tokenOffset); - - if (components.has(tokenText)) { - let tokenType = legend.tokenTypes.indexOf('component'); - if (tokenType === -1) { - tokenType = legend.tokenTypes.indexOf('class'); - } - result.push([tokenPosition.line, tokenPosition.character, tokenLength, tokenType, 0]); - } - } - } - token = scanner.scan(); - } - } - - return result; - }, }; async function provideHtmlData(sourceDocumentUri: string, vueCode: VueGeneratedCode) { - const casing = await getNameCasing(context, sourceDocumentUri); + const casing = await getNameCasing(context, sourceDocumentUri, tsPluginClient); if (builtInData.tags) { for (const tag of builtInData.tags) { @@ -410,7 +345,7 @@ export function create( provideTags: () => { if (!components) { promises.push((async () => { - components = (await namedPipeClient.getComponentNames(vueCode.fileName) ?? []) + components = (await tsPluginClient?.getComponentNames(vueCode.fileName) ?? []) .filter(name => name !== 'Transition' && name !== 'TransitionGroup' @@ -456,16 +391,16 @@ export function create( }, provideAttributes: (tag) => { - namedPipeClient.getTemplateContextProps; + tsPluginClient?.getTemplateContextProps; let failed = false; let tagInfo = tagInfos.get(tag); if (!tagInfo) { promises.push((async () => { - const attrs = await namedPipeClient.getElementAttrs(vueCode.fileName, tag) ?? []; - const props = await namedPipeClient.getComponentProps(vueCode.fileName, tag) ?? []; - const events = await namedPipeClient.getComponentEvents(vueCode.fileName, tag) ?? []; + const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? []; + const props = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? []; + const events = await tsPluginClient?.getComponentEvents(vueCode.fileName, tag) ?? []; tagInfos.set(tag, { attrs, props, @@ -488,7 +423,7 @@ export function create( if (_tsCodegen) { if (!templateContextProps) { promises.push((async () => { - templateContextProps = await namedPipeClient.getTemplateContextProps(vueCode.fileName) ?? []; + templateContextProps = await tsPluginClient?.getTemplateContextProps(vueCode.fileName) ?? []; version++; })()); return []; @@ -621,7 +556,7 @@ export function create( const replacement = getReplacement(completionList, sourceDocument); const componentNames = new Set( - (await namedPipeClient.getComponentNames(code.fileName) ?? []) + (await tsPluginClient?.getComponentNames(code.fileName) ?? []) .map(hyphenateTag) ); diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index 40aac2d417..e3ca3b25b4 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -1,11 +1,13 @@ import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import * as vue from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; -import { getQuickInfoAtPosition } from '@vue/typescript-plugin/lib/client'; const twoslashReg = //g; -export function create(ts: typeof import('typescript')): ServicePlugin { +export function create( + ts: typeof import('typescript'), + tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), +): ServicePlugin { return { name: 'vue-twoslash-queries', create(context): ServicePluginInstance { @@ -31,7 +33,7 @@ export function create(ts: typeof import('typescript')): ServicePlugin { for (const [pointerPosition, hoverOffset] of hoverOffsets) { for (const [_1, [_2, map]] of context.language.files.getMaps(virtualCode)) { for (const [sourceOffset] of map.getSourceOffsets(hoverOffset)) { - const quickInfo = await getQuickInfoAtPosition(sourceFile.generated.code.fileName, sourceOffset); + const quickInfo = await tsPluginClient?.getQuickInfoAtPosition(sourceFile.generated.code.fileName, sourceOffset); if (quickInfo) { inlayHints.push({ position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 6e2358a90b..ddcd3b2263 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -1,11 +1,11 @@ { "name": "@vue/language-service", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ "data", - "out/**/*.js", - "out/**/*.d.ts" + "**/*.js", + "**/*.d.ts" ], "repository": { "type": "git", @@ -20,9 +20,8 @@ "@volar/language-service": "~2.1.0", "@volar/typescript": "~2.1.0", "@vue/compiler-dom": "^3.4.0", - "@vue/language-core": "1.8.27", + "@vue/language-core": "2.0.4", "@vue/shared": "^3.4.0", - "@vue/typescript-plugin": "1.8.27", "computeds": "^0.0.1", "path-browserify": "^1.0.1", "volar-service-css": "0.0.31", @@ -40,6 +39,7 @@ "@types/node": "latest", "@types/path-browserify": "latest", "@volar/kit": "~2.1.0", + "@vue/typescript-plugin": "2.0.4", "vscode-languageserver-protocol": "^3.17.5", "vscode-uri": "^3.0.8" } diff --git a/packages/language-service/tests/complete.ts b/packages/language-service/tests/complete.ts index 114d8de024..41c20c95c5 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')(`complete: ${dirName}`, async () => { + describe.skipIf(dirName === 'core#8811' || dirName === '#2511' || dirName === 'component-auto-import')(`complete: ${dirName}`, async () => { const dir = path.join(baseDir, dirName); const inputFiles = readFiles(path.join(dir, 'input')); diff --git a/packages/tsc/package.json b/packages/tsc/package.json index 14a4b49698..fbe7cfb89b 100644 --- a/packages/tsc/package.json +++ b/packages/tsc/package.json @@ -1,11 +1,11 @@ { "name": "vue-tsc", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ "bin", - "out/**/*.js", - "out/**/*.d.ts" + "**/*.js", + "**/*.d.ts" ], "repository": { "type": "git", @@ -17,7 +17,7 @@ }, "dependencies": { "@volar/typescript": "~2.1.0", - "@vue/language-core": "1.8.27", + "@vue/language-core": "2.0.4", "semver": "^7.5.4" }, "peerDependencies": { diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index b75aa6c3f6..96325be49d 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -5,6 +5,8 @@ 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'; const windowsPathReg = /\\/g; const externalFiles = new WeakMap(); @@ -59,14 +61,126 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory { decorateLanguageService(files, info.languageService); decorateLanguageServiceHost(files, info.languageServiceHost, ts); - startNamedPipeServer(); + 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) { - result.entries = result.entries.filter(entry => entry.name.indexOf('__VLS_') === -1); + // 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; }; diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index 3712785602..b425fbc7e5 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -1,6 +1,10 @@ -import * as net from 'net'; +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 { pipeFile } from './utils'; +import type { NamedPipeServer } from './utils'; +import { connect, pipeTable } from './utils'; export function collectExtractProps( ...args: Parameters @@ -76,25 +80,60 @@ export function getElementAttrs( }); } -function sendRequest(request: Request) { - return new Promise(resolve => { - try { - const client = net.connect(pipeFile); - client.on('connect', () => { - client.write(JSON.stringify(request)); - }); - client.on('data', data => { - const text = data.toString(); - resolve(JSON.parse(text)); - client.end(); - }); - client.on('error', err => { - console.error('[Vue Named Pipe Client]', err); - return resolve(undefined); - }); - } catch (e) { - console.error('[Vue Named Pipe Client]', e); - return resolve(undefined); +async function sendRequest(request: Request) { + const server = await searchNamedPipeServerForFile(request.args[0]); + if (!server) { + console.warn('[Vue Named Pipe Client] No server found for', request.args[0]); + return; + } + const client = await connect(server.path); + if (!client) { + console.warn('[Vue Named Pipe Client] Failed to connect to', server.path); + return; + } + 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', () => { + const data = Buffer.concat(dataChunks); + const text = data.toString(); + resolve(JSON.parse(text)); + }); + client.write(JSON.stringify(request)); }); } diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index 8bef775384..f93fa6e1c2 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -200,6 +200,21 @@ export function getComponentNames(fileName: string) { ?? []; } +export function _getComponentNames( + ts: typeof import('typescript'), + tsLs: ts.LanguageService, + vueCode: vue.VueGeneratedCode, + vueOptions: vue.VueCompilerOptions, +) { + return getVariableType(ts, tsLs, vueCode, '__VLS_components') + ?.type + ?.getProperties() + .map(c => c.name) + .filter(entry => entry.indexOf('$') === -1 && !entry.startsWith('_')) + .filter(entry => !vueOptions.nativeTags.includes(entry)) + ?? []; +} + export function getElementAttrs(fileName: string, tagName: string) { const match = getProject(fileName); if (!match) { diff --git a/packages/typescript-plugin/lib/requests/containsFile.ts b/packages/typescript-plugin/lib/requests/containsFile.ts new file mode 100644 index 0000000000..d191f258ab --- /dev/null +++ b/packages/typescript-plugin/lib/requests/containsFile.ts @@ -0,0 +1,5 @@ +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 aa8eb171bd..fd39eded66 100644 --- a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts +++ b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts @@ -1,3 +1,4 @@ +import { isCompletionEnabled } from '@vue/language-core'; import { getProject } from '../utils'; import type * as ts from 'typescript'; @@ -10,6 +11,36 @@ export function getPropertiesAtLocation(fileName: string, position: number, isTs const { info, files, ts } = match; const languageService = info.languageService; + + // mapping + const file = files.get(fileName); + if (file?.generated) { + const virtualScript = file.generated.languagePlugin.typescript?.getScript(file.generated.code); + if (!virtualScript) { + return; + } + let mapped = false; + for (const [_1, [_2, map]] of files.getMaps(virtualScript.code)) { + for (const [position2, mapping] of map.getGeneratedOffsets(position)) { + if (isCompletionEnabled(mapping.data)) { + position = position2; + mapped = true; + break; + } + } + if (mapped) { + break; + } + } + if (!mapped) { + return; + } + if (isTsPlugin) { + position += file.snapshot.getLength(); + } + } + + const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; @@ -20,8 +51,7 @@ export function getPropertiesAtLocation(fileName: string, position: number, isTs return; } - const volarFile = files.get(fileName); - const node = findPositionIdentifier(sourceFile, sourceFile, position + (isTsPlugin ? (volarFile?.snapshot.getLength() ?? 0) : 0)); + const node = findPositionIdentifier(sourceFile, sourceFile, position); if (!node) { return; } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index 62f49afb54..029f7d03b6 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -1,9 +1,16 @@ import * as fs from 'fs'; import * as net from 'net'; -import { pipeFile } from './utils'; +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'; export interface Request { - type: 'collectExtractProps' + type: 'containsFile' + | 'collectExtractProps' | 'getPropertiesAtLocation' | 'getQuickInfoAtPosition' // Component Infos @@ -17,56 +24,98 @@ export interface Request { let started = false; -export function startNamedPipeServer() { +export function startNamedPipeServer(serverKind: ts.server.ProjectKind, currentDirectory: string) { + if (started) return; started = true; + + const pipeFile = process.platform === 'win32' + ? `\\\\.\\pipe\\vue-tsp-${process.pid}` + : `/tmp/vue-tsp-${process.pid}`; const server = net.createServer(connection => { - connection.on('data', async data => { - const request: Request = JSON.parse(data.toString()); - if (request.type === 'collectExtractProps') { - const result = (await import('./requests/collectExtractProps.js')).collectExtractProps.apply(null, request.args); + 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 = (await import('./requests/getPropertiesAtLocation.js')).getPropertiesAtLocation.apply(null, request.args); + const result = getPropertiesAtLocation.apply(null, request.args); connection.write(JSON.stringify(result ?? null)); } else if (request.type === 'getQuickInfoAtPosition') { - const result = (await import('./requests/getQuickInfoAtPosition.js')).getQuickInfoAtPosition.apply(null, request.args); + const result = getQuickInfoAtPosition.apply(null, request.args); connection.write(JSON.stringify(result ?? null)); } // Component Infos else if (request.type === 'getComponentProps') { - const result = (await import('./requests/componentInfos.js')).getComponentProps.apply(null, request.args); + const result = getComponentProps.apply(null, request.args); connection.write(JSON.stringify(result ?? null)); } else if (request.type === 'getComponentEvents') { - const result = (await import('./requests/componentInfos.js')).getComponentEvents.apply(null, request.args); + const result = getComponentEvents.apply(null, request.args); connection.write(JSON.stringify(result ?? null)); } else if (request.type === 'getTemplateContextProps') { - const result = (await import('./requests/componentInfos.js')).getTemplateContextProps.apply(null, request.args); + const result = getTemplateContextProps.apply(null, request.args); connection.write(JSON.stringify(result ?? null)); } else if (request.type === 'getComponentNames') { - const result = (await import('./requests/componentInfos.js')).getComponentNames.apply(null, request.args); + const result = getComponentNames.apply(null, request.args); connection.write(JSON.stringify(result ?? null)); } else if (request.type === 'getElementAttrs') { - const result = (await import('./requests/componentInfos.js')).getElementAttrs.apply(null, request.args); + const result = getElementAttrs.apply(null, request.args); connection.write(JSON.stringify(result ?? null)); } else { console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); connection.write(JSON.stringify(null)); } + connection.end(); }); connection.on('error', err => console.error('[Vue Named Pipe Server]', err.message)); }); + cleanupPipeTable(); + + if (!fs.existsSync(pipeTable)) { + fs.writeFileSync(pipeTable, JSON.stringify([] satisfies NamedPipeServer[])); + } + const table: NamedPipeServer[] = JSON.parse(fs.readFileSync(pipeTable, 'utf8')); + table.push({ + path: pipeFile, + serverKind, + currentDirectory, + }); + fs.writeFileSync(pipeTable, JSON.stringify(table, undefined, 2)); + try { fs.unlinkSync(pipeFile); } catch { } server.listen(pipeFile); } + +function cleanupPipeTable() { + if (!fs.existsSync(pipeTable)) { + return; + } + for (const server of JSON.parse(fs.readFileSync(pipeTable, 'utf8'))) { + connect(server.path).then(client => { + if (client) { + client.end(); + } + else { + let table: NamedPipeServer[] = JSON.parse(fs.readFileSync(pipeTable, 'utf8')); + table = table.filter(item => item.path !== server.path); + fs.writeFileSync(pipeTable, JSON.stringify(table, undefined, 2)); + } + }); + } +} diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index ff7f33cc6e..9d9c79915b 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -1,7 +1,18 @@ -import type * as ts from 'typescript'; 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'; + +export interface NamedPipeServer { + path: string; + serverKind: ts.server.ProjectKind; + currentDirectory: string; +} -export const pipeFile = process.platform === 'win32' ? '\\\\.\\pipe\\vue-tsp' : '/tmp/vue-tsp'; +const { version } = require('../package.json'); + +export const pipeTable = path.join(os.tmpdir(), `vue-tsp-table-${version}.json`); export const projects = new Map(resolve => { + const client = net.connect(path); + client.on('connect', () => { + resolve(client); + }); + client.on('error', () => { + return resolve(undefined); + }); + }); +} diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index b3d62b463a..3e3bb3d1f7 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -1,10 +1,10 @@ { "name": "@vue/typescript-plugin", - "version": "1.8.27", + "version": "2.0.4", "license": "MIT", "files": [ - "out/**/*.js", - "out/**/*.d.ts" + "**/*.js", + "**/*.d.ts" ], "repository": { "type": "git", @@ -13,7 +13,7 @@ }, "dependencies": { "@volar/typescript": "~2.1.0", - "@vue/language-core": "1.8.27", + "@vue/language-core": "2.0.4", "@vue/shared": "^3.4.0" }, "devDependencies": { diff --git a/test-workspace/package.json b/test-workspace/package.json index 86d9228e76..2a162f751e 100644 --- a/test-workspace/package.json +++ b/test-workspace/package.json @@ -1,9 +1,9 @@ { "private": true, - "version": "1.8.27", + "version": "2.0.4", "devDependencies": { "vue": "^3.4.0", - "vue-component-type-helpers": "1.8.27", + "vue-component-type-helpers": "2.0.4", "vue2": "npm:vue@2.7.16" } }