Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4760dd0
test(test-workspace): ✨ add hover scenario to test playground
jak-ch-ll Mar 29, 2026
077f5c1
fix(language-service): 🐛 fix code block in hover documentation
jak-ch-ll Mar 29, 2026
d88a51e
refactor(language-service): ♻️ refactor hover documentation
jak-ch-ll Mar 29, 2026
e8a9891
refactor(language-service): ♻️ create extractMetaLists function
jak-ch-ll Mar 29, 2026
4079f4b
feat(vscode): ✨ add settings enum for `vue.hover.rich`
jak-ch-ll Mar 29, 2026
5a7054b
feat(language-service): ✨ add rich hover markdown alternative
jak-ch-ll Mar 29, 2026
712e96c
feat(language-service): ✨ add rich hover jsdoc alternative
jak-ch-ll Mar 29, 2026
ec148bb
feat(language-service): ✨ add proper event type for hover docs
jak-ch-ll Mar 29, 2026
4dc0e54
feat(language-service): ✨ add event props to events list in hover docs
jak-ch-ll Mar 29, 2026
5159be5
refactor(language-service): ♻️ extract hover docs logic into separate…
jak-ch-ll Mar 29, 2026
316e7e0
test(test-workspace): ✨ add minimal hover scenario
jak-ch-ll Mar 29, 2026
d598766
test(language-service): 📸 add snapshot tests for component hover docs
jak-ch-ll Mar 29, 2026
cb124b1
refactor: unwrap iife
KazariEX May 24, 2026
4268876
refactor(test-workspace): 🔥 remove tsconfig from hover scenario
jak-ch-ll May 24, 2026
9d92bc4
refactor(language-service): ♻️ change import to `vue-component-meta`
jak-ch-ll May 24, 2026
648547d
refactor: ♻️ change vue.hover.rich options to lowercase
jak-ch-ll May 24, 2026
31eb897
simple interface
jak-ch-ll May 25, 2026
1af9ac1
single codeblock
jak-ch-ll May 25, 2026
d43ccce
legal interfaces
jak-ch-ll May 25, 2026
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
10 changes: 8 additions & 2 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,14 @@
"markdownDescription": "%configuration.codeActions.askNewComponentName%"
},
"vue.hover.rich": {
"type": "boolean",
"default": false,
"type": "string",
"enum": [
"off",
"table",
"jsdoc",
"markdown"
],
"default": "off",
"markdownDescription": "%configuration.hover.rich%"
},
"vue.suggest.componentNameCasing": {
Expand Down
140 changes: 22 additions & 118 deletions packages/language-service/lib/plugins/vue-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,16 @@ import { create as createPugService } from 'volar-service-pug';
import { applyCompletionEntryDetails, convertCompletionInfo } from 'volar-service-typescript/lib/utils/lspConverters';
import * as html from 'vscode-html-languageservice';
import { URI, Utils } from 'vscode-uri';
import type { PropertyMeta } from 'vue-component-meta';
import { loadModelModifiersData, loadTemplateData } from '../data';
import { format } from '../htmlFormatter';
import { AttrNameCasing, getAttrNameCasing, getTagNameCasing, TagNameCasing } from '../nameCasing';
import { resolveEmbeddedCode } from '../utils';
import { formatComponentMeta, getHoverDocsSetting } from './vue-template/componentHoverDocs';

const EVENT_PROP_REGEX = /^on[A-Z]/;

// String constants
const AUTO_IMPORT_PLACEHOLDER = 'AutoImportsPlaceholder';
const UPDATE_EVENT_PREFIX = 'update:';
const UPDATE_PROP_PREFIX = 'onUpdate:';

// Directive prefixes
Expand Down Expand Up @@ -405,132 +404,37 @@ export function create(
'hover',
() => baseServiceInstance.provideHover!(document, position, token),
);
const templateAst = info.root.ir.template?.ast;
const enabledRichMessage = await context.env.getConfiguration?.('vue.hover.rich');
const { template } = info.root.ir;
const hoverDocsSetting = await getHoverDocsSetting(context);

if (!templateAst || !enabledRichMessage || (htmlHover && hasContents(htmlHover.contents))) {
if (!template?.ast || !hoverDocsSetting || htmlHover && hasContents(htmlHover.contents)) {
return htmlHover;
}

for (const element of forEachElementNode(templateAst)) {
const tagStart = element.loc.start.offset + element.loc.source.indexOf(element.tag);
const tagEnd = tagStart + element.tag.length;
for (const element of forEachElementNode(template.ast)) {
const start = element.loc.start.offset + element.loc.source.indexOf(element.tag);
const end = start + element.tag.length;
const offset = document.offsetAt(position);

if (offset >= tagStart && offset <= tagEnd) {
const meta = await tsserver.getComponentMeta(info.root.fileName, element.tag);
const props = meta?.props.filter(p => !p.global);
const modelProps = new Set<PropertyMeta>();
let tableContents: string[] = [];

for (const event of meta?.events ?? []) {
if (event.name.startsWith(UPDATE_EVENT_PREFIX)) {
const modelName = event.name.slice(UPDATE_EVENT_PREFIX.length);
const modelProp = props?.find(p => p.name === modelName);
if (modelProp) {
modelProps.add(modelProp);
}
}
}
for (const prop of props ?? []) {
if (prop.name.startsWith(UPDATE_PROP_PREFIX)) {
const modelName = prop.name.slice(UPDATE_PROP_PREFIX.length);
const modelProp = props?.find(p => p.name === modelName);
if (modelProp) {
modelProps.add(modelProp);
}
}
}

if (props?.length) {
let table =
`<tr><th align="left">Prop</th><th align="left">Description</th><th align="left">Default</th></tr>\n`;
for (const p of props) {
table += `<tr>
<td>${printName(p, modelProps.has(p))}</td>
<td>${printDescription(p)}</td>
<td>${p.default ? `<code>${p.default}</code>` : ''}</td>
</tr>\n`;
}
tableContents.push(table);
}

if (meta?.events?.length) {
let table = `<tr><th align="left">Event</th><th align="left">Description</th><th></th></tr>\n`;
for (const e of meta.events) {
table += `<tr>
<td>${printName(e)}</td>
<td colspan="2">${printDescription(e)}</td>
</tr>\n`;
}
tableContents.push(table);
}

if (meta?.slots?.length) {
let table = `<tr><th align="left">Slot</th><th align="left">Description</th><th></th></tr>\n`;
for (const s of meta.slots) {
table += `<tr>
<td>${printName(s)}</td>
<td colspan="2">${printDescription(s)}</td>
</tr>\n`;
}
tableContents.push(table);
}

if (meta?.exposed.length) {
let table = `<tr><th align="left">Exposed</th><th align="left">Description</th><th></th></tr>\n`;
for (const e of meta.exposed) {
table += `<tr>
<td>${printName(e)}</td>
<td colspan="2">${printDescription(e)}</td>
</tr>\n`;
}
tableContents.push(table);
}

htmlHover ??= {
range: {
start: document.positionAt(tagStart),
end: document.positionAt(tagEnd),
},
contents: '',
};

// 2px height per <tr>
const tableGap = `<tr></tr>`.repeat(4);
htmlHover.contents = {
kind: 'markdown',
value: tableContents
? `<table>\n${tableContents.join(`\n${tableGap}\n`)}\n</table>`
: `No type information available.`,
};
if (offset < start || offset > end) {
continue;
}
}

return htmlHover;

function printName(meta: { name: string; tags: { name: string }[]; required?: boolean }, model?: boolean) {
let name = meta.name;
if (meta.tags.some(tag => tag.name === 'deprecated')) {
name = `<del>${name}</del>`;
}
if (meta.required) {
name += ' <sup><em>required</em></sup>';
}
if (model) {
name += ' <sup><em>model</em></sup>';
const meta = await tsserver.getComponentMeta(info.root.fileName, element.tag);
if (!meta) {
continue;
}
return name;
}

function printDescription(meta: { description?: string; type: string }) {
let desc = `<code>${meta.type}</code>`;
if (meta.description) {
// blank line for terminate HTML to support markdown
// see: https://github.github.com/gfm/#example-118
desc = `\n\n${meta.description}<br>${desc}`;
}
return desc;
return {
range: {
start: document.positionAt(start),
end: document.positionAt(end),
},
contents: {
kind: 'markdown',
value: formatComponentMeta(meta, hoverDocsSetting),
},
};
}

function hasContents(contents: html.MarkupContent | html.MarkedString | html.MarkedString[]) {
Expand Down
Loading
Loading