Files
mathew/frontend/app/utils/lean/lean-language.ts
Aran Roig 6df7950161
Some checks failed
Build and Deploy Nuxt / build (push) Failing after 41s
Ai slop
2026-10-03 14:13:08 +02:00

253 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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<string>()
/* 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())
}