typst: distinguish function calls from value identifiers; fix math outline
Build and Deploy Verso / deploy (push) Successful in 9m49s
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:
+38
-17
@@ -23,32 +23,53 @@ 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
|
||||
|
||||
const depth = match[1].length
|
||||
const level = LEVELS[Math.min(depth, LEVELS.length) - 1]
|
||||
// Strip a trailing line comment, then a trailing label.
|
||||
const title = match[2]
|
||||
.replace(/\s*\/\/.*$/, '')
|
||||
.replace(/\s*<[\w-]+>\s*$/, '')
|
||||
.trim()
|
||||
// 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.
|
||||
const title = match[2]
|
||||
.replace(/\s*\/\/.*$/, '')
|
||||
.replace(/\s*<[\w-]+>\s*$/, '')
|
||||
.trim()
|
||||
|
||||
items.push({
|
||||
line: n,
|
||||
toLine: n,
|
||||
title,
|
||||
from: line.from,
|
||||
to: line.to,
|
||||
level,
|
||||
} as FlatOutlineItem)
|
||||
items.push({
|
||||
line: n,
|
||||
toLine: n,
|
||||
title,
|
||||
from: line.from,
|
||||
to: line.to,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user