Skip to content

Commit d6bd797

Browse files
authored
feat(language-service): better sorting & filtering of completion (#4671)
1 parent 08dddce commit d6bd797

File tree

2 files changed

+133
-48
lines changed

2 files changed

+133
-48
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ describe('Completions', async () => {
3333
`);
3434
});
3535

36+
it('#4670', async () => {
37+
expect(
38+
(await requestCompletionList('fixture.vue', 'vue', `<template><div click| /></template>`)).items.map(item => item.label).filter(label => label.includes('click'))
39+
).toMatchInlineSnapshot(`
40+
[
41+
"onclick",
42+
"ondblclick",
43+
"v-on:auxclick",
44+
"@auxclick",
45+
"v-on:click",
46+
"@click",
47+
"v-on:dblclick",
48+
"@dblclick",
49+
]
50+
`);
51+
});
52+
3653
it('HTML tags and built-in components', async () => {
3754
expect(
3855
(await requestCompletionList('fixture.vue', 'vue', `<template><| /></template>`)).items.map(item => item.label)

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

Lines changed: 116 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Disposable, LanguageServiceContext, LanguageServicePluginInstance } from '@volar/language-service';
22
import { VueCompilerOptions, VueVirtualCode, hyphenateAttr, hyphenateTag, parseScriptSetupRanges, tsCodegen } from '@vue/language-core';
3-
import { camelize, capitalize, hyphenate } from '@vue/shared';
3+
import { camelize, capitalize } from '@vue/shared';
44
import { getComponentSpans } from '@vue/typescript-plugin/lib/common';
55
import { create as createHtmlService } from 'volar-service-html';
66
import { create as createPugService } from 'volar-service-pug';
@@ -12,10 +12,16 @@ import { getNameCasing } from '../ideFeatures/nameCasing';
1212
import { AttrNameCasing, LanguageServicePlugin, TagNameCasing } from '../types';
1313
import { loadModelModifiersData, loadTemplateData } from './data';
1414

15-
let builtInData: html.HTMLDataV1;
16-
let modelData: html.HTMLDataV1;
15+
type InternalItemId =
16+
| 'componentEvent'
17+
| 'componentProp'
18+
| 'specialTag';
1719

1820
const specialTags = new Set(['slot', 'component', 'template']);
21+
const specialProps = new Set(['class', 'is', 'key', 'ref', 'style']);
22+
23+
let builtInData: html.HTMLDataV1;
24+
let modelData: html.HTMLDataV1;
1925

2026
export function create(
2127
mode: 'html' | 'pug',
@@ -522,7 +528,6 @@ export function create(
522528
attrs,
523529
props: props.filter(prop =>
524530
!prop.startsWith('ref_')
525-
&& !hyphenate(prop).startsWith('on-vnode-')
526531
),
527532
events,
528533
});
@@ -566,7 +571,9 @@ export function create(
566571
const isGlobal = !propsSet.has(prop);
567572
const name = casing.attr === AttrNameCasing.Camel ? prop : hyphenateAttr(prop);
568573

569-
if (hyphenateAttr(name).startsWith('on-')) {
574+
const isEvent = hyphenateAttr(name).startsWith('on-');
575+
576+
if (isEvent) {
570577

571578
const propNameBase = name.startsWith('on-')
572579
? name.slice('on-'.length)
@@ -584,7 +591,7 @@ export function create(
584591
}
585592
);
586593
}
587-
{
594+
else {
588595

589596
const propName = name;
590597
const propKey = createInternalItemId('componentProp', [isGlobal ? '*' : tag, propName]);
@@ -611,14 +618,16 @@ export function create(
611618
const name = casing.attr === AttrNameCasing.Camel ? event : hyphenateAttr(event);
612619
const propKey = createInternalItemId('componentEvent', [tag, name]);
613620

614-
attributes.push({
615-
name: 'v-on:' + name,
616-
description: propKey,
617-
});
618-
attributes.push({
619-
name: '@' + name,
620-
description: propKey,
621-
});
621+
attributes.push(
622+
{
623+
name: 'v-on:' + name,
624+
description: propKey,
625+
},
626+
{
627+
name: '@' + name,
628+
description: propKey,
629+
}
630+
);
622631
}
623632

624633
const models: [boolean, string][] = [];
@@ -765,70 +774,116 @@ export function create(
765774
}
766775

767776
const itemIdKey = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;
768-
const itemId = itemIdKey ? readInternalItemId(itemIdKey) : undefined;
777+
let itemId = itemIdKey ? readInternalItemId(itemIdKey) : undefined;
769778

770779
if (itemId) {
771-
let label = hyphenate(itemId.args[1]);
772-
if (label.startsWith('on-')) {
773-
label = 'on' + label.slice('on-'.length);
774-
}
775-
else if (itemId.type === 'componentEvent') {
776-
label = 'on' + label;
780+
let [isEvent, name] = tryGetEventName(itemId);
781+
if (isEvent) {
782+
name = 'on' + name;
777783
}
778-
const original = originals.get(label);
784+
const original = originals.get(name);
779785
item.documentation = original?.documentation;
780786
}
781-
else if (!originals.has(item.label)) {
782-
originals.set(item.label, item);
787+
else {
788+
let name = item.label;
789+
const isVBind = name.startsWith('v-bind:') ? (
790+
name = name.slice('v-bind:'.length), true
791+
) : false;
792+
const isVBindAbbr = name.startsWith(':') && name !== ':' ? (
793+
name = name.slice(':'.length), true
794+
) : false;
795+
796+
/**
797+
* for `is`, `key` and `ref` starting with `v-bind:` or `:`
798+
* that without `internalItemId`.
799+
*/
800+
if (isVBind || isVBindAbbr) {
801+
itemId = {
802+
type: 'componentProp',
803+
args: ['^', name]
804+
};
805+
}
806+
else if (!originals.has(item.label)) {
807+
originals.set(item.label, item);
808+
}
783809
}
784810

811+
const tokens: string[] = [];
812+
785813
if (item.kind === 10 satisfies typeof vscode.CompletionItemKind.Property && lastCompletionComponentNames.has(hyphenateTag(item.label))) {
786814
item.kind = 6 satisfies typeof vscode.CompletionItemKind.Variable;
787-
item.sortText = '\u0000' + (item.sortText ?? item.label);
815+
tokens.push('\u0000');
788816
}
789-
else if (itemId && (itemId.type === 'componentProp' || itemId.type === 'componentEvent')) {
817+
else if (itemId) {
818+
819+
const isComponent = itemId.args[0] !== '*';
820+
const [isEvent, name] = tryGetEventName(itemId);
790821

791-
const [componentName] = itemId.args;
822+
if (itemId.type === 'componentProp') {
823+
if (isComponent || specialProps.has(name)) {
824+
item.kind = 5 satisfies typeof vscode.CompletionItemKind.Field;
825+
}
826+
}
827+
else if (isEvent) {
828+
item.kind = 23 satisfies typeof vscode.CompletionItemKind.Event;
829+
if (name.startsWith('vnode-')) {
830+
tokens.push('\u0004');
831+
}
832+
}
792833

793-
if (componentName !== '*') {
794-
if (
795-
item.label === 'class'
796-
|| item.label === 'ref'
797-
|| item.label.endsWith(':class')
798-
|| item.label.endsWith(':ref')
799-
) {
800-
item.sortText = '\u0000' + (item.sortText ?? item.label);
834+
if (
835+
isComponent
836+
|| (isComponent && isEvent)
837+
|| specialProps.has(name)
838+
) {
839+
tokens.push('\u0000');
840+
841+
if (item.label.startsWith(':')) {
842+
tokens.push('\u0001');
843+
}
844+
else if (item.label.startsWith('@')) {
845+
tokens.push('\u0002');
846+
}
847+
else if (item.label.startsWith('v-bind:')) {
848+
tokens.push('\u0003');
849+
}
850+
else if (item.label.startsWith('v-on:')) {
851+
tokens.push('\u0004');
801852
}
802853
else {
803-
item.sortText = '\u0000\u0000' + (item.sortText ?? item.label);
854+
tokens.push('\u0000');
804855
}
805-
}
806856

807-
if (itemId.type === 'componentProp') {
808-
if (componentName !== '*') {
809-
item.kind = 5 satisfies typeof vscode.CompletionItemKind.Field;
857+
if (specialProps.has(name)) {
858+
tokens.push('\u0001');
859+
}
860+
else {
861+
tokens.push('\u0000');
810862
}
811-
}
812-
else {
813-
item.kind = componentName !== '*' ? 3 satisfies typeof vscode.CompletionItemKind.Function : 23 satisfies typeof vscode.CompletionItemKind.Event;
814863
}
815864
}
865+
else if (specialProps.has(item.label)) {
866+
item.kind = 5 satisfies typeof vscode.CompletionItemKind.Field;
867+
tokens.push('\u0000', '\u0000', '\u0001');
868+
}
816869
else if (
817870
item.label === 'v-if'
818871
|| item.label === 'v-else-if'
819872
|| item.label === 'v-else'
820873
|| item.label === 'v-for'
821874
) {
822875
item.kind = 14 satisfies typeof vscode.CompletionItemKind.Keyword;
823-
item.sortText = '\u0003' + (item.sortText ?? item.label);
876+
tokens.push('\u0003');
824877
}
825878
else if (item.label.startsWith('v-')) {
826879
item.kind = 3 satisfies typeof vscode.CompletionItemKind.Function;
827-
item.sortText = '\u0002' + (item.sortText ?? item.label);
880+
tokens.push('\u0002');
828881
}
829882
else {
830-
item.sortText = '\u0001' + (item.sortText ?? item.label);
883+
tokens.push('\u0001');
831884
}
885+
886+
item.sortText = tokens.join('') + (item.sortText ?? item.label);
832887
}
833888

834889
updateExtraCustomData([]);
@@ -888,7 +943,7 @@ export function create(
888943
}
889944
};
890945

891-
function createInternalItemId(type: 'componentEvent' | 'componentProp' | 'specialTag', args: string[]) {
946+
function createInternalItemId(type: InternalItemId, args: string[]) {
892947
return '__VLS_::' + type + '::' + args.join(',');
893948
}
894949

@@ -900,7 +955,7 @@ function readInternalItemId(key: string) {
900955
if (isInternalItemId(key)) {
901956
const strs = key.split('::');
902957
return {
903-
type: strs[1] as 'componentEvent' | 'componentProp' | 'specialTag',
958+
type: strs[1] as InternalItemId,
904959
args: strs[2].split(','),
905960
};
906961
}
@@ -917,3 +972,16 @@ function getReplacement(list: html.CompletionList, doc: TextDocument) {
917972
}
918973
}
919974
}
975+
976+
function tryGetEventName(
977+
itemId: ReturnType<typeof readInternalItemId> & {}
978+
): [isEvent: boolean, name: string] {
979+
const name = hyphenateAttr(itemId.args[1]);
980+
if (name.startsWith('on-')) {
981+
return [true, name.slice('on-'.length)];
982+
}
983+
else if (itemId.type === 'componentEvent') {
984+
return [true, name];
985+
}
986+
return [false, name];
987+
}

0 commit comments

Comments
 (0)