From b13e7617a84383858fd4e9b42c002e56f3b22360 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Mon, 15 Sep 2025 22:28:54 +0800 Subject: [PATCH 1/7] refactor(typescript-plugin): extract reactivity analysis logic to `laplacenoma` --- .../vscode/lib/reactivityVisualization.ts | 14 +- packages/language-server/index.ts | 4 +- packages/typescript-plugin/index.ts | 8 +- .../lib/requests/getReactiveReferences.ts | 666 ------------------ .../lib/requests/getReactivityAnalysis.ts | 39 + .../typescript-plugin/lib/requests/index.ts | 4 +- packages/typescript-plugin/package.json | 1 + pnpm-lock.yaml | 8 + 8 files changed, 62 insertions(+), 682 deletions(-) delete mode 100644 packages/typescript-plugin/lib/requests/getReactiveReferences.ts create mode 100644 packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 192d4fe6b8..0d928d2e3e 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,6 +1,7 @@ -import type { getReactiveReferences } from '@vue/typescript-plugin/lib/requests/getReactiveReferences'; +import type { getReactivityAnalysis } from '@vue/typescript-plugin/lib/requests/getReactivityAnalysis'; import * as vscode from 'vscode'; import { config } from './config'; +import type ts = require('typescript'); const dependencyDecorations = vscode.window.createTextEditorDecorationType({ isWholeLine: true, @@ -84,11 +85,11 @@ export function activate( try { const result = await vscode.commands.executeCommand< { - body?: ReturnType; + body?: ReturnType; } | undefined >( 'typescript.tsserverRequest', - '_vue:getReactiveReferences', + '_vue:getReactivityAnalysis', [ document.uri.fsPath.replace(/\\/g, '/'), document.offsetAt(editor.selection.active), @@ -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), + document.positionAt(range.pos), document.positionAt(range.end), ) ) diff --git a/packages/language-server/index.ts b/packages/language-server/index.ts index a64c3b07b6..210d005bbf 100644 --- a/packages/language-server/index.ts +++ b/packages/language-server/index.ts @@ -122,8 +122,8 @@ connection.onInitialize(params => { getImportPathForFile(...args) { return sendTsServerRequest('_vue:getImportPathForFile', args); }, - getReactiveReferences(...args) { - return sendTsServerRequest('_vue:getReactiveReferences', args); + getReactivityAnalysis(...args) { + return sendTsServerRequest('_vue:getReactivityAnalysis', args); }, isRefAtPosition(...args) { return sendTsServerRequest('_vue:isRefAtPosition', args); diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 59bc03dc06..60a506109a 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -12,7 +12,7 @@ import { getComponentSlots } from './lib/requests/getComponentSlots'; import { getElementAttrs } from './lib/requests/getElementAttrs'; import { getElementNames } from './lib/requests/getElementNames'; import { getImportPathForFile } from './lib/requests/getImportPathForFile'; -import { getReactiveReferences } from './lib/requests/getReactiveReferences'; +import { getReactivityAnalysis } from './lib/requests/getReactivityAnalysis'; import { isRefAtPosition } from './lib/requests/isRefAtPosition'; const windowsPathReg = /\\/g; @@ -187,12 +187,12 @@ export = createLanguageServicePlugin( const { languageService } = getLanguageService(fileName); return createResponse(getElementNames(ts, languageService.getProgram()!, fileName)); }); - session.addProtocolHandler('_vue:getReactiveReferences', request => { - const [fileName, position]: Parameters = request.arguments; + session.addProtocolHandler('_vue:getReactivityAnalysis', request => { + const [fileName, position]: Parameters = request.arguments; const { languageService, language } = getLanguageService(fileName); const sourceScript = language.scripts.get(fileName); return createResponse( - getReactiveReferences( + getReactivityAnalysis( ts, language, languageService, diff --git a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts deleted file mode 100644 index 05f309ba1c..0000000000 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ /dev/null @@ -1,666 +0,0 @@ -/// - -import { collectBindingRanges, hyphenateAttr, type Language, type SourceScript } from '@vue/language-core'; -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 analyzeCache = new WeakMap>(); - -export function getReactiveReferences( - ts: typeof import('typescript'), - language: Language, - languageService: ts.LanguageService, - sourceScript: SourceScript | undefined, - fileName: string, - 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 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, 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 }); - } - } - 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; - - 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); - }); - - 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/getReactivityAnalysis.ts b/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts new file mode 100644 index 0000000000..27e06b18fd --- /dev/null +++ b/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts @@ -0,0 +1,39 @@ +/// + +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 analyzer = createAnalyzer({ + rules: rulesVue, +}); + +export function getReactivityAnalysis( + ts: typeof import('typescript'), + language: Language, + languageService: ts.LanguageService, + sourceScript: SourceScript | undefined, + fileName: string, + position: number, + leadingOffset: number = 0, +) { + const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!; + 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 }; + } + } + : (pos: number, end: number) => ({ pos, end }); + + return analyzer.analyze(sourceFile, position, { + typescript: ts, + languageService, + toSourceRange, + }); +} diff --git a/packages/typescript-plugin/lib/requests/index.ts b/packages/typescript-plugin/lib/requests/index.ts index fc0ac8e9d6..34f525e3e4 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -40,10 +40,10 @@ export interface Requests { tag: string, ): Response>; getElementNames(fileName: string): Response>; - getReactiveReferences( + getReactivityAnalysis( fileName: string, position: number, - ): Response>; + ): Response>; getDocumentHighlights(fileName: string, position: number): Response; getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): Response< ts.Classifications diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 4d736938ac..2657000103 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.2", "path-browserify": "^1.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1cf891204..69b61f8cb4 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.2 + version: 0.0.2 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.2: + resolution: {integrity: sha512-XAIalRKCXZapdVXALGG5y9Jc7d5AMnr5NyrBWGwBvKWjSGi99ftzmdLS8mVQQF07bk8poPl8tSnjOYpkU86P5A==} + 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.2: {} + leven@3.1.0: {} levn@0.4.1: From 785c8819a48fccb268b7252975b469c1f1337560 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Tue, 16 Sep 2025 21:13:28 +0800 Subject: [PATCH 2/7] chore: bump --- packages/typescript-plugin/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 2657000103..c2d1d4958f 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -16,7 +16,7 @@ "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.7", "@vue/shared": "^3.5.0", - "laplacenoma": "^0.0.2", + "laplacenoma": "^0.0.3", "path-browserify": "^1.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69b61f8cb4..c66afbfe19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -290,8 +290,8 @@ importers: specifier: ^3.5.0 version: 3.5.13 laplacenoma: - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.3 + version: 0.0.3 path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -2506,8 +2506,8 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - laplacenoma@0.0.2: - resolution: {integrity: sha512-XAIalRKCXZapdVXALGG5y9Jc7d5AMnr5NyrBWGwBvKWjSGi99ftzmdLS8mVQQF07bk8poPl8tSnjOYpkU86P5A==} + laplacenoma@0.0.3: + resolution: {integrity: sha512-fsQMsMozEzGg/DhQG73nf8Nzx0XvU8RJFRViYuocM6pQSa33lucnHvJzsb4udQUzH5p5zdX63t9qRahKAhWtiQ==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -6244,7 +6244,7 @@ snapshots: kind-of@6.0.3: {} - laplacenoma@0.0.2: {} + laplacenoma@0.0.3: {} leven@3.1.0: {} From 3eed60184bbe2d1d9521736c8d415ba6a44a68cb Mon Sep 17 00:00:00 2001 From: KazariEX Date: Mon, 22 Sep 2025 23:00:14 +0800 Subject: [PATCH 3/7] chore: lint --- .../typescript-plugin/lib/requests/getReactivityAnalysis.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts b/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts index 45eb61ce2d..103add3da7 100644 --- a/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts +++ b/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts @@ -46,10 +46,10 @@ export function getReactivityAnalysis( } const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!; - const serviceScript = sourceScript?.generated?.languagePlugin.typescript?.getServiceScript( + 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 ? (pos: number, end: number) => { for (const [mappedStart, mappedEnd] of map.toSourceRange(pos - leadingOffset, end - leadingOffset, false)) { From ed24f093105d299beef63f80609e9c2e384c70d7 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Mon, 22 Sep 2025 23:59:02 +0800 Subject: [PATCH 4/7] refactor: revert to `getReactiveReferences` --- extensions/vscode/lib/reactivityVisualization.ts | 6 +++--- packages/language-server/index.ts | 4 ++-- packages/typescript-plugin/index.ts | 8 ++++---- ...etReactivityAnalysis.ts => getReactivityReferences.ts} | 2 +- packages/typescript-plugin/lib/requests/index.ts | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) rename packages/typescript-plugin/lib/requests/{getReactivityAnalysis.ts => getReactivityReferences.ts} (98%) diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 659994308f..1af64199fc 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,4 +1,4 @@ -import type { getReactivityAnalysis } from '@vue/typescript-plugin/lib/requests/getReactivityAnalysis'; +import type { getReactiveReferences } from '@vue/typescript-plugin/lib/requests/getReactiveReferences'; import * as vscode from 'vscode'; import { config } from './config'; import type ts = require('typescript'); @@ -85,11 +85,11 @@ export function activate( try { const result = await vscode.commands.executeCommand< { - body?: ReturnType; + body?: ReturnType; } | undefined >( 'typescript.tsserverRequest', - '_vue:getReactivityAnalysis', + '_vue:getReactiveReferences', [ document.uri.fsPath.replace(/\\/g, '/'), document.offsetAt(editor.selection.active), diff --git a/packages/language-server/index.ts b/packages/language-server/index.ts index 210d005bbf..a64c3b07b6 100644 --- a/packages/language-server/index.ts +++ b/packages/language-server/index.ts @@ -122,8 +122,8 @@ connection.onInitialize(params => { getImportPathForFile(...args) { return sendTsServerRequest('_vue:getImportPathForFile', args); }, - getReactivityAnalysis(...args) { - return sendTsServerRequest('_vue:getReactivityAnalysis', args); + getReactiveReferences(...args) { + return sendTsServerRequest('_vue:getReactiveReferences', args); }, isRefAtPosition(...args) { return sendTsServerRequest('_vue:isRefAtPosition', args); diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index d3a592caac..21e1665a43 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -12,7 +12,7 @@ import { getComponentSlots } from './lib/requests/getComponentSlots'; import { getElementAttrs } from './lib/requests/getElementAttrs'; import { getElementNames } from './lib/requests/getElementNames'; import { getImportPathForFile } from './lib/requests/getImportPathForFile'; -import { getReactivityAnalysis } from './lib/requests/getReactivityAnalysis'; +import { getReactiveReferences } from './lib/requests/getReactiveReferences'; import { isRefAtPosition } from './lib/requests/isRefAtPosition'; const windowsPathReg = /\\/g; @@ -187,12 +187,12 @@ export = createLanguageServicePlugin( const { languageService } = getLanguageService(fileName); return createResponse(getElementNames(ts, languageService.getProgram()!, fileName)); }); - session.addProtocolHandler('_vue:getReactivityAnalysis', request => { - const [fileName, position]: Parameters = request.arguments; + session.addProtocolHandler('_vue:getReactiveReferences', request => { + const [fileName, position]: Parameters = request.arguments; const { language } = getLanguageService(fileName); const sourceScript = language.scripts.get(fileName)!; return createResponse( - getReactivityAnalysis( + getReactiveReferences( ts, language, sourceScript, diff --git a/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts b/packages/typescript-plugin/lib/requests/getReactivityReferences.ts similarity index 98% rename from packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts rename to packages/typescript-plugin/lib/requests/getReactivityReferences.ts index 103add3da7..4589cc3c33 100644 --- a/packages/typescript-plugin/lib/requests/getReactivityAnalysis.ts +++ b/packages/typescript-plugin/lib/requests/getReactivityReferences.ts @@ -14,7 +14,7 @@ let currentSnapshot: ts.IScriptSnapshot | undefined; let languageService: ts.LanguageService | undefined; let languageServiceHost: ts.LanguageServiceHost | undefined; -export function getReactivityAnalysis( +export function getReactiveReferences( ts: typeof import('typescript'), language: Language, sourceScript: SourceScript, diff --git a/packages/typescript-plugin/lib/requests/index.ts b/packages/typescript-plugin/lib/requests/index.ts index 34f525e3e4..fc0ac8e9d6 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -40,10 +40,10 @@ export interface Requests { tag: string, ): Response>; getElementNames(fileName: string): Response>; - getReactivityAnalysis( + getReactiveReferences( fileName: string, position: number, - ): Response>; + ): Response>; getDocumentHighlights(fileName: string, position: number): Response; getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): Response< ts.Classifications From e4d367b1bcda8a320479f46a5f912478b693e594 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Tue, 23 Sep 2025 00:05:22 +0800 Subject: [PATCH 5/7] fix: correct file name --- .../{getReactivityReferences.ts => getReactiveReferences.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/typescript-plugin/lib/requests/{getReactivityReferences.ts => getReactiveReferences.ts} (100%) diff --git a/packages/typescript-plugin/lib/requests/getReactivityReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts similarity index 100% rename from packages/typescript-plugin/lib/requests/getReactivityReferences.ts rename to packages/typescript-plugin/lib/requests/getReactiveReferences.ts From 8b7ab4a72351886190ebcaba046221f3cac91e83 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Tue, 23 Sep 2025 00:16:02 +0800 Subject: [PATCH 6/7] refactor: import `typescript` instead of require --- extensions/vscode/lib/reactivityVisualization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 1af64199fc..d8f0897be8 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,7 +1,7 @@ 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'; -import type ts = require('typescript'); const dependencyDecorations = vscode.window.createTextEditorDecorationType({ isWholeLine: true, From fa62527eca6c1c50facf818076ab1c7940e375f5 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Tue, 23 Sep 2025 00:21:21 +0800 Subject: [PATCH 7/7] refactor: use `sourceScript.id` --- packages/typescript-plugin/index.ts | 1 - .../lib/requests/getReactiveReferences.ts | 3 +- .../typescript-plugin/lib/requests/index.ts | 31 ++++++++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 21e1665a43..198290cb91 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -196,7 +196,6 @@ export = createLanguageServicePlugin( ts, language, sourceScript, - fileName, position, 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 4589cc3c33..f918fe3796 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts @@ -18,7 +18,6 @@ export function getReactiveReferences( ts: typeof import('typescript'), language: Language, sourceScript: SourceScript, - fileName: string, position: number, leadingOffset: number = 0, ) { @@ -45,7 +44,7 @@ export function getReactiveReferences( languageService = proxied.proxy; } - const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!; + const sourceFile = languageService.getProgram()!.getSourceFile(sourceScript.id)!; const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript( sourceScript.generated.root, ); 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; }