Skip to content

Commit 9eeab0c

Browse files
authored
feat(language-service): display deprecated info of props in completion (#5134)
1 parent a0511b8 commit 9eeab0c

4 files changed

Lines changed: 71 additions & 48 deletions

File tree

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

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Disposable, LanguageServiceContext, LanguageServicePluginInstance
22
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';
5+
import type { ComponentPropInfo } from '@vue/typescript-plugin/lib/requests/componentInfos';
56
import { create as createHtmlService } from 'volar-service-html';
67
import { create as createPugService } from 'volar-service-pug';
78
import * as html from 'vscode-html-languageservice';
@@ -44,7 +45,7 @@ export function create(
4445
let extraCustomData: html.IHTMLDataProvider[] = [];
4546
let lastCompletionComponentNames = new Set<string>();
4647

47-
const tsDocumentations = new Map<string, string>();
48+
const cachedPropInfos = new Map<string, ComponentPropInfo>();
4849
const onDidChangeCustomDataListeners = new Set<() => void>();
4950
const onDidChangeCustomData = (listener: () => void): Disposable => {
5051
onDidChangeCustomDataListeners.add(listener);
@@ -488,15 +489,15 @@ export function create(
488489
const promises: Promise<void>[] = [];
489490
const tagInfos = new Map<string, {
490491
attrs: string[];
491-
propsInfo: { name: string, commentMarkdown?: string; }[];
492+
propInfos: ComponentPropInfo[];
492493
events: string[];
493494
directives: string[];
494495
}>();
495496

496497
let version = 0;
497498
let components: string[] | undefined;
498499

499-
tsDocumentations.clear();
500+
cachedPropInfos.clear();
500501

501502
updateExtraCustomData([
502503
html.newHTMLDataProvider('vue-template-built-in', builtInData),
@@ -557,12 +558,12 @@ export function create(
557558
if (!tagInfo) {
558559
promises.push((async () => {
559560
const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? [];
560-
const propsInfo = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? [];
561+
const propInfos = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? [];
561562
const events = await tsPluginClient?.getComponentEvents(vueCode.fileName, tag) ?? [];
562563
const directives = await tsPluginClient?.getComponentDirectives(vueCode.fileName) ?? [];
563564
tagInfos.set(tag, {
564565
attrs,
565-
propsInfo: propsInfo.filter(prop =>
566+
propInfos: propInfos.filter(prop =>
566567
!prop.name.startsWith('ref_')
567568
),
568569
events,
@@ -573,8 +574,8 @@ export function create(
573574
return [];
574575
}
575576

576-
const { attrs, propsInfo, events, directives } = tagInfo;
577-
const props = propsInfo.map(prop =>
577+
const { attrs, propInfos, events, directives } = tagInfo;
578+
const props = propInfos.map(prop =>
578579
hyphenateTag(prop.name).startsWith('on-vnode-')
579580
? 'onVue:' + prop.name.slice('onVnode'.length)
580581
: prop.name
@@ -611,14 +612,14 @@ export function create(
611612
else {
612613

613614
const propName = name;
614-
const propKey = generateItemKey('componentProp', isGlobal ? '*' : tag, propName);
615-
const propDescription = propsInfo.find(prop => {
615+
const propInfo = propInfos.find(prop => {
616616
const name = casing.attr === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name);
617617
return name === propName;
618-
})?.commentMarkdown;
618+
});
619+
const propKey = generateItemKey('componentProp', isGlobal ? '*' : tag, propName, propInfo?.deprecated);
619620

620-
if (propDescription) {
621-
tsDocumentations.set(propName, propDescription);
621+
if (propInfo) {
622+
cachedPropInfos.set(propName, propInfo);
622623
}
623624

624625
attributes.push(
@@ -792,7 +793,7 @@ export function create(
792793

793794
for (const item of completionList.items) {
794795
const documentation = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;
795-
if (documentation && !isItemKey(documentation) && item.documentation) {
796+
if (documentation && !isItemKey(documentation)) {
796797
htmlDocumentations.set(item.label, documentation);
797798
}
798799
}
@@ -819,11 +820,14 @@ export function create(
819820
const itemKeyStr = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;
820821

821822
let parsedItemKey = itemKeyStr ? parseItemKey(itemKeyStr) : undefined;
823+
let propInfo: ComponentPropInfo | undefined;
824+
822825
if (parsedItemKey) {
823826
const documentations: string[] = [];
824827

825-
if (tsDocumentations.has(parsedItemKey.prop)) {
826-
documentations.push(tsDocumentations.get(parsedItemKey.prop)!);
828+
propInfo = cachedPropInfos.get(parsedItemKey.prop);
829+
if (propInfo?.commentMarkdown) {
830+
documentations.push(propInfo.commentMarkdown);
827831
}
828832

829833
let { isEvent, propName } = getPropName(parsedItemKey);
@@ -861,22 +865,28 @@ export function create(
861865
type: 'componentProp',
862866
tag: '^',
863867
prop: propName,
868+
deprecated: false,
864869
leadingSlash: false
865870
};
866871
}
867872

868-
if (tsDocumentations.has(propName)) {
873+
propInfo = cachedPropInfos.get(propName);
874+
if (propInfo?.commentMarkdown) {
869875
const originalDocumentation = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;
870876
item.documentation = {
871877
kind: 'markdown',
872878
value: [
873-
tsDocumentations.get(propName)!,
879+
propInfo.commentMarkdown,
874880
originalDocumentation,
875881
].filter(str => !!str).join('\n\n'),
876882
};
877883
}
878884
}
879885

886+
if (propInfo?.deprecated) {
887+
item.tags = [1 satisfies typeof vscode.CompletionItemTag.Deprecated];
888+
}
889+
880890
const tokens: string[] = [];
881891

882892
if (
@@ -1019,8 +1029,8 @@ function parseLabel(label: string) {
10191029
};
10201030
}
10211031

1022-
function generateItemKey(type: InternalItemId, tag: string, prop: string) {
1023-
return '__VLS_data=' + type + ',' + tag + ',' + prop;
1032+
function generateItemKey(type: InternalItemId, tag: string, prop: string, deprecated?: boolean) {
1033+
return `__VLS_data=${type},${tag},${prop},${Number(deprecated)}`;
10241034
}
10251035

10261036
function isItemKey(key: string) {
@@ -1035,6 +1045,7 @@ function parseItemKey(key: string) {
10351045
type: strs[0] as InternalItemId,
10361046
tag: strs[1],
10371047
prop: strs[2],
1048+
deprecated: strs[3] === '1',
10381049
leadingSlash
10391050
};
10401051
}

packages/typescript-plugin/lib/requests/componentInfos.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import * as path from 'node:path';
44
import type * as ts from 'typescript';
55
import type { RequestContext } from './types';
66

7+
export interface ComponentPropInfo {
8+
name: string;
9+
required?: boolean;
10+
deprecated?: boolean;
11+
commentMarkdown?: string;
12+
}
13+
714
export function getComponentProps(
815
this: RequestContext,
916
fileName: string,
@@ -27,11 +34,7 @@ export function getComponentProps(
2734
return [];
2835
}
2936

30-
const result = new Map<string, {
31-
name: string;
32-
required?: true;
33-
commentMarkdown?: string;
34-
}>();
37+
const result = new Map<string, ComponentPropInfo>();
3538

3639
for (const sig of componentType.getCallSignatures()) {
3740
const propParam = sig.parameters[0];
@@ -41,9 +44,17 @@ export function getComponentProps(
4144
for (const prop of props) {
4245
const name = prop.name;
4346
const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined;
44-
const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined;
45-
46-
result.set(name, { name, required, commentMarkdown });
47+
const {
48+
content: commentMarkdown,
49+
deprecated
50+
} = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags());
51+
52+
result.set(name, {
53+
name,
54+
required,
55+
deprecated,
56+
commentMarkdown
57+
});
4758
}
4859
}
4960
}
@@ -60,9 +71,17 @@ export function getComponentProps(
6071
}
6172
const name = prop.name;
6273
const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined;
63-
const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined;
64-
65-
result.set(name, { name, required, commentMarkdown });
74+
const {
75+
content: commentMarkdown,
76+
deprecated
77+
} = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags());
78+
79+
result.set(name, {
80+
name,
81+
required,
82+
deprecated,
83+
commentMarkdown
84+
});
6685
}
6786
}
6887
}
@@ -302,8 +321,12 @@ function searchVariableDeclarationNode(
302321
function generateCommentMarkdown(parts: ts.SymbolDisplayPart[], jsDocTags: ts.JSDocTagInfo[]) {
303322
const parsedComment = _symbolDisplayPartsToMarkdown(parts);
304323
const parsedJsDoc = _jsDocTagInfoToMarkdown(jsDocTags);
305-
let result = [parsedComment, parsedJsDoc].filter(str => !!str).join('\n\n');
306-
return result;
324+
const content = [parsedComment, parsedJsDoc].filter(str => !!str).join('\n\n');
325+
const deprecated = jsDocTags.some((tag) => tag.name === 'deprecated');
326+
return {
327+
content,
328+
deprecated
329+
};
307330
}
308331

309332
function _symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) {

packages/typescript-plugin/lib/server.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
33
import * as net from 'node:net';
44
import type * as ts from 'typescript';
55
import { collectExtractProps } from './requests/collectExtractProps';
6-
import { getComponentDirectives, getComponentEvents, getComponentNames, getComponentProps, getElementAttrs } from './requests/componentInfos';
6+
import { type ComponentPropInfo, getComponentDirectives, getComponentEvents, getComponentNames, getComponentProps, getElementAttrs } from './requests/componentInfos';
77
import { getImportPathForFile } from './requests/getImportPathForFile';
88
import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation';
99
import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition';
@@ -70,11 +70,7 @@ export async function startNamedPipeServer(
7070
const dataChunks: Buffer[] = [];
7171
const currentData = new FileMap<[
7272
componentNames: string[],
73-
Record<string, {
74-
name: string;
75-
required?: true;
76-
commentMarkdown?: string;
77-
}[]>,
73+
Record<string, ComponentPropInfo[]>,
7874
]>(false);
7975
const allConnections = new Set<net.Socket>();
8076
const pendingRequests = new Set<number>();

packages/typescript-plugin/lib/utils.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as net from 'node:net';
55
import * as os from 'node:os';
66
import * as path from 'node:path';
77
import type * as ts from 'typescript';
8+
import type { ComponentPropInfo } from './requests/componentInfos';
89
import type { NotificationData, ProjectInfo, RequestData, ResponseData } from './server';
910

1011
export { TypeScriptProjectHost } from '@volar/typescript';
@@ -29,11 +30,7 @@ class NamedPipeServer {
2930
projectInfo?: ProjectInfo;
3031
containsFileCache = new Map<string, Promise<boolean | undefined | null>>();
3132
componentNamesAndProps = new FileMap<
32-
Record<string, null | {
33-
name: string;
34-
required?: true;
35-
commentMarkdown?: string;
36-
}[]>
33+
Record<string, null | ComponentPropInfo[]>
3734
>(false);
3835

3936
constructor(kind: ts.server.ProjectKind, id: number) {
@@ -170,11 +167,7 @@ class NamedPipeServer {
170167
const components = this.componentNamesAndProps.get(fileName) ?? {};
171168
const [name, props]: [
172169
name: string,
173-
props: {
174-
name: string;
175-
required?: true;
176-
commentMarkdown?: string;
177-
}[],
170+
props: ComponentPropInfo[],
178171
] = data;
179172
components[name] = props;
180173
}

0 commit comments

Comments
 (0)