Skip to content

Commit 9c0f6dd

Browse files
committed
Scope anchors / tags in YAML explicit-key position
In `? &a key` / `? !!t key` the node-prefix anchor/tag was consumed by a NON-capturing group in the explicit-key rule and left unscoped (it fell to bare `source`). The justification was "anchors aren't a graded role" — no longer true. detectExplicitKey now returns the prefix tokens grouped BY SCOPE, and the explicit-key rule emits one CAPTURING group per scope (anchor → entity.name.type.anchor, tag → storage.type.tag), with the key scalar's capture index shifted past them. A `*`-repeated capture takes the last match, which scopes the common single decorator correctly; the key and the plain `? key` form are unchanged. YAML scope-gap anchor 82/83 -> 83/83 (gap to official +0.1). Other grammars byte-identical. Refs #12
1 parent f2f0066 commit 9c0f6dd

2 files changed

Lines changed: 34 additions & 19 deletions

File tree

src/gen-tm.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3271,7 +3271,7 @@ function detectDeclarations(grammar: CstGrammar, tokenNames: Set<string>): DeclI
32713271
* the lookahead (same head, broadest scope) supplies the key BODY pattern.
32723272
* Returns null when the shape is absent (so non-YAML grammars are unaffected).
32733273
*/
3274-
function detectExplicitKey(grammar: CstGrammar): { indicator: string; keyScope: string; keyBody: string; prefix: string | null } | null {
3274+
function detectExplicitKey(grammar: CstGrammar): { indicator: string; keyScope: string; keyBody: string; prefixGroups: { scope: string; pattern: string }[] } | null {
32753275
if (!grammar.indent) return null;
32763276

32773277
// Find a separator literal S that heads a nested seq sibling of a head-indicator literal I.
@@ -3322,15 +3322,13 @@ function detectExplicitKey(grammar: CstGrammar): { indicator: string; keyScope:
33223322
// placeholders are `(?!)` and excluded). Their patterns join into an optional, repeatable
33233323
// `(?:<pat>[\t ]+)*` group inserted between the indicator's whitespace and the key body — so a
33243324
// node decorated by any run of anchors/tags still resolves the trailing scalar as the key.
3325-
// The prefix group is NON-CAPTURING: it CONSUMES the anchor/tag so the key (group 3) is reached,
3326-
// which means an anchor/tag sitting in this rare `? <prefix> key` position is left UNSCOPED
3327-
// (variable-length, order-free → no fixed capture can scope it, and a begin/end region risks
3328-
// over-running the value on the next line). Those decorators are NOT a graded role (the oracle
3329-
// emits only key/value scalars + comments), and the KEY — the role that matters — is scoped
3330-
// correctly; outside the explicit-key indicator, anchors/tags keep their normal `#anchor`/`#tag`
3331-
// scopes. Derived from the grammar; `null` when the family has no such prefix token (most grammars).
3325+
// The prefix tokens are grouped BY SCOPE so §2b can emit one CAPTURING group per scope (anchor →
3326+
// entity.name.type.anchor, tag → storage.type.tag): a `*`-repeated capture takes the LAST match,
3327+
// which scopes the common single `? &a key` / `? !!t key` correctly (a rare multi-decorator run
3328+
// scopes only its last anchor + last tag — acceptable). Derived from the grammar; empty when the
3329+
// family has no such prefix token (most grammars).
33323330
const tokenByName = new Map(grammar.tokens.map(t => [t.name, t] as const));
3333-
const prefixPats = new Set<string>();
3331+
const prefixByScope = new Map<string, Set<string>>();
33343332
// A NODE-prefix token is an `opt(token)` that decorates a value: it sits in a seq whose tail
33353333
// (after it) eventually reaches the VALUE ALTERNATION — i.e. the seq contains an `alt` later on.
33363334
// This selects YAML's `opt(Anchor)`/`opt(Tag)` at the head of `Node`/`InlineNode` (each followed
@@ -3352,7 +3350,9 @@ function detectExplicitKey(grammar: CstGrammar): { indicator: string; keyScope:
33523350
const it = e.items[i];
33533351
if (it.type === 'quantifier' && it.kind === '?' && it.body.type === 'ref' && seqHasAltLater(e.items, i + 1)) {
33543352
const t = tokenByName.get(it.body.name);
3355-
if (t && !tokenPatternIsNever(t) && !t.string) prefixPats.add(tokenPatternSource(t));
3353+
if (t && !tokenPatternIsNever(t) && !t.string && t.scope) {
3354+
(prefixByScope.get(t.scope) ?? prefixByScope.set(t.scope, new Set()).get(t.scope)!).add(tokenPatternSource(t));
3355+
}
33563356
}
33573357
}
33583358
e.items.forEach(findPrefix);
@@ -3361,10 +3361,8 @@ function detectExplicitKey(grammar: CstGrammar): { indicator: string; keyScope:
33613361
else if (e.type === 'sep') findPrefix(e.element);
33623362
};
33633363
for (const r of grammar.rules) findPrefix(r.body);
3364-
const prefix = prefixPats.size
3365-
? `(?:(?:${[...prefixPats].join('|')})[\\t ]+)*`
3366-
: null;
3367-
return { indicator, keyScope, keyBody, prefix };
3364+
const prefixGroups = [...prefixByScope].map(([scope, pats]) => ({ scope, pattern: [...pats].join('|') }));
3365+
return { indicator, keyScope, keyBody, prefixGroups };
33683366
}
33693367

33703368
// ── Flow-collection detection (YAML `{ … }` mapping / `[ … ]` sequence) ──
@@ -4654,14 +4652,25 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
46544652
// (the indicator literal, the key scope, the key body); null for non-indentation grammars.
46554653
const explicitKey = detectExplicitKey(grammar);
46564654
if (explicitKey) {
4657-
// `(indicator)( whitespace )(optional node-prefix: anchors/tags)(key-scalar)` on one line —
4658-
// the dominant explicit-key shape. The prefix group is NON-capturing so the key stays group 3.
4659-
const prefixGroup = explicitKey.prefix ?? '';
4655+
// `(indicator)( whitespace )(optional node-prefix: anchors/tags)(key-scalar)` on one line — the
4656+
// dominant explicit-key shape. The node-prefix decorators (`? &a key` / `? !!t key`) get one
4657+
// CAPTURING group per scope so they are scoped (anchor/tag), not silently consumed; the key
4658+
// scalar's capture index follows them: 1=indicator, 2=ws, 3…N=prefix scopes, N+1=key.
4659+
const prefixCaps: Record<string, { name: string }> = {};
4660+
const prefixAlts: string[] = [];
4661+
let grp = 3;
4662+
for (const pg of explicitKey.prefixGroups) {
4663+
prefixAlts.push(`(${pg.pattern})`);
4664+
prefixCaps[String(grp)] = { name: `${pg.scope}.${langName}` };
4665+
grp++;
4666+
}
4667+
const prefixGroup = prefixAlts.length ? `(?:(?:${prefixAlts.join('|')})[\\t ]+)*` : '';
46604668
repository['explicit-key'] = {
46614669
match: `(${escapeRegex(explicitKey.indicator)})([\\t ]+)${prefixGroup}(${explicitKey.keyBody})`,
46624670
captures: {
46634671
'1': { name: `punctuation.definition.map.key.${langName}` },
4664-
'3': { name: `${explicitKey.keyScope}.${langName}` },
4672+
...prefixCaps,
4673+
[String(grp)]: { name: `${explicitKey.keyScope}.${langName}` },
46654674
},
46664675
};
46674676
topPatterns.push({ include: '#explicit-key' });

yaml.tmLanguage.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,18 @@
215215
"match": "(?!)"
216216
},
217217
"explicit-key": {
218-
"match": "(\\?)([\\t ]+)(?:(?:&[^\\t\\n\\f\\r \\[\\]{},]+|!(?:<[^>]*>|[^\\t\\n\\f\\r \\[\\]{},]*))[\\t ]+)*((?:[^\\t\\n\\f\\r \\-?:,\\[\\]{}#&*!|>'\"%@`]|[\\-?:](?=[^\\t\\n\\f\\r ,\\[\\]{}]))(?:[^:#\\n,\\[\\]{}]|:(?=[^\\t\\n\\f\\r ,\\]}])|#(?<=[^\\t\\n\\f\\r ]#))*)",
218+
"match": "(\\?)([\\t ]+)(?:(?:(&[^\\t\\n\\f\\r \\[\\]{},]+)|(!(?:<[^>]*>|[^\\t\\n\\f\\r \\[\\]{},]*)))[\\t ]+)*((?:[^\\t\\n\\f\\r \\-?:,\\[\\]{}#&*!|>'\"%@`]|[\\-?:](?=[^\\t\\n\\f\\r ,\\[\\]{}]))(?:[^:#\\n,\\[\\]{}]|:(?=[^\\t\\n\\f\\r ,\\]}])|#(?<=[^\\t\\n\\f\\r ]#))*)",
219219
"captures": {
220220
"1": {
221221
"name": "punctuation.definition.map.key.yaml"
222222
},
223223
"3": {
224+
"name": "entity.name.type.anchor.yaml"
225+
},
226+
"4": {
227+
"name": "storage.type.tag.yaml"
228+
},
229+
"5": {
224230
"name": "entity.name.tag.yaml"
225231
}
226232
}

0 commit comments

Comments
 (0)