@@ -4790,6 +4790,96 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
47904790 patterns : [ bsIntroRule ( bsIndicator , bsContent ) , ...bsHeaderIncs ] ,
47914791 } ;
47924792 topPatterns . push ( { include : `#${ bsKey } -seq` } ) ;
4793+
4794+ // ── 2a′. Multi-line PLAIN scalar continuation (monogram#12 §6/§7) ──
4795+ // A plain scalar may FOLD across a more-indented continuation line (`key: a\n b` → "a b";
4796+ // `? e\n 42` → the key "e 42"). The parser's lexer folds these into one scalar token, but a
4797+ // flat per-line TextMate grammar cannot see that a deeper line is the CONTINUATION of the scalar
4798+ // above it — so a continuation line that opens with a token-like char (`!`/digit/`%`) is wrongly
4799+ // re-scanned as a tag / number / directive (`#tag`/`#num`/`#directive` fire at top level). The
4800+ // HEADER line is already scoped correctly by the normal token includes (`#plain` paints the value),
4801+ // so the fix is a REGION that consumes only the DEEPER continuation lines as opaque
4802+ // `string.unquoted`, BEFORE those token rules get a chance. This mirrors the parser's `foldedPlain`
4803+ // (`Plain Indent Plain* Dedent`): a value-position plain scalar followed by a MORE-indented run of
4804+ // BARE plain lines (a continuation line is never a `key:`/`-`/`?` node — that would be a sibling).
4805+ // Structure reuses the block-scalar region (§2a): the begin captures the node indent with a FORWARD
4806+ // group `^([ \t]*)`; the `while` continues into BLANK lines and DEEPER lines that are BARE plain
4807+ // content, and RELEASES at a SIBLING at the node column (so a same-column doc-body fold —
4808+ // monogram#12 §9 — never over-fires) AND at the first deeper STRUCTURAL line (a mapping `key:`, seq
4809+ // `- `, explicit `? `, or a comment). Releasing at a structural line is what lets a compact nested
4810+ // mapping (`- a: x\n b: 1`), a nested block scalar (`a: x\n b: |`), or a comment line fall back to
4811+ // the TOP-LEVEL patterns (which scope `b`/`|`/`#…` correctly) instead of being swallowed as string —
4812+ // it mirrors the parser's foldedPlain, whose continuation is bare `Plain` lines only. The header
4813+ // line is re-scoped by the normal token includes (`plainHeaderIncs`, matched at the value position
4814+ // the begin's indent-consume leaves the engine at); a `continuationRule` matches each DEEPER
4815+ // bare-plain line (`\G[\t ]+…`, which only fires at a re-anchored line start, so it never touches the
4816+ // header line) and scopes it as string, stopping before an inline ` #` comment (which then falls to
4817+ // `#comment`). Gated on a plain-scalar token, so the OTHER six grammars (no `#plain`) regenerate
4818+ // byte-identical.
4819+ if ( repository [ 'plain' ] ) {
4820+ // The plain-scalar match, used ONLY as a zero-width `(?=plainSrc)` value-head probe below. The
4821+ // flow→block body loosening (`loosenBlockScalar`, in the flow section further down) runs AFTER
4822+ // this point, so this is the flow-body snapshot — irrelevant here, since a lookahead only needs
4823+ // the HEAD char class (identical in both variants) to confirm a plain value opens the line.
4824+ const plainSrc = repository [ 'plain' ] . match ! ;
4825+ // The continuation is PLAIN-scalar content, so it takes the plain token's own scope
4826+ // (`string.unquoted.<lang>`) — not the block-scalar body scope `bsContent`
4827+ // (`string.unquoted.block.<lang>`), which would mis-label a folded plain run as a block scalar.
4828+ const plainContent = repository [ 'plain' ] . name ! ;
4829+ // A line (after its leading indent) that opens a STRUCTURAL node or comment, NOT plain content:
4830+ // a `#` comment, a `- `/`? ` indicator (a `-1`/`?x` plain scalar is NOT one — the indicator needs
4831+ // a trailing space), or a mapping key (`…: ` / `…:`-EOL). `[^\n{}\[\]]*?` stops the key scan at a
4832+ // flow bracket (a `{`/`[` is flow, handled elsewhere), and `:(?:[\t ]|$)` requires the colon to be
4833+ // a real key separator (`http://x` keeps its glued `:` → still plain content). Used as a NEGATIVE
4834+ // lookahead to bound the fold at the first sibling/comment line, matching the parser's foldedPlain.
4835+ const structAhead = `(?:#|-[\\t ]|\\?[\\t ]|[^\\n{}\\[\\]]*?:(?:[\\t ]|$))` ;
4836+ // Value-position lookahead: after the node indent is stripped, the line must carry an INLINE
4837+ // BLOCK plain value — either `<key>: <plain>` (mapping value) or a `-`/`?` indicator + `<plain>`
4838+ // (sequence entry / explicit key). The plain value is confirmed by `(?=plainSrc)`, which only
4839+ // succeeds on a real plain-scalar head — so a tag/anchor/quoted/`|`/`>` value never triggers it,
4840+ // and a BARE `key:` introducing a NESTED block (no inline value, EOL after `:`) is excluded
4841+ // because both arms REQUIRE a value: the `:` arm needs `:[\t ]+` (colon + space + plain), and the
4842+ // indicator arm needs `[-?][\t ]+` + plain. This is what keeps `key:\n nested: v` and the bare
4843+ // `a: b\nc: d` sibling out of the fold. The mapping-key run is `[^\n{}\[\]]*?` (NO flow brackets):
4844+ // a flow collection (`{ a: b,\n c }` / `a: { b: c,\n d }`) is a multi-line begin/end region of
4845+ // its own — a `{`/`[` before the `:` means the `:` is a FLOW separator, not a block one, so the
4846+ // region must NOT open and steal those lines from #flow-mapping/#flow-sequence.
4847+ const plainVp = `(?:(?:---|\\.\\.\\.)[\\t ]+)?(?:(?:[-?][\\t ]+)+(?:[^\\n{}\\[\\]]*?:[\\t ]+)?|[^\\n{}\\[\\]]*?:[\\t ]+)(?=${ plainSrc } )` ;
4848+ // Header-line token includes: the same shape any plain `key: value` line gets, so the header is
4849+ // scoped identically to the top level (only the CONTINUATION changes). Includes the typed-value
4850+ // tokens (`#num`/`#boolnull`) so a SINGLE-line `a: 1` keeps `constant.numeric`, and the full
4851+ // key/sequence/comment set so a deeper STRUCTURAL line (which continuationRule skips) is scoped
4852+ // correctly. Listed only when the key exists.
4853+ const plainHeaderIncs = [
4854+ ...( repository [ 'docstart' ] ? [ { include : '#docstart' } ] : [ ] ) ,
4855+ ...( repository [ 'docend' ] ? [ { include : '#docend' } ] : [ ] ) ,
4856+ ...( bsHasExplicitKey ? [ { include : '#explicit-key' } , { include : '#explicit-key-indicator' } ] : [ ] ) ,
4857+ ...( repository [ 'dquotekey' ] ? [ { include : '#dquotekey' } ] : [ ] ) ,
4858+ ...( repository [ 'squotekey' ] ? [ { include : '#squotekey' } ] : [ ] ) ,
4859+ ...( repository [ 'key' ] ? [ { include : '#key' } ] : [ ] ) ,
4860+ ...( repository [ 'anchor' ] ? [ { include : '#anchor' } ] : [ ] ) ,
4861+ ...( repository [ 'alias' ] ? [ { include : '#alias' } ] : [ ] ) ,
4862+ ...( repository [ 'tag' ] ? [ { include : '#tag' } ] : [ ] ) ,
4863+ ...( repository [ 'num' ] ? [ { include : '#num' } ] : [ ] ) ,
4864+ ...( repository [ 'boolnull' ] ? [ { include : '#boolnull' } ] : [ ] ) ,
4865+ { include : '#plain' } ,
4866+ ...( repository [ 'comment' ] ? [ { include : '#comment' } ] : [ ] ) ,
4867+ { include : '#punctuation' } ,
4868+ ] ;
4869+ // The continuation-line consumer: `\G[\t ]+` anchors at a re-anchored LINE START with ≥1 leading
4870+ // space (so it never matches the header line, where the engine sits past the indent) — the `while`
4871+ // has already proven the line is a bare-plain continuation — and the body `(?:[^#\n]|#(?<=\S#))*`
4872+ // swallows the line as one opaque plain run, stopping before an inline ` #` comment (a `#` is
4873+ // content only when glued to a non-space, exactly as the plain-scalar body treats it) so the
4874+ // comment falls to `#comment`. Scoped with the plain string scope.
4875+ const continuationRule = { match : '\\G[\\t ]+(?:[^#\\n]|#(?<=[^\\t\\n\\f\\r ]#))*' , name : plainContent } ;
4876+ repository [ 'plain-continuation' ] = {
4877+ begin : `^([ \\t]*)(?=${ plainVp } )` ,
4878+ while : `\\G(?=[ \\t]*$|\\1[ \\t]+(?!${ structAhead } ))` ,
4879+ patterns : [ continuationRule , ...plainHeaderIncs ] ,
4880+ } ;
4881+ topPatterns . push ( { include : '#plain-continuation' } ) ;
4882+ }
47934883 }
47944884
47954885 // ── 2b. Explicit mapping key (`? key`) ──
@@ -7344,6 +7434,14 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
73447434 if ( key === 'blockscalar-key' ) return 0.55 ;
73457435 if ( bsRank && key === `${ bsRank } -doc` ) return 0.58 ;
73467436 if ( bsRank && key === bsRank ) return 0.6 ;
7437+ // The multi-line plain-scalar continuation region (§2a′) also begins AT LINE START (`^([ \t]*)`),
7438+ // so it competes at column 0 with #key / quoted-keys / #docstart / #explicit-key (rank ≥ 0.8) and
7439+ // MUST out-rank them: on a same-start tie oniguruma picks the FIRST listed pattern, and the region
7440+ // must open so it can claim the continuation lines (else #key/#explicit-key win the header and a
7441+ // deeper `!`/digit/`%` line falls through to #tag/#num/#directive). It ranks BELOW the
7442+ // block-scalar regions (≤ 0.6) so a `key: |` keeps its block-scalar region — its lookahead requires
7443+ // a real plain VALUE head (never `|`/`>`), so the two never collide on the same line anyway.
7444+ if ( key === 'plain-continuation' ) return 0.7 ;
73477445 // A flow collection (`{ … }` / `[ … ]`) is a begin/end region opened by a bracket; it must be
73487446 // tried before #punctuation (which would otherwise claim the `{`/`[` as a bare bracket) and
73497447 // before the scalar tokens. Its `{`/`[` can never lead a plain scalar, so this ranking is safe.
0 commit comments