@@ -4975,7 +4975,21 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
49754975 // a flow collection (`{ a: b,\n c }` / `a: { b: c,\n d }`) is a multi-line begin/end region of
49764976 // its own — a `{`/`[` before the `:` means the `:` is a FLOW separator, not a block one, so the
49774977 // region must NOT open and steal those lines from #flow-mapping/#flow-sequence.
4978- const plainVp = `(?:(?:${ docAlt } )[\\t ]+)?(?:(?:${ compactCls } [\\t ]+)+(?:${ flowEx } *?${ kvSep } [\\t ]+)?|${ flowEx } *?${ kvSep } [\\t ]+)(?=${ plainSrc } )` ;
4978+ // The key-scan up to the `:` separator. A leading / embedded QUOTED scalar is consumed as one
4979+ // WHOLE escape-aware token (`fc.dq`/`fc.sq`) so its INTERNAL `:` is never mistaken for the key
4980+ // separator: a line-start double/single-quoted scalar with an inner colon (`"a: b"`) is ONE
4981+ // scalar, not a `key: value`, and must NOT open a fold (it falls to #dquote/#squote). The bare
4982+ // run excludes the quote chars so a quote can ONLY match via the token branch — otherwise the
4983+ // engine skips the (optional) token and the bare class re-swallows the opening quote, re-mis-
4984+ // reading the inner colon. Derived from the grammar's quoted-scalar tokens; a grammar with no
4985+ // quoted scalar keeps the plain `flowEx` scan (byte-identical).
4986+ const fcQuote = detectFlowCollections ( grammar ) ;
4987+ const quotedScalarToks = [ fcQuote ?. dq , fcQuote ?. sq ] . filter ( ( s ) : s is string => ! ! s ) ;
4988+ const quoteCharCls = quotedScalarToks . map ( t => escapeForCharClass ( t [ 0 ] === '\\' ? t . slice ( 0 , 2 ) : t [ 0 ] ) ) . join ( '' ) ;
4989+ const keyToSep = quotedScalarToks . length
4990+ ? `(?:${ quotedScalarToks . join ( '|' ) } |${ flowEx . slice ( 0 , - 1 ) } ${ quoteCharCls } ])*?`
4991+ : `${ flowEx } *?` ;
4992+ const plainVp = `(?:(?:${ docAlt } )[\\t ]+)?(?:(?:${ compactCls } [\\t ]+)+(?:${ keyToSep } ${ kvSep } [\\t ]+)?|${ keyToSep } ${ kvSep } [\\t ]+)(?=${ plainSrc } )` ;
49794993 // Header-line token includes: the same shape any plain `key: value` line gets, so the header is
49804994 // scoped identically to the top level (only the CONTINUATION changes). Includes the typed-value
49814995 // tokens (`#num`/`#boolnull`) so a SINGLE-line `a: 1` keeps `constant.numeric`, and the full
@@ -5015,6 +5029,27 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
50155029 topPatterns . push ( { include : '#plain-continuation' } ) ;
50165030 }
50175031
5032+ // ── 2a‴. Multi-line EXPLICIT-KEY continuation (`? a\n true`) ──
5033+ // An explicit key (`? a`) may FOLD across deeper continuation lines exactly like a plain value —
5034+ // `? a\n true` is the ONE key "a true" (CST: a single key scalar). #plain-continuation already
5035+ // opens on `? a` (its `?` is in `compactCls`), but it scopes the continuation as the VALUE plain
5036+ // scope (`string.unquoted`); a KEY continuation must instead take the KEY scope (entity.name.tag),
5037+ // so the folded key reads consistently with its first line (which #explicit-key scopes as the key).
5038+ // Same structure as #plain-continuation, but pinned to the explicit-key INDICATOR (so a `- ` seq /
5039+ // `key:` value fold stays on #plain-continuation) and the continuation takes the key scope. Ranked
5040+ // ABOVE #plain-continuation (scopeOrder) so a `? `-led header takes the key-scoped continuation.
5041+ const ekFold = detectExplicitKey ( grammar ) ;
5042+ if ( ekFold && fold . hasDeeper ) {
5043+ const ekContRule = { match : '\\G[\\t ]+(?:[^#\\n]|#(?<=[^\\t\\n\\f\\r ]#))*' , name : `${ ekFold . keyScope } .${ langName } ` } ;
5044+ repository [ 'explicit-key-continuation' ] = emitIndentRegion ( {
5045+ lookahead : `(?=${ escapeRegex ( ekFold . indicator ) } [\\t ]+(?:${ keyToSep } ${ kvSep } [\\t ]+)?(?=${ plainSrc } ))` ,
5046+ cont : `\\1[ \\t]+(?!${ structAhead } )` ,
5047+ blankFirst : true ,
5048+ patterns : [ ekContRule , ...plainHeaderIncs ] ,
5049+ } ) ;
5050+ topPatterns . push ( { include : '#explicit-key-continuation' } ) ;
5051+ }
5052+
50185053 // ── 2a″. BARE plain-scalar SAME-COLUMN fold (monogram#12 §9) ──
50195054 // A plain scalar that is itself a NODE (a document value, or the leading value of an indented
50205055 // block) — NOT a `key:`/`-`/`?` — folds across SAME-COLUMN as well as deeper continuation lines:
@@ -5126,19 +5161,22 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
51265161 // scalar tokens, which paint it as a stray `string.unquoted`. But a `%` can never BEGIN a plain
51275162 // scalar (YAML §7.3.3 — `%` is a c-indicator, excluded from ns-plain-first), so a `%`-led line the
51285163 // clean directive tokens did NOT claim is always a malformed directive, never real scalar content.
5129- // Re-scope the whole line as an invalid directive. The indicator (`%`) is read from the directive
5130- // tokens' leading literal (never hardcoded); ranked just BELOW the clean directives and ABOVE the
5131- // plain scalars (scopeOrder 6.5) so it only catches what they left and beats the stray-scalar mis-
5132- // scope. Highlight-only — the parser still rejects the line. The `^` anchor pins it to a line-start
5133- // `%` (an indented `%` mid-line — e.g. a `key: %v` value — is left to the scalar tokens).
5164+ // Re-scope the whole line AS A DIRECTIVE (keyword.other.directive) — the malformed trailing token is
5165+ // directive content (#4 `%YAML 1.2 foo`, #8 glued `%YAML 1.1#…`), and Monogram highlights questionable-
5166+ // but-renderable content NORMALLY rather than splashing `invalid.illegal` (the #12 #3 stance; this also
5167+ // matches the neutral `yaml`-CST oracle, which recovers such a line as a directive). The indicator
5168+ // (`%`) is read from the directive tokens' leading literal (never hardcoded); ranked just BELOW the
5169+ // clean directives and ABOVE the plain scalars (scopeOrder 6.5) so it only catches what they left and
5170+ // beats the stray-scalar mis-scope. Highlight-only — the parser still rejects the line. The `^` anchor
5171+ // pins it to a line-start `%` (an indented `%` mid-line — e.g. a `key: %v` value — stays a scalar).
51345172 const directiveToks = grammar . tokens . filter ( t => / ( ^ | \. ) k e y w o r d \. o t h e r \. d i r e c t i v e ( \. | $ ) / . test ( t . scope ?? '' ) ) ;
51355173 if ( directiveToks . length ) {
51365174 const lead = directiveToks . map ( t => tokenPatternLeadingSource ( t ) ) . find ( ( s ) : s is string => ! ! s ) ;
51375175 const indicator = lead ? [ ...lead ] [ 0 ] : '' ;
51385176 if ( indicator ) {
51395177 repository [ 'directive-malformed' ] = {
51405178 match : `^[ \\t]*(${ escapeRegex ( indicator ) } [^\\n]*?)[\\t ]*$` ,
5141- captures : { '1' : { name : `invalid.illegal. keyword.other.directive.${ langName } ` } } ,
5179+ captures : { '1' : { name : `keyword.other.directive.${ langName } ` } } ,
51425180 } ;
51435181 topPatterns . push ( { include : '#directive-malformed' } ) ;
51445182 }
@@ -7648,6 +7686,11 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
76487686 // deeper `!`/digit/`%` line falls through to #tag/#num/#directive). It ranks BELOW the
76497687 // block-scalar regions (≤ 0.6) so a `key: |` keeps its block-scalar region — its lookahead requires
76507688 // a real plain VALUE head (never `|`/`>`), so the two never collide on the same line anyway.
7689+ // The explicit-key continuation (`? a\n true`) must out-rank #plain-continuation (0.7): both open
7690+ // on a `? `-led header (the `?` is in compactCls), but the explicit-key variant scopes the folded
7691+ // continuation as the KEY (entity.name.tag), so it must win for the `?` case; #plain-continuation
7692+ // still handles `key:`/`- ` folds (its lookahead, unlike this one, is not pinned to the `?`).
7693+ if ( key === 'explicit-key-continuation' ) return 0.68 ;
76517694 if ( key === 'plain-continuation' ) return 0.7 ;
76527695 // The BARE plain-scalar same-column fold (§2a″) likewise begins AT LINE START and must out-rank the
76537696 // scalar tokens (#key/#num/#boolnull/#plain ≥ 0.8) so it opens on a bare value scalar and claims its
0 commit comments