diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b20df75820..29460dfb52 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -31,7 +31,7 @@ }, "background": { "activeOnStart": true, - "beginsPattern": " ", + "beginsPattern": "> rolldown", "endsPattern": "Waiting for changes..." } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 444de3c3b1..5703433b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 3.0.8 (2025-09-23) + +### Features + +- feat(vscode): introduce `vue.server.path` setting (#5647) + +### Bug Fixes + +- fix(language-core): initialize properties of `VueVirtualCode` in constructor (#5635) - Thanks to @KazariEX! +- fix(vscode): flatten reactivity visualization decorators (#5642) - Thanks to @KazariEX! +- fix(vscode): normalize reactivity visualization ranges +- fix(vscode): patch `typescriptServerPlugin` languages without FS hack +- fix(language-service): do not provide semantic tokens and document highlights for non-`file` scheme files (#5653) - Thanks to @KazariEX! + +### Performance + +- perf(typescript-plugin): redo single-file language service for reactivity visualization (#5652) + +### Other Changes + +- refactor(typescript-plugin): externalize reactivity analysis logic (#5645) - Thanks to @KazariEX! + ## 3.0.7 (2025-09-12) ### Bug Fixes diff --git a/extensions/vscode/index.ts b/extensions/vscode/index.ts index 91eb46d69c..1762778c86 100644 --- a/extensions/vscode/index.ts +++ b/extensions/vscode/index.ts @@ -21,8 +21,8 @@ import * as interpolationDecorators from './lib/interpolationDecorators'; import * as reactivityVisualization from './lib/reactivityVisualization'; import * as welcome from './lib/welcome'; -let client: lsp.BaseLanguageClient | undefined; -let needRestart = false; +const serverPath = resolveServerPath(); +const neededRestart = !patchTypeScriptExtension(); for ( const incompatibleExtensionId of [ @@ -44,6 +44,8 @@ for ( } export = defineExtension(() => { + let client: lsp.BaseLanguageClient | undefined; + const context = extensionContext.value!; const volarLabs = createLabsInfo(); const activeTextEditor = useActiveTextEditor(); @@ -59,7 +61,7 @@ export = defineExtension(() => { nextTick(() => stop()); - if (needRestart) { + if (neededRestart) { vscode.window.showInformationMessage( 'Please restart the extension host to activate Vue support.', 'Restart Extension Host', @@ -75,9 +77,12 @@ export = defineExtension(() => { return; } - watch(() => config.server.includeLanguages, async () => { + watch(() => [ + config.server.path, + config.server.includeLanguages, + ], async () => { const reload = await vscode.window.showInformationMessage( - 'Please restart extension host to apply the new language settings.', + 'Please restart extension host to apply the new server settings.', 'Restart Extension Host', ); if (reload) { @@ -95,15 +100,22 @@ export = defineExtension(() => { ); } - volarLabs.addLanguageClient(client = launch(context)); + if (config.server.path && !serverPath) { + vscode.window.showErrorMessage('Cannot find @vue/language-server.'); + return; + } + + client = launch(serverPath ?? vscode.Uri.joinPath(context.extensionUri, 'dist', 'language-server.js').fsPath); + + volarLabs.addLanguageClient(client); const selectors = config.server.includeLanguages; activateAutoInsertion(selectors, client); activateDocumentDropEdit(selectors, client); - focusMode.activate(context, selectors, client); - interpolationDecorators.activate(context, selectors, client); + focusMode.activate(context, selectors); + interpolationDecorators.activate(context, selectors); reactivityVisualization.activate(context, selectors); welcome.activate(context); }, { immediate: true }); @@ -123,19 +135,18 @@ export = defineExtension(() => { return volarLabs.extensionExports; }); -function launch(context: vscode.ExtensionContext) { - const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'language-server.js'); +function launch(serverPath: string) { const client = new lsp.LanguageClient( 'vue', 'Vue', { run: { - module: serverModule.fsPath, + module: serverPath, transport: lsp.TransportKind.ipc, options: {}, }, debug: { - module: serverModule.fsPath, + module: serverPath, transport: lsp.TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=' + 6009] }, }, @@ -179,36 +190,62 @@ function launch(context: vscode.ExtensionContext) { return client; } -const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')!; -if (tsExtension.isActive) { - needRestart = true; +function resolveServerPath() { + const tsPluginPackPath = path.join(__dirname, '..', 'node_modules', 'vue-typescript-plugin-pack', 'index.js'); + + if (!config.server.path) { + fs.writeFileSync(tsPluginPackPath, `module.exports = require("../../dist/typescript-plugin.js");`); + return; + } + + if (path.isAbsolute(config.server.path)) { + const entryFile = require.resolve('./index.js', { paths: [config.server.path] }); + const tsPluginPath = require.resolve('@vue/typescript-plugin', { paths: [path.dirname(entryFile)] }); + fs.writeFileSync(tsPluginPackPath, `module.exports = require("${tsPluginPath}");`); + return entryFile; + } + + for (const { uri } of vscode.workspace.workspaceFolders ?? []) { + if (uri.scheme !== 'file') { + continue; + } + try { + const serverPath = path.join(uri.fsPath, config.server.path); + const entryFile = require.resolve('./index.js', { paths: [serverPath] }); + const tsPluginPath = require.resolve('@vue/typescript-plugin', { paths: [path.dirname(entryFile)] }); + fs.writeFileSync(tsPluginPackPath, `module.exports = require("${tsPluginPath}");`); + return entryFile; + } + catch {} + } } -else { + +function patchTypeScriptExtension() { + const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')!; + if (tsExtension.isActive) { + return false; + } + const fs = require('node:fs'); const readFileSync = fs.readFileSync; - const extensionJsPath = require.resolve('./dist/extension.js', { - paths: [tsExtension.extensionPath], - }); - const vuePluginName = require('./package.json').contributes.typescriptServerPlugins[0].name; + const extensionJsPath = require.resolve('./dist/extension.js', { paths: [tsExtension.extensionPath] }); + const { publisher, name } = require('./package.json'); + const vueExtension = vscode.extensions.getExtension(`${publisher}.${name}`)!; + const tsPluginName = 'vue-typescript-plugin-pack'; + + vueExtension.packageJSON.contributes.typescriptServerPlugins = [ + { + name: tsPluginName, + enableForWorkspaceTypeScriptVersions: true, + configNamespace: 'typescript', + languages: config.server.includeLanguages, + }, + ]; fs.readFileSync = (...args: any[]) => { if (args[0] === extensionJsPath) { let text = readFileSync(...args) as string; - // patch readPlugins - text = text.replace( - 'languages:Array.isArray(e.languages)', - [ - 'languages:', - `e.name==='${vuePluginName}'?[${ - config.server.includeLanguages - .map(lang => `'${lang}'`) - .join(',') - }]`, - ':Array.isArray(e.languages)', - ].join(''), - ); - // patch jsTsLanguageModes text = text.replace( 't.jsTsLanguageModes=[t.javascript,t.javascriptreact,t.typescript,t.typescriptreact]', @@ -223,7 +260,7 @@ else { // sort plugins for johnsoncodehk.tsslint, zardoy.ts-essential-plugins text = text.replace( '"--globalPlugins",i.plugins', - s => s + `.sort((a,b)=>(b.name==="${vuePluginName}"?-1:0)-(a.name==="${vuePluginName}"?-1:0))`, + s => s + `.sort((a,b)=>(b.name==="${tsPluginName}"?-1:0)-(a.name==="${tsPluginName}"?-1:0))`, ); return text; @@ -237,4 +274,5 @@ else { const patchedModule = require(extensionJsPath); Object.assign(loadedModule.exports, patchedModule); } + return true; } diff --git a/extensions/vscode/lib/focusMode.ts b/extensions/vscode/lib/focusMode.ts index c1561be09f..80af1463a9 100644 --- a/extensions/vscode/lib/focusMode.ts +++ b/extensions/vscode/lib/focusMode.ts @@ -1,4 +1,3 @@ -import type { BaseLanguageClient } from '@volar/vscode'; import * as vscode from 'vscode'; import { config } from './config'; @@ -9,13 +8,10 @@ const tagUnfocusDecorations = Array.from({ length: 8 }).map((_, i) => }) ); -export async function activate( +export function activate( context: vscode.ExtensionContext, selector: vscode.DocumentSelector, - client: BaseLanguageClient, ) { - await client.start(); - let timeout: ReturnType | undefined; const editor2Decorations = new Map, /** * @key `vue.editor.focusMode` - * @default `true` + * @default `false` * @type `boolean` */ editorFocusMode: { key: 'vue.editor.focusMode', - default: true, + default: false, } as ConfigItem<'vue.editor.focusMode'>, /** * @key `vue.editor.reactivityVisualization` @@ -185,6 +189,15 @@ export const configs = { key: 'vue.editor.templateInterpolationDecorators', default: true, } as ConfigItem<'vue.editor.templateInterpolationDecorators'>, + /** + * @key `vue.server.path` + * @default `undefined` + * @type `string` + */ + serverPath: { + key: 'vue.server.path', + default: undefined, + } as ConfigItem<'vue.server.path'>, /** * @key `vue.server.includeLanguages` * @default `["vue"]` @@ -336,6 +349,7 @@ export interface ScopedConfigKeyTypeMap { 'editor.focusMode': boolean; 'editor.reactivityVisualization': boolean; 'editor.templateInterpolationDecorators': boolean; + 'server.path': string | undefined; 'server.includeLanguages': string[]; 'codeActions.askNewComponentName': boolean; 'suggest.componentNameCasing': 'preferKebabCase' | 'preferPascalCase' | 'alwaysKebabCase' | 'alwaysPascalCase'; @@ -365,9 +379,10 @@ export const scopedConfigs = { scope: 'vue', defaults: { 'trace.server': 'off', - 'editor.focusMode': true, + 'editor.focusMode': false, 'editor.reactivityVisualization': true, 'editor.templateInterpolationDecorators': true, + 'server.path': undefined, 'server.includeLanguages': ['vue'], 'codeActions.askNewComponentName': true, 'suggest.componentNameCasing': 'preferPascalCase', @@ -398,6 +413,7 @@ export interface NestedConfigs { 'templateInterpolationDecorators': boolean; }; 'server': { + 'path': string | undefined; 'includeLanguages': string[]; }; 'codeActions': { @@ -451,6 +467,7 @@ export interface NestedScopedConfigs { 'templateInterpolationDecorators': boolean; }; 'server': { + 'path': string | undefined; 'includeLanguages': string[]; }; 'codeActions': { diff --git a/extensions/vscode/lib/interpolationDecorators.ts b/extensions/vscode/lib/interpolationDecorators.ts index 50cf476bc2..468969c37c 100644 --- a/extensions/vscode/lib/interpolationDecorators.ts +++ b/extensions/vscode/lib/interpolationDecorators.ts @@ -1,4 +1,3 @@ -import type { BaseLanguageClient } from '@volar/vscode'; import * as vscode from 'vscode'; import { config } from './config'; @@ -10,13 +9,10 @@ const decorationType = vscode.window.createTextEditorDecorationType({ borderRadius: '4px', }); -export async function activate( +export function activate( context: vscode.ExtensionContext, selector: vscode.DocumentSelector, - client: BaseLanguageClient, ) { - await client.start(); - let timeout: ReturnType | undefined; for (const editor of vscode.window.visibleTextEditors) { diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 88ae5b01f0..d8f0897be8 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,8 +1,9 @@ import type { getReactiveReferences } from '@vue/typescript-plugin/lib/requests/getReactiveReferences'; +import type * as ts from 'typescript'; import * as vscode from 'vscode'; import { config } from './config'; -const dependenciesDecorations = vscode.window.createTextEditorDecorationType({ +const dependencyDecorations = vscode.window.createTextEditorDecorationType({ isWholeLine: true, backgroundColor: 'rgba(120,120,255,0.1)', border: '1px solid rgba(120,120,255,0.6)', @@ -12,7 +13,7 @@ const dependenciesDecorations = vscode.window.createTextEditorDecorationType({ // color: 'rgba(120,120,255,0.6)', // }, }); -const subscribersDecorations = vscode.window.createTextEditorDecorationType({ +const dependentDecorations = vscode.window.createTextEditorDecorationType({ // outlineColor: 'rgba(80,200,80,0.6)', // outlineStyle: 'dashed', // borderRadius: '3px', @@ -26,6 +27,8 @@ export function activate( context: vscode.ExtensionContext, selector: vscode.DocumentSelector, ) { + const documentUpdateVersions = new WeakMap(); + let timeout: ReturnType | undefined; for (const editor of vscode.window.visibleTextEditors) { @@ -42,11 +45,23 @@ export function activate( const editor = vscode.window.activeTextEditor; if (editor) { clearTimeout(timeout); - timeout = setTimeout(() => updateDecorations(editor), 250); + timeout = setTimeout( + () => updateDecorations(editor), + getUpdateInterval(editor.document), + ); } }), ); + function getUpdateInterval(document: vscode.TextDocument) { + const prevVersion = documentUpdateVersions.get(document); + if (prevVersion !== document.version) { + documentUpdateVersions.set(document, document.version); + return 250; + } + return 100; + } + async function updateDecorations(editor: vscode.TextEditor) { const { document } = editor; if (document.uri.scheme !== 'file') { @@ -62,8 +77,8 @@ export function activate( return; } if (!config.editor.reactivityVisualization) { - editor.setDecorations(dependenciesDecorations, []); - editor.setDecorations(subscribersDecorations, []); + editor.setDecorations(dependencyDecorations, []); + editor.setDecorations(dependentDecorations, []); return; } @@ -82,27 +97,46 @@ export function activate( { isAsync: true, lowPriority: true }, ); editor.setDecorations( - dependenciesDecorations, - result?.body?.dependencyRanges.map(range => - new vscode.Range( - document.positionAt(range.start), - document.positionAt(range.end), - ) - ) ?? [], + dependencyDecorations, + getFlatRanges(document, result?.body?.dependencyRanges ?? []), ); editor.setDecorations( - subscribersDecorations, - result?.body?.dependentRanges.map(range => - new vscode.Range( - document.positionAt(range.start), - document.positionAt(range.end), - ) - ) ?? [], + dependentDecorations, + getFlatRanges(document, result?.body?.dependentRanges ?? []), ); } catch { - editor.setDecorations(dependenciesDecorations, []); - editor.setDecorations(subscribersDecorations, []); + editor.setDecorations(dependencyDecorations, []); + editor.setDecorations(dependentDecorations, []); + } + } +} + +function getFlatRanges(document: vscode.TextDocument, ranges: ts.TextRange[]) { + const documentRanges = ranges + .map(range => + new vscode.Range( + document.positionAt(range.pos).line, + 0, + document.positionAt(range.end).line, + 0, + ) + ) + .sort((a, b) => a.start.compareTo(b.start)); + + for (let i = 1; i < documentRanges.length; i++) { + const prev = documentRanges[i - 1]!; + const curr = documentRanges[i]!; + + if (prev.end.compareTo(curr.start) >= 0) { + if (curr.end.compareTo(prev.end) <= 0) { + documentRanges.splice(i, 1); + } + else { + documentRanges.splice(i - 1, 2, new vscode.Range(prev.start, curr.end)); + } + i--; } } + return documentRanges; } diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 048d13c12f..c536717d70 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "volar", - "version": "3.0.7", + "version": "3.0.8", "repository": { "type": "git", "url": "https://github.com/vuejs/language-tools.git", @@ -67,13 +67,6 @@ "configuration": "./languages/sfc-template-language-configuration.json" } ], - "typescriptServerPlugins": [ - { - "name": "vue-typescript-plugin-pack", - "enableForWorkspaceTypeScriptVersions": true, - "configNamespace": "typescript" - } - ], "grammars": [ { "language": "vue", @@ -279,6 +272,10 @@ "default": true, "markdownDescription": "%configuration.editor.templateInterpolationDecorators%" }, + "vue.server.path": { + "type": "string", + "markdownDescription": "%configuration.server.path%" + }, "vue.server.includeLanguages": { "type": "array", "items": { @@ -467,8 +464,8 @@ "@volar/vscode": "2.4.23", "@vscode/vsce": "^3.2.1", "@vue/compiler-sfc": "^3.5.0", - "@vue/language-server": "3.0.7", - "@vue/typescript-plugin": "3.0.7", + "@vue/language-server": "3.0.8", + "@vue/typescript-plugin": "3.0.8", "reactive-vscode": "^0.2.9", "rolldown": "1.0.0-beta.8", "semver": "^7.5.4", diff --git a/extensions/vscode/package.nls.ja.json b/extensions/vscode/package.nls.ja.json index 6b4ad3b554..746dee9bdf 100644 --- a/extensions/vscode/package.nls.ja.json +++ b/extensions/vscode/package.nls.ja.json @@ -1,5 +1,6 @@ { "configuration.trace.server": "VS Code と Vue 言語サーバー間の通信をトレースします。", + "configuration.server.path": "`@vue/language-server` モジュールへのパス。設定しない場合、拡張機能にバンドルされたサーバーが読み込まれます。", "configuration.server.includeLanguages": "拡張機能を有効にする言語を指定します。", "configuration.codeActions.askNewComponentName": "コンポーネントを抽出する時に新しいコンポーネント名を尋ねます。", "configuration.suggest.componentNameCasing": "コンポーネント名の命名規則。", diff --git a/extensions/vscode/package.nls.json b/extensions/vscode/package.nls.json index 0c0c585ed2..ac063bcf15 100644 --- a/extensions/vscode/package.nls.json +++ b/extensions/vscode/package.nls.json @@ -1,5 +1,6 @@ { "configuration.trace.server": "Traces the communication between VS Code and the language server.", + "configuration.server.path": "Path to the `@vue/language-server` module. If not set, the server will be loaded from the extension's bundled.", "configuration.server.includeLanguages": "Configure the languages for which the extension should be activated.", "configuration.codeActions.askNewComponentName": "Ask for new component name when extract component.", "configuration.suggest.componentNameCasing": "Preferred component name case.", diff --git a/extensions/vscode/package.nls.ru.json b/extensions/vscode/package.nls.ru.json index e04cf6c4da..fae2952b3a 100644 --- a/extensions/vscode/package.nls.ru.json +++ b/extensions/vscode/package.nls.ru.json @@ -1,5 +1,6 @@ { "configuration.trace.server": "Отслеживать коммуникацию между VS Code и языковым сервером Vue.", + "configuration.server.path": "Путь к модулю `@vue/language-server`. Если не установлен, сервер будет загружен из пакета расширения.", "configuration.server.includeLanguages": "Языки, для которых будет включено расширение.", "configuration.codeActions.askNewComponentName": "Запрашивать новое имя компонента при извлечении компонента.", "configuration.suggest.componentNameCasing": "Предпочитаемый формат имени компонента.", diff --git a/extensions/vscode/package.nls.zh-CN.json b/extensions/vscode/package.nls.zh-CN.json index 2364112567..201adc7cce 100644 --- a/extensions/vscode/package.nls.zh-CN.json +++ b/extensions/vscode/package.nls.zh-CN.json @@ -1,5 +1,6 @@ { "configuration.trace.server": "跟踪 VS Code 和 Vue 语言服务器之间的通信。", + "configuration.server.path": "`@vue/language-server` 模块的路径。如果未设置,服务器将从扩展的捆绑包中加载。", "configuration.server.includeLanguages": "配置扩展需要激活的语言类型。", "configuration.codeActions.askNewComponentName": "提取组件时询问新组件名称。", "configuration.suggest.componentNameCasing": "首选组件命名格式。", diff --git a/extensions/vscode/package.nls.zh-TW.json b/extensions/vscode/package.nls.zh-TW.json index 355cf97c3b..a88660677a 100644 --- a/extensions/vscode/package.nls.zh-TW.json +++ b/extensions/vscode/package.nls.zh-TW.json @@ -1,5 +1,6 @@ { "configuration.trace.server": "追蹤 VS Code 和 Vue 語言伺服器之間的通訊。", + "configuration.server.path": "`@vue/language-server` 模組的路徑。如果未設定,伺服器將從擴充功能的捆綁包中載入。", "configuration.server.includeLanguages": "配置擴充功能應該啟動的語言類型。", "configuration.codeActions.askNewComponentName": "提取元件時詢問新元件名稱。", "configuration.suggest.componentNameCasing": "首選元件名稱格式。", diff --git a/lerna.json b/lerna.json index 8a9a063c72..4d6cb0d23b 100644 --- a/lerna.json +++ b/lerna.json @@ -13,6 +13,6 @@ "packages/**", "test-workspace" ], - "version": "3.0.7", + "version": "3.0.8", "yes": true } diff --git a/packages/component-meta/package.json b/packages/component-meta/package.json index d9ad43407a..b924949f91 100644 --- a/packages/component-meta/package.json +++ b/packages/component-meta/package.json @@ -1,6 +1,6 @@ { "name": "vue-component-meta", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "**/*.js", @@ -14,9 +14,9 @@ }, "dependencies": { "@volar/typescript": "2.4.23", - "@vue/language-core": "3.0.7", + "@vue/language-core": "3.0.8", "path-browserify": "^1.0.1", - "vue-component-type-helpers": "3.0.7" + "vue-component-type-helpers": "3.0.8" }, "peerDependencies": { "typescript": "*" diff --git a/packages/component-type-helpers/package.json b/packages/component-type-helpers/package.json index 9017d31902..6f3bb5e592 100644 --- a/packages/component-type-helpers/package.json +++ b/packages/component-type-helpers/package.json @@ -1,6 +1,6 @@ { "name": "vue-component-type-helpers", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "**/*.js", diff --git a/packages/language-core/lib/virtualFile/vueFile.ts b/packages/language-core/lib/virtualFile/vueFile.ts index 13b0c63b4b..2220de4781 100644 --- a/packages/language-core/lib/virtualFile/vueFile.ts +++ b/packages/language-core/lib/virtualFile/vueFile.ts @@ -1,4 +1,4 @@ -import type { VirtualCode } from '@volar/language-core'; +import type { CodeInformation, Mapping, VirtualCode } from '@volar/language-core'; import { computed, signal } from 'alien-signals'; import type * as ts from 'typescript'; import { allCodeFeatures } from '../plugins'; @@ -8,28 +8,16 @@ import { computedSfc } from './computedSfc'; import { computedVueSfc } from './computedVueSfc'; export class VueVirtualCode implements VirtualCode { - // sources + readonly id = 'main'; + readonly sfc: ReturnType; - id = 'main'; - - private _snapshot = signal(undefined!); - - // computeds - - private _vueSfc = computedVueSfc(this.plugins, this.fileName, this.languageId, this._snapshot); - private _sfc = computedSfc(this.ts, this.plugins, this.fileName, this._snapshot, this._vueSfc); - private _embeddedCodes = computedEmbeddedCodes(this.plugins, this.fileName, this._sfc); - private _mappings = computed(() => { - const snapshot = this._snapshot(); - return [{ - sourceOffsets: [0], - generatedOffsets: [0], - lengths: [snapshot.getLength()], - data: allCodeFeatures, - }]; - }); - - // others + private _snapshot: { + (): ts.IScriptSnapshot; + (value: ts.IScriptSnapshot): void; + }; + private _vueSfc: ReturnType; + private _embeddedCodes: ReturnType; + private _mappings: () => Mapping[]; get snapshot() { return this._snapshot(); @@ -37,9 +25,6 @@ export class VueVirtualCode implements VirtualCode { get vueSfc() { return this._vueSfc(); } - get sfc() { - return this._sfc; - } get embeddedCodes() { return this._embeddedCodes(); } @@ -55,7 +40,19 @@ export class VueVirtualCode implements VirtualCode { public plugins: VueLanguagePluginReturn[], public ts: typeof import('typescript'), ) { - this._snapshot(initSnapshot); + this._snapshot = signal(initSnapshot); + this._vueSfc = computedVueSfc(this.plugins, this.fileName, this.languageId, this._snapshot); + this.sfc = computedSfc(this.ts, this.plugins, this.fileName, this._snapshot, this._vueSfc); + this._embeddedCodes = computedEmbeddedCodes(this.plugins, this.fileName, this.sfc); + this._mappings = computed(() => { + const snapshot = this._snapshot(); + return [{ + sourceOffsets: [0], + generatedOffsets: [0], + lengths: [snapshot.getLength()], + data: allCodeFeatures, + }]; + }); } update(newSnapshot: ts.IScriptSnapshot) { diff --git a/packages/language-core/package.json b/packages/language-core/package.json index b74ec5e38b..3f0dfdeb32 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-core", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "**/*.js", diff --git a/packages/language-plugin-pug/package.json b/packages/language-plugin-pug/package.json index aaa9fe8ca6..05b9f86588 100644 --- a/packages/language-plugin-pug/package.json +++ b/packages/language-plugin-pug/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-plugin-pug", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "**/*.js", @@ -19,6 +19,6 @@ "devDependencies": { "@types/node": "^22.10.4", "@vue/compiler-dom": "^3.5.0", - "@vue/language-core": "3.0.7" + "@vue/language-core": "3.0.8" } } diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 610d27935d..447a7ad460 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-server", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "**/*.js", @@ -17,9 +17,9 @@ }, "dependencies": { "@volar/language-server": "2.4.23", - "@vue/language-core": "3.0.7", - "@vue/language-service": "3.0.7", - "@vue/typescript-plugin": "3.0.7", + "@vue/language-core": "3.0.8", + "@vue/language-service": "3.0.8", + "@vue/typescript-plugin": "3.0.8", "vscode-uri": "^3.0.8" }, "peerDependencies": { diff --git a/packages/language-service/lib/plugins/typescript-semantic-tokens.ts b/packages/language-service/lib/plugins/typescript-semantic-tokens.ts index 0c0627e59e..6506b639eb 100644 --- a/packages/language-service/lib/plugins/typescript-semantic-tokens.ts +++ b/packages/language-service/lib/plugins/typescript-semantic-tokens.ts @@ -39,7 +39,7 @@ export function create( return { async provideDocumentSemanticTokens(document, range, legend) { const info = resolveEmbeddedCode(context, document.uri); - if (info?.code.id !== 'main') { + if (info?.script.id.scheme !== 'file' || info.code.id !== 'main') { return; } const start = document.offsetAt(range.start); diff --git a/packages/language-service/lib/plugins/vue-document-highlights.ts b/packages/language-service/lib/plugins/vue-document-highlights.ts index 02eb9fdb52..855b2ac3a9 100644 --- a/packages/language-service/lib/plugins/vue-document-highlights.ts +++ b/packages/language-service/lib/plugins/vue-document-highlights.ts @@ -14,7 +14,7 @@ export function create( return { async provideDocumentHighlights(document, position) { const info = resolveEmbeddedCode(context, document.uri); - if (info?.code.id !== 'main') { + if (info?.script.id.scheme !== 'file' || info.code.id !== 'main') { return; } diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 6a7f7e63bc..5bc74a43ec 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-service", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "data", @@ -18,7 +18,7 @@ }, "dependencies": { "@volar/language-service": "2.4.23", - "@vue/language-core": "3.0.7", + "@vue/language-core": "3.0.8", "@vue/shared": "^3.5.0", "path-browserify": "^1.0.1", "volar-service-css": "0.0.65", @@ -37,7 +37,7 @@ "@volar/kit": "2.4.23", "@volar/typescript": "2.4.23", "@vue/compiler-dom": "^3.5.0", - "@vue/typescript-plugin": "3.0.7", + "@vue/typescript-plugin": "3.0.8", "vscode-css-languageservice": "^6.3.1" } } diff --git a/packages/tsc/package.json b/packages/tsc/package.json index fdc15fceb0..0256674fa3 100644 --- a/packages/tsc/package.json +++ b/packages/tsc/package.json @@ -1,6 +1,6 @@ { "name": "vue-tsc", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "bin", @@ -21,7 +21,7 @@ }, "dependencies": { "@volar/typescript": "2.4.23", - "@vue/language-core": "3.0.7" + "@vue/language-core": "3.0.8" }, "devDependencies": { "@types/node": "^22.10.4" diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 59bc03dc06..198290cb91 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -189,17 +189,15 @@ export = createLanguageServicePlugin( }); session.addProtocolHandler('_vue:getReactiveReferences', request => { const [fileName, position]: Parameters = request.arguments; - const { languageService, language } = getLanguageService(fileName); - const sourceScript = language.scripts.get(fileName); + const { language } = getLanguageService(fileName); + const sourceScript = language.scripts.get(fileName)!; return createResponse( getReactiveReferences( ts, language, - languageService, sourceScript, - fileName, position, - sourceScript?.generated ? sourceScript.snapshot.getLength() : 0, + sourceScript.generated ? sourceScript.snapshot.getLength() : 0, ), ); }); diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 76473b8770..796996f7d2 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -159,8 +159,8 @@ function getCompletionEntryDetails( const { fileName } = args[6].__isAutoImport; const sourceScript = language.scripts.get(fileName); if (sourceScript?.generated?.root instanceof VueVirtualCode) { - const sfc = sourceScript.generated.root.vueSfc; - if (!sfc?.descriptor.script && !sfc?.descriptor.scriptSetup) { + const { vueSfc } = sourceScript.generated.root; + if (!vueSfc?.descriptor.script && !vueSfc?.descriptor.scriptSetup) { for (const codeAction of details?.codeActions ?? []) { for (const change of codeAction.changes) { for (const textChange of change.textChanges) { diff --git a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts index 9ec9f31133..f918fe3796 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts @@ -1,703 +1,65 @@ -/// - -import { collectBindingRanges, hyphenateAttr, type Language, type SourceScript } from '@vue/language-core'; +import { createProxyLanguageService, decorateLanguageServiceHost } from '@volar/typescript'; +import { type Language, type SourceScript } from '@vue/language-core'; +import { createAnalyzer } from 'laplacenoma'; +import * as rulesVue from 'laplacenoma/rules/vue'; import type * as ts from 'typescript'; -const enum ReactiveAccessType { - ValueProperty, - AnyProperty, - Call, -} - -interface TSNode { - ast: ts.Node; - start: number; - end: number; -} - -interface ReactiveNode { - isDependency: boolean; - isDependent: boolean; - binding?: TSNode & { - accessTypes: ReactiveAccessType[]; - }; - accessor?: TSNode & { - requiredAccess: boolean; - }; - callback?: TSNode; -} +const analyzer = createAnalyzer({ + rules: rulesVue, +}); -const analyzeCache = new WeakMap>(); +let currentVersion = -1; +let currentFileName = ''; +let currentSnapshot: ts.IScriptSnapshot | undefined; +let languageService: ts.LanguageService | undefined; +let languageServiceHost: ts.LanguageServiceHost | undefined; export function getReactiveReferences( ts: typeof import('typescript'), - language: Language, - languageService: ts.LanguageService, - sourceScript: SourceScript | undefined, - fileName: string, + language: Language, + sourceScript: SourceScript, position: number, leadingOffset: number = 0, ) { - const serviceScript = sourceScript?.generated?.languagePlugin.typescript?.getServiceScript( - sourceScript.generated.root, - ); - const map = serviceScript ? language.maps.get(serviceScript.code, sourceScript!) : undefined; - const toSourceRange = map - ? (start: number, end: number) => { - for (const [mappedStart, mappedEnd] of map.toSourceRange(start - leadingOffset, end - leadingOffset, false)) { - return { start: mappedStart, end: mappedEnd }; - } - } - : (start: number, end: number) => ({ start, end }); - - const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!; - - if (!analyzeCache.has(sourceFile)) { - analyzeCache.set(sourceFile, analyze(ts, sourceFile, toSourceRange)); - } - - const { - signals, - allValuePropertyAccess, - allPropertyAccess, - allFunctionCalls, - } = analyzeCache.get(sourceFile)!; - - const info = findSignalByBindingRange(position) ?? findSignalByCallbackRange(position); - if (!info) { - return; - } - - const dependents = info.binding ? findDependents(info.binding.ast, info.binding.accessTypes) : []; - const dependencies = findDependencies(info); - - if ((!info.isDependent && !dependents.length) || (!info.isDependency && !dependencies.length)) { - return; - } - - const dependencyRanges: { start: number; end: number }[] = []; - const dependentRanges: { start: number; end: number }[] = []; - - for (const dependency of dependencies) { - let { ast } = dependency; - if (ts.isBlock(ast) && ast.statements.length) { - const sourceRange = toSourceRange( - ast.statements[0]!.getStart(sourceFile), - ast.statements[ast.statements.length - 1]!.end, - ); - if (sourceRange) { - dependencyRanges.push({ start: sourceRange.start, end: sourceRange.end }); - } - } - else { - dependencyRanges.push({ start: dependency.start, end: dependency.end }); - } - } - for (const { callback } of dependents) { - if (!callback) { - continue; - } - if (ts.isBlock(callback.ast) && callback.ast.statements.length) { - const { statements } = callback.ast; - const sourceRange = toSourceRange( - statements[0]!.getStart(sourceFile), - statements[statements.length - 1]!.end, - ); - if (sourceRange) { - dependencyRanges.push({ start: sourceRange.start, end: sourceRange.end }); - } - } - else { - dependentRanges.push({ start: callback.start, end: callback.end }); - } - } - - return { dependencyRanges, dependentRanges }; - - function findDependencies(signal: ReactiveNode, visited = new Set()) { - if (visited.has(signal)) { - return []; - } - visited.add(signal); - - const nodes: TSNode[] = []; - let hasDependency = signal.isDependency; - - if (signal.accessor) { - const { requiredAccess } = signal.accessor; - visit(signal.accessor, requiredAccess); - signal.accessor.ast.forEachChild(child => { - const childRange = toSourceRange(child.getStart(sourceFile), child.end); - if (childRange) { - visit( - { - ...childRange, - ast: child, - }, - requiredAccess, - ); - } - }); - } - - if (!hasDependency) { - return []; - } - - return nodes; - - function visit(node: TSNode, requiredAccess: boolean, parentIsPropertyAccess = false) { - if (!requiredAccess) { - if (!parentIsPropertyAccess && ts.isIdentifier(node.ast)) { - const definition = languageService.getDefinitionAtPosition(sourceFile.fileName, node.start); - for (const info of definition ?? []) { - if (info.fileName !== sourceFile.fileName) { - continue; - } - const signal = findSignalByBindingRange(info.textSpan.start); - if (!signal) { - continue; - } - if (signal.binding) { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - if (signal.callback) { - nodes.push(signal.callback); - } - const deps = findDependencies(signal, visited); - nodes.push(...deps); - hasDependency ||= deps.length > 0; - } - } - } - else if ( - ts.isPropertyAccessExpression(node.ast) || ts.isElementAccessExpression(node.ast) - || ts.isCallExpression(node.ast) - ) { - const definition = languageService.getDefinitionAtPosition( - sourceFile.fileName, - node.start, - ); - for (const info of definition ?? []) { - if (info.fileName !== sourceFile.fileName) { - continue; - } - const signal = findSignalByBindingRange(info.textSpan.start); - if (!signal) { - continue; - } - const oldSize = nodes.length; - if (signal.binding) { - for (const accessType of signal.binding.accessTypes) { - if (ts.isPropertyAccessExpression(node.ast)) { - if (accessType === ReactiveAccessType.ValueProperty && node.ast.name.text === 'value') { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - if (accessType === ReactiveAccessType.AnyProperty && node.ast.name.text !== '') { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - } - else if (ts.isElementAccessExpression(node.ast)) { - if (accessType === ReactiveAccessType.AnyProperty) { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - } - else if (ts.isCallExpression(node.ast)) { - if (accessType === ReactiveAccessType.Call) { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - } - } - } - const signalDetected = nodes.length > oldSize; - if (signalDetected) { - if (signal.callback) { - nodes.push(signal.callback); - } - const deps = findDependencies(signal, visited); - nodes.push(...deps); - hasDependency ||= deps.length > 0; - } - } - } - node.ast.forEachChild(child => { - const childRange = toSourceRange(child.getStart(sourceFile), child.end); - if (childRange) { - visit( - { - ...childRange, - ast: child, - }, - requiredAccess, - ts.isPropertyAccessExpression(node.ast) || ts.isElementAccessExpression(node.ast), - ); - } - }); - } + if (currentSnapshot !== sourceScript.snapshot || currentFileName !== sourceScript.id) { + currentSnapshot = sourceScript.snapshot; + currentFileName = sourceScript.id; + currentVersion++; } - - function findDependents(node: ts.Node, trackKinds: ReactiveAccessType[], visited = new Set()) { - return collectBindingRanges(ts, node, sourceFile) - .map(range => { - const sourceRange = toSourceRange(range.start, range.end); - if (sourceRange) { - return findDependentsWorker(sourceRange.start, trackKinds, visited); - } - return []; - }) - .flat(); + if (!languageService) { + languageServiceHost = { + getProjectVersion: () => currentVersion.toString(), + getScriptVersion: () => currentVersion.toString(), + getScriptFileNames: () => [currentFileName], + getScriptSnapshot: fileName => fileName === currentFileName ? currentSnapshot : undefined, + getCompilationSettings: () => ({ allowJs: true, allowNonTsExtensions: true }), + getCurrentDirectory: () => '', + getDefaultLibFileName: () => '', + readFile: () => undefined, + fileExists: fileName => fileName === currentFileName, + }; + decorateLanguageServiceHost(ts, language, languageServiceHost); + const proxied = createProxyLanguageService(ts.createLanguageService(languageServiceHost)); + proxied.initialize(language); + languageService = proxied.proxy; } - function findDependentsWorker(pos: number, accessTypes: ReactiveAccessType[], visited = new Set()) { - if (visited.has(pos)) { - return []; - } - visited.add(pos); - - const references = languageService.findReferences(sourceFile.fileName, pos); - if (!references) { - return []; - } - const result: typeof signals = []; - for (const reference of references) { - for (const reference2 of reference.references) { - if (reference2.fileName !== sourceFile.fileName) { - continue; - } - const effect = findSignalByAccessorRange(reference2.textSpan.start); - if (effect?.accessor) { - let match = false; - if (effect.accessor.requiredAccess) { - for (const accessType of accessTypes) { - if (accessType === ReactiveAccessType.AnyProperty) { - match ||= allPropertyAccess.has(reference2.textSpan.start + reference2.textSpan.length); - } - else if (accessType === ReactiveAccessType.ValueProperty) { - match ||= allValuePropertyAccess.has(reference2.textSpan.start + reference2.textSpan.length); - } - else { - match ||= allFunctionCalls.has(reference2.textSpan.start + reference2.textSpan.length); - } - } - } - if (match) { - let hasDependent = effect.isDependent; - if (effect.binding) { - const dependents = findDependents(effect.binding.ast, effect.binding.accessTypes, visited); - result.push(...dependents); - hasDependent ||= dependents.length > 0; - } - if (hasDependent) { - result.push(effect); - } - } - } - } - } - return result; - } - - function findSignalByBindingRange(position: number): ReactiveNode | undefined { - return signals.find(ref => - ref.binding && ref.binding.start <= position - && ref.binding.end >= position - ); - } - - function findSignalByCallbackRange(position: number): ReactiveNode | undefined { - return signals.filter(ref => - ref.callback && ref.callback.start <= position - && ref.callback.end >= position - ).sort((a, b) => (a.callback!.end - a.callback!.start) - (b.callback!.end - b.callback!.start))[0]; - } - - function findSignalByAccessorRange(position: number): ReactiveNode | undefined { - return signals.filter(ref => - ref.accessor && ref.accessor.start <= position - && ref.accessor.end >= position - ).sort((a, b) => (a.accessor!.end - a.accessor!.start) - (b.accessor!.end - b.accessor!.start))[0]; - } -} - -function analyze( - ts: typeof import('typescript'), - sourceFile: ts.SourceFile, - toSourceRange: (start: number, end: number) => { start: number; end: number } | undefined, -) { - const signals: ReactiveNode[] = []; - const allValuePropertyAccess = new Set(); - const allPropertyAccess = new Set(); - const allFunctionCalls = new Set(); - - sourceFile.forEachChild(function visit(node) { - if (ts.isVariableDeclaration(node)) { - if (node.initializer && ts.isCallExpression(node.initializer)) { - const call = node.initializer; - if (ts.isIdentifier(call.expression)) { - const callName = call.expression.escapedText as string; - if ( - callName === 'ref' || callName === 'shallowRef' || callName === 'toRef' || callName === 'useTemplateRef' - || callName === 'defineModel' - ) { - const nameRange = toSourceRange(node.name.getStart(sourceFile), node.name.end); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - ast: node.name, - accessTypes: [ReactiveAccessType.ValueProperty], - }, - }); - } - } - else if ( - callName === 'reactive' || callName === 'shallowReactive' || callName === 'defineProps' - || callName === 'withDefaults' - ) { - const nameRange = toSourceRange(node.name.getStart(sourceFile), node.name.end); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - ast: node.name, - accessTypes: [ReactiveAccessType.AnyProperty], - }, - }); - } - } - // TODO: toRefs - } - } - } - else if (ts.isFunctionDeclaration(node)) { - if (node.name && node.body) { - const nameRange = toSourceRange(node.name.getStart(sourceFile), node.name.end); - const bodyRange = toSourceRange(node.body.getStart(sourceFile), node.body.end); - if (nameRange && bodyRange) { - signals.push({ - isDependency: false, - isDependent: false, - binding: { - ...nameRange, - ast: node.name, - accessTypes: [ReactiveAccessType.Call], - }, - accessor: { - ...bodyRange, - ast: node.body, - requiredAccess: true, - }, - callback: { - ...bodyRange, - ast: node.body, - }, - }); - } - } - } - else if (ts.isVariableStatement(node)) { - for (const declaration of node.declarationList.declarations) { - const name = declaration.name; - const callback = declaration.initializer; - if ( - callback && ts.isIdentifier(name) && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) - ) { - const nameRange = toSourceRange(name.getStart(sourceFile), name.end); - const callbackRange = toSourceRange(callback.getStart(sourceFile), callback.end); - if (nameRange && callbackRange) { - signals.push({ - isDependency: false, - isDependent: false, - binding: { - ...nameRange, - ast: name, - accessTypes: [ReactiveAccessType.Call], - }, - accessor: { - ...callbackRange, - ast: callback, - requiredAccess: true, - }, - callback: { - ...callbackRange, - ast: callback, - }, - }); - } - } - } - } - else if (ts.isParameter(node)) { - if (node.type && ts.isTypeReferenceNode(node.type)) { - const typeName = node.type.typeName.getText(sourceFile); - if (typeName.endsWith('Ref')) { - const nameRange = toSourceRange(node.name.getStart(sourceFile), node.name.end); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - ast: node.name, - accessTypes: [ReactiveAccessType.ValueProperty], - }, - }); - } - } - } - } - else if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { - const call = node; - const callName = node.expression.escapedText as string; - if ((callName === 'effect' || callName === 'watchEffect') && call.arguments.length) { - const callback = call.arguments[0]!; - if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) { - const callbackRange = toSourceRange(callback.getStart(sourceFile), callback.end); - if (callbackRange) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...callbackRange, - ast: callback.body, - requiredAccess: true, - }, - callback: { - ...callbackRange, - ast: callback.body, - }, - }); - } - } - } - if (callName === 'watch' && call.arguments.length >= 2) { - const depsCallback = call.arguments[0]!; - const effectCallback = call.arguments[1]!; - if (ts.isArrowFunction(effectCallback) || ts.isFunctionExpression(effectCallback)) { - const depsRange = toSourceRange(depsCallback.getStart(sourceFile), depsCallback.end); - const effectRange = toSourceRange(effectCallback.getStart(sourceFile), effectCallback.end); - if (depsRange && effectRange) { - if (ts.isArrowFunction(depsCallback) || ts.isFunctionExpression(depsCallback)) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...depsRange, - ast: depsCallback.body, - requiredAccess: true, - }, - callback: { - ...effectRange, - ast: effectCallback.body, - }, - }); - } - else { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...depsRange, - ast: depsCallback, - requiredAccess: false, - }, - callback: { - ...effectRange, - ast: effectCallback.body, - }, - }); - } - } - } - } - else if (hyphenateAttr(callName).startsWith('use-')) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceRange(call.parent.name.getStart(sourceFile), call.parent.name.end); - if (nameRange) { - binding = { - ...nameRange, - ast: call.parent.name, - accessTypes: [ReactiveAccessType.AnyProperty, ReactiveAccessType.Call], - }; - } - } - const callRange = toSourceRange(call.getStart(sourceFile), call.end); - if (callRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding, - accessor: { - ...callRange, - ast: call, - requiredAccess: false, - }, - }); - } - } - else if ((callName === 'computed' || hyphenateAttr(callName).endsWith('-computed')) && call.arguments.length) { - const arg = call.arguments[0]!; - if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceRange(call.parent.name.getStart(sourceFile), call.parent.name.end); - if (nameRange) { - binding = { - ...nameRange, - ast: call.parent.name, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - const argRange = toSourceRange(arg.getStart(sourceFile), arg.end); - if (argRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...argRange, - ast: arg.body, - requiredAccess: true, - }, - callback: { - ...argRange, - ast: arg.body, - }, - }); - } - } - else if (ts.isIdentifier(arg)) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceRange(call.parent.name.getStart(sourceFile), call.parent.name.end); - if (nameRange) { - binding = { - ...nameRange, - ast: call.parent.name, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - const argRange = toSourceRange(arg.getStart(sourceFile), arg.end); - if (argRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding, - accessor: { - ...argRange, - ast: arg, - requiredAccess: false, - }, - }); - } - } - else if (ts.isObjectLiteralExpression(arg)) { - for (const prop of arg.properties) { - if (prop.name?.getText(sourceFile) === 'get') { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceRange(call.parent.name.getStart(sourceFile), call.parent.name.end); - if (nameRange) { - binding = { - ...nameRange, - ast: call.parent.name, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - if (ts.isPropertyAssignment(prop)) { - const callback = prop.initializer; - if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) { - const callbackRange = toSourceRange(callback.getStart(sourceFile), callback.end); - if (callbackRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...callbackRange, - ast: callback.body, - requiredAccess: true, - }, - callback: { - ...callbackRange, - ast: callback.body, - }, - }); - } - } - } - else if (ts.isMethodDeclaration(prop) && prop.body) { - const bodyRange = toSourceRange(prop.body.getStart(sourceFile), prop.body.end); - if (bodyRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...bodyRange, - ast: prop.body, - requiredAccess: true, - }, - callback: { - ...bodyRange, - ast: prop.body, - }, - }); - } - } - } - } - } + const sourceFile = languageService.getProgram()!.getSourceFile(sourceScript.id)!; + const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript( + sourceScript.generated.root, + ); + const map = serviceScript ? language.maps.get(serviceScript.code, sourceScript) : undefined; + const toSourceRange = map + ? (pos: number, end: number) => { + for (const [mappedStart, mappedEnd] of map.toSourceRange(pos - leadingOffset, end - leadingOffset, false)) { + return { pos: mappedStart, end: mappedEnd }; } } - node.forEachChild(visit); - }); + : (pos: number, end: number) => ({ pos, end }); - sourceFile.forEachChild(function visit(node) { - if (ts.isPropertyAccessExpression(node)) { - const sourceRange = toSourceRange(node.expression.end, node.expression.end); - if (sourceRange) { - if (node.name.text === 'value') { - allValuePropertyAccess.add(sourceRange.end); - allPropertyAccess.add(sourceRange.end); - } - else if (node.name.text !== '') { - allPropertyAccess.add(sourceRange.end); - } - } - } - else if (ts.isElementAccessExpression(node)) { - const sourceRange = toSourceRange(node.expression.end, node.expression.end); - if (sourceRange) { - allPropertyAccess.add(sourceRange.end); - } - } - else if (ts.isCallExpression(node)) { - const sourceRange = toSourceRange(node.expression.end, node.expression.end); - if (sourceRange) { - allFunctionCalls.add(sourceRange.end); - } - } - node.forEachChild(visit); + return analyzer.analyze(sourceFile, position, { + typescript: ts, + languageService, + toSourceRange, }); - - return { - signals, - allValuePropertyAccess, - allPropertyAccess, - allFunctionCalls, - }; } diff --git a/packages/typescript-plugin/lib/requests/index.ts b/packages/typescript-plugin/lib/requests/index.ts index fc0ac8e9d6..97ed8c2b2d 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -15,12 +15,10 @@ export interface Requests { isRefAtPosition( fileName: string, position: number, - ): Response< - ReturnType - >; - getComponentDirectives(fileName: string): Response< - ReturnType - >; + ): Response>; + getComponentDirectives( + fileName: string, + ): Response>; getComponentEvents( fileName: string, tag: string, @@ -39,14 +37,23 @@ export interface Requests { fileName: string, tag: string, ): Response>; - getElementNames(fileName: string): Response>; + getElementNames( + fileName: string, + ): Response>; getReactiveReferences( fileName: string, position: number, ): Response>; - getDocumentHighlights(fileName: string, position: number): Response; - getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): Response< - ts.Classifications - >; - getQuickInfoAtPosition(fileName: string, position: ts.LineAndCharacter): Response; + getDocumentHighlights( + fileName: string, + position: number, + ): Response; + getEncodedSemanticClassifications( + fileName: string, + span: ts.TextSpan, + ): Response; + getQuickInfoAtPosition( + fileName: string, + position: ts.LineAndCharacter, + ): Response; } diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 4d736938ac..8f5cda0648 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vue/typescript-plugin", - "version": "3.0.7", + "version": "3.0.8", "license": "MIT", "files": [ "**/*.js", @@ -14,8 +14,9 @@ }, "dependencies": { "@volar/typescript": "2.4.23", - "@vue/language-core": "3.0.7", + "@vue/language-core": "3.0.8", "@vue/shared": "^3.5.0", + "laplacenoma": "^0.0.3", "path-browserify": "^1.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1cf891204..48c2eb1bb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,10 +57,10 @@ importers: specifier: ^3.5.0 version: 3.5.13 '@vue/language-server': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../../packages/language-server '@vue/typescript-plugin': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../../packages/typescript-plugin reactive-vscode: specifier: ^0.2.9 @@ -84,7 +84,7 @@ importers: specifier: 2.4.23 version: 2.4.23 '@vue/language-core': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../language-core path-browserify: specifier: ^1.0.1 @@ -93,7 +93,7 @@ importers: specifier: '*' version: 5.9.2 vue-component-type-helpers: - specifier: 3.0.7 + specifier: 3.0.8 version: link:../component-type-helpers devDependencies: '@types/node': @@ -167,7 +167,7 @@ importers: specifier: ^3.5.0 version: 3.5.13 '@vue/language-core': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../language-core packages/language-server: @@ -176,13 +176,13 @@ importers: specifier: 2.4.23 version: 2.4.23 '@vue/language-core': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../language-core '@vue/language-service': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../language-service '@vue/typescript-plugin': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../typescript-plugin typescript: specifier: '*' @@ -204,7 +204,7 @@ importers: specifier: 2.4.23 version: 2.4.23 '@vue/language-core': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../language-core '@vue/shared': specifier: ^3.5.0 @@ -256,7 +256,7 @@ importers: specifier: ^3.5.0 version: 3.5.13 '@vue/typescript-plugin': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../typescript-plugin vscode-css-languageservice: specifier: ^6.3.1 @@ -268,7 +268,7 @@ importers: specifier: 2.4.23 version: 2.4.23 '@vue/language-core': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../language-core typescript: specifier: '>=5.0.0' @@ -284,11 +284,14 @@ importers: specifier: 2.4.23 version: 2.4.23 '@vue/language-core': - specifier: 3.0.7 + specifier: 3.0.8 version: link:../language-core '@vue/shared': specifier: ^3.5.0 version: 3.5.13 + laplacenoma: + specifier: ^0.0.3 + version: 0.0.3 path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -309,7 +312,7 @@ importers: specifier: https://pkg.pr.new/vue@e1bc0eb02e22bc0c236e1471c11d96a368764b72 version: https://pkg.pr.new/vue@e1bc0eb02e22bc0c236e1471c11d96a368764b72(typescript@5.9.2) vue-component-meta: - specifier: 3.0.7 + specifier: 3.0.8 version: link:../packages/component-meta vue3.4: specifier: npm:vue@3.4.38 @@ -2503,6 +2506,9 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + laplacenoma@0.0.3: + resolution: {integrity: sha512-fsQMsMozEzGg/DhQG73nf8Nzx0XvU8RJFRViYuocM6pQSa33lucnHvJzsb4udQUzH5p5zdX63t9qRahKAhWtiQ==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -6238,6 +6244,8 @@ snapshots: kind-of@6.0.3: {} + laplacenoma@0.0.3: {} + leven@3.1.0: {} levn@0.4.1: diff --git a/test-workspace/package.json b/test-workspace/package.json index 5f49527de9..2705b62d67 100644 --- a/test-workspace/package.json +++ b/test-workspace/package.json @@ -1,10 +1,10 @@ { "private": true, - "version": "3.0.7", + "version": "3.0.8", "devDependencies": { "typescript": "latest", "vue": "https://pkg.pr.new/vue@e1bc0eb02e22bc0c236e1471c11d96a368764b72", - "vue-component-meta": "3.0.7", + "vue-component-meta": "3.0.8", "vue3.4": "npm:vue@3.4.38" } }