Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 117 additions & 1 deletion extensions/vscode/syntaxes/vue.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,9 @@
},
"vue-directives": {
"patterns": [
{
"include": "#vue-directives-match-arm"
},
{
"include": "#vue-directives-control"
},
Expand Down Expand Up @@ -1307,7 +1310,7 @@
]
},
"vue-directives-control": {
"begin": "(?:(v-for)|(v-if|v-else-if|v-else))(?=[=/>)\\s])",
"begin": "(?:(v-for)|(v-if|v-else-if|v-else|v-match))(?=[=/>)\\s])",
"beginCaptures": {
"1": {
"name": "keyword.control.loop.vue"
Expand Down Expand Up @@ -1585,6 +1588,119 @@
"include": "source.json#value"
}
]
},
"vue-directives-match-arm": {
"begin": "(v-when)(?=[=/>)\\s])",
"beginCaptures": {
"1": {
"name": "keyword.control.conditional.vue"
}
},
"end": "(?=\\s*[^=\\s])",
"name": "meta.attribute.directive.control.vue",
"patterns": [
{
"begin": "(=)\\s*('|\"|`)",
"beginCaptures": {
"1": {
"name": "punctuation.separator.key-value.html.vue"
},
"2": {
"name": "punctuation.definition.string.begin.html.vue"
}
},
"end": "(\\2)",
"endCaptures": {
"1": {
"name": "punctuation.definition.string.end.html.vue"
}
},
"contentName": "meta.pattern.vue",
"patterns": [
{
"include": "#vue-match-pattern"
}
]
}
]
},
"vue-match-pattern": {
"patterns": [
{
"begin": "\\b(if)\\s*(\\()",
"beginCaptures": {
"1": {
"name": "keyword.control.conditional.vue"
},
"2": {
"name": "punctuation.definition.parameters.begin.ts"
}
},
"end": "\\)(?=\\s*['\"`])",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.ts"
}
},
"contentName": "source.ts.embedded.html.vue",
"patterns": [
{
"include": "source.ts#expression"
}
]
},
{
"match": "'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\"",
"name": "string.quoted.vue"
},
{
"match": "\\b(const|as)\\b\\s*([\\w$]+)?",
"captures": {
"1": {
"name": "keyword.declaration.pattern.vue"
},
"2": {
"name": "variable.other.constant.ts"
}
}
},
{
"match": "\\b(let|var)\\b",
"name": "invalid.illegal.pattern.vue"
},
{
"match": "(?<![\\w$])_(?![\\w$])",
"name": "constant.language.wildcard.vue"
},
{
"match": "\\b(true|false|null|undefined|NaN)\\b",
"name": "constant.language.ts"
},
{
"match": "(?<![\\w$])[-+]?(?:0[xX][\\da-fA-F]+|\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)(?:n)?",
"name": "constant.numeric.ts"
},
{
"match": "\\.\\.\\.",
"name": "keyword.operator.rest.vue"
},
{
"match": "\\|",
"name": "keyword.operator.pattern.vue"
},
{
"match": "[{}\\[\\]()]",
"name": "punctuation.definition.pattern.vue"
},
{
"match": "[,:.]",
"name": "punctuation.separator.pattern.vue"
},
{
"match": "[\\w$]+",
"name": "variable.other.readwrite.ts"
}
]
}
}
}
464 changes: 464 additions & 0 deletions extensions/vscode/tests/__snapshots__/grammar.spec.ts.snap

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
const result = { kind: 'ok', data: [1, 2] }
</script>
<template>
<template v-match="result">
<p v-when="{ kind: 'ok', data: [const first, ...const tail], ...const metadata } as success if (first > 0 && tail.length)">{{ first }}</p>
<p v-when="('idle' | 'pending') as waiting">{{ waiting }}</p>
<p v-when="Status.Error">error</p>
<p v-when="{ kind: _, ... }">known</p>
<p v-when='[1, "two", ...]'>tuple</p>
<p v-when="_">empty</p>
</template>
<p :title="result.kind">after pattern</p>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
defineProps<{ result: { kind: 'ok'; data: string } | { kind: 'err' } }>()
</script>
<template v-match="result" lang="html">
<p v-when="{ kind: 'ok', const data } if (data.length)">{{ data }}</p>
<p v-when="{ kind: 'err' }">error</p>
</template>
10 changes: 5 additions & 5 deletions extensions/vscode/tests/embeddedGrammars/_lock.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
[
{
"file": "css.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/css/syntaxes/css.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/283e479dda1c4a4a0fed0fcf110bf48a8ee43aae/extensions/css/syntaxes/css.tmLanguage.json",
"checksum": "sha256-9678e7410bd051eafc1ad51d3c8562885a17e7d61602d76cc5030d06520724cd"
},
{
"file": "html.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/html/syntaxes/html.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/283e479dda1c4a4a0fed0fcf110bf48a8ee43aae/extensions/html/syntaxes/html.tmLanguage.json",
"checksum": "sha256-80dedf4fb27e88889ac8fb72763954a6d2660502c686f4415208d8c8d00352cd"
},
{
"file": "javascript.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/javascript/syntaxes/JavaScript.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/283e479dda1c4a4a0fed0fcf110bf48a8ee43aae/extensions/javascript/syntaxes/JavaScript.tmLanguage.json",
"checksum": "sha256-db6f17f15bc4f5e860a3b8fa6055a69720a53df845c8d5121cdc4f128c16291f"
},
{
"file": "scss.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/scss/syntaxes/scss.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/283e479dda1c4a4a0fed0fcf110bf48a8ee43aae/extensions/scss/syntaxes/scss.tmLanguage.json",
"checksum": "sha256-8f2824a80a7c6fd558fc538ec52d0a7a42a4d7ecb7ddf20d79f0d1f00fa6602b"
},
{
"file": "typescript.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode/283e479dda1c4a4a0fed0fcf110bf48a8ee43aae/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json",
"checksum": "sha256-4e92e0d7de560217d6c8d3236d85e6e17a5d77825b15729a230c761743122661"
}
]
4 changes: 4 additions & 0 deletions extensions/vscode/tests/grammar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ describe('embedded grammar', async () => {
it(fixture, async () => {
const result = await snapshot(`tests/embeddedGrammarFixtures/${fixture}`);

if (fixture === 'patterned-templates.vue') {
expect(result).toContain('constant.language.wildcard.vue');
expect(result).toContain('keyword.declaration.pattern.vue');
}
expect(result).toMatchSnapshot();
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/language-core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './lib/parsers/scriptRanges';
export * from './lib/parsers/scriptSetupRanges';
export * from './lib/plugins';
export * from './lib/template/compile';
export * from './lib/template/patterns/warnings';
export * from './lib/types';
export * from './lib/utils/collectBindings';
export * from './lib/utils/forEachTemplateNode';
Expand Down
4 changes: 4 additions & 0 deletions packages/language-core/lib/codegen/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const raw = {
slots: '',
src: '',

MatchPattern: '',
SubtractPattern: '',
CheckMatchExhaustive: '',

Emit: '',
EmitProps: '',
GlobalComponents: '',
Expand Down
6 changes: 5 additions & 1 deletion packages/language-core/lib/codegen/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function* generateWorker(
): Generator<Code> {
const { script, scriptRanges, scriptSetup, scriptSetupRanges, vueCompilerOptions, fileName } = options;

yield* generateGlobalTypesReference(vueCompilerOptions, fileName);
yield* generateGlobalTypesReference(vueCompilerOptions, fileName, options.templateAndStyleTypes);

// <script src="">
if (typeof script?.src === 'object') {
Expand Down Expand Up @@ -237,6 +237,7 @@ function* generateScriptWithExportDefault(
function* generateGlobalTypesReference(
{ typesRoot, lib, target, checkUnknownProps }: VueCompilerOptions,
fileName: string,
templateTypes: Set<string>,
): Generator<Code> {
let typesPath: string;
if (path.isAbsolute(typesRoot)) {
Expand All @@ -254,6 +255,9 @@ function* generateGlobalTypesReference(
typesPath = typesRoot;
}
yield `/// <reference types=${JSON.stringify(typesPath + '/template-helpers.d.ts')} />${newLine}`;
if (templateTypes.has(names.MatchPattern)) {
yield `/// <reference types=${JSON.stringify(typesPath + '/pattern-matching.d.ts')} />${newLine}`;
}
if (!checkUnknownProps) {
yield `/// <reference types=${JSON.stringify(typesPath + '/props-fallback.d.ts')} />${newLine}`;
}
Expand Down
17 changes: 15 additions & 2 deletions packages/language-core/lib/codegen/template/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function createTemplateCodegenContext() {
expVar: string;
propsVar: string;
}[] = [];
const blockConditions: string[] = [];
const blockConditions: { code: string; bindings?: Map<string, Set<string> | undefined> }[] = [];
const inlayHints: InlayHintInfo[] = [];
const inheritedAttrVars = new Set<string>();
const templateRefs = new Map<string, {
Expand All @@ -146,6 +146,14 @@ export function createTemplateCodegenContext() {
}[] = [];
const commentBuffer: CompilerDOM.CommentNode[] = [];

function getLocalBinding(name: string) {
for (let i = scopes.length - 1; i >= 0; i--) {
if (scopes[i]!.has(name)) {
return scopes[i];
}
}
}

return {
generatedTypes: new Set<string>(),
get currentInfo() {
Expand Down Expand Up @@ -184,6 +192,7 @@ export function createTemplateCodegenContext() {
}
},
scopes,
getLocalBinding,
components,
declare(...varNames: string[]) {
const scope = scopes.at(-1)!;
Expand Down Expand Up @@ -222,7 +231,11 @@ export function createTemplateCodegenContext() {
},
*generateConditionGuards() {
for (const condition of blockConditions) {
yield `if (!${condition}) return${endOfLine}`;
// A repeated guard must still refer to the bindings it originally tested.
if (condition.bindings && [...condition.bindings].some(([name, scope]) => getLocalBinding(name) !== scope)) {
continue;
}
yield `if (!${condition.code}) return${endOfLine}`;
}
},
enter(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.SimpleExpressionNode) {
Expand Down
10 changes: 10 additions & 0 deletions packages/language-core/lib/codegen/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ function* generateWorker(
if (vueCompilerOptions.inferTemplateDollarEl) {
ctx.dollarVars.add('$el');
}
for (const error of template.errors) {
if (error.code !== 'V_MATCH_SYNTAX' || !error.loc) {
continue;
}
yield 'const ';
const token = yield* startBoundary('template', error.loc.start.offset, codeFeatures.verification);
yield ctx.getInternalVariable();
yield endBoundary(token, error.loc.end.offset);
yield `: { ${JSON.stringify(error.message)}: never } = {};\n`;
}
if (template.ast) {
yield* generateTemplateChild(options, ctx, template.ast);
}
Expand Down
21 changes: 21 additions & 0 deletions packages/language-core/lib/codegen/template/interpolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ export function* generateInterpolation(
}
}

export function getConditionBindings(
ts: typeof import('typescript'),
ctx: TemplateCodegenContext,
block: IRBlock,
code: string,
) {
const bindings = new Map<string, Set<string> | undefined>();
const enclosingScopes = new Set(ctx.scopes);
const endScope = ctx.startScope();
const ast = getTypeScriptAST(ts, block, `(${code})`);
for (const [id] of forEachDeclarations(ts, ast, ast, ctx)) {
const name = getNodeText(ts, id, ast);
const scope = ctx.getLocalBinding(name);
if (!scope || enclosingScopes.has(scope)) {
bindings.set(name, scope);
}
}
endScope();
return bindings;
}

function* forEachIdentifiers(
ts: typeof import('typescript'),
ctx: TemplateCodegenContext,
Expand Down
8 changes: 7 additions & 1 deletion packages/language-core/lib/codegen/template/templateChild.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as CompilerDOM from '@vue/compiler-dom';
import { getTemplateMatch } from '../../template/patterns/prepare';
import type { Code } from '../../types';
import { hyphenateTag } from '../../utils/shared';
import { codeFeatures } from '../codeFeatures';
Expand All @@ -10,6 +11,7 @@ import { generateInterpolation } from './interpolation';
import { generateSlotOutlet } from './slotOutlet';
import { generateVFor } from './vFor';
import { generateVIf } from './vIf';
import { generateVMatch } from './vMatch';
import { generateVSlot } from './vSlot';

export function* generateTemplateChild(
Expand All @@ -32,7 +34,11 @@ export function* generateTemplateChild(
}
}
else if (node.type === CompilerDOM.NodeTypes.ELEMENT) {
if (node.tagType === CompilerDOM.ElementTypes.SLOT) {
const match = getTemplateMatch(node);
if (match) {
yield* generateVMatch(options, ctx, match);
}
else if (node.tagType === CompilerDOM.ElementTypes.SLOT) {
yield* generateSlotOutlet(options, ctx, node);
}
else {
Expand Down
Loading