Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(language-service): display deprecated info of props in completion #5134

Merged
merged 2 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions packages/language-service/lib/plugins/vue-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Disposable, LanguageServiceContext, LanguageServicePluginInstance
import { VueVirtualCode, hyphenateAttr, hyphenateTag, tsCodegen } from '@vue/language-core';
import { camelize, capitalize } from '@vue/shared';
import { getComponentSpans } from '@vue/typescript-plugin/lib/common';
import type { ComponentPropInfo } from '@vue/typescript-plugin/lib/requests/componentInfos';
import { create as createHtmlService } from 'volar-service-html';
import { create as createPugService } from 'volar-service-pug';
import * as html from 'vscode-html-languageservice';
Expand Down Expand Up @@ -44,7 +45,7 @@ export function create(
let extraCustomData: html.IHTMLDataProvider[] = [];
let lastCompletionComponentNames = new Set<string>();

const tsDocumentations = new Map<string, string>();
const cachedPropInfos = new Map<string, ComponentPropInfo>();
const onDidChangeCustomDataListeners = new Set<() => void>();
const onDidChangeCustomData = (listener: () => void): Disposable => {
onDidChangeCustomDataListeners.add(listener);
Expand Down Expand Up @@ -488,15 +489,15 @@ export function create(
const promises: Promise<void>[] = [];
const tagInfos = new Map<string, {
attrs: string[];
propsInfo: { name: string, commentMarkdown?: string; }[];
propInfos: ComponentPropInfo[];
events: string[];
directives: string[];
}>();

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

tsDocumentations.clear();
cachedPropInfos.clear();

updateExtraCustomData([
html.newHTMLDataProvider('vue-template-built-in', builtInData),
Expand Down Expand Up @@ -557,12 +558,12 @@ export function create(
if (!tagInfo) {
promises.push((async () => {
const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? [];
const propsInfo = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? [];
const propInfos = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? [];
const events = await tsPluginClient?.getComponentEvents(vueCode.fileName, tag) ?? [];
const directives = await tsPluginClient?.getComponentDirectives(vueCode.fileName) ?? [];
tagInfos.set(tag, {
attrs,
propsInfo: propsInfo.filter(prop =>
propInfos: propInfos.filter(prop =>
!prop.name.startsWith('ref_')
),
events,
Expand All @@ -573,8 +574,8 @@ export function create(
return [];
}

const { attrs, propsInfo, events, directives } = tagInfo;
const props = propsInfo.map(prop =>
const { attrs, propInfos, events, directives } = tagInfo;
const props = propInfos.map(prop =>
hyphenateTag(prop.name).startsWith('on-vnode-')
? 'onVue:' + prop.name.slice('onVnode'.length)
: prop.name
Expand Down Expand Up @@ -611,14 +612,14 @@ export function create(
else {

const propName = name;
const propKey = generateItemKey('componentProp', isGlobal ? '*' : tag, propName);
const propDescription = propsInfo.find(prop => {
const propInfo = propInfos.find(prop => {
const name = casing.attr === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name);
return name === propName;
})?.commentMarkdown;
});
const propKey = generateItemKey('componentProp', isGlobal ? '*' : tag, propName, propInfo?.deprecated);

if (propDescription) {
tsDocumentations.set(propName, propDescription);
if (propInfo) {
cachedPropInfos.set(propName, propInfo);
}

attributes.push(
Expand Down Expand Up @@ -792,7 +793,7 @@ export function create(

for (const item of completionList.items) {
const documentation = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;
if (documentation && !isItemKey(documentation) && item.documentation) {
if (documentation && !isItemKey(documentation)) {
htmlDocumentations.set(item.label, documentation);
}
}
Expand All @@ -819,11 +820,14 @@ export function create(
const itemKeyStr = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;

let parsedItemKey = itemKeyStr ? parseItemKey(itemKeyStr) : undefined;
let propInfo: ComponentPropInfo | undefined;

if (parsedItemKey) {
const documentations: string[] = [];

if (tsDocumentations.has(parsedItemKey.prop)) {
documentations.push(tsDocumentations.get(parsedItemKey.prop)!);
propInfo = cachedPropInfos.get(parsedItemKey.prop);
if (propInfo?.commentMarkdown) {
documentations.push(propInfo.commentMarkdown);
}

let { isEvent, propName } = getPropName(parsedItemKey);
Expand Down Expand Up @@ -861,22 +865,28 @@ export function create(
type: 'componentProp',
tag: '^',
prop: propName,
deprecated: false,
leadingSlash: false
};
}

if (tsDocumentations.has(propName)) {
propInfo = cachedPropInfos.get(propName);
if (propInfo?.commentMarkdown) {
const originalDocumentation = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;
item.documentation = {
kind: 'markdown',
value: [
tsDocumentations.get(propName)!,
propInfo.commentMarkdown,
originalDocumentation,
].filter(str => !!str).join('\n\n'),
};
}
}

if (propInfo?.deprecated) {
item.tags = [1 satisfies typeof vscode.CompletionItemTag.Deprecated];
}

const tokens: string[] = [];

if (
Expand Down Expand Up @@ -1019,8 +1029,8 @@ function parseLabel(label: string) {
};
}

function generateItemKey(type: InternalItemId, tag: string, prop: string) {
return '__VLS_data=' + type + ',' + tag + ',' + prop;
function generateItemKey(type: InternalItemId, tag: string, prop: string, deprecated?: boolean) {
return `__VLS_data=${type},${tag},${prop},${Number(deprecated)}`;
}

function isItemKey(key: string) {
Expand All @@ -1035,6 +1045,7 @@ function parseItemKey(key: string) {
type: strs[0] as InternalItemId,
tag: strs[1],
prop: strs[2],
deprecated: strs[3] === '1',
leadingSlash
};
}
Expand Down
49 changes: 36 additions & 13 deletions packages/typescript-plugin/lib/requests/componentInfos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import * as path from 'node:path';
import type * as ts from 'typescript';
import type { RequestContext } from './types';

export interface ComponentPropInfo {
name: string;
required?: boolean;
deprecated?: boolean;
commentMarkdown?: string;
}

export function getComponentProps(
this: RequestContext,
fileName: string,
Expand All @@ -27,11 +34,7 @@ export function getComponentProps(
return [];
}

const result = new Map<string, {
name: string;
required?: true;
commentMarkdown?: string;
}>();
const result = new Map<string, ComponentPropInfo>();

for (const sig of componentType.getCallSignatures()) {
const propParam = sig.parameters[0];
Expand All @@ -41,9 +44,17 @@ export function getComponentProps(
for (const prop of props) {
const name = prop.name;
const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined;
const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined;

result.set(name, { name, required, commentMarkdown });
const {
content: commentMarkdown,
deprecated
} = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags());

result.set(name, {
name,
required,
deprecated,
commentMarkdown
});
}
}
}
Expand All @@ -60,9 +71,17 @@ export function getComponentProps(
}
const name = prop.name;
const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined;
const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined;

result.set(name, { name, required, commentMarkdown });
const {
content: commentMarkdown,
deprecated
} = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags());

result.set(name, {
name,
required,
deprecated,
commentMarkdown
});
}
}
}
Expand Down Expand Up @@ -302,8 +321,12 @@ function searchVariableDeclarationNode(
function generateCommentMarkdown(parts: ts.SymbolDisplayPart[], jsDocTags: ts.JSDocTagInfo[]) {
const parsedComment = _symbolDisplayPartsToMarkdown(parts);
const parsedJsDoc = _jsDocTagInfoToMarkdown(jsDocTags);
let result = [parsedComment, parsedJsDoc].filter(str => !!str).join('\n\n');
return result;
const content = [parsedComment, parsedJsDoc].filter(str => !!str).join('\n\n');
const deprecated = jsDocTags.some((tag) => tag.name === 'deprecated');
return {
content,
deprecated
};
}

function _symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) {
Expand Down
8 changes: 2 additions & 6 deletions packages/typescript-plugin/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as fs from 'node:fs';
import * as net from 'node:net';
import type * as ts from 'typescript';
import { collectExtractProps } from './requests/collectExtractProps';
import { getComponentDirectives, getComponentEvents, getComponentNames, getComponentProps, getElementAttrs } from './requests/componentInfos';
import { type ComponentPropInfo, getComponentDirectives, getComponentEvents, getComponentNames, getComponentProps, getElementAttrs } from './requests/componentInfos';
import { getImportPathForFile } from './requests/getImportPathForFile';
import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation';
import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition';
Expand Down Expand Up @@ -70,11 +70,7 @@ export async function startNamedPipeServer(
const dataChunks: Buffer[] = [];
const currentData = new FileMap<[
componentNames: string[],
Record<string, {
name: string;
required?: true;
commentMarkdown?: string;
}[]>,
Record<string, ComponentPropInfo[]>,
]>(false);
const allConnections = new Set<net.Socket>();
const pendingRequests = new Set<number>();
Expand Down
13 changes: 3 additions & 10 deletions packages/typescript-plugin/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import type * as ts from 'typescript';
import type { ComponentPropInfo } from './requests/componentInfos';
import type { NotificationData, ProjectInfo, RequestData, ResponseData } from './server';

export { TypeScriptProjectHost } from '@volar/typescript';
Expand All @@ -29,11 +30,7 @@ class NamedPipeServer {
projectInfo?: ProjectInfo;
containsFileCache = new Map<string, Promise<boolean | undefined | null>>();
componentNamesAndProps = new FileMap<
Record<string, null | {
name: string;
required?: true;
commentMarkdown?: string;
}[]>
Record<string, null | ComponentPropInfo[]>
>(false);

constructor(kind: ts.server.ProjectKind, id: number) {
Expand Down Expand Up @@ -170,11 +167,7 @@ class NamedPipeServer {
const components = this.componentNamesAndProps.get(fileName) ?? {};
const [name, props]: [
name: string,
props: {
name: string;
required?: true;
commentMarkdown?: string;
}[],
props: ComponentPropInfo[],
] = data;
components[name] = props;
}
Expand Down
Loading