Skip to content

Commit 9dbe313

Browse files
Drive YAML highlighter scope-gap to 100% (3 residual fixes) (#22)
1 parent 8ff330a commit 9dbe313

4 files changed

Lines changed: 121 additions & 12 deletions

File tree

src/gen-tm.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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 => /(^|\.)keyword\.other\.directive(\.|$)/.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

yaml.monarch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@
725725
}
726726
],
727727
[
728-
"%YAML[ \\t]+[0-9]+\\.[0-9]+(?=[ \\t]*(?:#|\\r|\\n|$))",
728+
"%YAML[ \\t]+[0-9]+\\.[0-9]+(?=[ \\t]*(?:\\r|\\n|$)|[ \\t]+#)",
729729
{
730730
"token": "keyword",
731731
"switchTo": "@value"

yaml.tmLanguage.json

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
{
7878
"include": "#blockscalar"
7979
},
80+
{
81+
"include": "#explicit-key-continuation"
82+
},
8083
{
8184
"include": "#plain-continuation"
8285
},
@@ -231,6 +234,9 @@
231234
{
232235
"include": "#blockscalar"
233236
},
237+
{
238+
"include": "#explicit-key-continuation"
239+
},
234240
{
235241
"include": "#plain-continuation"
236242
},
@@ -323,7 +329,7 @@
323329
},
324330
"yamldirective": {
325331
"name": "keyword.other.directive.yaml",
326-
"match": "%YAML[ \\t]+[0-9]+\\.[0-9]+(?=[ \\t]*(?:#|\\r|\\n|$))"
332+
"match": "%YAML[ \\t]+[0-9]+\\.[0-9]+(?=[ \\t]*(?:\\r|\\n|$)|[ \\t]+#)"
327333
},
328334
"directive": {
329335
"name": "keyword.other.directive.yaml",
@@ -1723,7 +1729,7 @@
17231729
]
17241730
},
17251731
"plain-continuation": {
1726-
"begin": "^([ \\t]*)(?=(?:(?:---|\\.\\.\\.)[\\t ]+)?(?:(?:[\\-?][\\t ]+)+(?:[^\\n\\[{\\]}]*?:[\\t ]+)?|[^\\n\\[{\\]}]*?:[\\t ]+)(?=(?:[^\\t\\n\\f\\r \\-?:,\\[\\]{}#&*!|>'\"%@`]|[\\-?:](?=[^\\t\\n\\f\\r ,\\[\\]{}]))(?:[^:#\\n,\\[\\]{}]|:(?=[^\\t\\n\\f\\r ,\\]}])|#(?<=[^\\t\\n\\f\\r ]#))*))",
1732+
"begin": "^([ \\t]*)(?=(?:(?:---|\\.\\.\\.)[\\t ]+)?(?:(?:[\\-?][\\t ]+)+(?:(?:\"(?:\\\\(?:[0abtnvfre\"/\\\\N_LP \\t]|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|\\r?\\n)|[^\"\\\\])*\"|'(?:''|[^'])*'|[^\\n\\[{\\]}\"'])*?:[\\t ]+)?|(?:\"(?:\\\\(?:[0abtnvfre\"/\\\\N_LP \\t]|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|\\r?\\n)|[^\"\\\\])*\"|'(?:''|[^'])*'|[^\\n\\[{\\]}\"'])*?:[\\t ]+)(?=(?:[^\\t\\n\\f\\r \\-?:,\\[\\]{}#&*!|>'\"%@`]|[\\-?:](?=[^\\t\\n\\f\\r ,\\[\\]{}]))(?:[^:#\\n,\\[\\]{}]|:(?=[^\\t\\n\\f\\r ,\\]}])|#(?<=[^\\t\\n\\f\\r ]#))*))",
17271733
"while": "\\G(?=[ \\t]*$|\\1[ \\t]+(?!(?:#|-[\\t ]|\\?[\\t ]|[^\\n\\[{\\]}]*?:(?:[\\t ]|$))))",
17281734
"patterns": [
17291735
{
@@ -1777,6 +1783,61 @@
17771783
}
17781784
]
17791785
},
1786+
"explicit-key-continuation": {
1787+
"begin": "^([ \\t]*)(?=\\?[\\t ]+(?:(?:\"(?:\\\\(?:[0abtnvfre\"/\\\\N_LP \\t]|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|\\r?\\n)|[^\"\\\\])*\"|'(?:''|[^'])*'|[^\\n\\[{\\]}\"'])*?:[\\t ]+)?(?=(?:[^\\t\\n\\f\\r \\-?:,\\[\\]{}#&*!|>'\"%@`]|[\\-?:](?=[^\\t\\n\\f\\r ,\\[\\]{}]))(?:[^:#\\n,\\[\\]{}]|:(?=[^\\t\\n\\f\\r ,\\]}])|#(?<=[^\\t\\n\\f\\r ]#))*))",
1788+
"while": "\\G(?=[ \\t]*$|\\1[ \\t]+(?!(?:#|-[\\t ]|\\?[\\t ]|[^\\n\\[{\\]}]*?:(?:[\\t ]|$))))",
1789+
"patterns": [
1790+
{
1791+
"match": "\\G[\\t ]+(?:[^#\\n]|#(?<=[^\\t\\n\\f\\r ]#))*",
1792+
"name": "entity.name.tag.yaml"
1793+
},
1794+
{
1795+
"include": "#docstart"
1796+
},
1797+
{
1798+
"include": "#docend"
1799+
},
1800+
{
1801+
"include": "#explicit-key"
1802+
},
1803+
{
1804+
"include": "#explicit-key-indicator"
1805+
},
1806+
{
1807+
"include": "#dquotekey"
1808+
},
1809+
{
1810+
"include": "#squotekey"
1811+
},
1812+
{
1813+
"include": "#key"
1814+
},
1815+
{
1816+
"include": "#anchor"
1817+
},
1818+
{
1819+
"include": "#alias"
1820+
},
1821+
{
1822+
"include": "#tag"
1823+
},
1824+
{
1825+
"include": "#num"
1826+
},
1827+
{
1828+
"include": "#boolnull"
1829+
},
1830+
{
1831+
"include": "#plain"
1832+
},
1833+
{
1834+
"include": "#comment"
1835+
},
1836+
{
1837+
"include": "#punctuation"
1838+
}
1839+
]
1840+
},
17801841
"plain-bare-fold": {
17811842
"begin": "^([ \\t]*)(?=(?:[^\\t\\n\\f\\r \\-?:,\\[\\]{}#&*!|>'\"%@`]|[\\-?:](?=[^\\t\\n\\f\\r ,\\[\\]{}]))(?:[^:#\\n,\\[\\]{}]|:(?=[^\\t\\n\\f\\r ,\\]}])|#(?<=[^\\t\\n\\f\\r ]#))*)(?!(?:#|-[\\t ]|\\?[\\t ]|(?:---|\\.\\.\\.)(?:[\\t ]|$)|[^\\n\\[{\\]}]*?:(?:[\\t ]|$)))",
17821843
"while": "\\G(?=[ \\t]*$|\\1(?=[ \\t]*\\S)(?![ \\t]*(?:#|-[\\t ]|\\?[\\t ]|(?:---|\\.\\.\\.)(?:[\\t ]|$)|[^\\n\\[{\\]}]*?:(?:[\\t ]|$))))",
@@ -1951,7 +2012,7 @@
19512012
"match": "^[ \\t]*(%[^\\n]*?)[\\t ]*$",
19522013
"captures": {
19532014
"1": {
1954-
"name": "invalid.illegal.keyword.other.directive.yaml"
2015+
"name": "keyword.other.directive.yaml"
19552016
}
19562017
}
19572018
},

yaml.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ const Tag = token(seq('!', altPattern(seq('<', star(noneOf('>')), '>'), star(non
7474
// token and the stray `%` then fails to lex → reject (H7TQ / ZYU8). The trailing comment is left
7575
// OUTSIDE the token (only looked at) so a ` # comment` is tokenised/scoped as a Comment, not folded
7676
// into the directive — keeps the highlighter's comment scope intact.
77-
const YamlDirective = token(seq('%YAML', plus(hspace), plus(digit), '.', plus(digit), followedBy(seq(star(hspace), altPattern('#', '\r', '\n', end())))), { scope: 'keyword.other.directive', blockOnly: true });
77+
// The trailing context is EOL (with optional trailing spaces) OR a real ` #` comment — a comment
78+
// indicator needs a PRECEDING space (§6.6, the same rule the Comment token's `notPrecededBy` guard
79+
// applies), so a GLUED `#` (`%YAML 1.1#…`) is NOT a comment and makes the lookahead FAIL: the line
80+
// then matches no directive token and the highlighter scopes the whole malformed line as a directive
81+
// (gen-tm `#directive-malformed`) instead of leaving the glued `#…` stray (monogram#12 #8).
82+
const YamlDirective = token(seq('%YAML', plus(hspace), plus(digit), '.', plus(digit), followedBy(altPattern(seq(star(hspace), altPattern('\r', '\n', end())), seq(plus(hspace), '#')))), { scope: 'keyword.other.directive', blockOnly: true });
7883
// Directive (`%TAG …`, unknown `%FOO …`): runs to EOL but stops before a ` #` trailing comment — a
7984
// `#` is a comment indicator only after whitespace, so a glued `#` (`%YAML 1.1#x`) stays part of
8085
// the directive while a spaced ` # comment` falls to the Comment token (same rule as plain scalars).

0 commit comments

Comments
 (0)