@@ -8,6 +8,9 @@ import { tokenPatternBlockDelimiters, tokenPatternHasStartAnchor, tokenPatternLi
88// the grammar's token definitions, lexer hints, and `scopes` section.
99
1010interface 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 } ;
1114export 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
2427const 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}
0 commit comments