Skip to content

Commit eee32a7

Browse files
committed
Give YAML flow punctuation specific scopes (grammar-declared, agnostic)
Flow `{ } [ ] ,` were all scoped a generic `punctuation`, so the strict flowPunct role graded family-not-exact — the dominant exact% drain (Monogram correct 99.2% but exact only 82.2%). Emit the TextMate-convention scopes instead: `punctuation.definition.{mapping,sequence}.{begin,end}` for the brackets, `punctuation.separator.{mapping,sequence}` for the comma, and `punctuation.separator.key-value` for the flow `:`. Kept gen-tm AGNOSTIC: the language-specific names (`mapping`/`sequence` — a JS `{}` is an "object") live in the grammar via a new `IndentConfig.flowScopes`, declared in yaml.ts and threaded through detectFlowCollections (same pattern as blockScalar. indicatorScope); gen-tm only appends `.${lang}` and falls back to the generic `punctuation` when nothing is declared, so a grammar with no flowScopes is byte-identical. Block-context `:`/`-` stay generic — the specificity is flow-only. YAML exact 82.2% -> 98.5% (now also ahead of official's 98.4%); correct% unchanged at 99.2%, gap +0.2; parser 100% aligned; agnostic 9/9; other six grammars byte-identical. (The ~14 flow-comma tokens both grammars "miss" are an answer-key over-emission — a `,` inside a tag-URI / %TAG / comment — not a grammar gap.)
1 parent 84b3818 commit eee32a7

4 files changed

Lines changed: 73 additions & 19 deletions

File tree

src/gen-tm.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3388,7 +3388,10 @@ function detectExplicitKey(grammar: CstGrammar): { indicator: string; keyScope:
33883388
// • the KEY scope + plain-scalar shape come from the same Key/Plain token pair detectExplicitKey
33893389
// uses (a scalar token whose pattern is `<plain>(?=…sep…)` over a broader plain scalar).
33903390
// Returns null when the family has no flow collections (every non-YAML grammar).
3391-
interface FlowColl { open: string; close: string; sep: string; colon: string | null; }
3391+
// `punct` (when present) are the SPECIFIC open/close/separator scopes the grammar declared for THIS
3392+
// collection via `indent.flowScopes.byOpen[open]` (null → fall back to the generic `punctuation.*`).
3393+
// Scope strings are bare (no `.${lang}` suffix); the region builder appends it.
3394+
interface FlowColl { open: string; close: string; sep: string; colon: string | null; punct: { begin: string; end: string; separator: string } | null; }
33923395
function detectFlowCollections(grammar: CstGrammar): {
33933396
colls: FlowColl[];
33943397
plainStart: string; // plain-scalar LEADING char class (no enclosing group)
@@ -3400,6 +3403,8 @@ function detectFlowCollections(grammar: CstGrammar): {
34003403
sqScope: string | null; // single-quoted scalar scope
34013404
dqEscape: string | null; // in-string escape sub-pattern for the double quote
34023405
sqEscape: string | null; // in-string escape sub-pattern for the single quote
3406+
keyValueScope: string | null; // declared `:` key/value separator scope (indent.flowScopes.keyValue), bare
3407+
explicitKeyScope: string | null; // declared `?` explicit-key indicator scope (indent.flowScopes.explicitKey), bare
34033408
} | null {
34043409
const indent = grammar.indent;
34053410
if (!indent || !indent.flowOpen?.length || !indent.flowClose?.length) return null;
@@ -3447,7 +3452,10 @@ function detectFlowCollections(grammar: CstGrammar): {
34473452
const c = eLits.find(l => l.length === 1 && l !== sep && l !== open && l !== close && !/[\w\s]/.test(l));
34483453
if (c) { colon = c; break; }
34493454
}
3450-
colls.push({ open, close, sep, colon });
3455+
// The grammar may DECLARE specific open/close/separator scopes for this collection (keyed by the
3456+
// open bracket); absent → null → the region builder uses the generic `punctuation.${lang}`.
3457+
const punct = indent.flowScopes?.byOpen?.[open] ?? null;
3458+
colls.push({ open, close, sep, colon, punct });
34513459
}
34523460
if (!colls.length) return null;
34533461

@@ -3484,6 +3492,8 @@ function detectFlowCollections(grammar: CstGrammar): {
34843492
dq: dqTok ? tokenPatternSource(dqTok) : null, sq: sqTok ? tokenPatternSource(sqTok) : null,
34853493
dqScope: dqTok?.scope ?? null, sqScope: sqTok?.scope ?? null,
34863494
dqEscape: quoteEscape(dqTok), sqEscape: quoteEscape(sqTok),
3495+
keyValueScope: indent.flowScopes?.keyValue ?? null,
3496+
explicitKeyScope: indent.flowScopes?.explicitKey ?? null,
34873497
};
34883498
}
34893499

@@ -4861,6 +4871,12 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
48614871
if (flow) {
48624872
const ln = langName;
48634873
const P = `punctuation.${ln}`;
4874+
// Resolve a DECLARED bare scope (from indent.flowScopes) to a full scope, or fall back to the
4875+
// generic `P`. The grammar supplies the language-flavoured names (mapping/sequence/key-value);
4876+
// the engine only appends the language suffix — so gen-tm stays agnostic (no hardcoded names).
4877+
const withLang = (bare: string | null): string => bare ? `${bare}.${ln}` : P;
4878+
const kvScope = withLang(flow.keyValueScope); // the flow-mapping `:` key/value separator
4879+
const ekScope = withLang(flow.explicitKeyScope); // the flow `?` explicit-key indicator
48644880
// Comment includes (derived from the grammar's comment-scoped tokens — not a hardcoded key),
48654881
// spread into every flow sub-region so a `#…` comment is scoped even mid-entry / mid-value.
48664882
const commentIncs = commentIncludeKeys.map(k => ({ include: `#${k}` }));
@@ -4905,12 +4921,12 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
49054921
// The VALUE colon (a flow separator `:`); and the JSON-style value colon glued after a closed
49064922
// key (`{"a":b}` — `:` preceded by a quote/closer/line-start, where no space-separator exists).
49074923
repository['flow-map-value'] = {
4908-
begin: colonSep, beginCaptures: { '0': { name: P } }, end: beforeClose,
4924+
begin: colonSep, beginCaptures: { '0': { name: kvScope } }, end: beforeClose,
49094925
patterns: [...commentIncs, { include: '#flow-node' }],
49104926
};
49114927
repository['flow-map-value-json'] = {
49124928
begin: `(?<=[${quoteCls}${closeCls}])[\\t ]*${eColon}|(?<=^)[\\t ]*${eColon}`,
4913-
beginCaptures: { '0': { name: P } }, end: beforeClose,
4929+
beginCaptures: { '0': { name: kvScope } }, end: beforeClose,
49144930
patterns: [...commentIncs, { include: '#flow-node' }],
49154931
};
49164932
// A plain scalar as a VALUE / as a KEY (entity.name.tag); both end at the flow boundary.
@@ -4931,24 +4947,29 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
49314947
// boundary opens a key region. Derived from the explicit-key indicator when present.
49324948
const qmark = explicitKey ? escapeRegex(explicitKey.indicator) : null;
49334949
const explicitKeyEntry: TmPattern[] = qmark ? [{
4934-
begin: `${qmark}(?=[\\s${closeCls}]|$)`, beginCaptures: { '0': { name: P } }, end: beforeClose,
4950+
begin: `${qmark}(?=[\\s${closeCls}]|$)`, beginCaptures: { '0': { name: ekScope } }, end: beforeClose,
49354951
patterns: [...commentIncs, { include: '#flow-mapping-map-key' }, { include: '#flow-map-value' }, { include: '#flow-node' }],
49364952
}] : [];
49374953

49384954
// Emit one region per collection. A MAPPING (`colon != null`): every entry-leading scalar is a
49394955
// key. A SEQUENCE: a scalar is a key only when a `:` separator follows (single-pair map element).
49404956
for (const c of flow.colls) {
49414957
const eOpen = escapeRegex(c.open), eClose = escapeRegex(c.close), eSep = escapeRegex(c.sep);
4958+
// The SPECIFIC open/close/separator scopes the grammar declared for THIS collection (mapping vs
4959+
// sequence — the names are the grammar's, not the engine's); null → the generic `P`.
4960+
const beginScope = c.punct ? `${c.punct.begin}.${ln}` : P;
4961+
const endScope = c.punct ? `${c.punct.end}.${ln}` : P;
4962+
const sepScope = c.punct ? `${c.punct.separator}.${ln}` : P;
49424963
if (c.colon) {
49434964
// A YAML flow mapping `{ … }` is a bracket region. CALLER predicate: detectFlowCollections
49444965
// found a collection rule (`OPEN … CLOSE` seq) whose entry rule carries a `:` key/value
49454966
// separator (`c.colon != null`). Recurse is via the body's `#flow-node` include (which
49464967
// re-includes #flow-mapping/#flow-sequence for nested collections).
49474968
repository['flow-mapping'] = emitBracketRegion({
49484969
name: `meta.flow.mapping.${ln}`,
4949-
openLit: eOpen, closeLit: eClose, beginCapName: P, endCapName: P,
4970+
openLit: eOpen, closeLit: eClose, beginCapName: beginScope, endCapName: endScope,
49504971
bodyPatterns: [
4951-
...commentIncs, { match: eSep, name: P },
4972+
...commentIncs, { match: eSep, name: sepScope },
49524973
{ include: '#flow-mapping-map-key' }, { include: '#flow-map-value-json' }, { include: '#flow-map-value' }, { include: '#flow-node' },
49534974
],
49544975
});
@@ -4968,9 +4989,9 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
49684989
// shape, but the entry rule has NO `:` separator (`c.colon == null`). Same #flow-node recurse.
49694990
repository['flow-sequence'] = emitBracketRegion({
49704991
name: `meta.flow.sequence.${ln}`,
4971-
openLit: eOpen, closeLit: eClose, beginCapName: P, endCapName: P,
4992+
openLit: eOpen, closeLit: eClose, beginCapName: beginScope, endCapName: endScope,
49724993
bodyPatterns: [
4973-
...commentIncs, { match: eSep, name: P },
4994+
...commentIncs, { match: eSep, name: sepScope },
49744995
{ include: '#flow-sequence-map-key' }, { include: '#flow-map-value-json' }, { include: '#flow-map-value' }, { include: '#flow-node' },
49754996
],
49764997
});

src/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,23 @@ export interface IndentConfig {
275275
newlineToken: string; // token TYPE emitted at a same-column line boundary (sibling separator)
276276
flowOpen?: string[]; // punctuation that suspends indentation while open (e.g. ['[', '{'])
277277
flowClose?: string[]; // matching closers (e.g. [']', '}'])
278+
// Per-collection SCOPES for the flow structural punctuation, keyed by the OPEN bracket. The flow
279+
// region the highlighter derives (gen-tm §2c) otherwise paints every `{ } [ ] ,` as a generic
280+
// `punctuation.${lang}` — graded only at the FAMILY tier. Declaring a `begin`/`end`/`separator`
281+
// scope here lets the open/close brackets and the in-collection comma carry the SPECIFIC
282+
// convention (a `{…}` is a "mapping", a `[…]` a "sequence" — language-FLAVOURED names that must
283+
// come from the grammar, not the neutral engine). `keyValue` (optional) re-scopes the `:`
284+
// key/value separator inside a flow mapping, and `explicitKey` the `?` explicit-key indicator.
285+
// Scope strings are WITHOUT the trailing `.${lang}` segment (gen-tm appends it, like
286+
// `blockScalar.indicatorScope`). Absent → the generic `punctuation.${lang}` (legacy). A bracket
287+
// pair with no entry in `byOpen` likewise falls back to the generic scope, so partial
288+
// declarations are safe. (e.g. YAML: `{` → punctuation.definition.mapping.begin, `}` → …end,
289+
// `,` in `{…}` → punctuation.separator.mapping; `[` → …sequence.begin, etc.)
290+
flowScopes?: {
291+
byOpen: Record<string, { begin: string; end: string; separator: string }>;
292+
keyValue?: string; // the flow-mapping `:` key/value separator (e.g. punctuation.separator.key-value)
293+
explicitKey?: string; // the flow `?` explicit-key indicator (e.g. punctuation.definition.key-value)
294+
};
278295
comment?: string; // line-comment introducer ignored for indentation (e.g. '#')
279296
// Block scalars (YAML `|` / `>`): when the rest of a line is an introducer + indicators, the
280297
// following more-indented lines are verbatim content emitted as ONE token (like raw-text, but

yaml.tmLanguage.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@
755755
"begin": ":(?=[\\s\\[\\],{}]|$)",
756756
"beginCaptures": {
757757
"0": {
758-
"name": "punctuation.yaml"
758+
"name": "punctuation.separator.key-value.yaml"
759759
}
760760
},
761761
"end": "(?=[\\[\\],{}])",
@@ -772,7 +772,7 @@
772772
"begin": "(?<=[\"'\\[\\],{}])[\\t ]*:|(?<=^)[\\t ]*:",
773773
"beginCaptures": {
774774
"0": {
775-
"name": "punctuation.yaml"
775+
"name": "punctuation.separator.key-value.yaml"
776776
}
777777
},
778778
"end": "(?=[\\[\\],{}])",
@@ -837,13 +837,13 @@
837837
"begin": "\\[",
838838
"beginCaptures": {
839839
"0": {
840-
"name": "punctuation.yaml"
840+
"name": "punctuation.definition.sequence.begin.yaml"
841841
}
842842
},
843843
"end": "\\]",
844844
"endCaptures": {
845845
"0": {
846-
"name": "punctuation.yaml"
846+
"name": "punctuation.definition.sequence.end.yaml"
847847
}
848848
},
849849
"patterns": [
@@ -852,7 +852,7 @@
852852
},
853853
{
854854
"match": ",",
855-
"name": "punctuation.yaml"
855+
"name": "punctuation.separator.sequence.yaml"
856856
},
857857
{
858858
"include": "#flow-sequence-map-key"
@@ -874,7 +874,7 @@
874874
"begin": "\\?(?=[\\s\\[\\],{}]|$)",
875875
"beginCaptures": {
876876
"0": {
877-
"name": "punctuation.yaml"
877+
"name": "punctuation.definition.key-value.yaml"
878878
}
879879
},
880880
"end": "(?=[\\[\\],{}])",
@@ -936,13 +936,13 @@
936936
"begin": "\\{",
937937
"beginCaptures": {
938938
"0": {
939-
"name": "punctuation.yaml"
939+
"name": "punctuation.definition.mapping.begin.yaml"
940940
}
941941
},
942942
"end": "\\}",
943943
"endCaptures": {
944944
"0": {
945-
"name": "punctuation.yaml"
945+
"name": "punctuation.definition.mapping.end.yaml"
946946
}
947947
},
948948
"patterns": [
@@ -951,7 +951,7 @@
951951
},
952952
{
953953
"match": ",",
954-
"name": "punctuation.yaml"
954+
"name": "punctuation.separator.mapping.yaml"
955955
},
956956
{
957957
"include": "#flow-mapping-map-key"
@@ -973,7 +973,7 @@
973973
"begin": "\\?(?=[\\s\\[\\],{}]|$)",
974974
"beginCaptures": {
975975
"0": {
976-
"name": "punctuation.yaml"
976+
"name": "punctuation.definition.key-value.yaml"
977977
}
978978
},
979979
"end": "(?=[\\[\\],{}])",

yaml.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,22 @@ const indent: IndentConfig = {
597597
newlineToken: 'Newline',
598598
flowOpen: ['[', '{'],
599599
flowClose: [']', '}'],
600+
// SPECIFIC scopes for the flow structural punctuation `{ } [ ] ,` (the TextMate / maintained-RedCMD
601+
// convention), keyed by the OPEN bracket. A `{…}` is a MAPPING, a `[…]` a SEQUENCE — these are
602+
// YAML-flavoured names (a JS `{}` is an "object", a `[]` an "array"), so they belong in the grammar,
603+
// not the agnostic engine; gen-tm only threads them onto the flow region's begin/end/separator
604+
// captures and appends the `.yaml` suffix. Without this every flow bracket/comma was a generic
605+
// `punctuation.yaml` (graded only FAMILY); declaring them makes the flowPunct role grade EXACT and
606+
// gives bracket-pair colourisers the begin/end pairing. The `:` key/value separator and `?`
607+
// explicit-key indicator (NOT graded by the oracle, but coloured for consistency with official).
608+
flowScopes: {
609+
byOpen: {
610+
'{': { begin: 'punctuation.definition.mapping.begin', end: 'punctuation.definition.mapping.end', separator: 'punctuation.separator.mapping' },
611+
'[': { begin: 'punctuation.definition.sequence.begin', end: 'punctuation.definition.sequence.end', separator: 'punctuation.separator.sequence' },
612+
},
613+
keyValue: 'punctuation.separator.key-value',
614+
explicitKey: 'punctuation.definition.key-value',
615+
},
600616
comment: '#',
601617
blockScalar: { introducers: ['|', '>'], token: 'BlockScalar', documentMarkers: ['---', '...'], indicatorScope: 'keyword.control.flow.block-scalar' },
602618
compactIndicators: ['-', '?'],

0 commit comments

Comments
 (0)