Skip to content

Commit 66b6332

Browse files
fix(language-service): correct HTMLDocument structure
close #5745 Co-Authored-By: 山吹色御守 <[email protected]>
1 parent 4a89b6e commit 66b6332

File tree

5 files changed

+104
-138
lines changed

5 files changed

+104
-138
lines changed

packages/language-core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './lib/parsers/scriptSetupRanges';
66
export * from './lib/plugins';
77
export * from './lib/types';
88
export * from './lib/utils/collectBindings';
9+
export * from './lib/utils/forEachTemplateNode';
910
export * from './lib/utils/parseSfc';
1011
export * from './lib/utils/shared';
1112
export * from './lib/virtualFile/vueFile';

packages/language-core/lib/codegen/template/index.ts

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as CompilerDOM from '@vue/compiler-dom';
21
import type * as ts from 'typescript';
32
import type { Code, Sfc, VueCompilerOptions } from '../../types';
43
import { codeFeatures } from '../codeFeatures';
@@ -7,7 +6,7 @@ import { wrapWith } from '../utils/wrapWith';
76
import { createTemplateCodegenContext, type TemplateCodegenContext } from './context';
87
import { generateObjectProperty } from './objectProperty';
98
import { generateStyleScopedClassReferences } from './styleScopedClasses';
10-
import { generateTemplateChild, getVForNode } from './templateChild';
9+
import { generateTemplateChild } from './templateChild';
1110

1211
export interface TemplateCodegenOptions {
1312
ts: typeof ts;
@@ -203,39 +202,3 @@ function* generateRootEl(
203202
yield endOfLine;
204203
return `__VLS_RootEl`;
205204
}
206-
207-
export function* forEachElementNode(
208-
node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode,
209-
): Generator<CompilerDOM.ElementNode> {
210-
if (node.type === CompilerDOM.NodeTypes.ROOT) {
211-
for (const child of node.children) {
212-
yield* forEachElementNode(child);
213-
}
214-
}
215-
else if (node.type === CompilerDOM.NodeTypes.ELEMENT) {
216-
const patchForNode = getVForNode(node);
217-
if (patchForNode) {
218-
yield* forEachElementNode(patchForNode);
219-
}
220-
else {
221-
yield node;
222-
for (const child of node.children) {
223-
yield* forEachElementNode(child);
224-
}
225-
}
226-
}
227-
else if (node.type === CompilerDOM.NodeTypes.IF) {
228-
// v-if / v-else-if / v-else
229-
for (const branch of node.branches) {
230-
for (const childNode of branch.children) {
231-
yield* forEachElementNode(childNode);
232-
}
233-
}
234-
}
235-
else if (node.type === CompilerDOM.NodeTypes.FOR) {
236-
// v-for
237-
for (const child of node.children) {
238-
yield* forEachElementNode(child);
239-
}
240-
}
241-
}

packages/language-core/lib/plugins/vue-template-inline-css.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as CompilerDOM from '@vue/compiler-dom';
2-
import { forEachElementNode } from '../codegen/template';
32
import type { Code, VueLanguagePlugin } from '../types';
3+
import { forEachElementNode } from '../utils/forEachTemplateNode';
44
import { allCodeFeatures } from './shared';
55

66
const codeFeatures = {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as CompilerDOM from '@vue/compiler-dom';
2+
3+
export function* forEachElementNode(
4+
node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode,
5+
): Generator<CompilerDOM.ElementNode> {
6+
if (node.type === CompilerDOM.NodeTypes.ROOT) {
7+
for (const child of node.children) {
8+
yield* forEachElementNode(child);
9+
}
10+
}
11+
else if (node.type === CompilerDOM.NodeTypes.ELEMENT) {
12+
yield node;
13+
for (const child of node.children) {
14+
yield* forEachElementNode(child);
15+
}
16+
}
17+
else if (node.type === CompilerDOM.NodeTypes.IF) {
18+
for (const branch of node.branches) {
19+
for (const childNode of branch.children) {
20+
yield* forEachElementNode(childNode);
21+
}
22+
}
23+
}
24+
else if (node.type === CompilerDOM.NodeTypes.FOR) {
25+
for (const child of node.children) {
26+
yield* forEachElementNode(child);
27+
}
28+
}
29+
}
30+
31+
export function* forEachInterpolationNode(
32+
node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.SimpleExpressionNode,
33+
): Generator<CompilerDOM.InterpolationNode> {
34+
if (node.type === CompilerDOM.NodeTypes.ROOT) {
35+
for (const child of node.children) {
36+
yield* forEachInterpolationNode(child);
37+
}
38+
}
39+
else if (node.type === CompilerDOM.NodeTypes.ELEMENT) {
40+
for (const child of node.children) {
41+
yield* forEachInterpolationNode(child);
42+
}
43+
}
44+
else if (node.type === CompilerDOM.NodeTypes.TEXT_CALL) {
45+
yield* forEachInterpolationNode(node.content);
46+
}
47+
else if (node.type === CompilerDOM.NodeTypes.COMPOUND_EXPRESSION) {
48+
for (const child of node.children) {
49+
if (typeof child === 'object') {
50+
yield* forEachInterpolationNode(child);
51+
}
52+
}
53+
}
54+
else if (node.type === CompilerDOM.NodeTypes.INTERPOLATION) {
55+
yield node;
56+
}
57+
else if (node.type === CompilerDOM.NodeTypes.IF) {
58+
for (const branch of node.branches) {
59+
for (const childNode of branch.children) {
60+
yield* forEachInterpolationNode(childNode);
61+
}
62+
}
63+
}
64+
else if (node.type === CompilerDOM.NodeTypes.FOR) {
65+
for (const child of node.children) {
66+
yield* forEachInterpolationNode(child);
67+
}
68+
}
69+
}

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

Lines changed: 32 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import type {
66
LanguageServicePlugin,
77
TextDocument,
88
} from '@volar/language-service';
9-
import { hyphenateAttr, hyphenateTag, tsCodegen, type VueVirtualCode } from '@vue/language-core';
9+
import {
10+
forEachInterpolationNode,
11+
hyphenateAttr,
12+
hyphenateTag,
13+
tsCodegen,
14+
type VueVirtualCode,
15+
} from '@vue/language-core';
1016
import { camelize, capitalize } from '@vue/shared';
1117
import type { ComponentPropInfo } from '@vue/typescript-plugin/lib/requests/getComponentProps';
1218
import { create as createHtmlService } from 'volar-service-html';
@@ -97,6 +103,31 @@ export function create(
97103
create(context) {
98104
const baseServiceInstance = baseService.create(context);
99105

106+
if (baseServiceInstance.provide['html/languageService']) {
107+
const htmlService: html.LanguageService = baseServiceInstance.provide['html/languageService']();
108+
const parseHTMLDocument = htmlService.parseHTMLDocument.bind(htmlService);
109+
110+
htmlService.parseHTMLDocument = document => {
111+
const info = resolveEmbeddedCode(context, document.uri);
112+
if (info?.code.id === 'template') {
113+
const templateAst = info.root.sfc.template?.ast;
114+
if (templateAst) {
115+
let text = document.getText();
116+
for (const node of forEachInterpolationNode(templateAst)) {
117+
text = text.substring(0, node.loc.start.offset)
118+
+ ' '.repeat(node.loc.end.offset - node.loc.start.offset)
119+
+ text.substring(node.loc.end.offset);
120+
}
121+
return parseHTMLDocument({
122+
...document,
123+
getText: () => text,
124+
});
125+
}
126+
}
127+
return parseHTMLDocument(document);
128+
};
129+
}
130+
100131
builtInData ??= loadTemplateData(context.env.locale ?? 'en');
101132
modelData ??= loadModelModifiersData(context.env.locale ?? 'en');
102133

@@ -314,22 +345,6 @@ export function create(
314345
}
315346
},
316347

317-
async provideAutoInsertSnippet(document, selection, lastChange, token) {
318-
if (document.languageId !== languageId) {
319-
return;
320-
}
321-
const info = resolveEmbeddedCode(context, document.uri);
322-
if (info?.code.id !== 'template') {
323-
return;
324-
}
325-
326-
const snippet = await baseServiceInstance.provideAutoInsertSnippet?.(document, selection, lastChange, token);
327-
if (shouldSkipClosingTagFromInterpolation(document, selection, lastChange, snippet)) {
328-
return;
329-
}
330-
return snippet;
331-
},
332-
333348
provideHover(document, position, token) {
334349
if (document.languageId !== languageId) {
335350
return;
@@ -764,85 +779,3 @@ function getPropName(
764779
}
765780
return { isEvent, propName: name };
766781
}
767-
768-
function shouldSkipClosingTagFromInterpolation(
769-
doc: TextDocument,
770-
selection: html.Position,
771-
lastChange: { text: string } | undefined,
772-
snippet: string | null | undefined,
773-
) {
774-
if (!snippet || !lastChange || (lastChange.text !== '/' && lastChange.text !== '>')) {
775-
return false;
776-
}
777-
const tagName = /^\$0<\/([^\s>/]+)>$/.exec(snippet)?.[1] ?? /^([^\s>/]+)>$/.exec(snippet)?.[1];
778-
if (!tagName) {
779-
return false;
780-
}
781-
782-
// check if the open tag inside bracket
783-
const textUpToSelection = doc.getText({
784-
start: { line: 0, character: 0 },
785-
end: selection,
786-
});
787-
788-
const lowerText = textUpToSelection.toLowerCase();
789-
const targetTag = `<${tagName.toLowerCase()}`;
790-
let searchIndex = lowerText.lastIndexOf(targetTag);
791-
let foundInsideInterpolation = false;
792-
793-
while (searchIndex !== -1) {
794-
const nextChar = lowerText.charAt(searchIndex + targetTag.length);
795-
796-
// if the next character continues the tag name, skip this occurrence
797-
const isNameContinuation = nextChar && /[0-9a-z:_-]/.test(nextChar);
798-
if (isNameContinuation) {
799-
searchIndex = lowerText.lastIndexOf(targetTag, searchIndex - 1);
800-
continue;
801-
}
802-
803-
const tagPosition = doc.positionAt(searchIndex);
804-
if (!isInsideBracketExpression(doc, tagPosition)) {
805-
return false;
806-
}
807-
808-
foundInsideInterpolation = true;
809-
searchIndex = lowerText.lastIndexOf(targetTag, searchIndex - 1);
810-
}
811-
812-
return foundInsideInterpolation;
813-
}
814-
815-
function isInsideBracketExpression(doc: TextDocument, selection: html.Position) {
816-
const text = doc.getText({
817-
start: { line: 0, character: 0 },
818-
end: selection,
819-
});
820-
const tokenMatcher = /<!--|-->|{{|}}/g;
821-
let match: RegExpExecArray | null;
822-
let inComment = false;
823-
let lastOpen = -1;
824-
let lastClose = -1;
825-
826-
while ((match = tokenMatcher.exec(text)) !== null) {
827-
switch (match[0]) {
828-
case '<!--':
829-
inComment = true;
830-
break;
831-
case '-->':
832-
inComment = false;
833-
break;
834-
case '{{':
835-
if (!inComment) {
836-
lastOpen = match.index;
837-
}
838-
break;
839-
case '}}':
840-
if (!inComment) {
841-
lastClose = match.index;
842-
}
843-
break;
844-
}
845-
}
846-
847-
return lastOpen !== -1 && lastClose < lastOpen;
848-
}

0 commit comments

Comments
 (0)