typst: distinguish function calls from value identifiers; fix math outline
Build and Deploy Verso / deploy (push) Successful in 9m49s

Named value identifiers like 'left', 'center', 'right' were being
highlighted as tok-function (blue) because codeValue used CallExpr
(callSuffix*), which styled any identifier in value position as a
function.  Fix: add FuncExpr { CodeIdent callSuffix+ } (requires at
least one argument list or method suffix) and use it in codeValue
instead of CallExpr.  Plain identifiers in value position now fall
through to CodeIdent → tok-variableName.  CallExpr (callSuffix*) is
kept for codeExprBody and KeywordExpr where zero-suffix idents are
valid.

Tokenizer safety: only acceptToken(CodeIdent) when canShift(CodeIdent)
is true, preventing emission in LALR-merged states where neither
CodeArgKey nor CodeIdent is expected.

Outline: track '$'-parity across lines so that lines inside a display
math block (e.g. '= b+c$') are not incorrectly reported as headings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-09 12:04:33 +00:00
parent 4c6032bce0
commit f2b7034b51
4 changed files with 57 additions and 22 deletions
@@ -23,16 +23,33 @@ const LEVELS: NestingLevel[] = [
// after it, so this stays clear of code.
const HEADING_REGEX = /^(=+)[ \t]+(.*\S)[ \t]*$/
// Count unescaped '$' signs on a line to track math-mode parity.
function countDollars(text: string): number {
let count = 0
for (let i = 0; i < text.length; i++) {
if (text[i] === '\\') { i++; continue }
if (text[i] === '$') count++
}
return count
}
function computeOutline(
state: EditorState
): ProjectionResult<FlatOutlineItem> {
const items: FlatOutlineItem[] = []
// Track whether we are inside a multi-line display math block.
// Each line with an odd number of unescaped '$' toggles the flag.
let inMath = false
for (let n = 1; n <= state.doc.lines; n++) {
const line = state.doc.line(n)
const match = HEADING_REGEX.exec(line.text)
if (!match) continue
const text = line.text
// Only attempt heading detection when not inside a math block.
// (e.g. '= b+c$' on the second line of '$ a \n= b+c$' must be skipped.)
if (!inMath) {
const match = HEADING_REGEX.exec(text)
if (match) {
const depth = match[1].length
const level = LEVELS[Math.min(depth, LEVELS.length) - 1]
// Strip a trailing line comment, then a trailing label.
@@ -50,6 +67,10 @@ function computeOutline(
level,
} as FlatOutlineItem)
}
}
if (countDollars(text) % 2 === 1) inMath = !inMath
}
return { items, status: ProjectionStatus.Complete }
}
@@ -49,10 +49,12 @@ export const TypstLanguage = LRLanguage.define({
CodeBool: t.atom,
// Identifiers:
// - direct child of CallExpr → function/method name
// - CodeArgKey (named arg key, emitted by tokenizer before ':') → attributeName
// - everywhere else → plain variable
// CallExpr/CodeIdent — top-level #func or after keywords (#set text) → function style
// FuncExpr/CodeIdent — func call inside a value expr (has args/method) → function style
// CodeArgKey — named arg key (tokenizer pre-disambiguates on ':') → attributeName
// CodeIdent — plain variable/constant reference (e.g. 'left', 'center') → variable
'CallExpr/CodeIdent': t.function(t.variableName),
'FuncExpr/CodeIdent': t.function(t.variableName),
CodeArgKey: t.attributeName,
CodeIdent: t.variableName,
@@ -370,7 +370,12 @@ export const codeIdentTokenizer = new ExternalTokenizer(
}
for (let i = 0; i < len; i++) input.advance()
input.acceptToken(isArgKey ? CodeArgKey : CodeIdent)
if (isArgKey && couldBeKey) {
input.acceptToken(CodeArgKey)
} else if (couldBeIdent) {
input.acceptToken(CodeIdent)
}
// else: neither token is valid in this state — don't emit
},
{ contextual: true }
)
@@ -74,7 +74,13 @@ codeExprBody {
KeywordExpr { CodeKeyword CallExpr? }
AtomExpr { CodeBool }
// CallExpr allows zero suffixes — used at top level (#x) and after keywords
// (#set text(...)) where even a bare identifier is valid as a named reference.
CallExpr { CodeIdent callSuffix* }
// FuncExpr requires at least one suffix — used inside codeValue so that
// 'table(...)' gets tok-function while plain identifiers like 'left'/'center'
// get tok-variableName instead of being false-positively styled as functions.
FuncExpr { CodeIdent callSuffix+ }
callSuffix {
CodeArgs |
"." CodeIdent
@@ -91,7 +97,8 @@ codeValue {
CodeString |
CodeNumber |
CodeBool |
CallExpr |
FuncExpr |
CodeIdent |
ContentBlock |
CodeBlock |
InlineMath