From e84ed776b2085263c008f599c7446552fbb72368 Mon Sep 17 00:00:00 2001 From: flokchvtr <41383897+flokchvtr@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:33:06 +0400 Subject: [PATCH 1/3] editor: Typst word completion in math regions (slice 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sibling of the LaTeX source for notes whose typesetter is Typst: bare-word commands (sum, alpha, frac) complete from two letters on inside the same math regions, gated on mathRendererOf so each source stays out of the other typesetter's notes. Argument-taking functions insert as snippets with Typst syntax (frac(a, b), sum_(i=1)^(n), mat(1, 2; 3, 4)); the icon slot shows the exact Unicode glyph (α, ∑, ∫, ℝ) — no typesetting needed for previews yet. Starter table (~90 words): greek, core constructs, accents, set/logic symbols, named functions. --- .../app-core/src/components/EditorPane.tsx | 2 + .../src/lib/cm-typst-completions.test.ts | 67 ++++++++ .../app-core/src/lib/cm-typst-completions.ts | 144 ++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 packages/app-core/src/lib/cm-typst-completions.test.ts create mode 100644 packages/app-core/src/lib/cm-typst-completions.ts diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 80474045..a9521534 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -123,6 +123,7 @@ import { slashCommandSource, slashCommandRender } from '../lib/cm-slash-commands import { calloutTypeSource } from '../lib/cm-callouts' import { dateShortcutSource } from '../lib/cm-date-shortcuts' import { latexCommandSource } from '../lib/cm-latex-completions' +import { typstCommandSource } from '../lib/cm-typst-completions' import { wikilinkSource, wikilinkHeadingSource, atNoteSource } from '../lib/cm-wikilinks' import { linkRangeAtCursor, markdownLinkAt } from '../lib/internal-links' import { setBlockType, toggleWrap, wrapLink } from '../lib/cm-format' @@ -1789,6 +1790,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { calloutTypeSource, dateShortcutSource, latexCommandSource, + typstCommandSource, atNoteSource, frontmatterTagSource, hashtagSource, diff --git a/packages/app-core/src/lib/cm-typst-completions.test.ts b/packages/app-core/src/lib/cm-typst-completions.test.ts new file mode 100644 index 00000000..a44140d6 --- /dev/null +++ b/packages/app-core/src/lib/cm-typst-completions.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' +import { EditorState } from '@codemirror/state' +import { markdown } from '@codemirror/lang-markdown' +import { CompletionContext } from '@codemirror/autocomplete' +import { typstCommandSource, typstTokenBefore } from './cm-typst-completions' +import { mathRenderExtension } from './cm-math-render' + +function state(doc: string, renderer: 'katex' | 'typst' = 'typst'): EditorState { + return EditorState.create({ + doc, + extensions: [markdown(), mathRenderExtension(renderer)] + }) +} + +function sourceAt(doc: string, renderer: 'katex' | 'typst' = 'typst', explicit = false) { + return typstCommandSource(new CompletionContext(state(doc, renderer), doc.length, explicit)) +} + +describe('typstTokenBefore', () => { + it('matches the identifier being typed, from two letters on', () => { + const doc = '$su' + const token = typstTokenBefore(state(doc), doc.length) + expect(token).not.toBeNull() + expect(token!.query).toBe('su') + expect(token!.from).toBe(1) + }) + + it('stays silent on a single letter unless summoned explicitly', () => { + // One-letter variables are the normal case in math, not a prefix. + const doc = '$x' + expect(typstTokenBefore(state(doc), doc.length)).toBeNull() + expect(typstTokenBefore(state(doc), doc.length, true)).not.toBeNull() + }) + + it('matches dotted names and rejects non-identifiers', () => { + const dotted = '$dots.h' + expect(typstTokenBefore(state(dotted), dotted.length)!.query).toBe('dots.h') + + const afterDigits = '$12' + expect(typstTokenBefore(state(afterDigits), afterDigits.length)).toBeNull() + }) +}) + +describe('typstCommandSource', () => { + it('offers Typst words inside math when the note compiles as Typst', () => { + const result = sourceAt('formule $su') + expect(result).not.toBeNull() + const labels = result!.options.map((o) => o.label) + expect(labels).toContain('sum') + expect(labels).toContain('alpha') + }) + + it('stays out of the way when the note compiles as KaTeX', () => { + // The exact mirror of the LaTeX source's Typst gate: `sum_(i=1)^(n)` is + // not KaTeX, so offering it there would be wrong every time. + expect(sourceAt('formule $su', 'katex')).toBeNull() + }) + + it('stays out of prose and code even with Typst selected', () => { + expect(sourceAt('prose without math: su')).toBeNull() + expect(sourceAt('```bash\necho su')).toBeNull() + }) + + it('works in display math still being typed', () => { + expect(sourceAt('$$\nx = su')).not.toBeNull() + }) +}) diff --git a/packages/app-core/src/lib/cm-typst-completions.ts b/packages/app-core/src/lib/cm-typst-completions.ts new file mode 100644 index 00000000..120318c6 --- /dev/null +++ b/packages/app-core/src/lib/cm-typst-completions.ts @@ -0,0 +1,144 @@ +/** + * Typst math completion, the sibling of cm-latex-completions for notes whose + * typesetter is Typst. Typst has no backslash: commands are bare words + * (`sum`, `alpha`, `frac(a, b)`), so the trigger is the identifier being + * typed — from two letters on, to stay out of the way of one-letter + * variables — inside the same `$…$` / `$$…$$` regions. + * + * Where LaTeX previews need KaTeX, most Typst entries are single glyphs with + * an exact Unicode form (α, ∑, ∫, ℝ, ∀ …), shown directly in the icon slot. + */ +import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete' +import { snippet } from '@codemirror/autocomplete' +import type { EditorState } from '@codemirror/state' +import { isInMathContext } from './cm-latex-completions' +import { mathRendererOf } from './cm-math-render' + +interface TypstCommand { + /** The word as typed: `sum`, `alpha`, `frac`. */ + label: string + detail: string + /** Snippet template when the function takes arguments. */ + template?: string + /** Unicode glyph (or short sketch) for the icon slot. */ + icon: string + boost?: number +} + +const GREEK: Array<[string, string]> = [ + ['alpha', 'α'], ['beta', 'β'], ['gamma', 'γ'], ['delta', 'δ'], ['epsilon', 'ε'], + ['zeta', 'ζ'], ['eta', 'η'], ['theta', 'θ'], ['iota', 'ι'], ['kappa', 'κ'], + ['lambda', 'λ'], ['mu', 'μ'], ['nu', 'ν'], ['xi', 'ξ'], ['pi', 'π'], ['rho', 'ρ'], + ['sigma', 'σ'], ['tau', 'τ'], ['upsilon', 'υ'], ['phi', 'φ'], ['chi', 'χ'], + ['psi', 'ψ'], ['omega', 'ω'], + ['Gamma', 'Γ'], ['Delta', 'Δ'], ['Theta', 'Θ'], ['Lambda', 'Λ'], ['Xi', 'Ξ'], + ['Pi', 'Π'], ['Sigma', 'Σ'], ['Phi', 'Φ'], ['Psi', 'Ψ'], ['Omega', 'Ω'] +] + +const SYMBOLS: Array<[string, string, string]> = [ + // [word, glyph, detail] + ['oo', '∞', 'infinity'], + ['diff', '∂', 'partial'], + ['nabla', '∇', 'nabla'], + ['forall', '∀', 'for all'], + ['exists', '∃', 'exists'], + ['in', '∈', 'element of'], + ['union', '∪', 'set union'], + ['subset', '⊂', 'subset'], + ['supset', '⊃', 'superset'], + ['approx', '≈', 'approximately'], + ['equiv', '≡', 'equivalent'], + ['prop', '∝', 'proportional'], + ['times', '×', 'times'], + ['dot.op', '⋅', 'dot operator'], + ['plus.minus', '±', 'plus-minus'], + ['RR', 'ℝ', 'reals'], + ['NN', 'ℕ', 'naturals'], + ['ZZ', 'ℤ', 'integers'], + ['QQ', 'ℚ', 'rationals'], + ['CC', 'ℂ', 'complexes'], + ['dots.h', '⋯', 'horizontal dots'], + ['dots.v', '⋮', 'vertical dots'] +] + +const FUNCTIONS = [ + 'sin', 'cos', 'tan', 'arcsin', 'arccos', 'arctan', 'sinh', 'cosh', 'tanh', + 'exp', 'log', 'ln', 'det', 'max', 'min', 'sup', 'inf', 'arg', 'gcd', 'mod' +] + +const TYPST_COMMANDS: TypstCommand[] = [ + // Everyday constructs, boosted to the top — templates are valid Typst math. + { label: 'frac', detail: 'fraction — or just a/b', template: 'frac(${a}, ${b})', icon: '⅟', boost: 99 }, + { label: 'sqrt', detail: 'square root', template: 'sqrt(${x})', icon: '√', boost: 98 }, + { label: 'root', detail: 'nth root', template: 'root(${n}, ${x})', icon: '∛' }, + { label: 'sum', detail: 'sum', template: 'sum_(${i=1})^(${n})', icon: '∑', boost: 97 }, + { label: 'integral', detail: 'integral', template: 'integral_(${a})^(${b})', icon: '∫', boost: 96 }, + { label: 'lim', detail: 'limit', template: 'lim_(${x -> 0})', icon: 'lim', boost: 95 }, + { label: 'product', detail: 'product', template: 'product_(${i=1})^(${n})', icon: '∏', boost: 90 }, + { label: 'binom', detail: 'binomial', template: 'binom(${n}, ${k})', icon: '(ⁿₖ)' }, + { label: 'mat', detail: 'matrix — ; ends a row', template: 'mat(${1, 2; 3, 4})', icon: '⊞' }, + { label: 'vec', detail: 'column vector', template: 'vec(${a}, ${b})', icon: '⇣' }, + { label: 'cases', detail: 'case distinction', template: 'cases(${x &"if" x > 0}, ${-x &"else"})', icon: '{' }, + { label: 'abs', detail: 'absolute value', template: 'abs(${x})', icon: '|x|' }, + { label: 'norm', detail: 'norm', template: 'norm(${x})', icon: '‖x‖' }, + { label: 'floor', detail: 'floor', template: 'floor(${x})', icon: '⌊x⌋' }, + { label: 'ceil', detail: 'ceiling', template: 'ceil(${x})', icon: '⌈x⌉' }, + + // Accents. + { label: 'hat', detail: 'hat accent', template: 'hat(${x})', icon: 'x̂' }, + { label: 'tilde', detail: 'tilde accent', template: 'tilde(${x})', icon: 'x̃' }, + { label: 'dot', detail: 'dot accent', template: 'dot(${x})', icon: 'ẋ' }, + { label: 'arrow', detail: 'vector arrow', template: 'arrow(${x})', icon: 'x⃗' }, + { label: 'overline', detail: 'overline', template: 'overline(${x})', icon: 'x̄' }, + { label: 'underline', detail: 'underline', template: 'underline(${x})', icon: 'x̲' }, + + ...GREEK.map(([label, icon]): TypstCommand => ({ label, icon, detail: 'greek' })), + ...SYMBOLS.map(([label, icon, detail]): TypstCommand => ({ label, icon, detail })), + ...FUNCTIONS.map((label): TypstCommand => ({ label, icon: 'ƒ', detail: 'function' })) +] + +/** The word being typed at `pos`, or null. Two letters minimum unless the + * completion was summoned explicitly — one-letter variables are the normal + * case in math and must not pop a menu. Dotted names (`dots.h`) match too. */ +export function typstTokenBefore( + state: EditorState, + pos: number, + explicit = false +): { from: number; query: string } | null { + const line = state.doc.lineAt(pos) + const textBefore = state.doc.sliceString(line.from, pos) + const match = textBefore.match(/[A-Za-z][A-Za-z.]*$/) + if (!match) return null + if (!explicit && match[0].length < 2) return null + return { from: pos - match[0].length, query: match[0] } +} + +let cachedOptions: Completion[] | null = null + +function buildOptions(): Completion[] { + cachedOptions ??= TYPST_COMMANDS.map( + (cmd): Completion => + ({ + label: cmd.label, + detail: cmd.detail, + type: 'keyword', + boost: cmd.boost ?? 0, + _kind: 'typst', + _icon: cmd.icon, + apply: cmd.template ? snippet(cmd.template) : undefined + }) as Completion & { _kind: string; _icon: string } + ) + return cachedOptions +} + +export function typstCommandSource(context: CompletionContext): CompletionResult | null { + if (mathRendererOf(context.state) !== 'typst') return null + const token = typstTokenBefore(context.state, context.pos, context.explicit) + if (!token) return null + if (!isInMathContext(context.state, token.from)) return null + return { + from: token.from, + options: buildOptions(), + validFor: /^[A-Za-z][A-Za-z.]*$/ + } +} From 4630f2532f89ca63f6793392b5416b5569c66e9e Mon Sep 17 00:00:00 2001 From: flokchvtr <41383897+flokchvtr@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:59:04 +0400 Subject: [PATCH 2/3] editor: compiled Typst previews in the completion row (slice 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Templated options now typeset their preview through the shared Typst render queue: the Unicode glyph paints immediately, the compiled SVG swaps in when ready, and the svg cache makes every later popup instant. The preview source is the snippet template with its fields unwrapped (frac(${a}, ${b}) previews as frac(a, b)), so preview and insertion cannot drift apart. Glyph-only entries (greek, symbols) keep their exact Unicode form — no compile needed. --- .../app-core/src/lib/cm-slash-commands.ts | 3 + .../src/lib/cm-typst-completions.test.ts | 22 +++++- .../app-core/src/lib/cm-typst-completions.ts | 78 ++++++++++++++++++- 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/app-core/src/lib/cm-slash-commands.ts b/packages/app-core/src/lib/cm-slash-commands.ts index 53bdcccf..24a47f8e 100644 --- a/packages/app-core/src/lib/cm-slash-commands.ts +++ b/packages/app-core/src/lib/cm-slash-commands.ts @@ -2,6 +2,7 @@ import type { CompletionContext, CompletionResult, Completion } from '@codemirro import type { EditorView } from '@codemirror/view' import { useStore } from '../store' import { renderLatexCompletion } from './cm-latex-completions' +import { renderTypstCompletion } from './cm-typst-completions' interface SlashCmd { label: string @@ -70,6 +71,8 @@ const COMMANDS: SlashCmd[] = [ function renderCompletion(completion: Completion): HTMLElement { const latex = renderLatexCompletion(completion) if (latex) return latex + const typst = renderTypstCompletion(completion) + if (typst) return typst const decorated = completion as DecoratedCompletion if (decorated._kind === 'callout') { const el = document.createElement('div') diff --git a/packages/app-core/src/lib/cm-typst-completions.test.ts b/packages/app-core/src/lib/cm-typst-completions.test.ts index a44140d6..611081b5 100644 --- a/packages/app-core/src/lib/cm-typst-completions.test.ts +++ b/packages/app-core/src/lib/cm-typst-completions.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { EditorState } from '@codemirror/state' import { markdown } from '@codemirror/lang-markdown' import { CompletionContext } from '@codemirror/autocomplete' -import { typstCommandSource, typstTokenBefore } from './cm-typst-completions' +import { previewSourceOf, typstCommandSource, typstTokenBefore } from './cm-typst-completions' import { mathRenderExtension } from './cm-math-render' function state(doc: string, renderer: 'katex' | 'typst' = 'typst'): EditorState { @@ -65,3 +65,23 @@ describe('typstCommandSource', () => { expect(sourceAt('$$\nx = su')).not.toBeNull() }) }) + +describe('compiled previews', () => { + it('derives the preview by unwrapping the snippet fields', () => { + expect(previewSourceOf({ template: 'frac(${a}, ${b})' })).toBe('frac(a, b)') + expect(previewSourceOf({ template: 'sum_(${i=1})^(${n})' })).toBe('sum_(i=1)^(n)') + expect(previewSourceOf({})).toBeNull() + expect(previewSourceOf({ preview: 'mat(1;2)', template: 'mat(${})' })).toBe('mat(1;2)') + }) + + it('every templated option carries a well-formed preview', () => { + const result = sourceAt('formule $su')! + for (const option of result.options) { + const preview = (option as { _preview?: string | null })._preview + if ((option as { apply?: unknown }).apply === undefined) continue + expect(preview, option.label).toBeTruthy() + // No snippet syntax may leak into what the compiler will typeset. + expect(preview, option.label).not.toMatch(/\$\{|\}/) + } + }) +}) diff --git a/packages/app-core/src/lib/cm-typst-completions.ts b/packages/app-core/src/lib/cm-typst-completions.ts index 120318c6..8e7601ca 100644 --- a/packages/app-core/src/lib/cm-typst-completions.ts +++ b/packages/app-core/src/lib/cm-typst-completions.ts @@ -13,6 +13,7 @@ import { snippet } from '@codemirror/autocomplete' import type { EditorState } from '@codemirror/state' import { isInMathContext } from './cm-latex-completions' import { mathRendererOf } from './cm-math-render' +import { peekTypstMathSvg, renderTypstMathToSvg } from './typst-math-render' interface TypstCommand { /** The word as typed: `sum`, `alpha`, `frac`. */ @@ -22,6 +23,9 @@ interface TypstCommand { template?: string /** Unicode glyph (or short sketch) for the icon slot. */ icon: string + /** Typst math compiled for the icon slot; the glyph paints while it loads. + * Constructs need this — no single glyph says `mat(1, 2; 3, 4)`. */ + preview?: string boost?: number } @@ -113,6 +117,15 @@ export function typstTokenBefore( return { from: pos - match[0].length, query: match[0] } } +/** The compiled preview is the template with its `${…}` fields unwrapped: + * `frac(${a}, ${b})` previews as `frac(a, b)`. Deriving it keeps preview and + * insertion from ever drifting apart. Exported for tests. */ +export function previewSourceOf(cmd: { template?: string; preview?: string }): string | null { + if (cmd.preview) return cmd.preview + if (!cmd.template) return null + return cmd.template.replace(/\$\{([^}]*)\}/g, '$1') +} + let cachedOptions: Completion[] | null = null function buildOptions(): Completion[] { @@ -125,12 +138,75 @@ function buildOptions(): Completion[] { boost: cmd.boost ?? 0, _kind: 'typst', _icon: cmd.icon, + _preview: previewSourceOf(cmd), apply: cmd.template ? snippet(cmd.template) : undefined - }) as Completion & { _kind: string; _icon: string } + }) as Completion & { _kind: string; _icon: string; _preview: string | null } ) return cachedOptions } +/** Full option row for a Typst completion. The Unicode glyph paints + * immediately; entries with arguments swap in the compiled Typst preview as + * soon as the shared render queue produces it (cached across popups, so the + * swap only happens the first time). Null for every other completion kind. */ +export function renderTypstCompletion(completion: Completion): HTMLElement | null { + const { _kind, _icon, _preview } = completion as Completion & { + _kind?: string + _icon?: string + _preview?: string | null + } + if (_kind !== 'typst') return null + + const el = document.createElement('div') + el.className = 'slash-cmd-item' + + const icon = document.createElement('span') + icon.className = 'slash-cmd-icon typst-cmd-icon' + icon.style.fontSize = '0.8em' + icon.style.lineHeight = '1' + icon.style.display = 'inline-flex' + icon.style.alignItems = 'center' + icon.style.justifyContent = 'center' + icon.style.overflow = 'hidden' + icon.textContent = _icon ?? '' + if (_preview) { + const showSvg = (svg: string): void => { + icon.innerHTML = svg + const svgEl = icon.querySelector('svg') + if (svgEl) { + svgEl.style.maxWidth = '2.6em' + svgEl.style.maxHeight = '2.2em' + } + } + const cached = peekTypstMathSvg(_preview, false) + if (cached?.ok) { + showSvg(cached.svg) + } else if (!cached) { + renderTypstMathToSvg(_preview, false) + .then((res) => { + // A closed popup leaves the node detached; the warm cache still + // pays off on the next open. + if (res.ok && icon.isConnected) showSvg(res.svg) + }) + .catch(() => undefined) + } + // A cached error keeps the glyph: it said all it had to say once. + } + + const label = document.createElement('span') + label.className = 'slash-cmd-label' + label.textContent = completion.label + + const detail = document.createElement('span') + detail.className = 'slash-cmd-detail' + detail.textContent = completion.detail ?? '' + + el.appendChild(icon) + el.appendChild(label) + el.appendChild(detail) + return el +} + export function typstCommandSource(context: CompletionContext): CompletionResult | null { if (mathRendererOf(context.state) !== 'typst') return null const token = typstTokenBefore(context.state, context.pos, context.explicit) From 18f09c206bb98b0fa5ef5e1f3c406a064806f197 Mon Sep 17 00:00:00 2001 From: flokchvtr <41383897+flokchvtr@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:37:32 +0400 Subject: [PATCH 3/3] editor: arrows, comparisons, sets and text styles in Typst completion (slice 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the table with the remaining everyday families: arrows with their ASCII shorthands noted in the detail (arrow.r / ->), comparisons (lt.eq, gt.eq, eq.not), sets (inter, nothing, subset.eq, in.not), circled operators under their canonical post-0.13 names (plus.o, times.o — the .circle spellings are deprecated in the bundled compiler), the dif differential, and text styles (bold, upright, cal, bb) as snippets with compiled previews. Every name compile-checked against typst 0.15. --- .../src/lib/cm-typst-completions.test.ts | 12 +++++++ .../app-core/src/lib/cm-typst-completions.ts | 36 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/app-core/src/lib/cm-typst-completions.test.ts b/packages/app-core/src/lib/cm-typst-completions.test.ts index 611081b5..7b4d24bb 100644 --- a/packages/app-core/src/lib/cm-typst-completions.test.ts +++ b/packages/app-core/src/lib/cm-typst-completions.test.ts @@ -64,6 +64,18 @@ describe('typstCommandSource', () => { it('works in display math still being typed', () => { expect(sourceAt('$$\nx = su')).not.toBeNull() }) + + it('covers the slice-3 families: arrows, comparisons, sets, styles', () => { + const labels = sourceAt('formule $ar')!.options.map((o) => o.label) + for (const word of ['arrow.r.double', 'lt.eq', 'inter', 'nothing', 'dif', 'bold', 'bb']) { + expect(labels, word).toContain(word) + } + }) + + it('the token matcher reaches doubly-dotted names like arrow.l.r.double', () => { + const doc = '$arrow.l.r.double' + expect(typstTokenBefore(state(doc), doc.length)!.query).toBe('arrow.l.r.double') + }) }) describe('compiled previews', () => { diff --git a/packages/app-core/src/lib/cm-typst-completions.ts b/packages/app-core/src/lib/cm-typst-completions.ts index 8e7601ca..4ce65208 100644 --- a/packages/app-core/src/lib/cm-typst-completions.ts +++ b/packages/app-core/src/lib/cm-typst-completions.ts @@ -62,7 +62,35 @@ const SYMBOLS: Array<[string, string, string]> = [ ['QQ', 'ℚ', 'rationals'], ['CC', 'ℂ', 'complexes'], ['dots.h', '⋯', 'horizontal dots'], - ['dots.v', '⋮', 'vertical dots'] + ['dots.v', '⋮', 'vertical dots'], + + // Arrows — Typst also accepts the ASCII shorthands noted in the detail. + ['arrow.r', '→', 'right arrow — or ->'], + ['arrow.l', '←', 'left arrow — or <-'], + ['arrow.l.r', '↔', 'left-right arrow — or <->'], + ['arrow.r.double', '⇒', 'implies — or =>'], + ['arrow.l.r.double', '⇔', 'if and only if — or <=>'], + ['arrow.r.bar', '↦', 'maps to — or |->'], + ['arrow.t', '↑', 'up arrow'], + ['arrow.b', '↓', 'down arrow'], + + // Comparisons. + ['lt.eq', '≤', 'less or equal — or <='], + ['gt.eq', '≥', 'greater or equal — or >='], + ['eq.not', '≠', 'not equal — or !='], + + // Sets. + ['inter', '∩', 'set intersection'], + ['nothing', '∅', 'empty set'], + ['subset.eq', '⊆', 'subset or equal'], + ['supset.eq', '⊇', 'superset or equal'], + ['in.not', '∉', 'not element of'], + + // Operators. + ['compose', '∘', 'function composition'], + ['plus.o', '⊕', 'direct sum'], + ['times.o', '⊗', 'tensor product'], + ['dif', 'd', 'differential — dif x in integrals'] ] const FUNCTIONS = [ @@ -96,6 +124,12 @@ const TYPST_COMMANDS: TypstCommand[] = [ { label: 'overline', detail: 'overline', template: 'overline(${x})', icon: 'x̄' }, { label: 'underline', detail: 'underline', template: 'underline(${x})', icon: 'x̲' }, + // Text styles. + { label: 'bold', detail: 'bold', template: 'bold(${x})', icon: '𝐱' }, + { label: 'upright', detail: 'upright (non-italic)', template: 'upright(${x})', icon: 'x' }, + { label: 'cal', detail: 'calligraphic', template: 'cal(${A})', icon: '𝒜' }, + { label: 'bb', detail: 'blackboard bold', template: 'bb(${A})', icon: '𝔸' }, + ...GREEK.map(([label, icon]): TypstCommand => ({ label, icon, detail: 'greek' })), ...SYMBOLS.map(([label, icon, detail]): TypstCommand => ({ label, icon, detail })), ...FUNCTIONS.map((label): TypstCommand => ({ label, icon: 'ƒ', detail: 'function' }))