Skip to content

Commit 7ee6772

Browse files
committed
refactor(language-service): simplify vue-template plugin
1 parent a6d75f7 commit 7ee6772

File tree

2 files changed

+108
-91
lines changed

2 files changed

+108
-91
lines changed

packages/language-server/tests/completions.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ test('HTML tags and built-in components', async () => {
177177
"dialog",
178178
"script",
179179
"noscript",
180+
"template",
180181
"canvas",
182+
"slot",
181183
"data",
182184
"hgroup",
183185
"menu",
@@ -190,8 +192,6 @@ test('HTML tags and built-in components', async () => {
190192
"Teleport",
191193
"Suspense",
192194
"component",
193-
"slot",
194-
"template",
195195
"BaseTransition",
196196
"Fixture",
197197
]

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

Lines changed: 106 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ import { loadModelModifiersData, loadTemplateData } from './data';
1919

2020
type InternalItemId =
2121
| 'componentEvent'
22-
| 'componentProp'
23-
| 'specialTag';
22+
| 'componentProp';
2423

2524
const specialTags = new Set([
2625
'slot',
@@ -174,24 +173,20 @@ export function create(
174173
return;
175174
}
176175

177-
// #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver
178-
baseServiceInstance.provideCompletionItems?.(document, position, completionContext, token);
179-
180-
let sync = (await provideHtmlData(sourceScript.id, root)).sync;
181-
let currentVersion: number | undefined;
182-
let completionList: CompletionList | null | undefined;
183-
184-
while (currentVersion !== (currentVersion = await sync?.())) {
185-
completionList = await baseServiceInstance.provideCompletionItems?.(
186-
document,
187-
position,
188-
completionContext,
189-
token,
190-
);
191-
}
176+
const completionList = await runWithVueData(
177+
sourceScript.id,
178+
root,
179+
() =>
180+
baseServiceInstance.provideCompletionItems!(
181+
document,
182+
position,
183+
completionContext,
184+
token,
185+
),
186+
);
192187

193188
if (completionList) {
194-
transformCompletionList(completionList, document);
189+
postProcessCompletionList(completionList, document);
195190
return completionList;
196191
}
197192
},
@@ -209,30 +204,37 @@ export function create(
209204
},
210205
};
211206

207+
async function runWithVueData<T>(sourceDocumentUri: URI, vueCode: VueVirtualCode, fn: () => T) {
208+
// #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver
209+
await fn();
210+
211+
const { sync } = await provideHtmlData(sourceDocumentUri, vueCode);
212+
let lastVersion = await sync();
213+
let result = await fn();
214+
while (lastVersion !== (lastVersion = await sync())) {
215+
result = await fn();
216+
}
217+
return result;
218+
}
219+
212220
async function provideHtmlData(sourceDocumentUri: URI, vueCode: VueVirtualCode) {
213221
await (initializing ??= initialize());
214222

215223
const casing = await checkCasing(context, sourceDocumentUri);
216224

217-
if (builtInData.tags) {
218-
for (const tag of builtInData.tags) {
219-
if (isItemKey(tag.name)) {
220-
continue;
221-
}
222-
223-
if (specialTags.has(tag.name)) {
224-
tag.name = generateItemKey('specialTag', tag.name, '');
225-
}
226-
else if (casing.tag === TagNameCasing.Kebab) {
227-
tag.name = hyphenateTag(tag.name);
228-
}
229-
else {
230-
tag.name = camelize(capitalize(tag.name));
231-
}
225+
for (const tag of builtInData.tags ?? []) {
226+
if (specialTags.has(tag.name)) {
227+
continue;
228+
}
229+
if (casing.tag === TagNameCasing.Kebab) {
230+
tag.name = hyphenateTag(tag.name);
231+
}
232+
else {
233+
tag.name = camelize(capitalize(tag.name));
232234
}
233235
}
234236

235-
const promises: Promise<void>[] = [];
237+
const tasks: Promise<void>[] = [];
236238
const tagInfos = new Map<string, {
237239
attrs: string[];
238240
propInfos: ComponentPropInfo[];
@@ -252,7 +254,8 @@ export function create(
252254
isApplicable: () => true,
253255
provideTags: () => {
254256
if (!components) {
255-
promises.push((async () => {
257+
components = [];
258+
tasks.push((async () => {
256259
components = (await tsPluginClient?.getComponentNames(vueCode.fileName) ?? [])
257260
.filter(name =>
258261
name !== 'Transition'
@@ -264,7 +267,6 @@ export function create(
264267
lastCompletionComponentNames = new Set(components);
265268
version++;
266269
})());
267-
return [];
268270
}
269271
const scriptSetupRanges = tsCodegen.get(vueCode.sfc)?.getScriptSetupRanges();
270272
const names = new Set<string>();
@@ -299,10 +301,16 @@ export function create(
299301
return tags;
300302
},
301303
provideAttributes: tag => {
302-
const tagInfo = tagInfos.get(tag);
303-
304+
let tagInfo = tagInfos.get(tag);
304305
if (!tagInfo) {
305-
promises.push((async () => {
306+
tagInfo = {
307+
attrs: [],
308+
propInfos: [],
309+
events: [],
310+
directives: [],
311+
};
312+
tagInfos.set(tag, tagInfo);
313+
tasks.push((async () => {
306314
const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? [];
307315
const propInfos = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? [];
308316
const events = await tsPluginClient?.getComponentEvents(vueCode.fileName, tag) ?? [];
@@ -315,7 +323,6 @@ export function create(
315323
});
316324
version++;
317325
})());
318-
return [];
319326
}
320327

321328
const { attrs, propInfos, events, directives } = tagInfo;
@@ -458,63 +465,26 @@ export function create(
458465

459466
return {
460467
async sync() {
461-
await Promise.all(promises);
468+
await Promise.all(tasks);
462469
return version;
463470
},
464471
};
465472
}
466473

467-
function transformCompletionList(completionList: CompletionList, document: TextDocument) {
468-
addDirectiveModifiers();
474+
function postProcessCompletionList(completionList: CompletionList, document: TextDocument) {
475+
addDirectiveModifiers(completionList, document);
469476

470-
function addDirectiveModifiers() {
471-
const replacement = getReplacement(completionList, document);
472-
if (!replacement?.text.includes('.')) {
473-
return;
474-
}
477+
const tagMap = new Map<string, html.CompletionItem>();
475478

476-
const [text, ...modifiers] = replacement.text.split('.');
477-
const isVOn = text.startsWith('v-on:') || text.startsWith('@') && text.length > 1;
478-
const isVBind = text.startsWith('v-bind:') || text.startsWith(':') && text.length > 1;
479-
const isVModel = text.startsWith('v-model:') || text === 'v-model';
480-
const currentModifiers = isVOn
481-
? vOnModifiers
482-
: isVBind
483-
? vBindModifiers
484-
: isVModel
485-
? vModelModifiers
486-
: undefined;
487-
488-
if (!currentModifiers) {
489-
return;
479+
completionList.items = completionList.items.filter(item => {
480+
const key = item.kind + '_' + item.label;
481+
if (!tagMap.has(key)) {
482+
tagMap.set(key, item);
483+
return true;
490484
}
491-
492-
for (const modifier in currentModifiers) {
493-
if (modifiers.includes(modifier)) {
494-
continue;
495-
}
496-
497-
const description = currentModifiers[modifier];
498-
const insertText = text + modifiers.slice(0, -1).map(m => '.' + m).join('') + '.' + modifier;
499-
const newItem: html.CompletionItem = {
500-
label: modifier,
501-
filterText: insertText,
502-
documentation: {
503-
kind: 'markdown',
504-
value: description,
505-
},
506-
textEdit: {
507-
range: replacement.textEdit.range,
508-
newText: insertText,
509-
},
510-
kind: 20 satisfies typeof CompletionItemKind.EnumMember,
511-
};
512-
513-
completionList.items.push(newItem);
514-
}
515-
}
516-
517-
completionList.items = completionList.items.filter(item => !specialTags.has(parseLabel(item.label).name));
485+
tagMap.get(key)!.documentation = item.documentation;
486+
return false;
487+
});
518488

519489
const htmlDocumentations = new Map<string, string>();
520490

@@ -693,6 +663,53 @@ export function create(
693663
updateExtraCustomData([]);
694664
}
695665

666+
function addDirectiveModifiers(completionList: CompletionList, document: TextDocument) {
667+
const replacement = getReplacement(completionList, document);
668+
if (!replacement?.text.includes('.')) {
669+
return;
670+
}
671+
672+
const [text, ...modifiers] = replacement.text.split('.');
673+
const isVOn = text.startsWith('v-on:') || text.startsWith('@') && text.length > 1;
674+
const isVBind = text.startsWith('v-bind:') || text.startsWith(':') && text.length > 1;
675+
const isVModel = text.startsWith('v-model:') || text === 'v-model';
676+
const currentModifiers = isVOn
677+
? vOnModifiers
678+
: isVBind
679+
? vBindModifiers
680+
: isVModel
681+
? vModelModifiers
682+
: undefined;
683+
684+
if (!currentModifiers) {
685+
return;
686+
}
687+
688+
for (const modifier in currentModifiers) {
689+
if (modifiers.includes(modifier)) {
690+
continue;
691+
}
692+
693+
const description = currentModifiers[modifier];
694+
const insertText = text + modifiers.slice(0, -1).map(m => '.' + m).join('') + '.' + modifier;
695+
const newItem: html.CompletionItem = {
696+
label: modifier,
697+
filterText: insertText,
698+
documentation: {
699+
kind: 'markdown',
700+
value: description,
701+
},
702+
textEdit: {
703+
range: replacement.textEdit.range,
704+
newText: insertText,
705+
},
706+
kind: 20 satisfies typeof CompletionItemKind.EnumMember,
707+
};
708+
709+
completionList.items.push(newItem);
710+
}
711+
}
712+
696713
async function initialize() {
697714
customData = await getHtmlCustomData();
698715
}

0 commit comments

Comments
 (0)