Skip to content

Commit e06148e

Browse files
committed
1 parent 1fcd22d commit e06148e

File tree

7 files changed

+173
-5
lines changed

7 files changed

+173
-5
lines changed

packages/language-service/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { create as createVueInlayHintsPlugin } from './lib/plugins/vue-inlayhint
3030
import { parse, VueCompilerOptions } from '@vue/language-core';
3131
import { proxyLanguageServiceForVue } from '@vue/typescript-plugin/lib/common';
3232
import { collectExtractProps } from '@vue/typescript-plugin/lib/requests/collectExtractProps';
33-
import { getComponentEvents, getComponentNames, getComponentProps, getElementAttrs, getTemplateContextProps } from '@vue/typescript-plugin/lib/requests/componentInfos';
33+
import { getComponentEvents, getComponentNames, getComponentProps, getComponentPropsWithComment, getElementAttrs, getTemplateContextProps } from '@vue/typescript-plugin/lib/requests/componentInfos';
3434
import { getImportPathForFile } from '@vue/typescript-plugin/lib/requests/getImportPathForFile';
3535
import { getPropertiesAtLocation } from '@vue/typescript-plugin/lib/requests/getPropertiesAtLocation';
3636
import type { RequestContext } from '@vue/typescript-plugin/lib/requests/types';
@@ -120,6 +120,9 @@ export function getFullLanguageServicePlugins(ts: typeof import('typescript')):
120120
async getComponentProps(...args) {
121121
return await getComponentProps.apply(requestContext, args);
122122
},
123+
async getComponentPropsWithComment(...args) {
124+
return await getComponentPropsWithComment.apply(requestContext, args);
125+
},
123126
async getElementAttrs(...args) {
124127
return await getElementAttrs.apply(requestContext, args);
125128
},

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

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { URI, Utils } from 'vscode-uri';
1111
import { getNameCasing } from '../ideFeatures/nameCasing';
1212
import { AttrNameCasing, LanguageServicePlugin, TagNameCasing } from '../types';
1313
import { loadModelModifiersData, loadTemplateData } from './data';
14+
import * as ts from 'typescript';
1415

1516
let builtInData: html.HTMLDataV1;
1617
let modelData: html.HTMLDataV1;
@@ -449,6 +450,7 @@ export function create(
449450
attrs: string[];
450451
props: string[];
451452
events: string[];
453+
propsWithComment: { name: string, description: string; }[];
452454
}>();
453455

454456
let version = 0;
@@ -517,6 +519,7 @@ export function create(
517519
promises.push((async () => {
518520
const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? [];
519521
const props = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? [];
522+
const propsWithComment = await tsPluginClient?.getComponentPropsWithComment(vueCode.fileName, tag) ?? [];
520523
const events = await tsPluginClient?.getComponentEvents(vueCode.fileName, tag) ?? [];
521524
tagInfos.set(tag, {
522525
attrs,
@@ -525,13 +528,29 @@ export function create(
525528
&& !hyphenate(prop).startsWith('on-vnode-')
526529
),
527530
events,
531+
propsWithComment: propsWithComment.filter(({ name: prop }) =>
532+
typeof prop === 'string' && !prop.startsWith('ref_')
533+
&& !hyphenate(prop).startsWith('on-vnode-')
534+
).map(({ name, comment, jsdoc }) => {
535+
const markdownComment = symbolDisplayPartsToMarkdown(comment);
536+
const markdownJsdoc = jsDocTagInfoToMarkdown(jsdoc);
537+
let description = markdownComment;
538+
539+
if (markdownJsdoc) {
540+
if (description) {
541+
description += '\n\n';
542+
}
543+
description += jsDocTagInfoToMarkdown(jsdoc);
544+
}
545+
return { name, description };
546+
}),
528547
});
529548
version++;
530549
})());
531550
return [];
532551
}
533552

534-
const { attrs, props, events } = tagInfo;
553+
const { attrs, props, events, propsWithComment } = tagInfo;
535554
const attributes: html.IAttributeData[] = [];
536555
const _tsCodegen = tsCodegen.get(vueCode.sfc);
537556

@@ -587,7 +606,8 @@ export function create(
587606
{
588607

589608
const propName = name;
590-
const propKey = createInternalItemId('componentProp', [isGlobal ? '*' : tag, propName]);
609+
const propDesc = propsWithComment.find(p => p.name === propName)?.description;
610+
const propKey = propDesc || createInternalItemId('componentProp', [isGlobal ? '*' : tag, propName]);
591611

592612
attributes.push(
593613
{
@@ -917,3 +937,33 @@ function getReplacement(list: html.CompletionList, doc: TextDocument) {
917937
}
918938
}
919939
}
940+
941+
942+
943+
function jsDocTagInfoToMarkdown(jsDocTags: ts.JSDocTagInfo[]) {
944+
return jsDocTags.map(tag => {
945+
const tagName = `*@${tag.name}*`;
946+
const tagText = tag.text?.map(t => {
947+
if (t.kind === 'parameterName') {
948+
return `\`${t.text}\``;
949+
} else {
950+
return t.text;
951+
}
952+
}).join('') || '';
953+
954+
return `${tagName} ${tagText}`;
955+
}).join('\n\n');
956+
}
957+
958+
function symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) {
959+
return parts.map(part => {
960+
switch (part.kind) {
961+
case 'keyword':
962+
return `\`${part.text}\``;
963+
case 'functionName':
964+
return `**${part.text}**`;
965+
default:
966+
return part.text;
967+
}
968+
}).join('');
969+
}

packages/typescript-plugin/lib/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ export function getComponentProps(
4848
});
4949
}
5050

51+
export function getComponentPropsWithComment(
52+
...args: Parameters<typeof import('./requests/componentInfos.js')['getComponentPropsWithComment']>
53+
) {
54+
return sendRequest<ReturnType<typeof import('./requests/componentInfos')['getComponentPropsWithComment']>>({
55+
type: 'getComponentPropsWithComment',
56+
args,
57+
});
58+
}
59+
5160
export function getComponentEvents(
5261
...args: Parameters<typeof import('./requests/componentInfos.js')['getComponentEvents']>
5362
) {

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,96 @@ export function getComponentProps(
8686
return [...result];
8787
}
8888

89+
// most are copied from getComponentProps except for the return type
90+
export function getComponentPropsWithComment(
91+
this: RequestContext,
92+
fileName: string,
93+
tag: string,
94+
requiredOnly = false,
95+
) {
96+
const { typescript: ts, language, languageService, getFileId } = this;
97+
const volarFile = language.scripts.get(getFileId(fileName));
98+
if (!(volarFile?.generated?.root instanceof vue.VueVirtualCode)) {
99+
return;
100+
}
101+
const vueCode = volarFile.generated.root;
102+
const program: ts.Program = (languageService as any).getCurrentProgram();
103+
if (!program) {
104+
return;
105+
}
106+
107+
const checker = program.getTypeChecker();
108+
const components = getVariableType(ts, languageService, vueCode, '__VLS_components');
109+
if (!components) {
110+
return [];
111+
}
112+
113+
const name = tag.split('.');
114+
115+
let componentSymbol = components.type.getProperty(name[0]);
116+
117+
if (!componentSymbol) {
118+
componentSymbol = components.type.getProperty(camelize(name[0]))
119+
?? components.type.getProperty(capitalize(camelize(name[0])));
120+
}
121+
122+
if (!componentSymbol) {
123+
return [];
124+
}
125+
126+
let componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
127+
128+
for (let i = 1; i < name.length; i++) {
129+
componentSymbol = componentType.getProperty(name[i]);
130+
if (componentSymbol) {
131+
componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
132+
}
133+
else {
134+
return [];
135+
}
136+
}
137+
138+
const result = new Set<{ name: string, comment: ts.SymbolDisplayPart[], jsdoc: ts.JSDocTagInfo[]; }>();
139+
140+
for (const sig of componentType.getCallSignatures()) {
141+
const propParam = sig.parameters[0];
142+
if (propParam) {
143+
const propsType = checker.getTypeOfSymbolAtLocation(propParam, components.node);
144+
const props = propsType.getProperties();
145+
for (const prop of props) {
146+
if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) {
147+
const comment = prop.getDocumentationComment(checker);
148+
const jsdoc = prop.getJsDocTags();
149+
150+
result.add({ name: prop.name, comment, jsdoc });
151+
}
152+
}
153+
}
154+
}
155+
156+
for (const sig of componentType.getConstructSignatures()) {
157+
const instanceType = sig.getReturnType();
158+
const propsSymbol = instanceType.getProperty('$props');
159+
if (propsSymbol) {
160+
const propsType = checker.getTypeOfSymbolAtLocation(propsSymbol, components.node);
161+
const props = propsType.getProperties();
162+
for (const prop of props) {
163+
if (prop.flags & ts.SymbolFlags.Method) { // #2443
164+
continue;
165+
}
166+
if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) {
167+
const comment = prop.getDocumentationComment(checker);
168+
const jsdoc = prop.getJsDocTags();
169+
170+
result.add({ name: prop.name, comment, jsdoc });
171+
}
172+
}
173+
}
174+
}
175+
176+
return [...result];
177+
}
178+
89179
export function getComponentEvents(
90180
this: RequestContext,
91181
fileName: string,

packages/typescript-plugin/lib/server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as fs from 'fs';
33
import * as net from 'net';
44
import type * as ts from 'typescript';
55
import { collectExtractProps } from './requests/collectExtractProps';
6-
import { getComponentEvents, getComponentNames, getComponentProps, getElementAttrs, getTemplateContextProps } from './requests/componentInfos';
6+
import { getComponentEvents, getComponentNames, getComponentProps, getComponentPropsWithComment, getElementAttrs, getTemplateContextProps } from './requests/componentInfos';
77
import { getImportPathForFile } from './requests/getImportPathForFile';
88
import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation';
99
import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition';
@@ -19,6 +19,7 @@ export interface Request {
1919
| 'getQuickInfoAtPosition'
2020
// Component Infos
2121
| 'getComponentProps'
22+
| 'getComponentPropsWithComment'
2223
| 'getComponentEvents'
2324
| 'getTemplateContextProps'
2425
| 'getComponentNames'
@@ -88,6 +89,10 @@ export async function startNamedPipeServer(
8889
const result = getComponentProps.apply(requestContext, request.args as any);
8990
sendResponse(result);
9091
}
92+
else if (request.type === 'getComponentPropsWithComment') {
93+
const result = getComponentPropsWithComment.apply(requestContext, request.args as any);
94+
sendResponse(result);
95+
}
9196
else if (request.type === 'getComponentEvents') {
9297
const result = getComponentEvents.apply(requestContext, request.args as any);
9398
sendResponse(result);
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
<script setup lang="ts">
22
defineProps<{
3-
foo: number;
3+
/** comment from props: foo */
4+
foo: number;
5+
6+
/**
7+
* Represents a book.
8+
* this is new line?
9+
* @constructor
10+
* @param {string} title - The title of the book.
11+
* @param {string} author - The author of the book.
12+
*/
13+
book: { title: string; author: string };
414
}>();
515
</script>

test-workspace/language-service/inlay-hint/missing-props/entry.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ import Comp from './Comp.vue';
55
<template>
66
<Comp></Comp>
77
<!-- ^inlayHint: "foo!" -->
8+
<!-- ^inlayHint: "book!" -->
89
</template>

0 commit comments

Comments
 (0)