diff --git a/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs b/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs index 6b50dffc42..23205040ba 100644 --- a/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs +++ b/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs @@ -324,9 +324,10 @@ export const codeKeywordTokenizer = new ExternalTokenizer( // handle them without conflict. export const codeIdentTokenizer = new ExternalTokenizer( (input, stack) => { - const couldBeKey = stack.canShift(CodeArgKey) const couldBeIdent = stack.canShift(CodeIdent) - if (!couldBeKey && !couldBeIdent) return + // CodeArgKey detection is character-level (see below) — canShift is + // unreliable for it due to LALR merging, so don't gate on it here. + if (!couldBeIdent && !stack.canShift(CodeArgKey)) return // Guard: only fire in code context. // Walk back past whitespace to the nearest non-space character. @@ -360,22 +361,25 @@ export const codeIdentTokenizer = new ExternalTokenizer( if (KEYWORDS.has(word) || BOOLS.has(word)) return // Emit CodeArgKey when this identifier is a named arg key (followed by ':'). - // Pre-disambiguating here avoids relying on LALR lookahead to choose between - // codeArgItem alternatives, which is fragile under Lezer's state merging. + // canShift(CodeArgKey) is unreliable here because LALR state merging can + // fold the codeArgItem start state into others where CodeArgKey is absent. + // Instead, use character-level context: we are inside a call argument list + // only when the nearest non-whitespace predecessor is '(' or ','. That + // covers every named-arg position (#set text(size: …), align: …, etc.) + // while excluding keyword positions like '#show heading: …' (prev = 'g'). let isArgKey = false - if (couldBeKey) { + if (prev === OPEN_PAREN || prev === COMMA) { let afterLen = len while (input.peek(afterLen) === SPACE || input.peek(afterLen) === TAB) afterLen++ isArgKey = (input.peek(afterLen) === COLON) } for (let i = 0; i < len; i++) input.advance() - if (isArgKey && couldBeKey) { + if (isArgKey) { input.acceptToken(CodeArgKey) } else if (couldBeIdent) { input.acceptToken(CodeIdent) } - // else: neither token is valid in this state — don't emit }, { contextual: true } )