From 9bfbfcc650f1150bb238a9c2eeb9348f33b021b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:48:47 +0800 Subject: [PATCH 01/14] fix(language-core): initialize properties of `VueVirtualCode` in constructor (#5635) --- .../language-core/lib/virtualFile/vueFile.ts | 49 +++++++++---------- packages/typescript-plugin/lib/common.ts | 4 +- 2 files changed, 25 insertions(+), 28 deletions(-) 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/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) { From b6b0cb41f4f3de2ee7de5f74894e6b6a8ba932fc Mon Sep 17 00:00:00 2001 From: KazariEX Date: Fri, 12 Sep 2025 17:02:59 +0800 Subject: [PATCH 02/14] fix: correct background task start pattern for debug launch --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..." } } From 29570bdb654428bdf4454f49410301e91e5935f1 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Sat, 13 Sep 2025 16:18:45 +0800 Subject: [PATCH 03/14] fix(typescript-plugin): typo of `dependentRanges` --- .../typescript-plugin/lib/requests/getReactiveReferences.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts index 9ec9f31133..441648bf4e 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts @@ -104,7 +104,7 @@ export function getReactiveReferences( statements[statements.length - 1]!.end, ); if (sourceRange) { - dependencyRanges.push({ start: sourceRange.start, end: sourceRange.end }); + dependentRanges.push({ start: sourceRange.start, end: sourceRange.end }); } } else { From ce82facfcea64e0eb538f1276bdd6bc7a9fd5dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Sat, 13 Sep 2025 16:57:04 +0800 Subject: [PATCH 04/14] fix(vscode): flatten reactivity visualization decorators (#5642) --- .../vscode/lib/reactivityVisualization.ts | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 88ae5b01f0..79ca0240c6 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -2,7 +2,7 @@ import type { getReactiveReferences } from '@vue/typescript-plugin/lib/requests/ 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 +12,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', @@ -62,8 +62,8 @@ export function activate( return; } if (!config.editor.reactivityVisualization) { - editor.setDecorations(dependenciesDecorations, []); - editor.setDecorations(subscribersDecorations, []); + editor.setDecorations(dependencyDecorations, []); + editor.setDecorations(dependentDecorations, []); return; } @@ -82,27 +82,47 @@ 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: { + start: number; + end: number; +}[]) { + const documentRanges = ranges + .map(range => + new vscode.Range( + document.positionAt(range.start), + document.positionAt(range.end), + ) + ) + .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; +} From 743785b55dd33e7a070d0aa9fe725c497bb867c6 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 13 Sep 2025 22:04:08 +0800 Subject: [PATCH 05/14] feat(vscode): adjust reactivity visualization update interval --- extensions/vscode/lib/reactivityVisualization.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 79ca0240c6..192d4fe6b8 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -26,6 +26,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 +44,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') { From 7051894571ccbe6430d67c6f16c4ce2377f4cd67 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 14 Sep 2025 03:22:41 +0800 Subject: [PATCH 06/14] fix(typescript-plugin): ensure TS node corresponds to the mapping range (#5643) --- .../lib/requests/getReactiveReferences.ts | 157 +++++++----------- 1 file changed, 60 insertions(+), 97 deletions(-) diff --git a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts index 441648bf4e..05f309ba1c 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts @@ -50,10 +50,17 @@ export function getReactiveReferences( } : (start: number, end: number) => ({ start, end }); + const toSourceNode = (node: ts.Node, endNode = node) => { + const sourceRange = toSourceRange(node.getStart(sourceFile), endNode.end); + if (sourceRange) { + return { ...sourceRange, ast: node }; + } + }; + const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!; if (!analyzeCache.has(sourceFile)) { - analyzeCache.set(sourceFile, analyze(ts, sourceFile, toSourceRange)); + analyzeCache.set(sourceFile, analyze(ts, sourceFile, toSourceRange, toSourceNode)); } const { @@ -81,9 +88,9 @@ export function getReactiveReferences( 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, + const sourceRange = toSourceNode( + ast.statements[0]!, + ast.statements[ast.statements.length - 1], ); if (sourceRange) { dependencyRanges.push({ start: sourceRange.start, end: sourceRange.end }); @@ -99,9 +106,9 @@ export function getReactiveReferences( } 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, + const sourceRange = toSourceNode( + statements[0]!, + statements[statements.length - 1], ); if (sourceRange) { dependentRanges.push({ start: sourceRange.start, end: sourceRange.end }); @@ -127,13 +134,10 @@ export function getReactiveReferences( const { requiredAccess } = signal.accessor; visit(signal.accessor, requiredAccess); signal.accessor.ast.forEachChild(child => { - const childRange = toSourceRange(child.getStart(sourceFile), child.end); + const childRange = toSourceNode(child); if (childRange) { visit( - { - ...childRange, - ast: child, - }, + childRange, requiredAccess, ); } @@ -226,13 +230,10 @@ export function getReactiveReferences( } } node.ast.forEachChild(child => { - const childRange = toSourceRange(child.getStart(sourceFile), child.end); + const childRange = toSourceNode(child); if (childRange) { visit( - { - ...childRange, - ast: child, - }, + childRange, requiredAccess, ts.isPropertyAccessExpression(node.ast) || ts.isElementAccessExpression(node.ast), ); @@ -328,6 +329,7 @@ function analyze( ts: typeof import('typescript'), sourceFile: ts.SourceFile, toSourceRange: (start: number, end: number) => { start: number; end: number } | undefined, + toSourceNode: (node: ts.Node) => { ast: ts.Node; start: number; end: number } | undefined, ) { const signals: ReactiveNode[] = []; const allValuePropertyAccess = new Set(); @@ -344,14 +346,13 @@ function analyze( callName === 'ref' || callName === 'shallowRef' || callName === 'toRef' || callName === 'useTemplateRef' || callName === 'defineModel' ) { - const nameRange = toSourceRange(node.name.getStart(sourceFile), node.name.end); + const nameRange = toSourceNode(node.name); if (nameRange) { signals.push({ isDependency: true, isDependent: false, binding: { ...nameRange, - ast: node.name, accessTypes: [ReactiveAccessType.ValueProperty], }, }); @@ -361,14 +362,13 @@ function analyze( callName === 'reactive' || callName === 'shallowReactive' || callName === 'defineProps' || callName === 'withDefaults' ) { - const nameRange = toSourceRange(node.name.getStart(sourceFile), node.name.end); + const nameRange = toSourceNode(node.name); if (nameRange) { signals.push({ isDependency: true, isDependent: false, binding: { ...nameRange, - ast: node.name, accessTypes: [ReactiveAccessType.AnyProperty], }, }); @@ -380,26 +380,21 @@ function analyze( } 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); + const nameRange = toSourceNode(node.name); + const bodyRange = toSourceNode(node.body); 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, - }, + callback: bodyRange, }); } } @@ -411,26 +406,21 @@ function analyze( 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); + const nameRange = toSourceNode(name); + const callbackRange = toSourceNode(callback); 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, - }, + callback: callbackRange, }); } } @@ -440,14 +430,13 @@ function analyze( 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); + const nameRange = toSourceNode(node.name); if (nameRange) { signals.push({ isDependency: true, isDependent: false, binding: { ...nameRange, - ast: node.name, accessTypes: [ReactiveAccessType.ValueProperty], }, }); @@ -461,20 +450,16 @@ function analyze( 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) { + const bodyRange = toSourceNode(callback.body); + if (bodyRange) { signals.push({ isDependency: false, isDependent: true, accessor: { - ...callbackRange, - ast: callback.body, + ...bodyRange, requiredAccess: true, }, - callback: { - ...callbackRange, - ast: callback.body, - }, + callback: bodyRange, }); } } @@ -483,37 +468,33 @@ function analyze( 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)) { + if (ts.isArrowFunction(depsCallback) || ts.isFunctionExpression(depsCallback)) { + const depsBodyRange = toSourceNode(depsCallback.body); + const effectBodyRange = toSourceNode(effectCallback.body); + if (depsBodyRange && effectBodyRange) { signals.push({ isDependency: false, isDependent: true, accessor: { - ...depsRange, - ast: depsCallback.body, + ...depsBodyRange, requiredAccess: true, }, - callback: { - ...effectRange, - ast: effectCallback.body, - }, + callback: effectBodyRange, }); } - else { + } + else { + const depsRange = toSourceNode(depsCallback); + const effectBodyRange = toSourceNode(effectCallback.body); + if (depsRange && effectBodyRange) { signals.push({ isDependency: false, isDependent: true, accessor: { ...depsRange, - ast: depsCallback, requiredAccess: false, }, - callback: { - ...effectRange, - ast: effectCallback.body, - }, + callback: effectBodyRange, }); } } @@ -522,16 +503,15 @@ function analyze( 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); + const nameRange = toSourceNode(call.parent.name); if (nameRange) { binding = { ...nameRange, - ast: call.parent.name, accessTypes: [ReactiveAccessType.AnyProperty, ReactiveAccessType.Call], }; } } - const callRange = toSourceRange(call.getStart(sourceFile), call.end); + const callRange = toSourceNode(call); if (callRange) { signals.push({ isDependency: true, @@ -539,7 +519,6 @@ function analyze( binding, accessor: { ...callRange, - ast: call, requiredAccess: false, }, }); @@ -550,46 +529,40 @@ function analyze( 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); + const nameRange = toSourceNode(call.parent.name); if (nameRange) { binding = { ...nameRange, - ast: call.parent.name, accessTypes: [ReactiveAccessType.ValueProperty], }; } } - const argRange = toSourceRange(arg.getStart(sourceFile), arg.end); - if (argRange) { + const bodyRange = toSourceNode(arg.body); + if (bodyRange) { signals.push({ isDependency: true, isDependent: true, binding, accessor: { - ...argRange, - ast: arg.body, + ...bodyRange, requiredAccess: true, }, - callback: { - ...argRange, - ast: arg.body, - }, + callback: bodyRange, }); } } 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); + const nameRange = toSourceNode(call.parent.name); if (nameRange) { binding = { ...nameRange, - ast: call.parent.name, accessTypes: [ReactiveAccessType.ValueProperty], }; } } - const argRange = toSourceRange(arg.getStart(sourceFile), arg.end); + const argRange = toSourceNode(arg); if (argRange) { signals.push({ isDependency: true, @@ -597,7 +570,6 @@ function analyze( binding, accessor: { ...argRange, - ast: arg, requiredAccess: false, }, }); @@ -608,11 +580,10 @@ function analyze( 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); + const nameRange = toSourceNode(call.parent.name); if (nameRange) { binding = { ...nameRange, - ast: call.parent.name, accessTypes: [ReactiveAccessType.ValueProperty], }; } @@ -620,27 +591,23 @@ function analyze( 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) { + const bodyRange = toSourceNode(callback.body); + if (bodyRange) { signals.push({ isDependency: true, isDependent: true, binding, accessor: { - ...callbackRange, - ast: callback.body, + ...bodyRange, requiredAccess: true, }, - callback: { - ...callbackRange, - ast: callback.body, - }, + callback: bodyRange, }); } } } else if (ts.isMethodDeclaration(prop) && prop.body) { - const bodyRange = toSourceRange(prop.body.getStart(sourceFile), prop.body.end); + const bodyRange = toSourceNode(prop.body); if (bodyRange) { signals.push({ isDependency: true, @@ -648,13 +615,9 @@ function analyze( binding, accessor: { ...bodyRange, - ast: prop.body, requiredAccess: true, }, - callback: { - ...bodyRange, - ast: prop.body, - }, + callback: bodyRange, }); } } From c6c652bde54c3642b0bd6db8d40c633e2a41db9f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 16 Sep 2025 02:40:46 +0800 Subject: [PATCH 07/14] refactor(vscode): remove unused client parameter from activate functions --- extensions/vscode/index.ts | 4 ++-- extensions/vscode/lib/focusMode.ts | 6 +----- extensions/vscode/lib/interpolationDecorators.ts | 6 +----- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/extensions/vscode/index.ts b/extensions/vscode/index.ts index 91eb46d69c..2d47e3a1fb 100644 --- a/extensions/vscode/index.ts +++ b/extensions/vscode/index.ts @@ -102,8 +102,8 @@ export = defineExtension(() => { 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 }); 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 | undefined; for (const editor of vscode.window.visibleTextEditors) { From bd201b2b98affb45ee0dac4f7864d586b59855ab Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 16 Sep 2025 19:18:47 +0800 Subject: [PATCH 08/14] fix(vscode): normalize Reactivity Visualization ranges fix https://github.com/vuejs/language-tools/pull/5642#issuecomment-3288695993 --- extensions/vscode/lib/reactivityVisualization.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 192d4fe6b8..c1ee7a0b46 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -118,8 +118,10 @@ function getFlatRanges(document: vscode.TextDocument, ranges: { const documentRanges = ranges .map(range => new vscode.Range( - document.positionAt(range.start), - document.positionAt(range.end), + document.positionAt(range.start).line, + 0, + document.positionAt(range.end).line, + 0, ) ) .sort((a, b) => a.start.compareTo(b.start)); From a40c6d8c0ba7b2ae5fcf774098eda11242a49995 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 17 Sep 2025 00:15:51 +0800 Subject: [PATCH 09/14] fix(vscode): patch typescriptServerPlugin languages without FS hack --- extensions/vscode/index.ts | 31 +++++++++++++------------------ extensions/vscode/package.json | 7 ------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/extensions/vscode/index.ts b/extensions/vscode/index.ts index 2d47e3a1fb..ed61be63bf 100644 --- a/extensions/vscode/index.ts +++ b/extensions/vscode/index.ts @@ -186,29 +186,24 @@ if (tsExtension.isActive) { else { 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 vuePluginName = 'vue-typescript-plugin-pack'; + + vueExtension.packageJSON.contributes.typescriptServerPlugins = [ + { + name: vuePluginName, + 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]', diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 048d13c12f..d6218b49dd 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -67,13 +67,6 @@ "configuration": "./languages/sfc-template-language-configuration.json" } ], - "typescriptServerPlugins": [ - { - "name": "vue-typescript-plugin-pack", - "enableForWorkspaceTypeScriptVersions": true, - "configNamespace": "typescript" - } - ], "grammars": [ { "language": "vue", From b274db264c1083275c5c04958292f406838a71eb Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 22 Sep 2025 20:55:37 +0800 Subject: [PATCH 10/14] feat(vscode): introduce `vue.server.path` setting (#5647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 山吹色御守 <85992002+KazariEX@users.noreply.github.com> Co-authored-by: KazariEX --- extensions/vscode/index.ts | 77 ++++++++++++++++++------ extensions/vscode/lib/generated-meta.ts | 25 ++++++-- extensions/vscode/package.json | 4 ++ extensions/vscode/package.nls.ja.json | 1 + extensions/vscode/package.nls.json | 1 + extensions/vscode/package.nls.ru.json | 1 + extensions/vscode/package.nls.zh-CN.json | 1 + extensions/vscode/package.nls.zh-TW.json | 1 + 8 files changed, 90 insertions(+), 21 deletions(-) diff --git a/extensions/vscode/index.ts b/extensions/vscode/index.ts index ed61be63bf..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,7 +100,14 @@ 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; @@ -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,21 +190,52 @@ 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 { publisher, name } = require('./package.json'); const vueExtension = vscode.extensions.getExtension(`${publisher}.${name}`)!; - const vuePluginName = 'vue-typescript-plugin-pack'; + const tsPluginName = 'vue-typescript-plugin-pack'; vueExtension.packageJSON.contributes.typescriptServerPlugins = [ { - name: vuePluginName, + name: tsPluginName, enableForWorkspaceTypeScriptVersions: true, configNamespace: 'typescript', languages: config.server.includeLanguages, @@ -218,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; @@ -232,4 +274,5 @@ else { const patchedModule = require(extensionJsPath); Object.assign(loadedModule.exports, patchedModule); } + return true; } diff --git a/extensions/vscode/lib/generated-meta.ts b/extensions/vscode/lib/generated-meta.ts index 15a5674918..aff365e989 100644 --- a/extensions/vscode/lib/generated-meta.ts +++ b/extensions/vscode/lib/generated-meta.ts @@ -4,7 +4,7 @@ // Meta info export const publisher = 'Vue'; export const name = 'volar'; -export const version = '3.0.6'; +export const version = '3.0.7'; export const displayName = 'Vue (Official)'; export const description = 'Language Support for Vue'; export const extensionId = `${publisher}.${name}`; @@ -40,6 +40,7 @@ export type ConfigKey = | 'vue.editor.focusMode' | 'vue.editor.reactivityVisualization' | 'vue.editor.templateInterpolationDecorators' + | 'vue.server.path' | 'vue.server.includeLanguages' | 'vue.codeActions.askNewComponentName' | 'vue.suggest.componentNameCasing' @@ -62,6 +63,7 @@ export interface ConfigKeyTypeMap { 'vue.editor.focusMode': boolean; 'vue.editor.reactivityVisualization': boolean; 'vue.editor.templateInterpolationDecorators': boolean; + 'vue.server.path': string | undefined; 'vue.server.includeLanguages': string[]; 'vue.codeActions.askNewComponentName': boolean; 'vue.suggest.componentNameCasing': 'preferKebabCase' | 'preferPascalCase' | 'alwaysKebabCase' | 'alwaysPascalCase'; @@ -92,6 +94,7 @@ export interface ConfigShorthandMap { editorFocusMode: 'vue.editor.focusMode'; editorReactivityVisualization: 'vue.editor.reactivityVisualization'; editorTemplateInterpolationDecorators: 'vue.editor.templateInterpolationDecorators'; + serverPath: 'vue.server.path'; serverIncludeLanguages: 'vue.server.includeLanguages'; codeActionsAskNewComponentName: 'vue.codeActions.askNewComponentName'; suggestComponentNameCasing: 'vue.suggest.componentNameCasing'; @@ -115,6 +118,7 @@ export interface ConfigShorthandTypeMap { editorFocusMode: boolean; editorReactivityVisualization: boolean; editorTemplateInterpolationDecorators: boolean; + serverPath: string | undefined; serverIncludeLanguages: string[]; codeActionsAskNewComponentName: boolean; suggestComponentNameCasing: 'preferKebabCase' | 'preferPascalCase' | 'alwaysKebabCase' | 'alwaysPascalCase'; @@ -160,12 +164,12 @@ export const configs = { } as ConfigItem<'vue.trace.server'>, /** * @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/package.json b/extensions/vscode/package.json index d6218b49dd..8e3f50f406 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -272,6 +272,10 @@ "default": true, "markdownDescription": "%configuration.editor.templateInterpolationDecorators%" }, + "vue.server.path": { + "type": "string", + "markdownDescription": "%configuration.server.path%" + }, "vue.server.includeLanguages": { "type": "array", "items": { 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": "首選元件名稱格式。", From 3bfd059f5c3dadb2b25e1a82f52480e9d74f97bb Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 22 Sep 2025 22:23:57 +0800 Subject: [PATCH 11/14] perf(typescript-plugin): redo single-file language service for Reactivity Visualization (#5652) --- packages/typescript-plugin/index.ts | 8 ++- .../lib/requests/getReactiveReferences.ts | 54 +++++++++++++++---- 2 files changed, 48 insertions(+), 14 deletions(-) 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/requests/getReactiveReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts index 05f309ba1c..d228d0a1d3 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts @@ -1,5 +1,4 @@ -/// - +import { createProxyLanguageService, decorateLanguageServiceHost } from '@volar/typescript'; import { collectBindingRanges, hyphenateAttr, type Language, type SourceScript } from '@vue/language-core'; import type * as ts from 'typescript'; @@ -27,21 +26,58 @@ interface ReactiveNode { callback?: TSNode; } +let currentVersion = -1; +let currentFileName = ''; +let currentSnapshot: ts.IScriptSnapshot | undefined; +let languageService: ts.LanguageService | undefined; +let languageServiceHost: ts.LanguageServiceHost | undefined; + const analyzeCache = new WeakMap>(); 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( + if (currentSnapshot !== sourceScript.snapshot || currentFileName !== sourceScript.id) { + currentSnapshot = sourceScript.snapshot; + currentFileName = sourceScript.id; + currentVersion++; + } + 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; + } + return getReactiveReferencesWorker(ts, language, languageService, sourceScript, position, leadingOffset); +} + +function getReactiveReferencesWorker( + ts: typeof import('typescript'), + language: Language, + languageService: ts.LanguageService, + sourceScript: SourceScript, + position: number, + leadingOffset: number, +) { + const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript( sourceScript.generated.root, ); - const map = serviceScript ? language.maps.get(serviceScript.code, sourceScript!) : undefined; + 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)) { @@ -57,7 +93,7 @@ export function getReactiveReferences( } }; - const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!; + const sourceFile = languageService.getProgram()!.getSourceFile(sourceScript.id)!; if (!analyzeCache.has(sourceFile)) { analyzeCache.set(sourceFile, analyze(ts, sourceFile, toSourceRange, toSourceNode)); From cb6eef1e66e85b4b4c99a3d44d152bc1b2bba5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:22:56 +0800 Subject: [PATCH 12/14] refactor(typescript-plugin): externalize reactivity analysis logic (#5645) --- .../vscode/lib/reactivityVisualization.ts | 8 +- .../lib/requests/getReactiveReferences.ts | 667 +----------------- .../typescript-plugin/lib/requests/index.ts | 31 +- packages/typescript-plugin/package.json | 1 + pnpm-lock.yaml | 8 + 5 files changed, 46 insertions(+), 669 deletions(-) diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index c1ee7a0b46..d8f0897be8 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,4 +1,5 @@ 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'; @@ -111,14 +112,11 @@ export function activate( } } -function getFlatRanges(document: vscode.TextDocument, ranges: { - start: number; - end: number; -}[]) { +function getFlatRanges(document: vscode.TextDocument, ranges: ts.TextRange[]) { const documentRanges = ranges .map(range => new vscode.Range( - document.positionAt(range.start).line, + document.positionAt(range.pos).line, 0, document.positionAt(range.end).line, 0, diff --git a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts index d228d0a1d3..f918fe3796 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts @@ -1,30 +1,12 @@ import { createProxyLanguageService, decorateLanguageServiceHost } from '@volar/typescript'; -import { collectBindingRanges, hyphenateAttr, type Language, type SourceScript } from '@vue/language-core'; +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, +}); let currentVersion = -1; let currentFileName = ''; @@ -32,8 +14,6 @@ let currentSnapshot: ts.IScriptSnapshot | undefined; let languageService: ts.LanguageService | undefined; let languageServiceHost: ts.LanguageServiceHost | undefined; -const analyzeCache = new WeakMap>(); - export function getReactiveReferences( ts: typeof import('typescript'), language: Language, @@ -63,640 +43,23 @@ export function getReactiveReferences( proxied.initialize(language); languageService = proxied.proxy; } - return getReactiveReferencesWorker(ts, language, languageService, sourceScript, position, leadingOffset); -} -function getReactiveReferencesWorker( - ts: typeof import('typescript'), - language: Language, - languageService: ts.LanguageService, - sourceScript: SourceScript, - position: number, - leadingOffset: number, -) { + 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 - ? (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 toSourceNode = (node: ts.Node, endNode = node) => { - const sourceRange = toSourceRange(node.getStart(sourceFile), endNode.end); - if (sourceRange) { - return { ...sourceRange, ast: node }; - } - }; - - const sourceFile = languageService.getProgram()!.getSourceFile(sourceScript.id)!; - - if (!analyzeCache.has(sourceFile)) { - analyzeCache.set(sourceFile, analyze(ts, sourceFile, toSourceRange, toSourceNode)); - } - - 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 = toSourceNode( - ast.statements[0]!, - ast.statements[ast.statements.length - 1], - ); - if (sourceRange) { - dependencyRanges.push({ start: sourceRange.start, end: sourceRange.end }); + ? (pos: number, end: number) => { + for (const [mappedStart, mappedEnd] of map.toSourceRange(pos - leadingOffset, end - leadingOffset, false)) { + return { pos: mappedStart, end: mappedEnd }; } } - 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 = toSourceNode( - statements[0]!, - statements[statements.length - 1], - ); - if (sourceRange) { - dependentRanges.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; + : (pos: number, end: number) => ({ pos, end }); - if (signal.accessor) { - const { requiredAccess } = signal.accessor; - visit(signal.accessor, requiredAccess); - signal.accessor.ast.forEachChild(child => { - const childRange = toSourceNode(child); - if (childRange) { - visit( - childRange, - 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 = toSourceNode(child); - if (childRange) { - visit( - childRange, - requiredAccess, - ts.isPropertyAccessExpression(node.ast) || ts.isElementAccessExpression(node.ast), - ); - } - }); - } - } - - 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(); - } - - 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, - toSourceNode: (node: ts.Node) => { ast: ts.Node; 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 = toSourceNode(node.name); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }, - }); - } - } - else if ( - callName === 'reactive' || callName === 'shallowReactive' || callName === 'defineProps' - || callName === 'withDefaults' - ) { - const nameRange = toSourceNode(node.name); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.AnyProperty], - }, - }); - } - } - // TODO: toRefs - } - } - } - else if (ts.isFunctionDeclaration(node)) { - if (node.name && node.body) { - const nameRange = toSourceNode(node.name); - const bodyRange = toSourceNode(node.body); - if (nameRange && bodyRange) { - signals.push({ - isDependency: false, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.Call], - }, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - 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 = toSourceNode(name); - const callbackRange = toSourceNode(callback); - if (nameRange && callbackRange) { - signals.push({ - isDependency: false, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.Call], - }, - accessor: { - ...callbackRange, - requiredAccess: true, - }, - callback: callbackRange, - }); - } - } - } - } - 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 = toSourceNode(node.name); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - 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 bodyRange = toSourceNode(callback.body); - if (bodyRange) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - if (callName === 'watch' && call.arguments.length >= 2) { - const depsCallback = call.arguments[0]!; - const effectCallback = call.arguments[1]!; - if (ts.isArrowFunction(effectCallback) || ts.isFunctionExpression(effectCallback)) { - if (ts.isArrowFunction(depsCallback) || ts.isFunctionExpression(depsCallback)) { - const depsBodyRange = toSourceNode(depsCallback.body); - const effectBodyRange = toSourceNode(effectCallback.body); - if (depsBodyRange && effectBodyRange) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...depsBodyRange, - requiredAccess: true, - }, - callback: effectBodyRange, - }); - } - } - else { - const depsRange = toSourceNode(depsCallback); - const effectBodyRange = toSourceNode(effectCallback.body); - if (depsRange && effectBodyRange) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...depsRange, - requiredAccess: false, - }, - callback: effectBodyRange, - }); - } - } - } - } - else if (hyphenateAttr(callName).startsWith('use-')) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.AnyProperty, ReactiveAccessType.Call], - }; - } - } - const callRange = toSourceNode(call); - if (callRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding, - accessor: { - ...callRange, - 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 = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - const bodyRange = toSourceNode(arg.body); - if (bodyRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - else if (ts.isIdentifier(arg)) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - const argRange = toSourceNode(arg); - if (argRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding, - accessor: { - ...argRange, - 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 = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - if (ts.isPropertyAssignment(prop)) { - const callback = prop.initializer; - if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) { - const bodyRange = toSourceNode(callback.body); - if (bodyRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - else if (ts.isMethodDeclaration(prop) && prop.body) { - const bodyRange = toSourceNode(prop.body); - if (bodyRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - } - } - } - } - node.forEachChild(visit); + return analyzer.analyze(sourceFile, position, { + typescript: ts, + languageService, + toSourceRange, }); - - 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 { - 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..c2d1d4958f 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -16,6 +16,7 @@ "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.7", "@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..c66afbfe19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,9 @@ importers: '@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 @@ -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: From f7bdeaa7bb476df1fc8ff46c504d75c1869b0be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:23:33 +0800 Subject: [PATCH 13/14] fix(language-service): do not provide semantic tokens and document highlights for non-`file` scheme files (#5653) --- .../language-service/lib/plugins/typescript-semantic-tokens.ts | 2 +- .../language-service/lib/plugins/vue-document-highlights.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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; } From 10d056b79eb7a2d87f9e70b65afec1d63a163d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:38:20 +0800 Subject: [PATCH 14/14] v3.0.8 (#5654) --- CHANGELOG.md | 22 +++++++++++++++++ extensions/vscode/package.json | 6 ++--- lerna.json | 2 +- packages/component-meta/package.json | 6 ++--- packages/component-type-helpers/package.json | 2 +- packages/language-core/package.json | 2 +- packages/language-plugin-pug/package.json | 4 +-- packages/language-server/package.json | 8 +++--- packages/language-service/package.json | 6 ++--- packages/tsc/package.json | 4 +-- packages/typescript-plugin/package.json | 4 +-- pnpm-lock.yaml | 26 ++++++++++---------- test-workspace/package.json | 4 +-- 13 files changed, 59 insertions(+), 37 deletions(-) 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/package.json b/extensions/vscode/package.json index 8e3f50f406..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", @@ -464,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/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/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/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/package.json b/packages/typescript-plugin/package.json index c2d1d4958f..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,7 +14,7 @@ }, "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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c66afbfe19..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,7 +284,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 @@ -312,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 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" } }