Files
orca/src/shared/editor-language/editor-language-detect.ts
T
Yeji HanandNeil 4948524c84 fix(editor): enable Lean syntax highlighting (#31930)
* fix(editor): enable Lean syntax highlighting

* fix(editor): enable Lean syntax highlighting

Adapt #31930 onto current main and retain its bundled Lean grammar repair. Register the lazy TextMate provider and cover detection, proof syntax, nested comments, and bounded tokenization.

* test(editor): verify Lean highlighting in the full app

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-24 02:07:43 -07:00

175 lines
4.9 KiB
TypeScript

import { detectMonacoFilenameLanguage } from './monaco-filename-language'
function extname(filePath: string): string {
const lastDot = filePath.lastIndexOf('.')
const lastSep = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))
if (lastDot <= lastSep) {
return ''
}
return filePath.slice(lastDot)
}
const EXT_TO_LANGUAGE: Record<string, string> = {
// Why: Monaco's built-in language registry maps .tsx/.cts/.mts onto the
// 'typescript' language id and .jsx/.mjs/.cjs onto 'javascript' — there is
// no separate 'typescriptreact'/'javascriptreact' id. Returning the base id
// is what gives .tsx/.jsx files syntax highlighting in the editor.
'.ts': 'typescript',
'.tsx': 'typescript',
'.cts': 'typescript',
'.mts': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.mjs': 'javascript',
'.cjs': 'javascript',
'.json': 'json',
'.jsonc': 'json',
// Why: JSONL is one JSON value per line; a dedicated 'jsonl' language gives
// JSON-style color without attaching JSON whole-document diagnostics that
// would flag every record after line one as trailing content.
'.jsonl': 'jsonl',
'.ipynb': 'notebook',
'.md': 'markdown',
'.mdx': 'markdown',
// Why: Quarto/R Markdown are Markdown plus a YAML header and executable
// cells; the 'markdown' grammar leaves ```{r}/```{python} cells uncolored.
'.qmd': 'quarto',
'.rmd': 'quarto',
'.rmarkdown': 'quarto',
'.mmd': 'mermaid',
'.mermaid': 'mermaid',
'.css': 'css',
'.scss': 'scss',
'.less': 'less',
'.html': 'html',
'.htm': 'html',
// Why: stopgap until a real JSP grammar — 'html' colors the markup; <% %> and ${} stay plain.
'.jsp': 'html',
'.jspf': 'html',
// Why: Monaco declares Liquid as both '.liquid' and '.html.liquid'; the final-extension
// lookup below covers the compound form, so the single entry is enough.
'.liquid': 'liquid',
'.twig': 'twig',
'.xml': 'xml',
'.svg': 'xml',
'.py': 'python',
'.rs': 'rust',
'.go': 'go',
'.java': 'java',
'.kt': 'kotlin',
'.kts': 'kotlin',
'.c': 'c',
'.h': 'c',
'.cpp': 'cpp',
'.cc': 'cpp',
'.cxx': 'cpp',
'.hpp': 'cpp',
'.cs': 'csharp',
// Why: Monaco's apex grammar claims only '.cls'; triggers and anonymous-Apex scripts share it.
'.cls': 'apex',
'.trigger': 'apex',
'.apex': 'apex',
'.rb': 'ruby',
'.php': 'php',
'.swift': 'swift',
'.sh': 'shell',
'.bash': 'shell',
'.zsh': 'shell',
'.fish': 'shell',
'.bat': 'bat',
'.cmd': 'bat',
'.ps1': 'powershell',
'.yaml': 'yaml',
'.yml': 'yaml',
'.toml': 'ini',
'.ini': 'ini',
'.cfg': 'ini',
'.conf': 'ini',
'.sql': 'sql',
'.graphql': 'graphql',
'.gql': 'graphql',
'.dockerfile': 'dockerfile',
'.proto': 'proto',
'.lua': 'lua',
'.r': 'r',
'.scala': 'scala',
'.dart': 'dart',
'.ex': 'elixir',
'.exs': 'elixir',
'.erl': 'erlang',
'.hrl': 'erlang',
'.hs': 'haskell',
'.clj': 'clojure',
// Why: Monaco registers Solidity under the id 'sol'; 'solidity' is only an alias.
'.sol': 'sol',
'.vue': 'vue',
'.svelte': 'svelte',
'.astro': 'astro',
'.sv': 'systemverilog',
'.svh': 'systemverilog',
'.v': 'verilog',
'.vh': 'verilog',
'.nim': 'nim',
'.nims': 'nim',
'.nimble': 'nim',
'.lean': 'lean',
'.typ': 'typst',
'.tf': 'hcl',
'.hcl': 'hcl',
'.abap': 'abap',
'.prisma': 'graphql',
'.csv': 'csv',
'.tsv': 'tsv'
}
const FILENAME_TO_LANGUAGE: Record<string, string> = {
Dockerfile: 'dockerfile',
Makefile: 'makefile',
'CMakeLists.txt': 'cmake',
'.gitignore': 'ini',
'.gitattributes': 'ini',
'.editorconfig': 'ini',
'.env': 'ini',
'.env.local': 'ini',
'.env.development': 'ini',
'.env.production': 'ini',
'.bashrc': 'shell',
'.bash_profile': 'shell',
'.bash_login': 'shell',
'.bash_logout': 'shell',
'.profile': 'shell',
'.zshrc': 'shell',
'.zshenv': 'shell',
'.zprofile': 'shell',
'.zlogin': 'shell',
'.zlogout': 'shell'
}
// Exact match wins; lowercase map covers case-insensitive filesystems.
const FILENAME_LOWER_TO_LANGUAGE: Record<string, string> = Object.fromEntries(
Object.entries(FILENAME_TO_LANGUAGE).map(([name, language]) => [name.toLowerCase(), language])
)
export function detectLanguage(filePath: string): string {
// Check exact filename first
const parts = filePath.split(/[\\/]/)
const filename = parts.at(-1)!
if (Object.hasOwn(FILENAME_TO_LANGUAGE, filename)) {
return FILENAME_TO_LANGUAGE[filename]
}
const lowerFilename = filename.toLowerCase()
if (Object.hasOwn(FILENAME_LOWER_TO_LANGUAGE, lowerFilename)) {
return FILENAME_LOWER_TO_LANGUAGE[lowerFilename]
}
// Check extension
const ext = extname(filename).toLowerCase()
const lowerName = filename.toLowerCase()
// Scoped dotenv names fall back to INI only when no specific extension matches.
return (
EXT_TO_LANGUAGE[ext] ??
detectMonacoFilenameLanguage(filename) ??
(lowerName === '.env' || lowerName.startsWith('.env.') ? 'ini' : 'plaintext')
)
}