From a34bf4f30f241feae8e23f9580a8663a73d83488 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 9 Aug 2024 12:36:30 +0800 Subject: [PATCH 01/11] refactor(language-core): extract SFC root tags to separate virtual code --- packages/language-core/lib/plugins.ts | 2 + .../lib/plugins/vue-root-tags.ts | 69 ++++++++++++++++++ packages/language-core/lib/types.ts | 1 + ...putedFiles.ts => computedEmbeddedCodes.ts} | 2 +- .../lib/virtualFile/computedMappings.ts | 70 ------------------- .../lib/virtualFile/computedSfc.ts | 9 ++- .../language-core/lib/virtualFile/vueFile.ts | 30 +++++--- packages/language-service/index.ts | 2 +- .../language-service/lib/plugins/vue-sfc.ts | 24 ++----- 9 files changed, 107 insertions(+), 102 deletions(-) create mode 100644 packages/language-core/lib/plugins/vue-root-tags.ts rename packages/language-core/lib/virtualFile/{computedFiles.ts => computedEmbeddedCodes.ts} (99%) delete mode 100644 packages/language-core/lib/virtualFile/computedMappings.ts diff --git a/packages/language-core/lib/plugins.ts b/packages/language-core/lib/plugins.ts index e6645ebc60..a0175d30af 100644 --- a/packages/language-core/lib/plugins.ts +++ b/packages/language-core/lib/plugins.ts @@ -1,6 +1,7 @@ import useHtmlFilePlugin from './plugins/file-html'; import useMdFilePlugin from './plugins/file-md'; import useVueFilePlugin from './plugins/file-vue'; +import vueRootTagsPlugin from './plugins/vue-root-tags'; import vueScriptJsPlugin from './plugins/vue-script-js'; import vueSfcCustomBlocks from './plugins/vue-sfc-customblocks'; import vueSfcScriptsFormat from './plugins/vue-sfc-scripts'; @@ -20,6 +21,7 @@ export function createPlugins(pluginContext: Parameters[0]) { useVueFilePlugin, useMdFilePlugin, useHtmlFilePlugin, + vueRootTagsPlugin, vueScriptJsPlugin, vueTemplateHtmlPlugin, vueTemplateInlineCssPlugin, diff --git a/packages/language-core/lib/plugins/vue-root-tags.ts b/packages/language-core/lib/plugins/vue-root-tags.ts new file mode 100644 index 0000000000..73482c0b26 --- /dev/null +++ b/packages/language-core/lib/plugins/vue-root-tags.ts @@ -0,0 +1,69 @@ +import { replaceSourceRange } from 'muggle-string'; +import type { VueLanguagePlugin } from '../types'; +import { allCodeFeatures } from './shared'; + +const plugin: VueLanguagePlugin = () => { + + return { + + version: 2.1, + + getEmbeddedCodes() { + return [{ + id: 'root_tags', + lang: 'vue-root-tags', + }]; + }, + + resolveEmbeddedCode(_fileName, sfc, embeddedFile) { + if (embeddedFile.id === 'root_tags') { + embeddedFile.content.push([sfc.content, undefined, 0, allCodeFeatures]); + for (const block of [ + sfc.script, + sfc.scriptSetup, + sfc.template, + ...sfc.styles, + ...sfc.customBlocks, + ]) { + if (!block) { + continue; + } + let content = block.content; + if (content.endsWith('\r\n')) { + content = content.slice(0, -2); + } + else if (content.endsWith('\n')) { + content = content.slice(0, -1); + } + const offset = content.lastIndexOf('\n') + 1; + // fix folding range end position failed to mapping + replaceSourceRange( + embeddedFile.content, + undefined, + block.startTagEnd, + block.endTagStart, + sfc.content.substring( + block.startTagEnd, + block.startTagEnd + offset + ), + [ + '', + undefined, + block.startTagEnd + offset, + { structure: true }, + ], + sfc.content.substring( + block.startTagEnd + offset, + block.endTagStart + ), + ); + } + } + else { + embeddedFile.parentCodeId ??= 'root_tags'; + } + }, + }; +}; + +export default plugin; diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 72c14f4915..4a23a255a4 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -99,6 +99,7 @@ export interface SfcBlock { } export interface Sfc { + content: string; template: SfcBlock & { ast: CompilerDOM.RootNode | undefined; errors: CompilerDOM.CompilerError[]; diff --git a/packages/language-core/lib/virtualFile/computedFiles.ts b/packages/language-core/lib/virtualFile/computedEmbeddedCodes.ts similarity index 99% rename from packages/language-core/lib/virtualFile/computedFiles.ts rename to packages/language-core/lib/virtualFile/computedEmbeddedCodes.ts index 731e5dee20..c10beb8914 100644 --- a/packages/language-core/lib/virtualFile/computedFiles.ts +++ b/packages/language-core/lib/virtualFile/computedEmbeddedCodes.ts @@ -6,7 +6,7 @@ import type { Code, Sfc, SfcBlock, VueLanguagePluginReturn } from '../types'; import { buildMappings } from '../utils/buildMappings'; import { VueEmbeddedCode } from './embeddedFile'; -export function computedFiles( +export function computedEmbeddedCodes( plugins: VueLanguagePluginReturn[], fileName: string, sfc: Sfc diff --git a/packages/language-core/lib/virtualFile/computedMappings.ts b/packages/language-core/lib/virtualFile/computedMappings.ts deleted file mode 100644 index 27a7376a14..0000000000 --- a/packages/language-core/lib/virtualFile/computedMappings.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { CodeMapping } from '@volar/language-core'; -import { computed } from 'computeds'; -import { Segment, replaceSourceRange } from 'muggle-string'; -import type * as ts from 'typescript'; -import { allCodeFeatures } from '../plugins/shared'; -import type { Sfc, VueCodeInformation } from '../types'; - -export function computedMappings( - snapshot: () => ts.IScriptSnapshot, - sfc: Sfc -) { - return computed(() => { - const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, allCodeFeatures]]; - for (const block of [ - sfc.script, - sfc.scriptSetup, - sfc.template, - ...sfc.styles, - ...sfc.customBlocks, - ]) { - if (block) { - replaceSourceRange(str, undefined, block.startTagEnd, block.endTagStart, '\n\n'); - } - } - const mappings = str - .filter(s => typeof s !== 'string') - .map(m => { - const text = m[0]; - const start = m[2] as number; - return { - sourceOffsets: [start], - generatedOffsets: [start], - lengths: [text.length], - data: m[3] as VueCodeInformation, - }; - }); - - // fix folding range end position failed to mapping - for (const block of [ - sfc.script, - sfc.scriptSetup, - sfc.template, - ...sfc.styles, - ...sfc.customBlocks, - ]) { - const offsets: number[] = []; - if (block) { - let content = block.content; - if (content.endsWith('\r\n')) { - content = content.slice(0, -2); - } - else if (content.endsWith('\n')) { - content = content.slice(0, -1); - } - const offset = content.lastIndexOf('\n') + 1; - offsets.push(block.startTagEnd + offset); - } - if (offsets.length) { - mappings.push({ - sourceOffsets: offsets, - generatedOffsets: offsets, - lengths: offsets.map(() => 0), - data: { structure: true }, - }); - } - } - - return mappings; - }); -} diff --git a/packages/language-core/lib/virtualFile/computedSfc.ts b/packages/language-core/lib/virtualFile/computedSfc.ts index 5b0fc75cbc..9a138f9158 100644 --- a/packages/language-core/lib/virtualFile/computedSfc.ts +++ b/packages/language-core/lib/virtualFile/computedSfc.ts @@ -10,16 +10,20 @@ export function computedSfc( ts: typeof import('typescript'), plugins: VueLanguagePluginReturn[], fileName: string, - snapshot: () => ts.IScriptSnapshot, + getSnapshot: () => ts.IScriptSnapshot, parsed: () => SFCParseResult | undefined ): Sfc { const untrackedSnapshot = () => { pauseTracking(); - const res = snapshot(); + const res = getSnapshot(); resetTracking(); return res; }; + const content = computed(() => { + const snapshot = getSnapshot(); + return snapshot.getText(0, snapshot.getLength()); + }); const template = computedNullableSfcBlock( 'template', 'html', @@ -137,6 +141,7 @@ export function computedSfc( ); return { + get content() { return content(); }, get template() { return template(); }, get script() { return script(); }, get scriptSetup() { return scriptSetup(); }, diff --git a/packages/language-core/lib/virtualFile/vueFile.ts b/packages/language-core/lib/virtualFile/vueFile.ts index d3115fac68..16b98bac87 100644 --- a/packages/language-core/lib/virtualFile/vueFile.ts +++ b/packages/language-core/lib/virtualFile/vueFile.ts @@ -1,11 +1,11 @@ import type { VirtualCode } from '@volar/language-core'; -import { Signal, signal } from 'computeds'; +import { computed, Signal, signal } from 'computeds'; import type * as ts from 'typescript'; import type { VueCompilerOptions, VueLanguagePluginReturn } from '../types'; -import { computedFiles } from './computedFiles'; -import { computedMappings } from './computedMappings'; +import { computedEmbeddedCodes } from './computedEmbeddedCodes'; import { computedSfc } from './computedSfc'; import { computedVueSfc } from './computedVueSfc'; +import { allCodeFeatures } from '../plugins'; export class VueVirtualCode implements VirtualCode { @@ -13,14 +13,22 @@ export class VueVirtualCode implements VirtualCode { id = 'main'; - _snapshot: Signal; + getSnapshot: Signal; // computeds - getVueSfc = computedVueSfc(this.plugins, this.fileName, this.languageId, () => this._snapshot()); - sfc = computedSfc(this.ts, this.plugins, this.fileName, () => this._snapshot(), this.getVueSfc); - getMappings = computedMappings(() => this._snapshot(), this.sfc); - getEmbeddedCodes = computedFiles(this.plugins, this.fileName, this.sfc); + getVueSfc = computedVueSfc(this.plugins, this.fileName, this.languageId, () => this.getSnapshot()); + sfc = computedSfc(this.ts, this.plugins, this.fileName, () => this.getSnapshot(), this.getVueSfc); + getMappings = computed(() => { + const snapshot = this.getSnapshot(); + return [{ + sourceOffsets: [0], + generatedOffsets: [0], + lengths: [snapshot.getLength()], + data: allCodeFeatures, + }]; + }); + getEmbeddedCodes = computedEmbeddedCodes(this.plugins, this.fileName, this.sfc); // others @@ -28,7 +36,7 @@ export class VueVirtualCode implements VirtualCode { return this.getEmbeddedCodes(); } get snapshot() { - return this._snapshot(); + return this.getSnapshot(); } get mappings() { return this.getMappings(); @@ -42,10 +50,10 @@ export class VueVirtualCode implements VirtualCode { public plugins: VueLanguagePluginReturn[], public ts: typeof import('typescript'), ) { - this._snapshot = signal(initSnapshot); + this.getSnapshot = signal(initSnapshot); } update(newSnapshot: ts.IScriptSnapshot) { - this._snapshot.set(newSnapshot); + this.getSnapshot.set(newSnapshot); } } diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index e837df7c6e..8d3955cc83 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -201,7 +201,7 @@ function getCommonLanguageServicePlugins( createVueExtractFilePlugin(ts, getTsPluginClient), createEmmetPlugin({ mappedLanguages: { - 'vue': 'html', + 'vue-root-tags': 'html', 'postcss': 'scss', }, }), diff --git a/packages/language-service/lib/plugins/vue-sfc.ts b/packages/language-service/lib/plugins/vue-sfc.ts index e02ca62ad7..696a4797b1 100644 --- a/packages/language-service/lib/plugins/vue-sfc.ts +++ b/packages/language-service/lib/plugins/vue-sfc.ts @@ -9,13 +9,9 @@ import { URI } from 'vscode-uri'; let sfcDataProvider: html.IHTMLDataProvider | undefined; -export interface Provide { - 'vue/vueFile': (document: TextDocument) => vue.VueVirtualCode | undefined; -} - export function create(): LanguageServicePlugin { const htmlPlugin = createHtmlService({ - documentSelector: ['vue'], + documentSelector: ['vue-root-tags'], useDefaultDataProvider: false, getCustomData(context) { sfcDataProvider ??= html.newHTMLDataProvider('vue', loadLanguageBlocks(context.env.locale ?? 'en')); @@ -47,21 +43,13 @@ export function create(): LanguageServicePlugin { return { ...htmlPlugin, name: 'vue-sfc', - create(context): LanguageServicePluginInstance { + create(context): LanguageServicePluginInstance { const htmlPluginInstance = htmlPlugin.create(context); return { ...htmlPluginInstance, - provide: { - 'vue/vueFile': document => { - return worker(document, context, vueFile => { - return vueFile; - }); - }, - }, - provideDocumentLinks: undefined, async resolveEmbeddedCodeFormattingOptions(sourceScript, virtualCode, options) { @@ -234,11 +222,13 @@ export function create(): LanguageServicePlugin { }; function worker(document: TextDocument, context: LanguageServiceContext, callback: (vueSourceFile: vue.VueVirtualCode) => T) { + if (document.languageId !== 'vue-root-tags') { + return; + } const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (virtualCode instanceof vue.VueVirtualCode) { - return callback(virtualCode); + if (sourceScript?.generated?.root instanceof vue.VueVirtualCode) { + return callback(sourceScript.generated.root); } } } From 023de046b1a47a426b6082b060b37534ad0d9241 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 04:37:08 +0000 Subject: [PATCH 02/11] ci(lint): auto-fix --- packages/language-core/lib/plugins/vue-root-tags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-core/lib/plugins/vue-root-tags.ts b/packages/language-core/lib/plugins/vue-root-tags.ts index 73482c0b26..2e9e5c56f0 100644 --- a/packages/language-core/lib/plugins/vue-root-tags.ts +++ b/packages/language-core/lib/plugins/vue-root-tags.ts @@ -55,7 +55,7 @@ const plugin: VueLanguagePlugin = () => { sfc.content.substring( block.startTagEnd + offset, block.endTagStart - ), + ) ); } } From fae422020d65a5eaa4b37978887b196725112a85 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 9 Aug 2024 13:16:20 +0800 Subject: [PATCH 03/11] refactor(language-core): removed `__hint` trick --- .../lib/codegen/script/context.ts | 5 +- .../language-core/lib/codegen/script/index.ts | 57 ++++++++----------- .../lib/codegen/template/context.ts | 3 + .../lib/codegen/template/elementEvents.ts | 27 +++++---- .../lib/codegen/template/elementProps.ts | 31 +++++----- .../lib/codegen/template/interpolation.ts | 4 +- packages/language-core/lib/codegen/types.ts | 9 +++ packages/language-core/lib/plugins/vue-tsx.ts | 14 ++++- packages/language-core/lib/types.ts | 7 --- .../vue-visualize-hidden-callback-param.ts | 28 +++++---- 10 files changed, 97 insertions(+), 88 deletions(-) create mode 100644 packages/language-core/lib/codegen/types.ts diff --git a/packages/language-core/lib/codegen/script/context.ts b/packages/language-core/lib/codegen/script/context.ts index a78d05a82d..968ba61e33 100644 --- a/packages/language-core/lib/codegen/script/context.ts +++ b/packages/language-core/lib/codegen/script/context.ts @@ -1,8 +1,9 @@ import { getSlotsPropertyName } from '../../utils/shared'; import { newLine } from '../common'; +import { InlayHintInfo } from '../types'; import type { ScriptCodegenOptions } from './index'; -interface HelperType { +export interface HelperType { name: string; used?: boolean; generated?: boolean; @@ -102,6 +103,7 @@ export function createScriptCodegenContext(options: ScriptCodegenOptions) { }, } satisfies HelperType as HelperType, }; + const inlayHints: InlayHintInfo[] = []; return { generatedTemplate: false, @@ -113,6 +115,7 @@ export function createScriptCodegenContext(options: ScriptCodegenOptions) { ...options.scriptSetupRanges?.bindings.map(range => options.sfc.scriptSetup!.content.substring(range.start, range.end)) ?? [], ]), helperTypes, + inlayHints, generateHelperTypes, }; diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index c12ce1d839..c0da1f863a 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -5,7 +5,7 @@ import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges'; import type { Code, Sfc, VueCodeInformation, VueCompilerOptions } from '../../types'; import { endOfLine, generateSfcBlockSection, newLine } from '../common'; import type { TemplateCodegenContext } from '../template/context'; -import { createScriptCodegenContext } from './context'; +import { createScriptCodegenContext, ScriptCodegenContext } from './context'; import { generateGlobalTypes } from './globalTypes'; import { generateScriptSetup, generateScriptSetupImports } from './scriptSetup'; import { generateSrc } from './src'; @@ -49,7 +49,7 @@ export interface ScriptCodegenOptions { linkedCodeMappings: Mapping[]; } -export function* generateScript(options: ScriptCodegenOptions): Generator { +export function* generateScript(options: ScriptCodegenOptions): Generator { const ctx = createScriptCodegenContext(options); yield `/* __placeholder__ */${newLine}`; @@ -74,40 +74,29 @@ export function* generateScript(options: ScriptCodegenOptions): Generator } } else if (exportDefault && isExportRawObject && options.vueCompilerOptions.optionsWrapper.length) { + ctx.inlayHints.push({ + blockName: options.sfc.script.name, + offset: exportDefault.expression.start, + setting: 'vue.inlayHints.optionsWrapper', + label: options.vueCompilerOptions.optionsWrapper.length + ? options.vueCompilerOptions.optionsWrapper[0] + : '[Missing optionsWrapper[0]]', + tooltip: [ + 'This is virtual code that is automatically wrapped for type support, it does not affect your runtime behavior, you can customize it via `vueCompilerOptions.optionsWrapper` option in tsconfig / jsconfig.', + 'To hide it, you can set `"vue.inlayHints.optionsWrapper": false` in IDE settings.', + ].join('\n\n'), + }, { + blockName: options.sfc.script.name, + offset: exportDefault.expression.end, + setting: 'vue.inlayHints.optionsWrapper', + label: options.vueCompilerOptions.optionsWrapper.length >= 2 + ? options.vueCompilerOptions.optionsWrapper[1] + : '[Missing optionsWrapper[1]]', + tooltip: '', + }); yield generateSfcBlockSection(options.sfc.script, 0, exportDefault.expression.start, codeFeatures.all); yield options.vueCompilerOptions.optionsWrapper[0]; - yield [ - '', - 'script', - exportDefault.expression.start, - { - __hint: { - setting: 'vue.inlayHints.optionsWrapper', - label: options.vueCompilerOptions.optionsWrapper.length - ? options.vueCompilerOptions.optionsWrapper[0] - : '[Missing optionsWrapper]', - tooltip: [ - 'This is virtual code that is automatically wrapped for type support, it does not affect your runtime behavior, you can customize it via `vueCompilerOptions.optionsWrapper` option in tsconfig / jsconfig.', - 'To hide it, you can set `"vue.inlayHints.optionsWrapper": false` in IDE settings.', - ].join('\n\n'), - } - }, - ]; yield generateSfcBlockSection(options.sfc.script, exportDefault.expression.start, exportDefault.expression.end, codeFeatures.all); - yield [ - '', - 'script', - exportDefault.expression.end, - { - __hint: { - setting: 'vue.inlayHints.optionsWrapper', - label: options.vueCompilerOptions.optionsWrapper.length === 2 - ? options.vueCompilerOptions.optionsWrapper[1] - : '[Missing optionsWrapper]', - tooltip: '', - } - }, - ]; yield options.vueCompilerOptions.optionsWrapper[1]; yield generateSfcBlockSection(options.sfc.script, exportDefault.expression.end, options.sfc.script.content.length, codeFeatures.all); } @@ -156,6 +145,8 @@ export function* generateScript(options: ScriptCodegenOptions): Generator codeFeatures.verification, ]; } + + return ctx; } function* generateDefineProp( diff --git a/packages/language-core/lib/codegen/template/context.ts b/packages/language-core/lib/codegen/template/context.ts index 54c7d3d259..54b3c5ef03 100644 --- a/packages/language-core/lib/codegen/template/context.ts +++ b/packages/language-core/lib/codegen/template/context.ts @@ -2,6 +2,7 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { Code, VueCodeInformation } from '../../types'; import { endOfLine, newLine, wrapWith } from '../common'; import type { TemplateCodegenOptions } from './index'; +import { InlayHintInfo } from '../types'; const _codeFeatures = { all: { @@ -110,6 +111,7 @@ export function createTemplateCodegenContext(scriptSetupBindingNames: TemplateCo const usedComponentCtxVars = new Set(); const scopedClasses: { className: string, offset: number; }[] = []; const emptyClassOffsets: number[] = []; + const inlayHints: InlayHintInfo[] = []; return { slots, @@ -121,6 +123,7 @@ export function createTemplateCodegenContext(scriptSetupBindingNames: TemplateCo usedComponentCtxVars, scopedClasses, emptyClassOffsets, + inlayHints, hasSlot: false, accessExternalVariable(name: string, offset?: number) { let arr = accessExternalVariables.get(name); diff --git a/packages/language-core/lib/codegen/template/elementEvents.ts b/packages/language-core/lib/codegen/template/elementEvents.ts index a4fa92d959..372b7add15 100644 --- a/packages/language-core/lib/codegen/template/elementEvents.ts +++ b/packages/language-core/lib/codegen/template/elementEvents.ts @@ -151,22 +151,21 @@ export function* generateEventExpression( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - () => { + offset => { if (_isCompoundExpression && isFirstMapping) { isFirstMapping = false; - return { - ...ctx.codeFeatures.all, - __hint: { - setting: 'vue.inlayHints.inlineHandlerLeading', - label: '$event =>', - tooltip: [ - '`$event` is a hidden parameter, you can use it in this callback.', - 'To hide this hint, set `vue.inlayHints.inlineHandlerLeading` to `false` in IDE settings.', - '[More info](https://github.com/vuejs/language-tools/issues/2445#issuecomment-1444771420)', - ].join('\n\n'), - paddingRight: true, - }, - }; + ctx.inlayHints.push({ + blockName: 'template', + offset, + setting: 'vue.inlayHints.inlineHandlerLeading', + label: '$event =>', + paddingRight: true, + tooltip: [ + '`$event` is a hidden parameter, you can use it in this callback.', + 'To hide this hint, set `vue.inlayHints.inlineHandlerLeading` to `false` in IDE settings.', + '[More info](https://github.com/vuejs/language-tools/issues/2445#issuecomment-1444771420)', + ].join('\n\n'), + }); } return ctx.codeFeatures.all; }, diff --git a/packages/language-core/lib/codegen/template/elementProps.ts b/packages/language-core/lib/codegen/template/elementProps.ts index 7bbb49dbcc..150b5ef5b6 100644 --- a/packages/language-core/lib/codegen/template/elementProps.ts +++ b/packages/language-core/lib/codegen/template/elementProps.ts @@ -269,7 +269,7 @@ function* genereatePropExp( exp: CompilerDOM.SimpleExpressionNode | undefined, features: VueCodeInformation, isShorthand: boolean, - inlayHints: boolean + enableCodeFeatures: boolean ): Generator { if (exp && exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { // style='z-index: 2' will compile to {'z-index':'2'} if (!isShorthand) { // vue 3.4+ @@ -296,23 +296,18 @@ function* genereatePropExp( exp.loc.start.offset, features ); - if (inlayHints) { - yield [ - '', - 'template', - exp.loc.end.offset, - { - __hint: { - setting: 'vue.inlayHints.vBindShorthand', - label: `="${propVariableName}"`, - tooltip: [ - `This is a shorthand for \`${exp.loc.source}="${propVariableName}"\`.`, - 'To hide this hint, set `vue.inlayHints.vBindShorthand` to `false` in IDE settings.', - '[More info](https://github.com/vuejs/core/pull/9451)', - ].join('\n\n'), - }, - } as VueCodeInformation, - ]; + if (enableCodeFeatures) { + ctx.inlayHints.push({ + blockName: 'template', + offset: exp.loc.end.offset, + setting: 'vue.inlayHints.vBindShorthand', + label: `="${propVariableName}"`, + tooltip: [ + `This is a shorthand for \`${exp.loc.source}="${propVariableName}"\`.`, + 'To hide this hint, set `vue.inlayHints.vBindShorthand` to `false` in IDE settings.', + '[More info](https://github.com/vuejs/core/pull/9451)', + ].join('\n\n'), + }); } } } diff --git a/packages/language-core/lib/codegen/template/interpolation.ts b/packages/language-core/lib/codegen/template/interpolation.ts index 3e2760f576..dd01218024 100644 --- a/packages/language-core/lib/codegen/template/interpolation.ts +++ b/packages/language-core/lib/codegen/template/interpolation.ts @@ -12,7 +12,7 @@ export function* generateInterpolation( _code: string, astHolder: any, start: number | undefined, - data: VueCodeInformation | (() => VueCodeInformation) | undefined, + data: VueCodeInformation | ((offset: number) => VueCodeInformation) | undefined, prefix: string, suffix: string ): Generator { @@ -53,7 +53,7 @@ export function* generateInterpolation( start + offset, onlyError ? ctx.codeFeatures.verification - : typeof data === 'function' ? data() : data, + : typeof data === 'function' ? data(start + offset) : data, ]; } else { diff --git a/packages/language-core/lib/codegen/types.ts b/packages/language-core/lib/codegen/types.ts new file mode 100644 index 0000000000..8ffd6f3466 --- /dev/null +++ b/packages/language-core/lib/codegen/types.ts @@ -0,0 +1,9 @@ +export interface InlayHintInfo { + blockName: string; + offset: number; + setting: string; + label: string; + tooltip: string; + paddingRight?: boolean; + paddingLeft?: boolean; +} diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index 21ff38353a..fcbb3a4208 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -140,7 +140,7 @@ function createTsx( const linkedCodeMappings: Mapping[] = []; const _template = generatedTemplate(); let generatedLength = 0; - for (const code of generateScript({ + const codegen = generateScript({ ts, fileBaseName: path.basename(fileName), globalTypes: ctx.globalTypesHolder === fileName, @@ -153,13 +153,21 @@ function createTsx( vueCompilerOptions: ctx.vueCompilerOptions, getGeneratedLength: () => generatedLength, linkedCodeMappings, - })) { + }); + + let current = codegen.next(); + + while (!current.done) { + const code = current.value; codes.push(code); generatedLength += typeof code === 'string' ? code.length : code[0].length; - }; + current = codegen.next(); + } + return { + ...current.value, codes, linkedCodeMappings, }; diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 4a23a255a4..ac0e9faca8 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -15,13 +15,6 @@ export type RawVueCompilerOptions = Partial block?.name === hint.blockName); + const hintOffset = (block?.startTagEnd ?? 0) + hint.offset; - if ( - mapping.generatedOffsets[0] >= start - && mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + mapping.lengths[mapping.lengths.length - 1] <= end - && hint - ) { + if (hintOffset >= start && hintOffset <= end) { settings[hint.setting] ??= await context.env.getConfiguration?.(hint.setting) ?? false; @@ -44,7 +52,7 @@ export function create(): LanguageServicePlugin { label: hint.label, paddingRight: hint.paddingRight, paddingLeft: hint.paddingLeft, - position: document.positionAt(mapping.generatedOffsets[0]), + position: document.positionAt(hintOffset), kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, tooltip: { kind: 'markdown', From fd4749370127fed8fd1a5d82baf729250e4eac3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:40:04 +0800 Subject: [PATCH 04/11] feat(language-core): inlay hints for destructured props (#4634) --- extensions/vscode/package.json | 5 + packages/language-core/lib/codegen/common.ts | 28 +- .../language-core/lib/codegen/script/index.ts | 1 - packages/language-core/lib/codegen/types.ts | 2 +- .../lib/parsers/scriptSetupRanges.ts | 13 +- .../lib/utils/findDestructuredProps.ts | 0 packages/language-service/index.ts | 4 +- .../lib/plugins/vue-inlayhints.ts | 310 ++++++++++++++++++ .../vue-visualize-hidden-callback-param.ts | 70 ---- .../tests/utils/createTester.ts | 1 + .../inlay-hint/destructured-props/entry.vue | 52 +++ 11 files changed, 404 insertions(+), 82 deletions(-) create mode 100644 packages/language-core/lib/utils/findDestructuredProps.ts create mode 100644 packages/language-service/lib/plugins/vue-inlayhints.ts delete mode 100644 packages/language-service/lib/plugins/vue-visualize-hidden-callback-param.ts create mode 100644 test-workspace/language-service/inlay-hint/destructured-props/entry.vue diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 908bc07e50..60315bcb01 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -383,6 +383,11 @@ "default": true, "description": "Auto add space between double curly brackets: {{|}} -> {{ | }}" }, + "vue.inlayHints.destructuredProps": { + "type": "boolean", + "default": false, + "description": "Show inlay hints for destructured prop." + }, "vue.inlayHints.missingProps": { "type": "boolean", "default": false, diff --git a/packages/language-core/lib/codegen/common.ts b/packages/language-core/lib/codegen/common.ts index 8bb32d3434..5c8814310e 100644 --- a/packages/language-core/lib/codegen/common.ts +++ b/packages/language-core/lib/codegen/common.ts @@ -45,27 +45,45 @@ export function collectVars( ts: typeof import('typescript'), node: ts.Node, ast: ts.SourceFile, - result: string[] + results: string[] = [], + includesRest = true +) { + const identifiers = collectIdentifiers(ts, node, [], includesRest); + for (const id of identifiers) { + results.push(getNodeText(ts, id, ast)); + } + return results; +} + +export function collectIdentifiers( + ts: typeof import('typescript'), + node: ts.Node, + results: ts.Identifier[] = [], + includesRest = true ) { if (ts.isIdentifier(node)) { - result.push(getNodeText(ts, node, ast)); + results.push(node); } else if (ts.isObjectBindingPattern(node)) { for (const el of node.elements) { - collectVars(ts, el.name, ast, result); + if (includesRest || !el.dotDotDotToken) { + collectIdentifiers(ts, el.name, results, includesRest); + } } } else if (ts.isArrayBindingPattern(node)) { for (const el of node.elements) { if (ts.isBindingElement(el)) { - collectVars(ts, el.name, ast, result); + collectIdentifiers(ts, el.name, results, includesRest); } } } else { - ts.forEachChild(node, node => collectVars(ts, node, ast, result)); + ts.forEachChild(node, node => collectIdentifiers(ts, node, results, includesRest)); } + return results; } + export function createTsAst(ts: typeof import('typescript'), astHolder: any, text: string) { if (astHolder.__volar_ast_text !== text) { astHolder.__volar_ast_text = text; diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index c0da1f863a..ec48ad0a81 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -92,7 +92,6 @@ export function* generateScript(options: ScriptCodegenOptions): Generator= 2 ? options.vueCompilerOptions.optionsWrapper[1] : '[Missing optionsWrapper[1]]', - tooltip: '', }); yield generateSfcBlockSection(options.sfc.script, 0, exportDefault.expression.start, codeFeatures.all); yield options.vueCompilerOptions.optionsWrapper[0]; diff --git a/packages/language-core/lib/codegen/types.ts b/packages/language-core/lib/codegen/types.ts index 8ffd6f3466..964bb46076 100644 --- a/packages/language-core/lib/codegen/types.ts +++ b/packages/language-core/lib/codegen/types.ts @@ -3,7 +3,7 @@ export interface InlayHintInfo { offset: number; setting: string; label: string; - tooltip: string; + tooltip?: string; paddingRight?: boolean; paddingLeft?: boolean; } diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index a1caccbe1e..4281e3d8e0 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -1,5 +1,6 @@ import type * as ts from 'typescript'; import type { VueCompilerOptions, TextRange } from '../types'; +import { collectVars } from '../codegen/common'; export interface ScriptSetupRanges extends ReturnType { } @@ -14,6 +15,7 @@ export function parseScriptSetupRanges( const props: { name?: string; + destructured?: string[]; define?: ReturnType & { statement: TextRange; }; @@ -231,6 +233,14 @@ export function parseScriptSetupRanges( expose.define = parseDefineFunction(node); } else if (vueCompilerOptions.macros.defineProps.includes(callText)) { + if (ts.isVariableDeclaration(parent)) { + if (ts.isObjectBindingPattern(parent.name)) { + props.destructured = collectVars(ts, parent.name, ast, [], false); + } + else { + props.name = getNodeText(ts, parent.name, ast); + } + } let statementRange: TextRange | undefined; for (let i = parents.length - 1; i >= 0; i--) { @@ -253,9 +263,6 @@ export function parseScriptSetupRanges( statement: statementRange, }; - if (ts.isVariableDeclaration(parent)) { - props.name = getNodeText(ts, parent.name, ast); - } if (node.arguments.length) { props.define.arg = _getStartEnd(node.arguments[0]); } diff --git a/packages/language-core/lib/utils/findDestructuredProps.ts b/packages/language-core/lib/utils/findDestructuredProps.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 8d3955cc83..42fef932bc 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -25,7 +25,7 @@ import { create as createVueExtractFilePlugin } from './lib/plugins/vue-extract- import { create as createVueSfcPlugin } from './lib/plugins/vue-sfc'; import { create as createVueTemplatePlugin } from './lib/plugins/vue-template'; import { create as createVueTwoslashQueriesPlugin } from './lib/plugins/vue-twoslash-queries'; -import { create as createVueVisualizeHiddenCallbackParamPlugin } from './lib/plugins/vue-visualize-hidden-callback-param'; +import { create as createVueInlayHintsPlugin } from './lib/plugins/vue-inlayhints'; import { parse, VueCompilerOptions } from '@vue/language-core'; import { proxyLanguageServiceForVue } from '@vue/typescript-plugin/lib/common'; @@ -196,7 +196,7 @@ function getCommonLanguageServicePlugins( createVueDocumentDropPlugin(ts, getTsPluginClient), createVueAutoDotValuePlugin(ts, getTsPluginClient), createVueAutoAddSpacePlugin(), - createVueVisualizeHiddenCallbackParamPlugin(), + createVueInlayHintsPlugin(ts), createVueDirectiveCommentsPlugin(), createVueExtractFilePlugin(ts, getTsPluginClient), createEmmetPlugin({ diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts new file mode 100644 index 0000000000..c740bcdff9 --- /dev/null +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -0,0 +1,310 @@ +import type { LanguageServicePluginInstance } from '@volar/language-service'; +import { tsCodegen, VueVirtualCode } from '@vue/language-core'; +import type * as vscode from 'vscode-languageserver-protocol'; +import { URI } from 'vscode-uri'; +import type { LanguageServicePlugin } from '../types'; +import type * as ts from 'typescript'; +import { collectIdentifiers } from '@vue/language-core/lib/codegen/common'; + +export function create(ts: typeof import('typescript')): LanguageServicePlugin { + return { + name: 'vue-inlay-hints', + capabilities: { + inlayHintProvider: {}, + }, + create(context): LanguageServicePluginInstance { + return { + async provideInlayHints(document, range) { + + const settings: Record = {}; + const result: vscode.InlayHint[] = []; + const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + + if (virtualCode instanceof VueVirtualCode) { + + const codegen = tsCodegen.get(virtualCode.sfc); + const inlayHints = [ + ...codegen?.generatedTemplate()?.inlayHints ?? [], + ...codegen?.generatedScript()?.inlayHints ?? [], + ]; + const scriptSetupRanges = codegen?.scriptSetupRanges(); + + if (scriptSetupRanges?.props.destructured && virtualCode.sfc.scriptSetup?.ast) { + for (const [prop, isShorthand] of findDestructuredProps(ts, virtualCode.sfc.scriptSetup.ast, scriptSetupRanges.props.destructured)) { + const name = prop.text; + const end = prop.getEnd(); + const pos = isShorthand ? end : end - name.length; + const label = isShorthand ? `: props.${name}` : 'props.'; + inlayHints.push({ + blockName: 'scriptSetup', + offset: pos, + setting: 'vue.inlayHints.destructuredProps', + label, + }); + } + } + + const blocks = [ + virtualCode.sfc.template, + virtualCode.sfc.script, + virtualCode.sfc.scriptSetup, + ]; + const start = document.offsetAt(range.start); + const end = document.offsetAt(range.end); + + for (const hint of inlayHints) { + + const block = blocks.find(block => block?.name === hint.blockName); + const hintOffset = (block?.startTagEnd ?? 0) + hint.offset; + + if (hintOffset >= start && hintOffset <= end) { + + settings[hint.setting] ??= await context.env.getConfiguration?.(hint.setting) ?? false; + + if (!settings[hint.setting]) { + continue; + } + + result.push({ + label: hint.label, + paddingRight: hint.paddingRight, + paddingLeft: hint.paddingLeft, + position: document.positionAt(hintOffset), + kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, + tooltip: hint.tooltip ? { + kind: 'markdown', + value: hint.tooltip, + } : undefined, + }); + } + } + } + return result; + }, + }; + }, + }; +} + +/** + * true -> prop binding + * false -> local binding + */ +type Scope = Record; + +/** + * Refactored from https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/script/definePropsDestructure.ts + */ +export function findDestructuredProps( + ts: typeof import('typescript'), + ast: ts.SourceFile, + props: string[] +) { + const rootScope: Scope = {}; + const scopeStack: Scope[] = [rootScope]; + let currentScope: Scope = rootScope; + const excludedIds = new WeakSet(); + const parentStack: ts.Node[] = []; + + for (const prop of props) { + rootScope[prop] = true; + } + + function pushScope() { + scopeStack.push((currentScope = Object.create(currentScope))); + } + + function popScope() { + scopeStack.pop(); + currentScope = scopeStack[scopeStack.length - 1] || null; + } + + function registerLocalBinding(id: ts.Identifier) { + excludedIds.add(id); + if (currentScope) { + currentScope[id.text] = false; + } + } + + const references: [ts.Identifier, boolean][] = []; + + walkScope(ast, true); + walk(ast); + + return references; + + function walkScope(node: ts.Node, isRoot = false) { + ts.forEachChild(node, stmt => { + if (ts.isVariableStatement(stmt)) { + for (const decl of stmt.declarationList.declarations) { + walkVariableDeclaration(decl, isRoot); + } + } + else if ( + ts.isFunctionDeclaration(stmt) || + ts.isClassDeclaration(stmt) + ) { + const declare = ts.getModifiers(stmt)?.find(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword); + if (!stmt.name || declare) { + return; + } + registerLocalBinding(stmt.name); + } + else if ( + (ts.isForOfStatement(stmt) || ts.isForInStatement(stmt)) && + ts.isVariableDeclarationList(stmt.initializer) + ) { + walkVariableDeclaration(stmt.initializer.declarations[0], isRoot); + } + else if ( + ts.isLabeledStatement(stmt) && + ts.isVariableDeclaration(stmt.statement) + ) { + walkVariableDeclaration(stmt.statement, isRoot); + } + }); + } + + function walkVariableDeclaration(decl: ts.VariableDeclaration, isRoot = false) { + const { initializer, name } = decl; + const isDefineProps = + isRoot + && initializer + && ts.isCallExpression(initializer) + && initializer.expression.getText(ast) === 'defineProps'; + + for (const id of collectIdentifiers(ts, name)) { + if (isDefineProps) { + excludedIds.add(id); + } else { + registerLocalBinding(id); + } + } + } + + function walkFunctionDeclaration(node: ts.SignatureDeclaration) { + const { name, parameters } = node; + if (name && ts.isIdentifier(name)) { + registerLocalBinding(name); + } + + for (const p of parameters) { + for (const id of collectIdentifiers(ts, p)) { + registerLocalBinding(id); + } + } + } + + function walk(parent: ts.Node) { + ts.forEachChild(parent, node => { + if (enter(node) ?? true) { + walk(node); + leave(node); + } + }); + + function enter(node: ts.Node) { + parent && parentStack.push(parent); + + if ( + ts.isTypeLiteralNode(node) || + ts.isTypeReferenceNode(node) + ) { + return false; + } + + if (ts.isFunctionLike(node)) { + pushScope(); + walkFunctionDeclaration(node); + if ('body' in node) { + walkScope(node.body!); + } + return; + } + + if (ts.isCatchClause(node)) { + pushScope(); + const { variableDeclaration: p } = node; + if (p && ts.isIdentifier(p.name)) { + registerLocalBinding(p.name); + } + walkScope(node.block); + return; + } + + if ( + ts.isBlock(node) + && !ts.isFunctionLike(parent) + && !ts.isCatchClause(parent) + ) { + pushScope(); + walkScope(node); + return; + } + + if ( + ts.isIdentifier(node) + && isReferencedIdentifier(node, parent) + && !excludedIds.has(node) + ) { + const name = node.text; + if (currentScope[name]) { + const isShorthand = ts.isShorthandPropertyAssignment(parent); + references.push([node, isShorthand]); + } + } + } + + function leave(node: ts.Node) { + parent && parentStack.pop(); + if ( + ts.isFunctionLike(node) + || ts.isCatchClause(node) + || ( + ts.isBlock(node) + && !ts.isFunctionLike(parent) + && !ts.isCatchClause(parent) + ) + ) { + popScope(); + } + } + } + + // TODO: more conditions + function isReferencedIdentifier( + id: ts.Identifier, + parent: ts.Node | null + ) { + if (!parent) { + return false; + } + + if (id.text === 'arguments') { + return false; + } + + if ( + ts.isExpressionWithTypeArguments(parent) || + ts.isInterfaceDeclaration(parent) || + ts.isTypeAliasDeclaration(parent) || + ts.isPropertySignature(parent) + ) { + return false; + } + + if ( + ts.isPropertyAccessExpression(parent) || + ts.isPropertyAssignment(parent) || + ts.isPropertyDeclaration(parent) + ) { + if (parent.name === id) { + return false; + } + } + + return true; + } +} diff --git a/packages/language-service/lib/plugins/vue-visualize-hidden-callback-param.ts b/packages/language-service/lib/plugins/vue-visualize-hidden-callback-param.ts deleted file mode 100644 index cfc648e5d7..0000000000 --- a/packages/language-service/lib/plugins/vue-visualize-hidden-callback-param.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { LanguageServicePluginInstance } from '@volar/language-service'; -import { tsCodegen, VueVirtualCode } from '@vue/language-core'; -import type * as vscode from 'vscode-languageserver-protocol'; -import { URI } from 'vscode-uri'; -import type { LanguageServicePlugin } from '../types'; - -export function create(): LanguageServicePlugin { - return { - name: 'vue-inlay-hints-hidden-callback-param', - capabilities: { - inlayHintProvider: {}, - }, - create(context): LanguageServicePluginInstance { - return { - async provideInlayHints(document, range) { - - const settings: Record = {}; - const result: vscode.InlayHint[] = []; - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const vitualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - - if (vitualCode instanceof VueVirtualCode) { - - const codegen = tsCodegen.get(vitualCode.sfc); - const inlayHints = [ - ...codegen?.generatedTemplate()?.inlayHints ?? [], - ...codegen?.generatedScript()?.inlayHints ?? [], - ]; - const blocks = [ - vitualCode.sfc.template, - vitualCode.sfc.script, - vitualCode.sfc.scriptSetup, - ]; - const start = document.offsetAt(range.start); - const end = document.offsetAt(range.end); - - for (const hint of inlayHints) { - - const block = blocks.find(block => block?.name === hint.blockName); - const hintOffset = (block?.startTagEnd ?? 0) + hint.offset; - - if (hintOffset >= start && hintOffset <= end) { - - settings[hint.setting] ??= await context.env.getConfiguration?.(hint.setting) ?? false; - - if (!settings[hint.setting]) { - continue; - } - - result.push({ - label: hint.label, - paddingRight: hint.paddingRight, - paddingLeft: hint.paddingLeft, - position: document.positionAt(hintOffset), - kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, - tooltip: { - kind: 'markdown', - value: hint.tooltip, - }, - }); - } - } - } - return result; - }, - }; - }, - }; -} diff --git a/packages/language-service/tests/utils/createTester.ts b/packages/language-service/tests/utils/createTester.ts index 10f6c7d77d..df4c6b9102 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -39,6 +39,7 @@ function createTester(rootUri: URI) { const defaultVSCodeSettings: any = { 'typescript.preferences.quoteStyle': 'single', 'javascript.preferences.quoteStyle': 'single', + 'vue.inlayHints.destructuredProps': true, 'vue.inlayHints.missingProps': true, 'vue.inlayHints.optionsWrapper': true, 'vue.inlayHints.inlineHandlerLeading': true, diff --git a/test-workspace/language-service/inlay-hint/destructured-props/entry.vue b/test-workspace/language-service/inlay-hint/destructured-props/entry.vue new file mode 100644 index 0000000000..abc2c07f58 --- /dev/null +++ b/test-workspace/language-service/inlay-hint/destructured-props/entry.vue @@ -0,0 +1,52 @@ + \ No newline at end of file From a24205080eaa335e60ab5daefd1b969c0fd365df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:51:34 +0800 Subject: [PATCH 05/11] feat(language-service): better completion for directives (#4640) --- packages/language-service/data/template/cs.json | 3 +++ packages/language-service/data/template/en.json | 3 +++ packages/language-service/data/template/fr.json | 3 +++ packages/language-service/data/template/it.json | 3 +++ packages/language-service/data/template/ja.json | 3 +++ packages/language-service/data/template/ko.json | 3 +++ packages/language-service/data/template/pt.json | 3 +++ packages/language-service/data/template/ru.json | 3 +++ packages/language-service/data/template/zh-cn.json | 3 +++ packages/language-service/data/template/zh-hk.json | 3 +++ .../language-service/scripts/update-html-data.js | 7 ++++++- packages/language-service/tests/complete.ts | 12 ++++++------ .../complete/directives/input/entry.vue | 12 ++++++++++++ .../complete/directives/output/entry.vue | 12 ++++++++++++ 14 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 test-workspace/language-service/complete/directives/input/entry.vue create mode 100644 test-workspace/language-service/complete/directives/output/entry.vue diff --git a/packages/language-service/data/template/cs.json b/packages/language-service/data/template/cs.json index c18b678fb3..a160b597ba 100644 --- a/packages/language-service/data/template/cs.json +++ b/packages/language-service/data/template/cs.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nPřeskočit kompilaci tohoto elementu a všech jeho potomků.\n\n- **Nepředpokládá výraz** \n\n- **Podrobnosti**\n\n Uvnitř elementu s `v-pre` budou všechny syntaxe Vue šablony zachovány a vykresleny tak, jak jsou. Nejběžnějším použitím je zobrazení nezpracovaných „mustache“ tagů.\n\n- **Příklad**\n\n ```html\n {{ toto nebude zkompilováno }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nVykreslit element nebo komponentu pouze jednou a přeskočit budoucí aktualizace.\n\n- **Nepředpokládá výraz** \n\n- **Podrobnosti**\n\n Při dalších překreslováních budou element/komponenta a všichni potomci považováni za statický obsah a přeskočeni. To lze použít k optimalizaci výkonu aktualizace.\n\n ```html\n \n Toto se nikdy nezmění: {{msg}}\n \n
\n

Komentář

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n Od verze 3.2 si můžete také část šablony „zapamatovat“ (memoize) s podmínkami neplatnosti pomocí [`v-memo`](#v-memo).\n\n- **Viz také:**\n - [Syntaxe šablon - Interpolace textu](https://cs.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nPoužívá se k skrytí nezkompilované šablony, dokud není připravena.\n\n- **Nepředpokládá výraz**\n\n- **Podrobnosti**\n\n **Tato direktiva je potřeba pouze při použití bez build fáze.**\n\n Při použití in-DOM šablon může dojít k „blikání (flashing) nezkompilovaných šablon“: uživatel může vidět nezpracované „mustache“ značky, dokud je připojená (mounted) komponenta nenahradí vykresleným obsahem.\n\n `v-cloak` zůstane na elementu, dokud není připojena příslušná instance komponenty. Spolu s CSS pravidly jako `[v-cloak] { display: none }` lze použít k skrytí nezpracovaných šablon, dokud není komponenta připravena.\n\n- **Příklad**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n `
` nebude viditelný, dokud nebude dokončena kompilace.\n" diff --git a/packages/language-service/data/template/en.json b/packages/language-service/data/template/en.json index 28fb9e240c..903477224c 100644 --- a/packages/language-service/data/template/en.json +++ b/packages/language-service/data/template/en.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nSkip compilation for this element and all its children.\n\n- **Does not expect expression**\n\n- **Details**\n\n Inside the element with `v-pre`, all Vue template syntax will be preserved and rendered as-is. The most common use case of this is displaying raw mustache tags.\n\n- **Example**\n\n ```html\n {{ this will not be compiled }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nRender the element and component once only, and skip future updates.\n\n- **Does not expect expression**\n\n- **Details**\n\n On subsequent re-renders, the element/component and all its children will be treated as static content and skipped. This can be used to optimize update performance.\n\n ```html\n \n This will never change: {{msg}}\n \n
\n

Comment

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n Since 3.2, you can also memoize part of the template with invalidation conditions using [`v-memo`](#v-memo).\n\n- **See also**\n - [Data Binding Syntax - interpolations](https://vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nUsed to hide un-compiled template until it is ready.\n\n- **Does not expect expression**\n\n- **Details**\n\n **This directive is only needed in no-build-step setups.**\n\n When using in-DOM templates, there can be a \"flash of un-compiled templates\": the user may see raw mustache tags until the mounted component replaces them with rendered content.\n\n `v-cloak` will remain on the element until the associated component instance is mounted. Combined with CSS rules such as `[v-cloak] { display: none }`, it can be used to hide the raw templates until the component is ready.\n\n- **Example**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n The `
` will not be visible until the compilation is done.\n" diff --git a/packages/language-service/data/template/fr.json b/packages/language-service/data/template/fr.json index 9f535fc8d6..b4386b0dab 100644 --- a/packages/language-service/data/template/fr.json +++ b/packages/language-service/data/template/fr.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nIgnore la compilation pour cet élément et tous ses enfants.\n\n- **N'attend pas d'expression**\n\n- **Détails**\n\n À l'intérieur de l'élément contenant `v-pre`, toute la syntaxe du template Vue sera préservée et rendue telle quelle. Le cas d'utilisation le plus courant est l'affichage brut des balises moustaches.\n\n- **Exemple**\n\n ```html\n {{ this will not be compiled }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nRend l'élément et le composant une seule fois, et ignore les mises à jour futures.\n\n- **N'attend pas d'expression**\n\n- **Détails**\n\n Lors des rendus suivants, l'élément/composant et tous ses enfants seront traités comme du contenu statique et ignorés. Cela peut être utilisé pour optimiser les performances de mise à jour.\n\n ```html\n \n This will never change: {{msg}}\n \n
\n

Comment

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n Depuis la version 3.2, vous pouvez également mémoriser une partie du template avec des conditions d'invalidation en utilisant [`v-memo`](#v-memo).\n\n- **Voir aussi**\n - [Syntaxe de la liaison bidirectionnelle - interpolations](https://fr.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nUtilisée pour cacher un template non compilé jusqu'à ce qu'il soit prêt.\n\n- **N'attend pas d'expression**\n\n- **Détails**\n\n **Cette directive n'est nécessaire que dans les configurations sans étape de build.**\n\n Lors de l'utilisation de templates à l'intérieur du DOM, il peut y avoir un \"flash de templates non compilés\" : l'utilisateur peut voir des balises moustaches brutes jusqu'à ce que le composant monté les remplace par du contenu rendu.\n\n `v-cloak` restera sur l'élément jusqu'à ce que l'instance du composant associé soit montée. Combiné à des règles CSS telles que `[v-cloak] { display : none }`, elle peut être utilisée pour masquer les templates bruts jusqu'à ce que le composant soit prêt.\n\n- **Exemple**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n La `
` ne sera pas visible tant que la compilation n'est pas terminée.\n" diff --git a/packages/language-service/data/template/it.json b/packages/language-service/data/template/it.json index 5a3fd06542..7faf1fbf0d 100644 --- a/packages/language-service/data/template/it.json +++ b/packages/language-service/data/template/it.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nSkip compilation for this element and all its children.\n\n- **Does not expect expression**\n\n- **Details**\n\n Inside the element with `v-pre`, all Vue template syntax will be preserved and rendered as-is. The most common use case of this is displaying raw mustache tags.\n\n- **Example**\n\n ```html\n {{ this will not be compiled }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nRender the element and component once only, and skip future updates.\n\n- **Does not expect expression**\n\n- **Details**\n\n On subsequent re-renders, the element/component and all its children will be treated as static content and skipped. This can be used to optimize update performance.\n\n ```html\n \n This will never change: {{msg}}\n \n
\n

comment

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n Since 3.2, you can also memoize part of the template with invalidation conditions using [`v-memo`](#v-memo).\n\n- **See also**\n - [Data Binding Syntax - interpolations](https://it.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nUsed to hide un-compiled template until it is ready.\n\n- **Does not expect expression**\n\n- **Details**\n\n **This directive is only needed in no-build-step setups.**\n\n When using in-DOM templates, there can be a \"flash of un-compiled templates\": the user may see raw mustache tags until the mounted component replaces them with rendered content.\n\n `v-cloak` will remain on the element until the associated component instance is mounted. Combined with CSS rules such as `[v-cloak] { display: none }`, it can be used to hide the raw templates until the component is ready.\n\n- **Example**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n The `
` will not be visible until the compilation is done.\n" diff --git a/packages/language-service/data/template/ja.json b/packages/language-service/data/template/ja.json index d7ea157223..ee3ca4835b 100644 --- a/packages/language-service/data/template/ja.json +++ b/packages/language-service/data/template/ja.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nこの要素とすべての子要素のコンパイルをスキップします。\n\n- **式を受け取りません**\n\n- **詳細**\n\n `v-pre` を指定した要素の内部では、Vue テンプレートの構文はすべて維持され、そのままレンダリングされます。この最も一般的な使用例は、未加工のマスタッシュタグを表示することです。\n\n- **例**\n\n ```html\n {{ this will not be compiled }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n要素やコンポーネントを一度だけレンダリングし、その後の更新はスキップします。\n\n- **式を受け取りません**\n\n- **詳細**\n\n その後の再レンダリングでは、要素/コンポーネントとそのすべての子要素は静的コンテンツとして扱われ、スキップされます。これは、更新のパフォーマンスを最適化するために使用できます。\n\n ```html\n \n This will never change: {{msg}}\n \n
\n

Comment

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n 3.2 以降では、[`v-memo`](#v-memo) を使って、テンプレートの一部を無効化する条件付きでメモ化できます。\n\n- **参照**\n - [データバインディング構文 - 展開](https://ja.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nコンパイルされていないテンプレートを、準備が整うまで非表示にするために使用します。\n\n- **式を受け取りません**\n\n- **詳細**\n\n **このディレクティブは、ビルドステップがないセットアップでのみ必要です。**\n\n DOM 内テンプレートを使用する場合、「コンパイルされていないテンプレートのフラッシュ」が発生することがあります。マウントされたコンポーネントがそれをレンダリングされたコンテンツに置き換えるまで、未加工のマスタッシュタグがユーザーに表示される場合があります。\n\n `v-cloak` は関連するコンポーネントインスタンスがマウントされるまで、その要素に残ります。`[v-cloak] { display: none }` のような CSS ルールと組み合わせることで、コンポーネントの準備が整うまで未加工のテンプレートを非表示にできます。\n\n- **例**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n この `
` はコンパイルが完了するまで表示されません。\n" diff --git a/packages/language-service/data/template/ko.json b/packages/language-service/data/template/ko.json index 9438fd8fce..273cae89b8 100644 --- a/packages/language-service/data/template/ko.json +++ b/packages/language-service/data/template/ko.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n이 엘리먼트와 모든 자식 엘리먼트의 컴파일을 생략합니다.\n\n- **표현식을 허용하지 않습니다**.\n\n- **세부 사항**:\n\n `v-pre`가 있는 엘리먼트 내에서 모든 Vue 템플릿 구문은 그대로 유지되고 렌더링됩니다. 가장 일반적인 사용 사례는 이중 중괄호 태그를 표시하는 것입니다.\n\n- **예제**\n\n ```html\n {{ 이곳은 컴파일되지 않습니다. }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n엘리먼트와 컴포넌트를 한 번만 렌더링하고, 향후 업데이트를 생략합니다.\n\n- **표현식을 허용하지 않습니다**.\n\n- **세부 사항**:\n\n 이후 다시 렌더링할 때 엘리먼트/컴포넌트 및 모든 자식들은 정적 컨텐츠로 처리되어 생략됩니다. 이것은 업데이트 성능을 최적화하는 데 사용할 수 있습니다.\n\n ```html\n \n 절대 바뀌지 않음: {{msg}}\n \n
\n

댓글

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n 3.2부터는 [`v-memo`](#v-memo)를 사용하여 무효화 조건으로 템플릿의 일부를 메모화할 수도 있습니다.\n\n- **참고**:\n - [가이드 - 템플릿 문법: 텍스트 보간법](https://ko.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n준비될 때까지 컴파일되지 않은 템플릿을 숨기는 데 사용됩니다.\n\n- **표현식을 허용하지 않습니다**.\n\n- **세부 사항**:\n\n **이 디렉티브는 빌드 과정이 없는 설정에서만 필요합니다**.\n\n DOM 내 템플릿을 사용할 때, \"컴파일되지 않은 템플릿이 순간 보이는 현상\"이 있을 수 있습니다. 이러면 사용자는 컴포넌트가 렌더링된 컨텐츠로 대체할 때까지 이중 중괄호 태그를 볼 수 있습니다.\n\n `v-cloak`은 연결된 컴포넌트 인스턴스가 마운트될 때까지 엘리먼트에 남아 있습니다. `[v-cloak] { display: none }`과 같은 CSS 규칙과 결합하여, 컴포넌트가 준비될 때까지 템플릿을 숨기는 데 사용할 수 있습니다.\n\n- **예제**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n `
`는 컴파일이 완료될 때까지 표시되지 않습니다.\n" diff --git a/packages/language-service/data/template/pt.json b/packages/language-service/data/template/pt.json index d59400a444..1183af57e2 100644 --- a/packages/language-service/data/template/pt.json +++ b/packages/language-service/data/template/pt.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nIgnora a compilação para este elemento e todos os seus filhos.\n\n- **Não espera expressão**\n\n- **Detalhes**\n\n Dentro do elemento com `v-pre`, toda a sintaxe de modelo de marcação da Vue será preservada e desenhada como está. O caso de uso mais comum disto é a exibição de marcadores de bigodes puros.\n\n- **Exemplo**\n\n ```html\n {{ this will not be compiled }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nDesenha o elemento e o componente apenas uma vez, e ignora as futuras atualizações.\n\n- **Não espera expressão**\n\n- **Detalhes**\n\n Nos redesenhos subsequentes, o elemento ou componente e todos os seus filhos serão tratados como conteúdo estático e ignorados. Isto pode ser usado para otimizar o desempenho da atualização.\n\n ```html\n \n This will never change: {{msg}}\n \n
\n

comment

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n Desde a 3.2, também podemos memorizar parte do modelo de marcação com condições de invalidação usando a [`v-memo`](#v-memo).\n\n- **Consultar também**\n - [Sintaxe de Vínculo de Dados - Interpolações](https://pt.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [`v-memo`](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nUsada para esconder o modelo de marcação que ainda não foi compilado até que estiver pronto.\n\n- **Não espera expressão**\n\n- **Detalhes**\n\n **Esta diretiva apenas é necessária nas configurações sem etapa de construção.**\n\n Quando usamos os modelos de marcação no DOM, pode existir um \"piscar de modelos de marcação não compilados\": o utilizador pode ver os marcadores de bigodes puros até o componente montado substituí-los com componente desenhado.\n\n `v-cloak` permanecerá no elemento até que a instância do componente associado for montada. Combinada com as regras de CSS como `[v-cloak] { display: none }`, pode ser usada para esconder os modelos de marcação puros até o componente estiver pronto.\n\n- **Exemplo**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n O `
` não será visível até que a compilação estiver concluída.\n" diff --git a/packages/language-service/data/template/ru.json b/packages/language-service/data/template/ru.json index cbe8e31264..452a5ea90e 100644 --- a/packages/language-service/data/template/ru.json +++ b/packages/language-service/data/template/ru.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nПропускает компиляцию для элемента и всех его потомков.\n\n- **Не ожидает выражения**\n\n- **Подробности**\n\n Внутри элемента с `v-pre` весь синтаксис шаблона Vue будет сохранен и отображён как есть. Наиболее распространённый вариант использования этого элемента - отображение тегов фигурных скобок.\n\n- **Пример**\n\n ```html\n {{ это не будет скомпилировано }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nОтрисовка элемента и компонента выполняется только один раз, а последующие обновления пропускаются.\n\n- **Не ожидает выражения**\n\n- **Подробности**\n\n При последующих повторных отрисовках этот элемент/компонент и все его дочерние элементы будут рассматриваться как статическое содержимое и пропускаться. Это может быть использовано для оптимизации производительности при обновлении.\n\n ```html\n \n Это значение никогда не изменится: {{msg}}\n \n
\n

Комментарий

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n Начиная с версии 3.2, можно использовать мемоизацию части шаблона, с возможностью указания условий для инвалидации, с помощью директивы [`v-memo`](#v-memo).\n\n- **См. также**\n - [Синтаксис шаблонов - Текстовые интерполяции](https://ru.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\nИспользуется для скрытия еще нескомпилированного шаблона до тех пор, пока он не будет готов.\n\n- **Не ожидает выражения**\n\n- **Подробности**\n\n **Данная директива нужна только для окружения без этапа сборки.**\n\n При использовании DOM шаблонов может возникнуть \"вспышка некомпилированных шаблонов\": пользователь может видеть необработанные теги фигурных скобок, пока монтируемый компонент не заменит их отрисованным содержимым.\n\n `v-cloak` будет оставаться на элементе до тех пор, пока не будет смонтирован связанный с ним экземпляр компонента. В сочетании с правилами CSS, такими как `[v-cloak] { display: none }`, это может быть использовано для скрытия необработанных шаблонов до тех пор, пока компонент не будет готов.\n\n- **Пример**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n До завершения компиляции `
` не будет виден.\n" diff --git a/packages/language-service/data/template/zh-cn.json b/packages/language-service/data/template/zh-cn.json index 526b8ada4b..955f182f0f 100644 --- a/packages/language-service/data/template/zh-cn.json +++ b/packages/language-service/data/template/zh-cn.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n跳过该元素及其所有子元素的编译。\n\n- **无需传入**\n\n- **详细信息**\n\n 元素内具有 `v-pre`,所有 Vue 模板语法都会被保留并按原样渲染。最常见的用例就是显示原始双大括号标签及内容。\n\n- **示例**\n\n ```html\n {{ this will not be compiled }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n仅渲染元素和组件一次,并跳过之后的更新。\n\n- **无需传入**\n\n- **详细信息**\n\n 在随后的重新渲染,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。\n\n ```html\n \n This will never change: {{msg}}\n \n
\n

Comment

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n 从 3.2 起,你也可以搭配 [`v-memo`](#v-memo) 的无效条件来缓存部分模板。\n\n- **参考**\n - [数据绑定语法 - 插值](https://cn.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n用于隐藏尚未完成编译的 DOM 模板。\n\n- **无需传入**\n\n- **详细信息**\n\n **该指令只在没有构建步骤的环境下需要使用。**\n\n 当使用直接在 DOM 中书写的模板时,可能会出现一种叫做“未编译模板闪现”的情况:用户可能先看到的是还没编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。\n\n `v-cloak` 会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。配合像 `[v-cloak] { display: none }` 这样的 CSS 规则,它可以在组件编译完毕前隐藏原始模板。\n\n- **示例**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n 直到编译完成前,`
` 将不可见。\n" diff --git a/packages/language-service/data/template/zh-hk.json b/packages/language-service/data/template/zh-hk.json index 659f88b83f..031d680f02 100644 --- a/packages/language-service/data/template/zh-hk.json +++ b/packages/language-service/data/template/zh-hk.json @@ -1173,6 +1173,7 @@ }, { "name": "v-pre", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n跳过该元素及其所有子元素的编译。\n\n- **无需传入**\n\n- **详细信息**\n\n 元素内具有 `v-pre`,所有 Vue 模板语法都会被保留并按原样渲染。最常见的用例就是显示原始双大括号标签及内容。\n\n- **示例**\n\n ```html\n {{ this will not be compiled }}\n ```\n" @@ -1234,6 +1235,7 @@ }, { "name": "v-once", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n仅渲染元素和组件一次,并跳过之后的更新。\n\n- **无需传入**\n\n- **详细信息**\n\n 在随后的重新渲染,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。\n\n ```html\n \n This will never change: {{msg}}\n \n
\n

Comment

\n

{{msg}}

\n
\n \n \n \n
    \n
  • {{i}}
  • \n
\n ```\n\n 从 3.2 起,你也可以搭配 [`v-memo`](#v-memo) 的无效条件来缓存部分模板。\n\n- **参考**\n - [数据绑定语法 - 插值](https://zh-hk.vuejs.org/guide/essentials/template-syntax.html#text-interpolation)\n - [v-memo](#v-memo)\n" @@ -1356,6 +1358,7 @@ }, { "name": "v-cloak", + "valueSet": "v", "description": { "kind": "markdown", "value": "\n用于隐藏尚未完成编译的 DOM 模板。\n\n- **无需传入**\n\n- **详细信息**\n\n **该指令只在没有构建步骤的环境下需要使用。**\n\n 当使用直接在 DOM 中书写的模板时,可能会出现一种叫做“未编译模板闪现”的情况:用户可能先看到的是还没编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。\n\n `v-cloak` 会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。配合像 `[v-cloak] { display: none }` 这样的 CSS 规则,它可以在组件编译完毕前隐藏原始模板。\n\n- **示例**\n\n ```css\n [v-cloak] {\n display: none;\n }\n ```\n\n ```html\n
\n {{ message }}\n
\n ```\n\n 直到编译完成前,`
` 将不可见。\n" diff --git a/packages/language-service/scripts/update-html-data.js b/packages/language-service/scripts/update-html-data.js index 3e9a52d06e..abea7ed549 100644 --- a/packages/language-service/scripts/update-html-data.js +++ b/packages/language-service/scripts/update-html-data.js @@ -304,7 +304,12 @@ async function templateWorker(lang) { */ const data = { name, - valueSet: name === 'v-else' ? 'v' : undefined, + valueSet: + name === 'v-cloak' || + name === 'v-else' || + name === 'v-once' || + name === 'v-pre' + ? 'v' : undefined, description: { kind: 'markdown', value: lines.slice(1).join('\n'), diff --git a/packages/language-service/tests/complete.ts b/packages/language-service/tests/complete.ts index a7ca4fe0cb..959ae6efac 100644 --- a/packages/language-service/tests/complete.ts +++ b/packages/language-service/tests/complete.ts @@ -8,7 +8,7 @@ import { fileNameToUri } from './utils/mockEnv'; const baseDir = path.resolve(__dirname, '../../../test-workspace/language-service/complete'); const testDirs = fs.readdirSync(baseDir); -const normalizeNewline = (text: string) => text.replace(/\r\n/g, '\n'); +const getLineText = (text: string, line: number) => text.replace(/\r\n/g, '\n').split('\n')[line]; for (const dirName of testDirs) { @@ -26,6 +26,8 @@ for (const dirName of testDirs) { const document = TextDocument.create('', '', 0, fileText); const actions = findCompleteActions(fileText); + const expectedFileText = outputFiles[file]; + for (const action of actions) { const position = document.positionAt(action.offset); @@ -36,6 +38,8 @@ for (const dirName of testDirs) { it(`${location} => ${action.label}`, async () => { + expect(expectedFileText).toBeDefined(); + let complete = await tester.languageService.getCompletionItems( uri, position, @@ -57,10 +61,6 @@ for (const dirName of testDirs) { item = await tester.languageService.resolveCompletionItem(item); - const expectedFileText = outputFiles[file]; - - expect(expectedFileText).toBeDefined(); - let edits: vscode.TextEdit[] = []; if (item.textEdit) { @@ -83,7 +83,7 @@ for (const dirName of testDirs) { result = result.replace(/\$0/g, '').replace(/\$1/g, ''); - expect(normalizeNewline(result)).toBe(normalizeNewline(expectedFileText)); + expect(getLineText(result, position.line)).toBe(getLineText(expectedFileText, position.line)); }); } } diff --git a/test-workspace/language-service/complete/directives/input/entry.vue b/test-workspace/language-service/complete/directives/input/entry.vue new file mode 100644 index 0000000000..7608ec9344 --- /dev/null +++ b/test-workspace/language-service/complete/directives/input/entry.vue @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/test-workspace/language-service/complete/directives/output/entry.vue b/test-workspace/language-service/complete/directives/output/entry.vue new file mode 100644 index 0000000000..4800a2ef2f --- /dev/null +++ b/test-workspace/language-service/complete/directives/output/entry.vue @@ -0,0 +1,12 @@ + \ No newline at end of file From 98b77590efb06301d2a2d5978ab7515e8637ac40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:53:26 +0800 Subject: [PATCH 06/11] fix(language-service): completion documentations for binding attributes (#4667) --- packages/language-service/lib/plugins/data.ts | 5 ++++- .../language-service/lib/plugins/vue-template.ts | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/language-service/lib/plugins/data.ts b/packages/language-service/lib/plugins/data.ts index 06a63e33dd..d4f0ffce19 100644 --- a/packages/language-service/lib/plugins/data.ts +++ b/packages/language-service/lib/plugins/data.ts @@ -39,7 +39,10 @@ export function loadTemplateData(lang: string) { for (const attr of [...data.globalAttributes ?? []]) { if (!attr.name.startsWith('v-')) { - data.globalAttributes?.push({ ...attr, name: `:${attr.name}` }); + data.globalAttributes?.push( + { ...attr, name: `:${attr.name}` }, + { ...attr, name: `v-bind:${attr.name}` } + ); } } diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 7d67146123..57a2c24125 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -729,6 +729,8 @@ export function create( } } + const originals = new Map(); + for (const item of completionList.items) { if (specialTags.has(item.label)) { @@ -755,7 +757,18 @@ export function create( const itemId = itemIdKey ? readInternalItemId(itemIdKey) : undefined; if (itemId) { - item.documentation = undefined; + let label = hyphenate(itemId.args[1]); + if (label.startsWith('on-')) { + label = 'on' + label.slice('on-'.length); + } + else if (itemId.type === 'componentEvent') { + label = 'on' + label; + } + const original = originals.get(label); + item.documentation = original?.documentation; + } + else if (!originals.has(item.label)) { + originals.set(item.label, item); } if (item.kind === 10 satisfies typeof vscode.CompletionItemKind.Property && lastCompletionComponentNames.has(hyphenateTag(item.label))) { From 35474d2cbb0f0ff6e65063a19067896c2a085c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:55:35 +0800 Subject: [PATCH 07/11] fix(language-service): avoid converting internal id of special tags (#4643) --- .../language-service/lib/plugins/vue-template.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 57a2c24125..e5105e0ffc 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -421,10 +421,14 @@ export function create( if (builtInData.tags) { for (const tag of builtInData.tags) { + if (isInternalItemId(tag.name)) { + continue; + } + if (specialTags.has(tag.name)) { tag.name = createInternalItemId('specialTag', [tag.name]); } - if (casing.tag === TagNameCasing.Kebab) { + else if (casing.tag === TagNameCasing.Kebab) { tag.name = hyphenateTag(tag.name); } else { @@ -881,8 +885,12 @@ function createInternalItemId(type: 'componentEvent' | 'componentProp' | 'specia return '__VLS_::' + type + '::' + args.join(','); } +function isInternalItemId(key: string) { + return key.startsWith('__VLS_::'); +} + function readInternalItemId(key: string) { - if (key.startsWith('__VLS_::')) { + if (isInternalItemId(key)) { const strs = key.split('::'); return { type: strs[1] as 'componentEvent' | 'componentProp' | 'specialTag', From eef948d454f69f93a8e8211bf287bc9dcca2eaba Mon Sep 17 00:00:00 2001 From: David Matter Date: Fri, 9 Aug 2024 07:57:58 +0200 Subject: [PATCH 08/11] fix(language-core): nullable modelvalues (#4648) --- .../lib/codegen/script/scriptSetup.ts | 2 +- test-workspace/tsc/vue2/tsconfig.json | 1 + test-workspace/tsc/vue3/#4646/child.vue | 13 +++++++++++++ test-workspace/tsc/vue3/#4646/child2.vue | 5 +++++ test-workspace/tsc/vue3/#4646/parent.vue | 15 +++++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 test-workspace/tsc/vue3/#4646/child.vue create mode 100644 test-workspace/tsc/vue3/#4646/child2.vue create mode 100644 test-workspace/tsc/vue3/#4646/parent.vue diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts index 07024b5757..36cdb4aa15 100644 --- a/packages/language-core/lib/codegen/script/scriptSetup.ts +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -413,7 +413,7 @@ function* generateDefinePropType(scriptSetup: NonNullable, p } else if ((defineProp.name && defineProp.nameIsString) || !defineProp.nameIsString) { // Infer from actual prop declaration code - yield `NonNullable`; + yield `typeof ${propName}['value']`; } else if (defineProp.defaultValue) { // Infer from defineProp({default: T}) diff --git a/test-workspace/tsc/vue2/tsconfig.json b/test-workspace/tsc/vue2/tsconfig.json index 9aaa7ba292..a425bb6dd7 100644 --- a/test-workspace/tsc/vue2/tsconfig.json +++ b/test-workspace/tsc/vue2/tsconfig.json @@ -21,6 +21,7 @@ "../vue3/#4327", "../vue3/#4512", "../vue3/#4540", + "../vue3/#4646", "../vue3/components", "../vue3/defineEmits", "../vue3/defineModel", diff --git a/test-workspace/tsc/vue3/#4646/child.vue b/test-workspace/tsc/vue3/#4646/child.vue new file mode 100644 index 0000000000..89d4a68525 --- /dev/null +++ b/test-workspace/tsc/vue3/#4646/child.vue @@ -0,0 +1,13 @@ + + + diff --git a/test-workspace/tsc/vue3/#4646/child2.vue b/test-workspace/tsc/vue3/#4646/child2.vue new file mode 100644 index 0000000000..3c9de244f0 --- /dev/null +++ b/test-workspace/tsc/vue3/#4646/child2.vue @@ -0,0 +1,5 @@ + + + diff --git a/test-workspace/tsc/vue3/#4646/parent.vue b/test-workspace/tsc/vue3/#4646/parent.vue new file mode 100644 index 0000000000..136a744672 --- /dev/null +++ b/test-workspace/tsc/vue3/#4646/parent.vue @@ -0,0 +1,15 @@ + + From c5beb97fffb6aafeaabb3f88a942ca9b86ca8568 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Fri, 9 Aug 2024 14:00:28 +0800 Subject: [PATCH 09/11] fix(language-core): should try casting dynamic slot name into constant (#4669) --- .../lib/codegen/script/globalTypes.ts | 1 + .../lib/codegen/template/element.ts | 4 +++- .../lib/codegen/template/objectProperty.ts | 19 +++++++++++++++++-- test-workspace/tsc/vue3/#4668/child.vue | 8 ++++++++ test-workspace/tsc/vue3/#4668/main.vue | 16 ++++++++++++++++ 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 test-workspace/tsc/vue3/#4668/child.vue create mode 100644 test-workspace/tsc/vue3/#4668/main.vue diff --git a/packages/language-core/lib/codegen/script/globalTypes.ts b/packages/language-core/lib/codegen/script/globalTypes.ts index 7148e107b0..923d971aa9 100644 --- a/packages/language-core/lib/codegen/script/globalTypes.ts +++ b/packages/language-core/lib/codegen/script/globalTypes.ts @@ -100,6 +100,7 @@ declare global { : false; function __VLS_normalizeSlot(s: S): S extends () => infer R ? (props: {}) => R : S; + function __VLS_tryAsConstant(t: T): T; /** * emit diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index a7a55b479f..763d9237e2 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -444,7 +444,9 @@ function* generateComponentSlot( slotDir.arg.loc.source, slotDir.arg.loc.start.offset, slotDir.arg.isStatic ? ctx.codeFeatures.withoutHighlight : ctx.codeFeatures.all, - slotDir.arg.loc + slotDir.arg.loc, + false, + true, ); yield ': __VLS_thisSlot'; } diff --git a/packages/language-core/lib/codegen/template/objectProperty.ts b/packages/language-core/lib/codegen/template/objectProperty.ts index 702908d19b..eadf2a3a97 100644 --- a/packages/language-core/lib/codegen/template/objectProperty.ts +++ b/packages/language-core/lib/codegen/template/objectProperty.ts @@ -14,10 +14,25 @@ export function* generateObjectProperty( offset: number, features: VueCodeInformation, astHolder?: any, - shouldCamelize = false + shouldCamelize = false, + shouldBeConstant = false ): Generator { if (code.startsWith('[') && code.endsWith(']') && astHolder) { - yield* generateInterpolation(options, ctx, code, astHolder, offset, features, '', ''); + if (shouldBeConstant) { + yield* generateInterpolation( + options, + ctx, + code.slice(1, -1), + astHolder, + offset + 1, + features, + '[__VLS_tryAsConstant(', + ')]', + ); + } + else { + yield* generateInterpolation(options, ctx, code, astHolder, offset, features, '', ''); + } } else if (shouldCamelize) { if (variableNameRegex.test(camelize(code))) { diff --git a/test-workspace/tsc/vue3/#4668/child.vue b/test-workspace/tsc/vue3/#4668/child.vue new file mode 100644 index 0000000000..3163f4d0d3 --- /dev/null +++ b/test-workspace/tsc/vue3/#4668/child.vue @@ -0,0 +1,8 @@ + + + diff --git a/test-workspace/tsc/vue3/#4668/main.vue b/test-workspace/tsc/vue3/#4668/main.vue new file mode 100644 index 0000000000..5e701d317a --- /dev/null +++ b/test-workspace/tsc/vue3/#4668/main.vue @@ -0,0 +1,16 @@ + + From 0c0ed2a8667e49bec7db2f89e08c596854e9fc37 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 06:00:53 +0000 Subject: [PATCH 10/11] ci(lint): auto-fix --- packages/language-core/lib/codegen/template/element.ts | 2 +- packages/language-core/lib/codegen/template/objectProperty.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index 763d9237e2..c9bd547e13 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -446,7 +446,7 @@ function* generateComponentSlot( slotDir.arg.isStatic ? ctx.codeFeatures.withoutHighlight : ctx.codeFeatures.all, slotDir.arg.loc, false, - true, + true ); yield ': __VLS_thisSlot'; } diff --git a/packages/language-core/lib/codegen/template/objectProperty.ts b/packages/language-core/lib/codegen/template/objectProperty.ts index eadf2a3a97..5d01f92fe9 100644 --- a/packages/language-core/lib/codegen/template/objectProperty.ts +++ b/packages/language-core/lib/codegen/template/objectProperty.ts @@ -27,7 +27,7 @@ export function* generateObjectProperty( offset + 1, features, '[__VLS_tryAsConstant(', - ')]', + ')]' ); } else { From 0f14cde64b48ffc9c7437135a33464ce0a2d0ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:11:48 +0800 Subject: [PATCH 11/11] fix(language-service): reinstate the completion for modifiers (#4639) --- .../lib/plugins/vue-template.ts | 18 +++++++++++------- .../complete/#4639/input/entry.vue | 7 +++++++ .../complete/#4639/output/entry.vue | 7 +++++++ 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 test-workspace/language-service/complete/#4639/input/entry.vue create mode 100644 test-workspace/language-service/complete/#4639/output/entry.vue diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index e5105e0ffc..719005db53 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -159,11 +159,15 @@ export function create( return; } - if (sourceScript?.generated?.root instanceof VueVirtualCode) { - await afterHtmlCompletion( - htmlComplete, - context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot) - ); + if (sourceScript?.generated) { + const virtualCode = sourceScript.generated.embeddedCodes.get('template'); + if (virtualCode) { + const embeddedDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, virtualCode.id); + afterHtmlCompletion( + htmlComplete, + context.documents.get(embeddedDocumentUri, virtualCode.languageId, virtualCode.snapshot) + ); + } } return htmlComplete; @@ -660,9 +664,9 @@ export function create( }; } - function afterHtmlCompletion(completionList: vscode.CompletionList, sourceDocument: TextDocument) { + function afterHtmlCompletion(completionList: vscode.CompletionList, document: TextDocument) { - const replacement = getReplacement(completionList, sourceDocument); + const replacement = getReplacement(completionList, document); if (replacement) { diff --git a/test-workspace/language-service/complete/#4639/input/entry.vue b/test-workspace/language-service/complete/#4639/input/entry.vue new file mode 100644 index 0000000000..730350bee7 --- /dev/null +++ b/test-workspace/language-service/complete/#4639/input/entry.vue @@ -0,0 +1,7 @@ + + + diff --git a/test-workspace/language-service/complete/#4639/output/entry.vue b/test-workspace/language-service/complete/#4639/output/entry.vue new file mode 100644 index 0000000000..f470a01d83 --- /dev/null +++ b/test-workspace/language-service/complete/#4639/output/entry.vue @@ -0,0 +1,7 @@ + + +