Skip to content

Commit 5a8f82b

Browse files
committed
Detect YAML plain-scalar folds from the rules instead of a proxy gate
The multi-line plain-scalar fold regions (gen-tm §2a′/§2a″) were emitted whenever a plain token existed (`repository['plain']`) — a proxy that re-encoded, by hand, fold logic the parser already expresses in its rules (foldedPlain / foldedPlainBlock / DocFold, assembled from the lexer's Indent/Dedent/Newline tokens). Drive the emission from the rules instead: a new `detectFold(grammar)` walks the rules and reports whether a plain-scalar LEAF token repeats across an indent boundary — `Indent <leaf> … Dedent` (a DEEPER continuation → §2a′) and/or `Newline <leaf>` (a SAME-COLUMN continuation → §2a″). A leaf is a token with a `blockPattern` + scope; an `Indent`/`Newline` followed by a RULE ref (a sequence item / mapping entry) is a sibling, not a fold, so only a leaf token after the boundary counts. Now each region is emitted only when its fold actually exists in the grammar: `#plain-continuation` iff `hasDeeper`, `#plain-bare-fold` iff `hasSameColumn`. An indentation grammar whose plain scalars never fold gets neither (no over-emission), so the highlighter generalises beyond YAML. This makes parser↔highlighter consistency by-construction: the fold is expressed once (in the rules) and consumed by both. YAML has both fold kinds, so both regions are still emitted and the generated grammar is BYTE-IDENTICAL (git diff yaml.tmLanguage.json empty) — proving this is purely a hand-gated → rule-detected change with zero output difference. yaml-issue12-regressions 6 pass / 4 known-bug / 0 regression; agnostic 9/9; sanity 15/15; scope-roles 47/47 + 9/9; RedCMD Onigmo diagnostics clean; the other six grammars unchanged. Refs #12
1 parent 9c21b3f commit 5a8f82b

1 file changed

Lines changed: 64 additions & 13 deletions

File tree

src/gen-tm.ts

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3256,6 +3256,45 @@ function detectDeclarations(grammar: CstGrammar, tokenNames: Set<string>): DeclI
32563256
* the lookahead (same head, broadest scope) supplies the key BODY pattern.
32573257
* Returns null when the shape is absent (so non-YAML grammars are unaffected).
32583258
*/
3259+
// Whether — and HOW — a grammar's plain scalars FOLD across lines, DETECTED from its rules (not
3260+
// hardcoded). The fold logic already lives in the PARSER rules (e.g. YAML's foldedPlain / foldedPlainBlock
3261+
// / DocFold, assembled from the lexer's Indent/Dedent/Newline tokens), so the derived highlighter reads
3262+
// it from there instead of re-encoding the same logic. A FOLD is a plain-scalar LEAF token that repeats
3263+
// across an indent boundary: `Indent <leaf> … Dedent` is a DEEPER continuation (§2a′), `Newline <leaf>`
3264+
// is a SAME-COLUMN continuation (§2a″). A leaf is a token declared with a `blockPattern` (the plain-scalar
3265+
// marker) and a scope. Crucially, an `Indent`/`Newline` followed by a RULE ref (a sequence item / mapping
3266+
// entry — e.g. BlockSequence's `many(Newline, SeqItem)`) is a SIBLING separator, not a fold — only a LEAF
3267+
// TOKEN after the boundary counts, which is what distinguishes a scalar continuing itself from a new node.
3268+
// Returns null when the grammar's plain scalars never fold → NO fold regions are emitted, so the
3269+
// highlighter generalises to any indentation language (a non-folding one gets nothing).
3270+
function detectFold(grammar: CstGrammar): { hasDeeper: boolean; hasSameColumn: boolean } | null {
3271+
if (!grammar.indent) return null;
3272+
const { indentToken, newlineToken } = grammar.indent;
3273+
const leafNames = new Set(grammar.tokens.filter(t => t.blockPattern && t.scope).map(t => t.name));
3274+
if (!leafNames.size) return null;
3275+
const refsLeaf = (e: RuleExpr): boolean =>
3276+
e.type === 'ref' ? leafNames.has(e.name)
3277+
: e.type === 'seq' || e.type === 'alt' ? e.items.some(refsLeaf)
3278+
: e.type === 'quantifier' || e.type === 'group' || e.type === 'not' ? refsLeaf(e.body)
3279+
: e.type === 'sep' ? refsLeaf(e.element)
3280+
: false;
3281+
const isRefTo = (e: RuleExpr, name: string): boolean => e.type === 'ref' && e.name === name;
3282+
let hasDeeper = false, hasSameColumn = false;
3283+
const visit = (e: RuleExpr): void => {
3284+
if (e.type === 'seq') {
3285+
for (let i = 0; i < e.items.length - 1; i++) {
3286+
if (isRefTo(e.items[i], indentToken) && refsLeaf(e.items[i + 1])) hasDeeper = true;
3287+
if (isRefTo(e.items[i], newlineToken) && refsLeaf(e.items[i + 1])) hasSameColumn = true;
3288+
}
3289+
e.items.forEach(visit);
3290+
} else if (e.type === 'alt') e.items.forEach(visit);
3291+
else if (e.type === 'quantifier' || e.type === 'group' || e.type === 'not') visit(e.body);
3292+
else if (e.type === 'sep') visit(e.element);
3293+
};
3294+
for (const r of grammar.rules) visit(r.body);
3295+
return (hasDeeper || hasSameColumn) ? { hasDeeper, hasSameColumn } : null;
3296+
}
3297+
32593298
function detectExplicitKey(grammar: CstGrammar): { indicator: string; keyScope: string; keyBody: string; prefixGroups: { scope: string; pattern: string }[] } | null {
32603299
if (!grammar.indent) return null;
32613300

@@ -4830,7 +4869,13 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
48304869
// header line) and scopes it as string, stopping before an inline ` #` comment (which then falls to
48314870
// `#comment`). Gated on a plain-scalar token, so the OTHER six grammars (no `#plain`) regenerate
48324871
// byte-identical.
4833-
if (repository['plain']) {
4872+
// Whether — and HOW — this grammar's plain scalars fold is DETECTED from its rules (detectFold):
4873+
// a deeper continuation (`Indent <leaf> … Dedent`) → the §2a′ region, a same-column continuation
4874+
// (`Newline <leaf>`) → the §2a″ region. A grammar whose plain scalars never fold gets NEITHER (no
4875+
// over-emission). This drives the fold-region emission FROM the grammar's fold rules — the same
4876+
// rules the parser uses — rather than from the `repository['plain']` proxy.
4877+
const fold = detectFold(grammar);
4878+
if (repository['plain'] && fold) {
48344879
// The plain-scalar match, used ONLY as a zero-width `(?=plainSrc)` value-head probe below. The
48354880
// flow→block body loosening (`loosenBlockScalar`, in the flow section further down) runs AFTER
48364881
// this point, so this is the flow-body snapshot — irrelevant here, since a lookahead only needs
@@ -4887,12 +4932,15 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
48874932
// content only when glued to a non-space, exactly as the plain-scalar body treats it) so the
48884933
// comment falls to `#comment`. Scoped with the plain string scope.
48894934
const continuationRule = { match: '\\G[\\t ]+(?:[^#\\n]|#(?<=[^\\t\\n\\f\\r ]#))*', name: plainContent };
4890-
repository['plain-continuation'] = {
4891-
begin: `^([ \\t]*)(?=${plainVp})`,
4892-
while: `\\G(?=[ \\t]*$|\\1[ \\t]+(?!${structAhead}))`,
4893-
patterns: [continuationRule, ...plainHeaderIncs],
4894-
};
4895-
topPatterns.push({ include: '#plain-continuation' });
4935+
// Emitted only when the grammar actually has a DEEPER fold (`Indent <leaf> … Dedent`).
4936+
if (fold.hasDeeper) {
4937+
repository['plain-continuation'] = {
4938+
begin: `^([ \\t]*)(?=${plainVp})`,
4939+
while: `\\G(?=[ \\t]*$|\\1[ \\t]+(?!${structAhead}))`,
4940+
patterns: [continuationRule, ...plainHeaderIncs],
4941+
};
4942+
topPatterns.push({ include: '#plain-continuation' });
4943+
}
48964944

48974945
// ── 2a″. BARE plain-scalar SAME-COLUMN fold (monogram#12 §9) ──
48984946
// A plain scalar that is itself a NODE (a document value, or the leading value of an indented
@@ -4922,12 +4970,15 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
49224970
// listed first, win the tie, so header typing is preserved. It stops before a ` #` comment (a `#`
49234971
// is content only when glued to a non-space, as the plain body treats it).
49244972
const bareCont = { match: '(?:[^#\\n]|#(?<=[^\\t\\n\\f\\r ]#))+', name: plainContent };
4925-
repository['plain-bare-fold'] = {
4926-
begin: `^([ \\t]*)(?=${plainSrc})(?!${structRelease})`,
4927-
while: `\\G(?=[ \\t]*$|\\1(?=[ \\t]*\\S)(?![ \\t]*${structRelease}))`,
4928-
patterns: [...plainHeaderIncs, bareCont],
4929-
};
4930-
topPatterns.push({ include: '#plain-bare-fold' });
4973+
// Emitted only when the grammar actually has a SAME-COLUMN fold (`Newline <leaf>`).
4974+
if (fold.hasSameColumn) {
4975+
repository['plain-bare-fold'] = {
4976+
begin: `^([ \\t]*)(?=${plainSrc})(?!${structRelease})`,
4977+
while: `\\G(?=[ \\t]*$|\\1(?=[ \\t]*\\S)(?![ \\t]*${structRelease}))`,
4978+
patterns: [...plainHeaderIncs, bareCont],
4979+
};
4980+
topPatterns.push({ include: '#plain-bare-fold' });
4981+
}
49314982
}
49324983
}
49334984

0 commit comments

Comments
 (0)