Skip to content

Commit b8cb0ac

Browse files
refactor(language-server): reimplement Reactivity Visualization in typescript plugin (#5632)
1 parent dbbc6f5 commit b8cb0ac

File tree

10 files changed

+885
-746
lines changed

10 files changed

+885
-746
lines changed

extensions/vscode/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from 'reactive-vscode';
1717
import * as vscode from 'vscode';
1818
import { config } from './lib/config';
19+
import * as reactivityVisualization from './lib/reactivityVisualization';
1920
import * as welcome from './lib/welcome';
2021

2122
let client: lsp.BaseLanguageClient | undefined;
@@ -98,6 +99,8 @@ export = defineExtension(() => {
9899

99100
activateAutoInsertion(selectors, client);
100101
activateDocumentDropEdit(selectors, client);
102+
103+
reactivityVisualization.activate(context, selectors);
101104
welcome.activate(context);
102105
}, { immediate: true });
103106

@@ -157,7 +160,7 @@ function launch(context: vscode.ExtensionContext) {
157160
);
158161

159162
client.onNotification('tsserver/request', ([seq, command, args]) => {
160-
vscode.commands.executeCommand<{ body: unknown } | undefined>(
163+
vscode.commands.executeCommand<{ body?: unknown } | undefined>(
161164
'typescript.tsserverRequest',
162165
command,
163166
args,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { getReactiveReferences } from '@vue/typescript-plugin/lib/requests/getReactiveReferences';
2+
import * as vscode from 'vscode';
3+
import { config } from './config';
4+
5+
const dependenciesDecorations = vscode.window.createTextEditorDecorationType({
6+
isWholeLine: true,
7+
backgroundColor: 'rgba(120,120,255,0.1)',
8+
border: '1px solid rgba(120,120,255,0.6)',
9+
borderWidth: '0 0 0 3px',
10+
// after: {
11+
// contentText: ' dependents',
12+
// color: 'rgba(120,120,255,0.6)',
13+
// },
14+
});
15+
const subscribersDecorations = vscode.window.createTextEditorDecorationType({
16+
// outlineColor: 'rgba(80,200,80,0.6)',
17+
// outlineStyle: 'dashed',
18+
// borderRadius: '3px',
19+
isWholeLine: true,
20+
backgroundColor: 'rgba(80,200,80,0.1)',
21+
border: '1px solid rgba(80,200,80,0.6)',
22+
borderWidth: '0 0 0 3px',
23+
});
24+
25+
export function activate(
26+
context: vscode.ExtensionContext,
27+
selector: vscode.DocumentSelector,
28+
) {
29+
let timeout: ReturnType<typeof setTimeout> | undefined;
30+
31+
for (const editor of vscode.window.visibleTextEditors) {
32+
updateDecorations(editor);
33+
}
34+
35+
context.subscriptions.push(
36+
vscode.window.onDidChangeActiveTextEditor(editor => {
37+
if (editor) {
38+
updateDecorations(editor);
39+
}
40+
}),
41+
vscode.window.onDidChangeTextEditorSelection(() => {
42+
const editor = vscode.window.activeTextEditor;
43+
if (editor) {
44+
clearTimeout(timeout);
45+
timeout = setTimeout(() => updateDecorations(editor), 100);
46+
}
47+
}),
48+
);
49+
50+
async function updateDecorations(editor: vscode.TextEditor) {
51+
const { document } = editor;
52+
if (document.uri.scheme !== 'file') {
53+
return;
54+
}
55+
if (
56+
!vscode.languages.match(selector, document)
57+
&& document.languageId !== 'typescript'
58+
&& document.languageId !== 'javascript'
59+
&& document.languageId !== 'typescriptreact'
60+
&& document.languageId !== 'javascriptreact'
61+
) {
62+
return;
63+
}
64+
if (!config.editor.reactivityVisualization) {
65+
editor.setDecorations(dependenciesDecorations, []);
66+
editor.setDecorations(subscribersDecorations, []);
67+
return;
68+
}
69+
70+
try {
71+
const result = await vscode.commands.executeCommand<
72+
{
73+
body?: ReturnType<typeof getReactiveReferences>;
74+
} | undefined
75+
>(
76+
'typescript.tsserverRequest',
77+
'_vue:getReactiveReferences',
78+
[
79+
document.uri.fsPath.replace(/\\/g, '/'),
80+
document.offsetAt(editor.selection.active),
81+
],
82+
{ isAsync: true, lowPriority: true },
83+
);
84+
editor.setDecorations(
85+
dependenciesDecorations,
86+
result?.body?.dependencyRanges.map(range =>
87+
new vscode.Range(
88+
document.positionAt(range.start),
89+
document.positionAt(range.end),
90+
)
91+
) ?? [],
92+
);
93+
editor.setDecorations(
94+
subscribersDecorations,
95+
result?.body?.dependentRanges.map(range =>
96+
new vscode.Range(
97+
document.positionAt(range.start),
98+
document.positionAt(range.end),
99+
)
100+
) ?? [],
101+
);
102+
}
103+
catch {
104+
editor.setDecorations(dependenciesDecorations, []);
105+
editor.setDecorations(subscribersDecorations, []);
106+
}
107+
}
108+
}

packages/language-server/index.ts

Lines changed: 4 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { LanguageServer, Position, TextDocumentIdentifier } from '@volar/language-server';
2-
import { type Range, TextDocument } from '@volar/language-server';
1+
import type { LanguageServer, TextDocumentIdentifier } from '@volar/language-server';
32
import { createLanguageServiceEnvironment } from '@volar/language-server/lib/project/simpleProject';
43
import { createConnection, createServer } from '@volar/language-server/node';
54
import {
@@ -8,20 +7,15 @@ import {
87
createParsedCommandLineByJson,
98
createVueLanguagePlugin,
109
forEachEmbeddedCode,
11-
isReferencesEnabled,
1210
} from '@vue/language-core';
1311
import {
1412
createLanguageService,
1513
createUriMap,
1614
createVueLanguageServicePlugins,
17-
type DocumentsAndMap,
18-
getSourceRange,
1915
type LanguageService,
2016
} from '@vue/language-service';
2117
import * as ts from 'typescript';
2218
import { URI } from 'vscode-uri';
23-
import { analyze } from './lib/reactivityAnalyze';
24-
import { getLanguageService } from './lib/reactivityAnalyzeLS';
2519

2620
const connection = createConnection();
2721
const server = createServer(connection);
@@ -129,6 +123,9 @@ connection.onInitialize(params => {
129123
getImportPathForFile(...args) {
130124
return sendTsServerRequest('_vue:getImportPathForFile', args);
131125
},
126+
getReactiveReferences(...args) {
127+
return sendTsServerRequest('_vue:getReactiveReferences', args);
128+
},
132129
isRefAtPosition(...args) {
133130
return sendTsServerRequest('_vue:isRefAtPosition', args);
134131
},
@@ -246,154 +243,3 @@ connection.onRequest('vue/interpolationRanges', async (params: {
246243
}
247244
return [];
248245
});
249-
250-
const cacheDocuments = new Map<string, [TextDocument, import('typescript').IScriptSnapshot]>();
251-
252-
connection.onRequest('vue/reactivityAnalyze', async (params: {
253-
textDocument: TextDocumentIdentifier;
254-
position: Position;
255-
syncDocument?: {
256-
content: string;
257-
languageId: string;
258-
};
259-
}): Promise<
260-
{
261-
subscribers: Range[];
262-
dependencies: Range[];
263-
} | undefined
264-
> => {
265-
if (params.syncDocument) {
266-
const document = TextDocument.create(
267-
params.textDocument.uri,
268-
params.syncDocument.languageId,
269-
0,
270-
params.syncDocument.content,
271-
);
272-
const snapshot = ts.ScriptSnapshot.fromString(params.syncDocument.content);
273-
cacheDocuments.set(params.textDocument.uri, [document, snapshot]);
274-
}
275-
const uri = URI.parse(params.textDocument.uri);
276-
const languageService = await server.project.getLanguageService(uri);
277-
const sourceScript = languageService.context.language.scripts.get(uri);
278-
let document: TextDocument | undefined;
279-
let snapshot: import('typescript').IScriptSnapshot | undefined;
280-
if (sourceScript) {
281-
document = languageService.context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot);
282-
snapshot = sourceScript.snapshot;
283-
}
284-
else if (cacheDocuments.has(params.textDocument.uri)) {
285-
const [doc, snap] = cacheDocuments.get(params.textDocument.uri)!;
286-
document = doc;
287-
snapshot = snap;
288-
}
289-
if (!document || !snapshot) {
290-
return;
291-
}
292-
let offset = document.offsetAt(params.position);
293-
if (sourceScript?.generated) {
294-
const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(
295-
sourceScript.generated.root,
296-
);
297-
if (!serviceScript) {
298-
return;
299-
}
300-
const map = languageService.context.language.maps.get(serviceScript.code, sourceScript);
301-
let embeddedOffset: number | undefined;
302-
for (const [mapped, mapping] of map.toGeneratedLocation(offset)) {
303-
if (isReferencesEnabled(mapping.data)) {
304-
embeddedOffset = mapped;
305-
break;
306-
}
307-
}
308-
if (embeddedOffset === undefined) {
309-
return;
310-
}
311-
offset = embeddedOffset;
312-
313-
const embeddedUri = languageService.context.encodeEmbeddedDocumentUri(sourceScript.id, serviceScript.code.id);
314-
document = languageService.context.documents.get(
315-
embeddedUri,
316-
serviceScript.code.languageId,
317-
serviceScript.code.snapshot,
318-
);
319-
snapshot = serviceScript.code.snapshot;
320-
}
321-
const { languageService: tsLs, fileName } = getLanguageService(ts, snapshot, document.languageId);
322-
const result = analyze(ts, tsLs, fileName, offset);
323-
if (!result) {
324-
return;
325-
}
326-
const subscribers: Range[] = [];
327-
const dependencies: Range[] = [];
328-
if (sourceScript?.generated) {
329-
const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(
330-
sourceScript.generated.root,
331-
);
332-
if (!serviceScript) {
333-
return;
334-
}
335-
const docs: DocumentsAndMap = [
336-
languageService.context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot),
337-
document,
338-
languageService.context.language.maps.get(serviceScript.code, sourceScript),
339-
];
340-
for (const dependency of result.dependencies) {
341-
let start = document.positionAt(dependency.getStart(result.sourceFile));
342-
let end = document.positionAt(dependency.getEnd());
343-
if (ts.isBlock(dependency) && dependency.statements.length) {
344-
const { statements } = dependency;
345-
start = document.positionAt(statements[0]!.getStart(result.sourceFile));
346-
end = document.positionAt(statements[statements.length - 1]!.getEnd());
347-
}
348-
const sourceRange = getSourceRange(docs, { start, end });
349-
if (sourceRange) {
350-
dependencies.push(sourceRange);
351-
}
352-
}
353-
for (const subscriber of result.subscribers) {
354-
if (!subscriber.sideEffectInfo) {
355-
continue;
356-
}
357-
let start = document.positionAt(subscriber.sideEffectInfo.handler.getStart(result.sourceFile));
358-
let end = document.positionAt(subscriber.sideEffectInfo.handler.getEnd());
359-
if (ts.isBlock(subscriber.sideEffectInfo.handler) && subscriber.sideEffectInfo.handler.statements.length) {
360-
const { statements } = subscriber.sideEffectInfo.handler;
361-
start = document.positionAt(statements[0]!.getStart(result.sourceFile));
362-
end = document.positionAt(statements[statements.length - 1]!.getEnd());
363-
}
364-
const sourceRange = getSourceRange(docs, { start, end });
365-
if (sourceRange) {
366-
subscribers.push(sourceRange);
367-
}
368-
}
369-
}
370-
else {
371-
for (const dependency of result.dependencies) {
372-
let start = document.positionAt(dependency.getStart(result.sourceFile));
373-
let end = document.positionAt(dependency.getEnd());
374-
if (ts.isBlock(dependency) && dependency.statements.length) {
375-
const { statements } = dependency;
376-
start = document.positionAt(statements[0]!.getStart(result.sourceFile));
377-
end = document.positionAt(statements[statements.length - 1]!.getEnd());
378-
}
379-
dependencies.push({ start, end });
380-
}
381-
for (const subscriber of result.subscribers) {
382-
if (!subscriber.sideEffectInfo) {
383-
continue;
384-
}
385-
let start = document.positionAt(subscriber.sideEffectInfo.handler.getStart(result.sourceFile));
386-
let end = document.positionAt(subscriber.sideEffectInfo.handler.getEnd());
387-
if (ts.isBlock(subscriber.sideEffectInfo.handler) && subscriber.sideEffectInfo.handler.statements.length) {
388-
const { statements } = subscriber.sideEffectInfo.handler;
389-
start = document.positionAt(statements[0]!.getStart(result.sourceFile));
390-
end = document.positionAt(statements[statements.length - 1]!.getEnd());
391-
}
392-
subscribers.push({ start, end });
393-
}
394-
}
395-
return {
396-
subscribers,
397-
dependencies,
398-
};
399-
});

0 commit comments

Comments
 (0)