Skip to content

Commit aa0d1fe

Browse files
authored
fix(language-service): read ast from codegen instead of parsing it repeatedly (#5086)
1 parent 7eafbb2 commit aa0d1fe

File tree

7 files changed

+59
-68
lines changed

7 files changed

+59
-68
lines changed

packages/language-core/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export * from './lib/utils/parseSfc';
88
export * from './lib/utils/ts';
99
export * from './lib/virtualFile/vueFile';
1010

11-
export * as scriptRanges from './lib/parsers/scriptRanges';
1211
export { tsCodegen } from './lib/plugins/vue-tsx';
1312
export * from './lib/utils/shared';
1413

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { TextDocument } from 'vscode-languageserver-textdocument';
2+
3+
export function sleep(ms: number) {
4+
return new Promise(resolve => setTimeout(resolve, ms));
5+
}
6+
7+
export function isTsDocument(document: TextDocument) {
8+
return document.languageId === 'javascript' ||
9+
document.languageId === 'typescript' ||
10+
document.languageId === 'javascriptreact' ||
11+
document.languageId === 'typescriptreact';
12+
}

packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts

Lines changed: 35 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service';
2-
import { hyphenateAttr } from '@vue/language-core';
2+
import { hyphenateAttr, VueVirtualCode } from '@vue/language-core';
33
import type * as ts from 'typescript';
44
import type { TextDocument } from 'vscode-languageserver-textdocument';
55
import { URI } from 'vscode-uri';
6-
7-
const asts = new WeakMap<ts.IScriptSnapshot, ts.SourceFile>();
8-
9-
function getAst(ts: typeof import('typescript'), fileName: string, snapshot: ts.IScriptSnapshot, scriptKind?: ts.ScriptKind) {
10-
let ast = asts.get(snapshot);
11-
if (!ast) {
12-
ast = ts.createSourceFile(fileName, snapshot.getText(0, snapshot.getLength()), ts.ScriptTarget.Latest, undefined, scriptKind);
13-
asts.set(snapshot, ast);
14-
}
15-
return ast;
16-
}
6+
import { isTsDocument, sleep } from './utils';
177

188
export function create(
199
ts: typeof import('typescript'),
@@ -61,46 +51,49 @@ export function create(
6151
const decoded = context.decodeEmbeddedDocumentUri(uri);
6252
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
6353
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
64-
if (!sourceScript) {
54+
if (!sourceScript?.generated || !virtualCode) {
6555
return;
6656
}
6757

68-
let ast: ts.SourceFile | undefined;
69-
let sourceCodeOffset = document.offsetAt(selection);
58+
const root = sourceScript.generated.root;
59+
if (!(root instanceof VueVirtualCode)) {
60+
return;
61+
}
7062

71-
const fileName = context.project.typescript?.uriConverter.asFileName(sourceScript.id)
72-
?? sourceScript.id.fsPath.replace(/\\/g, '/');
63+
const blocks = [
64+
root._sfc.script,
65+
root._sfc.scriptSetup,
66+
].filter(block => !!block);
67+
if (!blocks.length) {
68+
return;
69+
}
7370

74-
if (sourceScript.generated) {
75-
const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root);
76-
if (!serviceScript || serviceScript?.code !== virtualCode) {
77-
return;
78-
}
79-
ast = getAst(ts, fileName, virtualCode.snapshot, serviceScript.scriptKind);
80-
let mapped = false;
81-
for (const [_sourceScript, map] of context.language.maps.forEach(virtualCode)) {
82-
for (const [sourceOffset] of map.toSourceLocation(document.offsetAt(selection))) {
83-
sourceCodeOffset = sourceOffset;
84-
mapped = true;
85-
break;
86-
}
87-
if (mapped) {
88-
break;
89-
}
71+
let sourceCodeOffset = document.offsetAt(selection);
72+
let mapped = false;
73+
for (const [, map] of context.language.maps.forEach(virtualCode)) {
74+
for (const [sourceOffset] of map.toSourceLocation(sourceCodeOffset)) {
75+
sourceCodeOffset = sourceOffset;
76+
mapped = true;
77+
break;
9078
}
91-
if (!mapped) {
92-
return;
79+
if (mapped) {
80+
break;
9381
}
9482
}
95-
else {
96-
ast = getAst(ts, fileName, sourceScript.snapshot);
83+
if (!mapped) {
84+
return;
9785
}
9886

99-
if (isBlacklistNode(ts, ast, document.offsetAt(selection), false)) {
100-
return;
87+
for (const { ast, startTagEnd, endTagStart } of blocks) {
88+
if (sourceCodeOffset < startTagEnd || sourceCodeOffset > endTagStart) {
89+
continue;
90+
}
91+
if (isBlacklistNode(ts, ast, sourceCodeOffset - startTagEnd, false)) {
92+
return;
93+
}
10194
}
10295

103-
const props = await tsPluginClient?.getPropertiesAtLocation(fileName, sourceCodeOffset) ?? [];
96+
const props = await tsPluginClient?.getPropertiesAtLocation(root.fileName, sourceCodeOffset) ?? [];
10497
if (props.some(prop => prop === 'value')) {
10598
return '${1:.value}';
10699
}
@@ -110,20 +103,9 @@ export function create(
110103
};
111104
}
112105

113-
function sleep(ms: number) {
114-
return new Promise(resolve => setTimeout(resolve, ms));
115-
}
116-
117-
export function isTsDocument(document: TextDocument) {
118-
return document.languageId === 'javascript' ||
119-
document.languageId === 'typescript' ||
120-
document.languageId === 'javascriptreact' ||
121-
document.languageId === 'typescriptreact';
122-
}
123-
124106
const charReg = /\w/;
125107

126-
export function isCharacterTyping(document: TextDocument, change: { text: string; rangeOffset: number; rangeLength: number; }) {
108+
function isCharacterTyping(document: TextDocument, change: { text: string; rangeOffset: number; rangeLength: number; }) {
127109
const lastCharacter = change.text[change.text.length - 1];
128110
const nextCharacter = document.getText().slice(
129111
change.rangeOffset + change.text.length,
@@ -138,7 +120,7 @@ export function isCharacterTyping(document: TextDocument, change: { text: string
138120
return charReg.test(lastCharacter) && !charReg.test(nextCharacter);
139121
}
140122

141-
export function isBlacklistNode(ts: typeof import('typescript'), node: ts.Node, pos: number, allowAccessDotValue: boolean) {
123+
function isBlacklistNode(ts: typeof import('typescript'), node: ts.Node, pos: number, allowAccessDotValue: boolean) {
142124
if (ts.isVariableDeclaration(node) && pos >= node.name.getFullStart() && pos <= node.name.getEnd()) {
143125
return true;
144126
}

packages/language-service/lib/plugins/vue-complete-define-assignment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { LanguageServicePlugin } from '@volar/language-service';
22
import { TextRange, tsCodegen, VueVirtualCode } from '@vue/language-core';
33
import type * as vscode from 'vscode-languageserver-protocol';
44
import { URI } from 'vscode-uri';
5-
import { isTsDocument } from './vue-autoinsert-dotvalue';
5+
import { isTsDocument } from './utils';
66

77
export function create(): LanguageServicePlugin {
88
return {

packages/language-service/lib/plugins/vue-document-drop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function create(
111111
});
112112

113113
if (sfc.script) {
114-
const edit = createAddComponentToOptionEdit(ts, sfc.script.ast, newName);
114+
const edit = createAddComponentToOptionEdit(ts, sfc, sfc.script.ast, newName);
115115
if (edit) {
116116
additionalEdit.changes[embeddedDocumentUriStr].push({
117117
range: {

packages/language-service/lib/plugins/vue-extract-file.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CreateFile, LanguageServiceContext, LanguageServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service';
22
import type { ExpressionNode, TemplateChildNode } from '@vue/compiler-dom';
3-
import { Sfc, VueVirtualCode, scriptRanges } from '@vue/language-core';
3+
import { Sfc, VueVirtualCode, tsCodegen } from '@vue/language-core';
44
import type * as ts from 'typescript';
55
import type * as vscode from 'vscode-languageserver-protocol';
66
import { URI } from 'vscode-uri';
@@ -158,7 +158,7 @@ export function create(
158158
];
159159

160160
if (sfc.script) {
161-
const edit = createAddComponentToOptionEdit(ts, sfc.script.ast, newName);
161+
const edit = createAddComponentToOptionEdit(ts, sfc, sfc.script.ast, newName);
162162
if (edit) {
163163
sfcEdits.push({
164164
range: {
@@ -304,12 +304,13 @@ export function getLastImportNode(ts: typeof import('typescript'), sourceFile: t
304304
return lastImportNode;
305305
}
306306

307-
export function createAddComponentToOptionEdit(ts: typeof import('typescript'), ast: ts.SourceFile, componentName: string) {
307+
export function createAddComponentToOptionEdit(ts: typeof import('typescript'), sfc: Sfc, ast: ts.SourceFile, componentName: string) {
308308

309-
const exportDefault = scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault;
310-
if (!exportDefault) {
309+
const scriptRanges = tsCodegen.get(sfc)?.scriptRanges.get();
310+
if (!scriptRanges?.exportDefault) {
311311
return;
312312
}
313+
const { exportDefault } = scriptRanges;
313314

314315
// https://github.com/microsoft/TypeScript/issues/36174
315316
const printer = ts.createPrinter();

packages/language-service/lib/plugins/vue-template.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Disposable, LanguageServiceContext, LanguageServicePluginInstance } from '@volar/language-service';
2-
import { VueCompilerOptions, VueVirtualCode, hyphenateAttr, hyphenateTag, parseScriptSetupRanges } from '@vue/language-core';
2+
import { VueVirtualCode, hyphenateAttr, hyphenateTag, tsCodegen } from '@vue/language-core';
33
import { camelize, capitalize } from '@vue/shared';
44
import { getComponentSpans } from '@vue/typescript-plugin/lib/common';
55
import { create as createHtmlService } from 'volar-service-html';
@@ -157,7 +157,6 @@ export function create(
157157
if (!context.project.vue) {
158158
return;
159159
}
160-
const vueCompilerOptions = context.project.vue.compilerOptions;
161160

162161
let sync: (() => Promise<number>) | undefined;
163162
let currentVersion: number | undefined;
@@ -172,7 +171,7 @@ export function create(
172171
// #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver
173172
baseServiceInstance.provideCompletionItems?.(document, position, completionContext, token);
174173

175-
sync = (await provideHtmlData(vueCompilerOptions, sourceScript!.id, root)).sync;
174+
sync = (await provideHtmlData(sourceScript!.id, root)).sync;
176175
currentVersion = await sync();
177176
}
178177

@@ -462,7 +461,7 @@ export function create(
462461
},
463462
};
464463

465-
async function provideHtmlData(vueCompilerOptions: VueCompilerOptions, sourceDocumentUri: URI, vueCode: VueVirtualCode) {
464+
async function provideHtmlData(sourceDocumentUri: URI, vueCode: VueVirtualCode) {
466465

467466
await (initializing ??= initialize());
468467

@@ -520,9 +519,7 @@ export function create(
520519
})());
521520
return [];
522521
}
523-
const scriptSetupRanges = vueCode._sfc.scriptSetup
524-
? parseScriptSetupRanges(ts, vueCode._sfc.scriptSetup.ast, vueCompilerOptions)
525-
: undefined;
522+
const scriptSetupRanges = tsCodegen.get(vueCode._sfc)?.scriptSetupRanges.get();
526523
const names = new Set<string>();
527524
const tags: html.ITagData[] = [];
528525

0 commit comments

Comments
 (0)