/* The Lean 4 language for Monaco — registered once, client-side. * * A Monarch tokenizer (keywords, tactics, commands, the nested `/- -/` * comments Lean uses for docstrings) plus themes built from the wiki's live * palette, so the editor matches the page around it and follows the wiki's * theme *and* accent switches live: the accent — caret, selection, active * line number, keywords — is sampled from the `--accent` custom properties * that `[data-accent]` swaps. (Lean's own server also offers semantic * highlighting; keyword colours are the honest first pass — see the note in * lean/README.) */ import * as monaco from 'monaco-editor' export const LEAN_LANGUAGE = 'lean4' export const LEAN_THEMES = { light: 'mathew-light', dark: 'mathew-dark' } as const /* words that introduce declarations, terms, and structuring */ const keywords = [ 'abstract', 'axiom', 'by', 'cases', 'catch', 'class', 'command', 'deriving', 'do', 'else', 'end', 'example', 'expansion', 'explicit', 'export', 'def', 'theorem', 'lemma', 'corollary', 'proposition', 'instance', 'structure', 'inductive', 'abbrev', 'mutual', 'namespace', 'section', 'open', 'protected', 'private', 'partial', 'noncomputable', 'set_option', 'unset_option', 'option', 'attribute', 'variable', 'variables', 'include', 'hiding', 'renaming', 'as', 'where', 'from', 'show', 'have', 'suffices', 'let', 'in', 'if', 'then', 'match', 'with', 'without', 'fun', 'forall', 'at', 'using', 'via', 'override', 'obsolete', 'inherit', 'field', 'generalizing', 'replacing', 'renaming', 'specializing', 'unshuffling', 'case', 'dive', 'guard', 'modifies', 'macro', 'macro_rules', 'syntax', 'elab', 'elab_rules', 'notation', 'infix', 'infixl', 'infixr', 'prefix', 'postfix', 'binders', 'declare_syntax_cat', 'syntax_cat', 'extend', 'scoped', 'unused', 'auto_unfold', 'extern', 'implemented_by', 'native_decide', 'lift', 'coe', 'coe_struct', 'classical', 'decidable', 'universe', 'universes', 'exit', ] /* tactics — the words after `by` and inside tactic blocks */ const tactics = [ 'intro', 'intros', 'exact', 'apply', 'refine', 'have', 'obtain', 'use', 'exists', 'constructor', 'left', 'right', 'simp', 'simp_all', 'simp_rw', 'rw', 'rewrite', 'show', 'conv', 'calc', 'ring', 'ring_nf', 'field_simp', 'norm_num', 'omega', 'linarith', 'nlinarith', 'positivity', 'decide', 'trivial', 'rfl', 'refl', 'symm', 'trans', 'ext', 'funext', 'congr', 'congr!', 'cases', 'induction', 'rcases', 'obtain', 'subst', 'contradiction', 'exfalso', 'push_neg', 'contrapose', 'constructor', 'aesop', 'tauto', 'bv_decide', 'gcongr', 'change', 'generalize', 'revert', 'clear', 'rename_i', 'next', 'any_goals', 'all_goals', 'focus', 'dotl', 'by_contra', 'by_contra!', 'apply_fun', 'congr_arg', 'congrFun', 'congrArg', 'simpa', 'simpa using', 'norm_cast', 'exact?', 'apply?', 'sorry', 'native_decide', 'unfold', 'dsimp', 'unfold_projs', 'whisker', 'thm_to_tactic', 'tauto', 'mt', 'absurd', 'injection', 'no_index', 'guard_goals', 'expect_failure', 'stop', 'skip', 'trace', 'log', 'sleep', 'wait', 'recover', 'error', ] const commands = ['#check', '#eval', '#reduce', '#synth', '#print', '#exit', '#where', '#hole', '#why', '#loogle', '#guard'] let registered = false export function registerLeanLanguage() { if (registered) return registered = true monaco.languages.register({ id: LEAN_LANGUAGE }) monaco.languages.setLanguageConfiguration(LEAN_LANGUAGE, { comments: { lineComment: '--', blockComment: ['/-', '-/'] }, brackets: [ ['{', '}'], ['[', ']'], ['(', ')'], ['⟨', '⟩'], ['⟦', '⟧'], ['‹', '›'], ['⦃', '⦄'], ['⁅', '⁆'], ['⌈', '⌉'], ['⌊', '⌋'], ], autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, { open: '(', close: ')' }, { open: '"', close: '"' }, { open: '/-', close: '-/' }, { open: '⟨', close: '⟩' }, { open: '⟦', close: '⟧' }, { open: '‹', close: '›' }, ], surroundingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, { open: '(', close: ')' }, { open: '"', close: '"' }, { open: '⟨', close: '⟩' }, { open: '⟦', close: '⟧' }, { open: '‹', close: '›' }, ], indentationRules: { increaseIndentPattern: /^\s*(\bby\b|\bdo\b|\bthen\b|\bcalc\b|\{[^}]*$|\[[^\]]*$|\([^)]*$)/, decreaseIndentPattern: /^\s*(\bend\b|\}|\\|\)|\/-})/, }, }) monaco.languages.setMonarchTokensProvider(LEAN_LANGUAGE, { keywords, tactics, commands, tokenizer: { root: [ [/--.*$/, 'comment'], [/\/-/, { token: 'comment', next: '@blockcomment' }], [/"/, { token: 'string', next: '@string' }], [/[0-9]+\.[0-9]+([eE][-+]?[0-9]+)?/, 'number.float'], [/[0-9]+([eE][-+]?[0-9]+)?[nNzQqR]?/, 'number'], [ // one identifier run (ascii word, or a unicode run — Lean names // carry α, →, ≤, ⁻¹), then decided against the word lists /[a-zA-Z_#][a-zA-Z0-9_?!'∆∇]*|[^\x00-\x7F][a-zA-Z0-9_?!'∆∇]*/, { cases: { '@commands': 'keyword.command', '@tactics': 'keyword.tactic', '@keywords': 'keyword', '@default': 'identifier', }, }, ], [/^\s*@\[[^\]]*\]/, 'annotation'], [/[→←↔∧∨¬∀∃≠≤≥<>!=+\-*/^|&~:;.,@#][=]?/, 'operator'], ], blockcomment: [ [/\/-/, { token: 'comment', next: '@blockcomment' }], [/-\//, { token: 'comment', next: '@pop' }], [/[^\-\/]+/, 'comment'], [/[\/-]/, 'comment'], ], string: [ [/[^\\"]+/, 'string'], [/\\./, 'string.escape'], [/"/, { token: 'string', next: '@pop' }], ], }, }) } /* ---------- the wiki's palette, read live, as editor themes ---------- */ /* Monaco themes are plain hex, not CSS variables, so the accent is sampled * off the document each time a theme is built — the same read the canvas * surfaces make. The fallbacks are violet, the wiki's default accent. */ function wikiAccent(dark: boolean) { const css = getComputedStyle(document.documentElement) const at = (name: string, fallback: string) => css.getPropertyValue(name).trim() || fallback return { accent: at('--accent', dark ? '#7c8cff' : '#4f5fe8'), strong: at('--accent-strong', dark ? '#99a5ff' : '#3d4dd6'), second: at('--accent-2', dark ? '#38d3ee' : '#0ea5c9'), } } /* `#rrggbbaa` — Monaco's translucent form, standing in for the wiki's * `color-mix`. Shorthand hex is widened; anything else passes through. */ function alpha(hex: string, a: string) { if (/^#[0-9a-f]{3}$/i.test(hex)) return `#${hex.slice(1).split('').map(c => c + c).join('')}${a}` return /^#[0-9a-f]{6}$/i.test(hex) ? `${hex}${a}` : hex } /* token rules take hex without the leading `#` */ function ink(hex: string) { return hex.replace(/^#/, '') } /* Themes are named by their palette (mathew-dark-fb7185…): a new accent * yields a fresh theme rather than one Monaco may have cached under an old * palette, and a reader cycling back reuses a definition already made. */ const definedThemes = new Set() /* the theme matching the wiki right now — [data-theme] picks the surfaces, * [data-accent] the caret, selection, active line number and keywords */ export function currentLeanTheme(): string { const dark = document.documentElement.getAttribute('data-theme') === 'dark' const a = wikiAccent(dark) const name = `${dark ? LEAN_THEMES.dark : LEAN_THEMES.light}-${ink(a.accent)}-${ink(a.strong)}-${ink(a.second)}`.toLowerCase() if (definedThemes.has(name)) return name definedThemes.add(name) monaco.editor.defineTheme(name, dark ? { base: 'vs-dark', inherit: true, rules: [ { token: 'keyword', foreground: ink(a.strong) }, { token: 'keyword.tactic', foreground: '38d3ee' }, { token: 'keyword.command', foreground: ink(a.strong), fontStyle: 'bold' }, { token: 'string', foreground: '3fce8b' }, { token: 'number', foreground: 'f5a83c' }, { token: 'comment', foreground: '6b7488', fontStyle: 'italic' }, { token: 'annotation', foreground: '99a1b5' }, { token: 'operator', foreground: '99a1b5' }, { token: 'type.identifier', foreground: '38d3ee' }, ], colors: { 'editor.background': '#141826', 'editor.foreground': '#e7eaf3', 'editor.lineHighlightBackground': '#1a2030', 'editorLineNumber.foreground': '#6b7488', 'editorLineNumber.activeForeground': a.accent, 'editorCursor.foreground': a.accent, 'editor.selectionBackground': alpha(a.accent, '40'), 'editor.selectionHighlightBackground': alpha(a.accent, '1f'), 'editorIndentGuide.background1': '#232a3c', 'editorIndentGuide.activeBackground1': '#313a52', 'editorWhitespace.foreground': '#313a52', 'editorWidget.background': '#10131c', 'editorWidget.border': '#232a3c', 'editorSuggestWidget.selectedBackground': '#1a2030', 'editorHoverWidget.background': '#10131c', 'editorHoverWidget.border': '#232a3c', 'scrollbarSlider.background': '#e7eaf329', 'scrollbarSlider.hoverBackground': '#e7eaf347', 'scrollbarSlider.activeBackground': '#e7eaf366', 'editorError.foreground': '#f47085', 'editorWarning.foreground': '#f5a83c', 'editorInfo.foreground': a.second, }, } : { base: 'vs', inherit: true, rules: [ { token: 'keyword', foreground: ink(a.strong) }, { token: 'keyword.tactic', foreground: '0891b2' }, { token: 'keyword.command', foreground: ink(a.strong), fontStyle: 'bold' }, { token: 'string', foreground: '189a5c' }, { token: 'number', foreground: 'd97706' }, { token: 'comment', foreground: '8a93a8', fontStyle: 'italic' }, { token: 'annotation', foreground: '5b6478' }, { token: 'operator', foreground: '5b6478' }, { token: 'type.identifier', foreground: '0891b2' }, ], colors: { 'editor.background': '#ffffff', 'editor.foreground': '#1b2130', 'editor.lineHighlightBackground': '#eef1f7', 'editorLineNumber.foreground': '#8a93a8', 'editorLineNumber.activeForeground': a.accent, 'editorCursor.foreground': a.accent, 'editor.selectionBackground': alpha(a.accent, '33'), 'editor.selectionHighlightBackground': alpha(a.accent, '1a'), 'editorIndentGuide.background1': '#e2e6ef', 'editorIndentGuide.activeBackground1': '#cfd5e3', 'editorWhitespace.foreground': '#cfd5e3', 'editorWidget.background': '#ffffff', 'editorWidget.border': '#e2e6ef', 'editorSuggestWidget.selectedBackground': '#eef1f7', 'editorHoverWidget.background': '#ffffff', 'editorHoverWidget.border': '#e2e6ef', 'scrollbarSlider.background': '#1b213024', 'scrollbarSlider.hoverBackground': '#1b21304d', 'scrollbarSlider.activeBackground': '#1b213066', 'editorError.foreground': '#d1344b', 'editorWarning.foreground': '#d97706', 'editorInfo.foreground': a.second, }, }) return name } /* move the editor to the palette the wiki is wearing right now */ export function applyLeanTheme() { monaco.editor.setTheme(currentLeanTheme()) }