Skip to content

Commit 70394c0

Browse files
committed
feat(config): derive HTML/Vue editor behavior in generateLanguageConfig
A markup grammar (grammar.markup) now derives the tag-structural language configuration a hand-written HTML/Vue config provides — tag-aware onEnter / indentation rules, region folding from the comment delimiters, comment and (when the file embeds source.*) {} () brackets, plus the general word pattern. Everything is derived from the markup DATA (void / raw-text tag sets, comment delimiters, embed scopes); token-stream grammars (TS/JS/YAML) are unaffected. Gated by the agnostic check.
1 parent 5eda203 commit 70394c0

3 files changed

Lines changed: 305 additions & 15 deletions

File tree

html.language-configuration.json

Lines changed: 99 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/gen-vscode-config.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { tokenPatternBlockDelimiters, tokenPatternHasStartAnchor, tokenPatternLi
88
// the grammar's token definitions, lexer hints, and `scopes` section.
99

1010
interface AutoPair { open: string; close: string; notIn?: string[]; }
11+
// A VS Code onEnter `beforeText`/`afterText` is a regex SOURCE — a bare string (case-sensitive)
12+
// or `{ pattern, flags }` (e.g. `i`, for case-insensitive tag matching in markup).
13+
type EnterText = string | { pattern: string; flags: string };
1114
export interface LanguageConfig {
1215
comments?: { lineComment?: string; blockComment?: [string, string] };
1316
brackets?: [string, string][];
@@ -18,7 +21,7 @@ export interface LanguageConfig {
1821
folding?: { markers: { start: string; end: string } };
1922
wordPattern?: string;
2023
indentationRules?: { increaseIndentPattern: string; decreaseIndentPattern: string };
21-
onEnterRules?: { beforeText: string; afterText?: string; action: { indent: string; appendText?: string; removeText?: number } }[];
24+
onEnterRules?: { beforeText: EnterText; afterText?: EnterText; action: { indent: string; appendText?: string; removeText?: number } }[];
2225
}
2326

2427
const escRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -147,5 +150,108 @@ export function generateLanguageConfig(grammar: CstGrammar): LanguageConfig {
147150
}
148151
if (onEnter.length) config.onEnterRules = onEnter;
149152

153+
// ── Markup languages (HTML / Vue) — tag-structural editor behavior ──
154+
// A markup grammar (`grammar.markup`) carries the structural DATA — tag delimiters, the
155+
// void / raw-text element sets, the comment delimiters, the embed scopes — needed to DERIVE
156+
// the editor behavior a hand-written HTML/Vue language-configuration provides (tag-aware
157+
// indent / onEnter, region folding, comment + embedded-language brackets), which the token /
158+
// scope heuristics above cannot produce for a markup language. Everything below is derived
159+
// from that data; nothing is HTML-hardcoded — the same branch fits any markup grammar.
160+
const mk = grammar.markup;
161+
if (mk) {
162+
const tagOpen = mk.tagOpen, tagClose = mk.tagClose, closeMarker = mk.closeMarker ?? '/';
163+
const to = escRe(tagOpen), tc = escRe(tagClose), cm = escRe(closeMarker);
164+
const tcCls = escClass(tagClose), toCls = escClass(tagOpen);
165+
const tagName = ident ? tokenPatternSource(ident) : '[A-Za-z][\\w:.\\-]*'; // tag-name capture
166+
const nameClass = '[A-Za-z0-9_:.\\-]'; // tag-name body char class
167+
const cOpen = mk.comment?.open, cClose = mk.comment?.close;
168+
169+
// A file that EMBEDS a programming language (a rawText / attribute / interpolation embed whose
170+
// scope is `source.*`) is edited with that language's brackets — `{}` `[]` `()`, backtick, the
171+
// JSDoc `/** */` — and a general word pattern, exactly as VS Code's HTML config does for its
172+
// embedded JS/CSS. A markup-only embed (e.g. Vue `<template>` → text.html.*) does not trigger it.
173+
const embedScopes: string[] = [];
174+
if (mk.rawText?.embed) for (const e of Object.values(mk.rawText.embed)) embedScopes.push(typeof e === 'string' ? e : e.default);
175+
if (mk.attributeEmbed) for (const a of mk.attributeEmbed) embedScopes.push(a.embed);
176+
if (mk.customBlockEmbed) embedScopes.push(...Object.values(mk.customBlockEmbed));
177+
if (mk.inject?.exprEmbed) embedScopes.push(mk.inject.exprEmbed);
178+
const embedsCode = embedScopes.some(s => s?.startsWith('source.'));
179+
180+
// Tags that must NOT indent-on-Enter / increase indent: void elements (no children) + raw-text
181+
// elements whose body is a PROGRAMMING language (`source.*`, its own grammar owns indentation).
182+
// A raw-text block embedding MARKUP (Vue `<template>` → text.html.*) is excluded, so it indents.
183+
const codeRawTags = (mk.rawText?.tags ?? []).filter(t => {
184+
const e = mk.rawText!.embed?.[t];
185+
const s = typeof e === 'string' ? e : e?.default;
186+
return !!s?.startsWith('source.');
187+
});
188+
const voidAlt = [...new Set([...(mk.voidTags ?? []), ...codeRawTags])].map(escRe).join('|');
189+
190+
// ── Brackets ── the comment pair, plus the embedded language's `{}` `()`.
191+
const brk: [string, string][] = [];
192+
if (cOpen && cClose) brk.push([cOpen, cClose]);
193+
if (embedsCode) brk.push(['{', '}'], ['(', ')']);
194+
if (brk.length) config.brackets = brk;
195+
config.colorizedBracketPairs = []; // markup defers bracket colorization to the editor default
196+
197+
// ── Auto-closing / surrounding ── embedded brackets, the attribute quotes, the comment, and
198+
// (when code is embedded) the backtick string + JSDoc opener.
199+
const quotes = mk.attributeQuotes ?? ['"', "'"];
200+
const auto: AutoPair[] = [];
201+
if (embedsCode) auto.push({ open: '{', close: '}' }, { open: '[', close: ']' }, { open: '(', close: ')' });
202+
for (const q of quotes) auto.push({ open: q, close: q });
203+
if (cOpen && cClose) auto.push({ open: cOpen, close: cClose, notIn: ['comment', 'string'] });
204+
if (embedsCode) auto.push({ open: '`', close: '`', notIn: ['string', 'comment'] }, { open: '/**', close: ' */', notIn: ['string'] });
205+
config.autoClosingPairs = auto;
206+
207+
const surround: [string, string][] = [...quotes.map(q => [q, q] as [string, string])];
208+
if (embedsCode) surround.push(['{', '}'], ['[', ']'], ['(', ')']);
209+
if (hasAngle) surround.push(['<', '>']);
210+
if (embedsCode) surround.push(['`', '`']);
211+
config.surroundingPairs = surround;
212+
213+
// ── autoCloseBefore ── embedded-code separators / closers, the angle brackets, quotes.
214+
const before: string[] = [];
215+
if (embedsCode) before.push(';', ':', '.', ',', '=', '}', ')', ']');
216+
before.push('>', '<');
217+
if (embedsCode) before.push('`');
218+
before.push(...quotes);
219+
config.autoCloseBefore = [...new Set(before)].join('') + ' \n\t';
220+
221+
// ── Folding ── region markers expressed with the block-comment delimiters.
222+
if (cOpen && cClose) {
223+
const o = escRe(cOpen), c = escRe(cClose);
224+
config.folding = { markers: { start: `^\\s*${o}\\s*#region\\b.*${c}`, end: `^\\s*${o}\\s*#endregion\\b.*${c}` } };
225+
}
226+
227+
// ── Word pattern ── an embedded-code file gets the general word pattern (a tag-name pattern
228+
// would stop selection at the first separator and never select code identifiers); otherwise
229+
// leave the identifier-token pattern derived above. `-` stays a word char (kebab-case names,
230+
// CSS custom properties), matching the editor default for HTML.
231+
if (embedsCode) {
232+
config.wordPattern = '(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\$\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\.\\<\\>\\/\\s]+)';
233+
}
234+
235+
// ── onEnter ── indent inside an opened tag (outdent when the close tag is on the next line),
236+
// skipping void / code-raw elements. The attribute-skip clause is built from the quote chars.
237+
const qAlts = quotes.map(q => `${escRe(q)}[^${escClass(q)}]*${escRe(q)}`).join('|');
238+
const attrSkip = `(?:[^${escClass(quotes.join(''))}${escClass(closeMarker)}${tcCls}]|${qAlts})*?`;
239+
const beforeOpen = `${to}(?!(?:${voidAlt}))(${tagName})(?:${attrSkip}(?!${cm})${tc})[^${toCls}]*$`;
240+
const afterClose = `^${to}${cm}(${tagName})\\s*${tc}`;
241+
config.onEnterRules = [
242+
{ beforeText: { pattern: beforeOpen, flags: 'i' }, afterText: { pattern: afterClose, flags: 'i' }, action: { indent: 'indentOutdent' } },
243+
{ beforeText: { pattern: beforeOpen, flags: 'i' }, action: { indent: 'indent' } },
244+
];
245+
246+
// ── Indentation ── increase after a non-void / non-self-closed open tag (and an open comment /
247+
// brace when code is embedded); decrease on a close tag / comment-close / `}`.
248+
const incComment = cOpen && cClose ? `|${escRe(cOpen)}(?!.*${escRe(cClose)})` : '';
249+
const decComment = cClose ? `|${escRe(cClose)}` : '';
250+
config.indentationRules = {
251+
increaseIndentPattern: `${to}(?!\\?|(?:${voidAlt})\\b|[^${tcCls}]*${cm}${tc})(${nameClass}+)(?=\\s|${tc})\\b[^${tcCls}]*${tc}(?!.*${to}${cm}\\1${tc})${incComment}${embedsCode ? `|\\{[^}"']*$` : ''}`,
252+
decreaseIndentPattern: `^\\s*(${to}${cm}${nameClass}+\\b[^${tcCls}]*${tc}${decComment}${embedsCode ? `|\\}` : ''})`,
253+
};
254+
}
255+
150256
return config;
151257
}

vue.language-configuration.json

Lines changed: 99 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)