From 0beb904fc0fed9e3f296dc41a0d4e65ac304d464 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 26 Nov 2025 12:32:32 +0800 Subject: [PATCH 1/4] fix(vscode): analyze interpolation highlight ranges based on AST --- .../vscode/lib/interpolationDecorators.ts | 37 +++++++++---- .../vscode/reactivityAnalysis/plugin.ts | 55 ++++++++++++++++++- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/extensions/vscode/lib/interpolationDecorators.ts b/extensions/vscode/lib/interpolationDecorators.ts index 01e284224c..2c75a1dc7f 100644 --- a/extensions/vscode/lib/interpolationDecorators.ts +++ b/extensions/vscode/lib/interpolationDecorators.ts @@ -41,7 +41,7 @@ export function activate(selector: vscode.DocumentSelector) { } }); - function updateDecorations(editor: vscode.TextEditor) { + async function updateDecorations(editor: vscode.TextEditor) { if (!vscode.languages.match(selector, editor.document)) { return; } @@ -49,16 +49,29 @@ export function activate(selector: vscode.DocumentSelector) { editor.setDecorations(decorationType, []); return; } - editor.setDecorations( - decorationType, - [...editor.document.getText().matchAll(/{{[\s\S]*?}}/g)].map(match => { - const start = match.index + 2; - const end = match.index + match[0].length - 2; - return new vscode.Range( - editor.document.positionAt(start), - editor.document.positionAt(end), - ); - }), - ); + try { + const result = await vscode.commands.executeCommand< + { + body?: [number, number][]; + } | undefined + >( + 'typescript.tsserverRequest', + '_vue:getInterpolationRanges', + [editor.document.uri.fsPath.replace(/\\/g, '/')], + { isAsync: true, lowPriority: true }, + ); + editor.setDecorations( + decorationType, + (result?.body ?? []).map(range => + new vscode.Range( + editor.document.positionAt(range[0]), + editor.document.positionAt(range[1]), + ) + ), + ); + } + catch { + editor.setDecorations(decorationType, []); + } } } diff --git a/extensions/vscode/reactivityAnalysis/plugin.ts b/extensions/vscode/reactivityAnalysis/plugin.ts index 48a8b3feae..ecbbfed47d 100644 --- a/extensions/vscode/reactivityAnalysis/plugin.ts +++ b/extensions/vscode/reactivityAnalysis/plugin.ts @@ -1,5 +1,5 @@ import { createProxyLanguageService, decorateLanguageServiceHost } from '@volar/typescript'; -import type { Language } from '@vue/language-core'; +import { forEachEmbeddedCode, type Language } from '@vue/language-core'; import { createAnalyzer } from 'laplacenoma'; // @ts-expect-error import rulesVue from 'laplacenoma/rules/vue'; @@ -22,6 +22,13 @@ const plugin: ts.server.PluginModuleFactory = ({ typescript: ts }) => { responseRequired: true, }; }); + info.session.addProtocolHandler('_vue:getInterpolationRanges', request => { + const [fileName]: [string, number] = request.arguments; + return { + response: getInterpolationRanges(info.session!, fileName), + responseRequired: true, + }; + }); } return info.languageService; @@ -98,3 +105,49 @@ function getReactivityAnalysis( toSourceRange, }); } + +function getInterpolationRanges( + session: ts.server.Session, + fileName: string, +) { + const { project } = session['getFileAndProject']({ + file: fileName, + projectFileName: undefined, + }) as { + file: ts.server.NormalizedPath; + project: ts.server.Project; + }; + + const language: Language | undefined = (project as any).__vue__?.language; + if (!language) { + return; + } + + const sourceScript = language.scripts.get(fileName); + if (!sourceScript?.generated) { + return; + } + + const ranges: [number, number][] = []; + for (const code of forEachEmbeddedCode(sourceScript.generated.root)) { + const codeText = code.snapshot.getText(0, code.snapshot.getLength()); + if ( + ( + code.id.startsWith('template_inline_ts_') + && codeText.startsWith('0 +') + && codeText.endsWith('+ 0;') + ) + || (code.id.startsWith('style_') && code.id.endsWith('_inline_ts')) + ) { + for (const mapping of code.mappings) { + for (let i = 0; i < mapping.sourceOffsets.length; i++) { + ranges.push([ + mapping.sourceOffsets[i]!, + mapping.sourceOffsets[i]! + mapping.lengths[i]!, + ]); + } + } + } + } + return ranges; +} From dd32f8d8737da2746e64832f6da370584bb128a1 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 26 Nov 2025 12:35:14 +0800 Subject: [PATCH 2/4] Update plugin.ts --- extensions/vscode/reactivityAnalysis/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode/reactivityAnalysis/plugin.ts b/extensions/vscode/reactivityAnalysis/plugin.ts index ecbbfed47d..80958df398 100644 --- a/extensions/vscode/reactivityAnalysis/plugin.ts +++ b/extensions/vscode/reactivityAnalysis/plugin.ts @@ -23,7 +23,7 @@ const plugin: ts.server.PluginModuleFactory = ({ typescript: ts }) => { }; }); info.session.addProtocolHandler('_vue:getInterpolationRanges', request => { - const [fileName]: [string, number] = request.arguments; + const [fileName]: [string] = request.arguments; return { response: getInterpolationRanges(info.session!, fileName), responseRequired: true, From 156e789146a9ba29bc6cf1ad5a81b5250f1ae78d Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 26 Nov 2025 12:45:10 +0800 Subject: [PATCH 3/4] Update rolldown.config.ts --- extensions/vscode/rolldown.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/vscode/rolldown.config.ts b/extensions/vscode/rolldown.config.ts index 619aa38b84..db19d35b73 100644 --- a/extensions/vscode/rolldown.config.ts +++ b/extensions/vscode/rolldown.config.ts @@ -8,7 +8,7 @@ const resolve = (...paths: string[]) => path.resolve(__dirname, ...paths); const config: RolldownOptions = { input: { 'extension': './index.ts', - 'reactivity-analysis': './reactivityAnalysis/plugin.ts', + 'reactivity-analysis-plugin': './reactivityAnalysis/plugin.ts', }, output: { format: 'cjs', @@ -37,7 +37,7 @@ const config: RolldownOptions = { fs.mkdirSync(resolve('./node_modules/vue-reactivity-analysis-plugin-pack'), { recursive: true }); fs.writeFileSync( resolve('./node_modules/vue-reactivity-analysis-plugin-pack/index.js'), - `module.exports = require('../../dist/reactivity-analysis.js');`, + `module.exports = require('../../dist/reactivity-analysis-plugin.js');`, ); if (isDev) { From 40c576923b7a95931e3b2f4ad0202850e4b7d06d Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 26 Nov 2025 12:46:16 +0800 Subject: [PATCH 4/4] update entry file --- .../plugin.ts => lib/reactivityAnalysisPlugin.ts} | 0 extensions/vscode/rolldown.config.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename extensions/vscode/{reactivityAnalysis/plugin.ts => lib/reactivityAnalysisPlugin.ts} (100%) diff --git a/extensions/vscode/reactivityAnalysis/plugin.ts b/extensions/vscode/lib/reactivityAnalysisPlugin.ts similarity index 100% rename from extensions/vscode/reactivityAnalysis/plugin.ts rename to extensions/vscode/lib/reactivityAnalysisPlugin.ts diff --git a/extensions/vscode/rolldown.config.ts b/extensions/vscode/rolldown.config.ts index db19d35b73..03e5e868d0 100644 --- a/extensions/vscode/rolldown.config.ts +++ b/extensions/vscode/rolldown.config.ts @@ -8,7 +8,7 @@ const resolve = (...paths: string[]) => path.resolve(__dirname, ...paths); const config: RolldownOptions = { input: { 'extension': './index.ts', - 'reactivity-analysis-plugin': './reactivityAnalysis/plugin.ts', + 'reactivity-analysis-plugin': './lib/reactivityAnalysisPlugin.ts', }, output: { format: 'cjs',