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.
|
// after it, so this stays clear of code.
|
||||||
const HEADING_REGEX = /^(=+)[ \t]+(.*\S)[ \t]*$/
|
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(
|
function computeOutline(
|
||||||
state: EditorState
|
state: EditorState
|
||||||
): ProjectionResult<FlatOutlineItem> {
|
): ProjectionResult<FlatOutlineItem> {
|
||||||
const items: 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++) {
|
for (let n = 1; n <= state.doc.lines; n++) {
|
||||||
const line = state.doc.line(n)
|
const line = state.doc.line(n)
|
||||||
const match = HEADING_REGEX.exec(line.text)
|
const text = line.text
|
||||||
if (!match) continue
|
|
||||||
|
|
||||||
const depth = match[1].length
|
// Only attempt heading detection when not inside a math block.
|
||||||
const level = LEVELS[Math.min(depth, LEVELS.length) - 1]
|
// (e.g. '= b+c$' on the second line of '$ a \n= b+c$' must be skipped.)
|
||||||
// Strip a trailing line comment, then a trailing label.
|
if (!inMath) {
|
||||||
const title = match[2]
|
const match = HEADING_REGEX.exec(text)
|
||||||
.replace(/\s*\/\/.*$/, '')
|
if (match) {
|
||||||
.replace(/\s*<[\w-]+>\s*$/, '')
|
const depth = match[1].length
|
||||||
.trim()
|
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({
|
items.push({
|
||||||
line: n,
|
line: n,
|
||||||
toLine: n,
|
toLine: n,
|
||||||
title,
|
title,
|
||||||
from: line.from,
|
from: line.from,
|
||||||
to: line.to,
|
to: line.to,
|
||||||
level,
|
level,
|
||||||
} as FlatOutlineItem)
|
} as FlatOutlineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countDollars(text) % 2 === 1) inMath = !inMath
|
||||||
}
|
}
|
||||||
|
|
||||||
return { items, status: ProjectionStatus.Complete }
|
return { items, status: ProjectionStatus.Complete }
|
||||||
|
|||||||
@@ -49,10 +49,12 @@ export const TypstLanguage = LRLanguage.define({
|
|||||||
CodeBool: t.atom,
|
CodeBool: t.atom,
|
||||||
|
|
||||||
// Identifiers:
|
// Identifiers:
|
||||||
// - direct child of CallExpr → function/method name
|
// CallExpr/CodeIdent — top-level #func or after keywords (#set text) → function style
|
||||||
// - CodeArgKey (named arg key, emitted by tokenizer before ':') → attributeName
|
// FuncExpr/CodeIdent — func call inside a value expr (has args/method) → function style
|
||||||
// - everywhere else → plain variable
|
// 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),
|
'CallExpr/CodeIdent': t.function(t.variableName),
|
||||||
|
'FuncExpr/CodeIdent': t.function(t.variableName),
|
||||||
CodeArgKey: t.attributeName,
|
CodeArgKey: t.attributeName,
|
||||||
CodeIdent: t.variableName,
|
CodeIdent: t.variableName,
|
||||||
|
|
||||||
|
|||||||
@@ -370,7 +370,12 @@ export const codeIdentTokenizer = new ExternalTokenizer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) input.advance()
|
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 }
|
{ contextual: true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -74,7 +74,13 @@ codeExprBody {
|
|||||||
KeywordExpr { CodeKeyword CallExpr? }
|
KeywordExpr { CodeKeyword CallExpr? }
|
||||||
AtomExpr { CodeBool }
|
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* }
|
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 {
|
callSuffix {
|
||||||
CodeArgs |
|
CodeArgs |
|
||||||
"." CodeIdent
|
"." CodeIdent
|
||||||
@@ -91,7 +97,8 @@ codeValue {
|
|||||||
CodeString |
|
CodeString |
|
||||||
CodeNumber |
|
CodeNumber |
|
||||||
CodeBool |
|
CodeBool |
|
||||||
CallExpr |
|
FuncExpr |
|
||||||
|
CodeIdent |
|
||||||
ContentBlock |
|
ContentBlock |
|
||||||
CodeBlock |
|
CodeBlock |
|
||||||
InlineMath
|
InlineMath
|
||||||
|
|||||||
Reference in New Issue
Block a user