From b76807817573c8c4b83627cfb917f43d17a64503 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 9 May 2026 06:44:18 +0200 Subject: [PATCH] feat(editor): broaden language detection in web code editor The CodeMirror language registry only ships a narrow set of extensions and filenames per language, so common config and DSL files (.gitconfig, Brewfile, Vagrantfile, Containerfile, Cargo.lock, *.gemspec, *.tcc, Snakefile, etc.) render as plain text in the file editor. Pull authoritative extension/filename data from github-linguist via a new `make generate-codemirror-languages` script, write a curated subset to `assets/codemirror-languages.json`, and wire it into the editor as overrides on top of `@codemirror/language-data`. A small set of manual entries fill gaps Linguist classifies under separate languages (.editorconfig, .gitconfig, .npmrc) or doesn't list at all (*.conf, Snakefile, Containerfile.*, Dockerfile.*, Makefile.am, BSDmakefile). The derived data structures are memoised at module scope so the work runs once per page session, and the JSON moves into the dynamic `importCodemirror()` chunk so it doesn't bloat the entry bundle. Co-Authored-By: Claude (Opus 4.7) --- Makefile | 4 + assets/codemirror-languages.json | 1004 ++++++++++++++++++++++++ tools/generate-codemirror-languages.ts | 151 ++++ web_src/js/modules/codeeditor/main.ts | 53 +- 4 files changed, 1207 insertions(+), 5 deletions(-) create mode 100644 assets/codemirror-languages.json create mode 100755 tools/generate-codemirror-languages.ts diff --git a/Makefile b/Makefile index 27b2c30295..7db366c57f 100644 --- a/Makefile +++ b/Makefile @@ -661,6 +661,10 @@ generate-gitignore: ## update gitignore files generate-images: | node_modules ## generate images cd tools && node generate-images.ts $(TAGS) +.PHONY: generate-codemirror-languages +generate-codemirror-languages: | node_modules ## refresh assets/codemirror-languages.json from github-linguist + node tools/generate-codemirror-languages.ts + .PHONY: generate-manpage generate-manpage: ## generate manpage @[ -f gitea ] || make backend diff --git a/assets/codemirror-languages.json b/assets/codemirror-languages.json new file mode 100644 index 0000000000..5bab971eb7 --- /dev/null +++ b/assets/codemirror-languages.json @@ -0,0 +1,1004 @@ +[ + { + "name": "C", + "extensions": [ + "c", + "cats", + "h", + "idc" + ], + "filenames": [] + }, + { + "name": "C#", + "extensions": [ + "cs", + "cake", + "csx", + "linq" + ], + "filenames": [] + }, + { + "name": "C++", + "extensions": [ + "cpp", + "c++", + "cc", + "cp", + "cppm", + "cxx", + "h", + "h++", + "hh", + "hpp", + "hxx", + "inl", + "ino", + "ipp", + "ixx", + "re", + "tcc", + "tpp", + "txx" + ], + "filenames": [] + }, + { + "name": "Clojure", + "extensions": [ + "clj", + "bb", + "boot", + "cl2", + "cljc", + "cljs", + "cljscm", + "cljx", + "hic" + ], + "filenames": [ + "riemann.config" + ] + }, + { + "name": "CMake", + "extensions": [ + "cmake" + ], + "filenames": [ + "CMakeLists.txt" + ] + }, + { + "name": "Cobol", + "extensions": [ + "cob", + "cbl", + "ccp", + "cobol", + "cpy" + ], + "filenames": [] + }, + { + "name": "CoffeeScript", + "extensions": [ + "coffee", + "_coffee", + "cake", + "cjsx", + "iced" + ], + "filenames": [ + "Cakefile" + ] + }, + { + "name": "Common Lisp", + "extensions": [ + "lisp", + "asd", + "cl", + "l", + "lsp", + "ny", + "podsl", + "sexp" + ], + "filenames": [] + }, + { + "name": "Crystal", + "extensions": [ + "cr" + ], + "filenames": [] + }, + { + "name": "CSS", + "extensions": [ + "css" + ], + "filenames": [] + }, + { + "name": "Cython", + "extensions": [ + "pyx", + "pxd", + "pxi" + ], + "filenames": [] + }, + { + "name": "D", + "extensions": [ + "d", + "di" + ], + "filenames": [] + }, + { + "name": "Dart", + "extensions": [ + "dart" + ], + "filenames": [] + }, + { + "name": "diff", + "extensions": [ + "diff", + "patch" + ], + "filenames": [] + }, + { + "name": "Dockerfile", + "extensions": [ + "dockerfile", + "containerfile" + ], + "filenames": [ + "Containerfile", + "Dockerfile" + ] + }, + { + "name": "Elm", + "extensions": [ + "elm" + ], + "filenames": [] + }, + { + "name": "Erlang", + "extensions": [ + "erl", + "app", + "es", + "escript", + "hrl", + "xrl", + "yrl" + ], + "filenames": [ + "Emakefile", + "rebar.config", + "rebar.config.lock", + "rebar.lock" + ] + }, + { + "name": "F#", + "extensions": [ + "fs", + "fsi", + "fsx" + ], + "filenames": [] + }, + { + "name": "Fortran", + "extensions": [ + "f", + "f77", + "for", + "fpp" + ], + "filenames": [] + }, + { + "name": "Go", + "extensions": [ + "go" + ], + "filenames": [] + }, + { + "name": "Groovy", + "extensions": [ + "groovy", + "grt", + "gtpl", + "gvy" + ], + "filenames": [ + "Jenkinsfile" + ] + }, + { + "name": "Haskell", + "extensions": [ + "hs", + "hs-boot", + "hsc" + ], + "filenames": [] + }, + { + "name": "HTML", + "extensions": [ + "html", + "hta", + "htm", + "inc", + "xht", + "xhtml" + ], + "filenames": [] + }, + { + "name": "Java", + "extensions": [ + "java", + "jav", + "jsh" + ], + "filenames": [] + }, + { + "name": "JavaScript", + "extensions": [ + "js", + "cjs", + "jsx", + "mjs" + ], + "filenames": [ + "Jakefile" + ] + }, + { + "name": "JSON", + "extensions": [ + "json", + "4DForm", + "4DProject", + "avsc", + "geojson", + "gltf", + "har", + "ice", + "JSON-tmLanguage", + "jsonl", + "mcmeta", + "sarif", + "tact", + "tfstate", + "topojson", + "webapp", + "webmanifest", + "yy", + "yyp" + ], + "filenames": [ + ".all-contributorsrc", + ".arcconfig", + ".auto-changelog", + ".c8rc", + ".htmlhintrc", + ".imgbotconfig", + ".nycrc", + ".tern-config", + ".tern-project", + ".watchmanconfig", + "MODULE.bazel.lock", + "Package.resolved", + "Pipfile.lock", + "bun.lock", + "composer.lock", + "deno.lock", + "flake.lock", + "mcmod.info" + ] + }, + { + "name": "Julia", + "extensions": [ + "jl" + ], + "filenames": [] + }, + { + "name": "Kotlin", + "extensions": [ + "kt", + "ktm", + "kts" + ], + "filenames": [] + }, + { + "name": "LaTeX", + "extensions": [ + "tex", + "aux", + "bbx", + "cbx", + "cls", + "dtx", + "ins", + "lbx", + "ltx", + "mkii", + "mkiv", + "mkvi", + "sty", + "toc" + ], + "filenames": [] + }, + { + "name": "LESS", + "extensions": [ + "less" + ], + "filenames": [] + }, + { + "name": "LiveScript", + "extensions": [ + "ls", + "_ls" + ], + "filenames": [ + "Slakefile" + ] + }, + { + "name": "Lua", + "extensions": [ + "lua", + "nse", + "p8", + "pd_lua", + "rbxs", + "rockspec", + "wlua" + ], + "filenames": [ + ".luacheckrc" + ] + }, + { + "name": "Markdown", + "extensions": [ + "md", + "livemd", + "markdown", + "mdown", + "mdwn", + "mkd", + "mkdn", + "mkdown", + "ronn", + "scd", + "workbook" + ], + "filenames": [ + "contents.lr" + ] + }, + { + "name": "Nginx", + "extensions": [ + "nginx", + "nginxconf", + "vhost" + ], + "filenames": [ + "nginx.conf" + ] + }, + { + "name": "OCaml", + "extensions": [ + "ml", + "eliom", + "eliomi", + "ml4", + "mli", + "mll", + "mly" + ], + "filenames": [] + }, + { + "name": "Pascal", + "extensions": [ + "pas", + "dfm", + "dpr", + "inc", + "lpr", + "pascal", + "pp" + ], + "filenames": [] + }, + { + "name": "Perl", + "extensions": [ + "pl", + "al", + "perl", + "ph", + "plx", + "pm", + "psgi", + "t" + ], + "filenames": [ + ".latexmkrc", + "Makefile.PL", + "Rexfile", + "ack", + "cpanfile", + "latexmkrc" + ] + }, + { + "name": "PHP", + "extensions": [ + "php", + "aw", + "ctp", + "php3", + "php4", + "php5", + "phps", + "phpt" + ], + "filenames": [ + ".php", + ".php_cs", + ".php_cs.dist", + "Phakefile" + ] + }, + { + "name": "PowerShell", + "extensions": [ + "ps1", + "psd1", + "psm1" + ], + "filenames": [] + }, + { + "name": "Properties files", + "extensions": [ + "ini", + "cfg", + "cnf", + "dof", + "lektorproject", + "prefs", + "pro", + "properties", + "url" + ], + "filenames": [ + ".buckconfig", + ".coveragerc", + ".flake8", + ".pylintrc", + "HOSTS", + "buildozer.spec", + "hosts", + "pylintrc", + "vlcrc" + ] + }, + { + "name": "ProtoBuf", + "extensions": [ + "proto" + ], + "filenames": [] + }, + { + "name": "Pug", + "extensions": [ + "jade", + "pug" + ], + "filenames": [] + }, + { + "name": "Puppet", + "extensions": [ + "pp" + ], + "filenames": [ + "Modulefile" + ] + }, + { + "name": "Python", + "extensions": [ + "py", + "gyp", + "gypi", + "lmi", + "py3", + "pyde", + "pyi", + "pyp", + "pyt", + "pyw", + "rpy", + "tac", + "wsgi", + "xpy" + ], + "filenames": [ + ".gclient", + "DEPS", + "SConscript", + "SConstruct", + "wscript" + ] + }, + { + "name": "R", + "extensions": [ + "r", + "rd", + "rsx" + ], + "filenames": [ + ".Rprofile", + "expr-dist" + ] + }, + { + "name": "Ruby", + "extensions": [ + "rb", + "builder", + "eye", + "gemspec", + "god", + "jbuilder", + "mspec", + "pluginspec", + "podspec", + "prawn", + "rabl", + "rake", + "rbi", + "rbuild", + "rbw", + "rbx", + "ru", + "ruby", + "thor", + "watchr" + ], + "filenames": [ + ".irbrc", + ".pryrc", + ".simplecov", + "Appraisals", + "Berksfile", + "Brewfile", + "Buildfile", + "Capfile", + "Dangerfile", + "Deliverfile", + "Fastfile", + "Gemfile", + "Guardfile", + "Jarfile", + "Mavenfile", + "Podfile", + "Puppetfile", + "Rakefile", + "Snapfile", + "Steepfile", + "Thorfile", + "Vagrantfile", + "buildfile" + ] + }, + { + "name": "Rust", + "extensions": [ + "rs" + ], + "filenames": [] + }, + { + "name": "Sass", + "extensions": [ + "sass" + ], + "filenames": [] + }, + { + "name": "Scala", + "extensions": [ + "scala", + "kojo", + "sbt", + "sc" + ], + "filenames": [] + }, + { + "name": "Scheme", + "extensions": [ + "scm", + "sch", + "sld", + "sls", + "sps", + "ss" + ], + "filenames": [] + }, + { + "name": "SCSS", + "extensions": [ + "scss" + ], + "filenames": [] + }, + { + "name": "Shell", + "extensions": [ + "sh", + "bash", + "bats", + "command", + "ksh", + "sbatch", + "slurm", + "tmux", + "tool", + "trigger", + "zsh", + "zsh-theme" + ], + "filenames": [ + ".bash_aliases", + ".bash_functions", + ".bash_history", + ".bash_logout", + ".bash_profile", + ".bashrc", + ".cshrc", + ".envrc", + ".flaskenv", + ".kshrc", + ".login", + ".profile", + ".tmux.conf", + ".xinitrc", + ".xsession", + ".zlogin", + ".zlogout", + ".zprofile", + ".zshenv", + ".zshrc", + "9fs", + "PKGBUILD", + "bash_aliases", + "bash_logout", + "bash_profile", + "bashrc", + "cshrc", + "gradlew", + "kshrc", + "login", + "man", + "mvnw", + "profile", + "tmux.conf", + "xinitrc", + "xsession", + "zlogin", + "zlogout", + "zprofile", + "zshenv", + "zshrc" + ] + }, + { + "name": "Smalltalk", + "extensions": [ + "st", + "cs" + ], + "filenames": [] + }, + { + "name": "SQL", + "extensions": [ + "sql", + "ddl", + "inc", + "mysql", + "prc", + "tab", + "udf", + "viw" + ], + "filenames": [] + }, + { + "name": "Stylus", + "extensions": [ + "styl" + ], + "filenames": [] + }, + { + "name": "Swift", + "extensions": [ + "swift" + ], + "filenames": [] + }, + { + "name": "SystemVerilog", + "extensions": [ + "sv", + "svh", + "vh" + ], + "filenames": [] + }, + { + "name": "Tcl", + "extensions": [ + "tcl", + "adp", + "sdc", + "tm", + "xdc" + ], + "filenames": [ + "owh", + "starfield" + ] + }, + { + "name": "TOML", + "extensions": [ + "toml" + ], + "filenames": [ + "Cargo.lock", + "Cargo.toml.orig", + "Gopkg.lock", + "Pipfile", + "mise.local.lock", + "mise.lock", + "pdm.lock", + "poetry.lock", + "uv.lock" + ] + }, + { + "name": "TSX", + "extensions": [ + "tsx" + ], + "filenames": [] + }, + { + "name": "TypeScript", + "extensions": [ + "ts", + "cts", + "mts" + ], + "filenames": [] + }, + { + "name": "Verilog", + "extensions": [ + "v", + "veo" + ], + "filenames": [] + }, + { + "name": "VHDL", + "extensions": [ + "vhdl", + "vhd", + "vhf", + "vhi", + "vho", + "vhs", + "vht", + "vhw" + ], + "filenames": [] + }, + { + "name": "Vue", + "extensions": [ + "vue" + ], + "filenames": [] + }, + { + "name": "WebAssembly", + "extensions": [ + "wast", + "wat" + ], + "filenames": [] + }, + { + "name": "XML", + "extensions": [ + "xml", + "adml", + "admx", + "ant", + "axaml", + "axml", + "builds", + "ccproj", + "ccxml", + "clixml", + "cproject", + "cscfg", + "csdef", + "csl", + "csproj", + "ct", + "depproj", + "dita", + "ditamap", + "ditaval", + "dotsettings", + "filters", + "fsproj", + "fxml", + "glade", + "gml", + "gmx", + "gpx", + "grxml", + "gst", + "hzp", + "icls", + "iml", + "ivy", + "jelly", + "kml", + "launch", + "mdpolicy", + "mjml", + "mm", + "mod", + "mojo", + "mxml", + "natvis", + "ncl", + "ndproj", + "nproj", + "nuspec", + "odd", + "osm", + "pkgproj", + "pluginspec", + "proj", + "props", + "ps1xml", + "psc1", + "pt", + "pubxml", + "qhelp", + "rdf", + "res", + "resx", + "rs", + "rss", + "sch", + "scxml", + "sfproj", + "shproj", + "slnx", + "srdf", + "storyboard", + "sublime-snippet", + "sw", + "targets", + "tml", + "typ", + "ui", + "urdf", + "ux", + "vbproj", + "vcxproj", + "vsixmanifest", + "vssettings", + "vstemplate", + "vxml", + "wixproj", + "workflow", + "wsdl", + "wsf", + "wxi", + "wxl", + "wxs", + "x3d", + "xacro", + "xaml", + "xib", + "xlf", + "xliff", + "xmi", + "xmp", + "xproj", + "xsd", + "xspec", + "xul", + "zcml" + ], + "filenames": [ + ".classpath", + ".cproject", + ".project", + "App.config", + "NuGet.config", + "Settings.StyleCop", + "Web.Debug.config", + "Web.Release.config", + "Web.config", + "packages.config" + ] + }, + { + "name": "YAML", + "extensions": [ + "yml", + "mir", + "reek", + "rviz", + "sublime-syntax", + "syntax", + "yaml", + "yaml-tmlanguage" + ], + "filenames": [ + ".clang-format", + ".clang-tidy", + ".clangd", + ".gemrc", + "CITATION.cff", + "glide.lock", + "pixi.lock", + "yarn.lock" + ] + } +] diff --git a/tools/generate-codemirror-languages.ts b/tools/generate-codemirror-languages.ts new file mode 100755 index 0000000000..522da8b7d3 --- /dev/null +++ b/tools/generate-codemirror-languages.ts @@ -0,0 +1,151 @@ +#!/usr/bin/env node +import {load as parseYaml} from 'js-yaml'; +import {writeFile} from 'node:fs/promises'; + +const LINGUIST_URL = 'https://raw.githubusercontent.com/github-linguist/linguist/main/lib/linguist/languages.yml'; + +// Map github-linguist language names to the names CodeMirror's @codemirror/language-data +// uses. Only languages that we want to load with extended extension/filename data are +// listed; everything else falls through to language-data's defaults at runtime. +const linguistToCm: Record = { + 'C': 'C', + 'C++': 'C++', + 'C#': 'C#', + 'CMake': 'CMake', + 'COBOL': 'Cobol', + 'CSS': 'CSS', + 'Clojure': 'Clojure', + 'CoffeeScript': 'CoffeeScript', + 'Common Lisp': 'Common Lisp', + 'Crystal': 'Crystal', + 'Cython': 'Cython', + 'D': 'D', + 'Dart': 'Dart', + 'Diff': 'diff', + 'Dockerfile': 'Dockerfile', + 'Elm': 'Elm', + 'Erlang': 'Erlang', + 'F#': 'F#', + 'Fortran': 'Fortran', + 'Go': 'Go', + 'Groovy': 'Groovy', + 'HTML': 'HTML', + 'Haskell': 'Haskell', + 'INI': 'Properties files', + 'JSON': 'JSON', + 'Java': 'Java', + 'JavaScript': 'JavaScript', + 'Julia': 'Julia', + 'Kotlin': 'Kotlin', + 'Less': 'LESS', + 'LiveScript': 'LiveScript', + 'Lua': 'Lua', + 'Markdown': 'Markdown', + 'Nginx': 'Nginx', + 'OCaml': 'OCaml', + 'PHP': 'PHP', + 'Pascal': 'Pascal', + 'Perl': 'Perl', + 'PowerShell': 'PowerShell', + 'Protocol Buffer': 'ProtoBuf', + 'Pug': 'Pug', + 'Puppet': 'Puppet', + 'Python': 'Python', + 'R': 'R', + 'Ruby': 'Ruby', + 'Rust': 'Rust', + 'SCSS': 'SCSS', + 'SQL': 'SQL', + 'Sass': 'Sass', + 'Scala': 'Scala', + 'Scheme': 'Scheme', + 'Shell': 'Shell', + 'Smalltalk': 'Smalltalk', + 'Stylus': 'Stylus', + 'Swift': 'Swift', + 'SystemVerilog': 'SystemVerilog', + 'TOML': 'TOML', + 'TSX': 'TSX', + 'Tcl': 'Tcl', + 'TeX': 'LaTeX', + 'TypeScript': 'TypeScript', + 'VHDL': 'VHDL', + 'Verilog': 'Verilog', + 'Vue': 'Vue', + 'WebAssembly': 'WebAssembly', + 'XML': 'XML', + 'YAML': 'YAML', +}; + +// Per-language extensions to drop. Use only for extensions that would actively collide +// with another language (e.g. .inc claimed by both PHP and C++) or where the syntax is +// genuinely incompatible with the CodeMirror mode (e.g. .csh vs sh). +const excludeExt: Record = { + 'C++': ['inc'], + 'INI': ['frm'], + 'JavaScript': ['_js', 'bones', 'es', 'es6', 'frag', 'gs', 'jake', 'javascript', 'jsb', 'jscad', 'jsfl', 'jslib', 'jsm', 'jspre', 'jss', 'njs', 'pac', 'sjs', 'ssjs', 'xsjs', 'xsjslib'], + 'Lua': ['fcgi'], + 'PHP': ['fcgi', 'inc'], + 'Perl': ['cgi', 'fcgi'], + 'Python': ['cgi', 'fcgi', 'spec'], + 'Ruby': ['fcgi', 'spec'], + 'Shell': ['cgi', 'csh', 'fcgi'], + 'XML': ['inc', 'jsproj', 'tmpl', 'ts', 'tsx'], +}; + +type LinguistEntry = { + type: string; + extensions?: string[]; + filenames?: string[]; +}; + +type CmLanguage = { + name: string; + extensions: string[]; + filenames: string[]; +}; + +async function main() { + const res = await fetch(LINGUIST_URL); // eslint-disable-line no-restricted-globals -- node build script, not browser code + if (!res.ok) throw new Error(`fetch ${LINGUIST_URL} failed: ${res.status}`); + const linguist = parseYaml(await res.text()) as Record; + + const out: CmLanguage[] = []; + const missing: string[] = []; + for (const [linguistName, cmName] of Object.entries(linguistToCm)) { + const entry = linguist[linguistName]; + if (!entry) { + missing.push(linguistName); + continue; + } + const exExt = new Set(excludeExt[linguistName]); + // CodeMirror's matchFilename uses /\.([^.]+)$/ to extract the suffix, so multi-dot + // extensions like ".cmake.in" cannot match as extensions and are dropped here. + const extensions = (entry.extensions ?? []) + .map((e) => e.replace(/^\./, '')) + .filter((e) => !e.includes('.') && !exExt.has(e)); + const filenames = entry.filenames ?? []; + out.push({ + name: cmName, + extensions: Array.from(new Set(extensions)), + filenames: Array.from(new Set(filenames)), + }); + } + + if (missing.length) { + console.warn(`linguist entries not found: ${missing.join(', ')}`); + } + + out.sort((a, b) => a.name.localeCompare(b.name)); + + const outPath = new URL('../assets/codemirror-languages.json', import.meta.url); + await writeFile(outPath, `${JSON.stringify(out, null, 2)}\n`); + console.info(`wrote ${out.length} languages to ${outPath.pathname}`); +} + +try { + await main(); +} catch (err) { + console.error(err); + process.exit(1); +} diff --git a/web_src/js/modules/codeeditor/main.ts b/web_src/js/modules/codeeditor/main.ts index e847f357cb..793a4e9491 100644 --- a/web_src/js/modules/codeeditor/main.ts +++ b/web_src/js/modules/codeeditor/main.ts @@ -43,8 +43,10 @@ export type CodemirrorEditor = { export type CodemirrorModules = Awaited>; +type LinguistLanguage = {name: string; extensions: string[]; filenames: string[]}; + async function importCodemirror() { - const [autocomplete, commands, language, languageData, lint, search, state, view, highlight, indentMarkers, vscodeKeymap] = await Promise.all([ + const [autocomplete, commands, language, languageData, lint, search, state, view, highlight, indentMarkers, vscodeKeymap, linguistJson] = await Promise.all([ import('@codemirror/autocomplete'), import('@codemirror/commands'), import('@codemirror/language'), @@ -56,8 +58,42 @@ async function importCodemirror() { import('@lezer/highlight'), import('@replit/codemirror-indentation-markers'), import('@replit/codemirror-vscode-keymap'), + import('../../../../assets/codemirror-languages.json', {with: {type: 'json'}}), ]); - return {autocomplete, commands, language, languageData, lint, search, state, view, highlight, indentMarkers, vscodeKeymap}; + return {autocomplete, commands, language, languageData, lint, search, state, view, highlight, indentMarkers, vscodeKeymap, linguistLanguages: linguistJson.default as LinguistLanguage[]}; +} + +const manualFilenames: Record = { + 'Properties files': ['.editorconfig', '.gitconfig', '.npmrc'], + 'Python': ['Snakefile'], +}; +const manualExtensions: Record = { + 'Properties files': ['conf'], +}; +const handledByCustomEntry = new Set(['Dockerfile', 'Markdown']); +const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const filenameUnion = (filenames: string[]) => + filenames.length ? new RegExp(`^(${filenames.map(escapeRegex).join('|')})$`) : undefined; + +let baseLanguagesCache: LanguageDescription[] | null = null; +function buildBaseLanguages(cm: CodemirrorModules): LanguageDescription[] { + if (baseLanguagesCache) return baseLanguagesCache; + const loadByName = new Map( + cm.languageData.languages.map((l: LanguageDescription) => [l.name, l.load.bind(l)]), + ); + const overrides = cm.linguistLanguages + .filter((l) => loadByName.has(l.name) && !handledByCustomEntry.has(l.name)) + .map((l) => cm.language.LanguageDescription.of({ + name: l.name, + extensions: [...l.extensions, ...(manualExtensions[l.name] ?? [])], + filename: filenameUnion([...l.filenames, ...(manualFilenames[l.name] ?? [])]), + load: loadByName.get(l.name)!, + })); + const overrideNames = new Set(overrides.map((o) => o.name)); + const fallback = cm.languageData.languages.filter( + (l: LanguageDescription) => !overrideNames.has(l.name) && !handledByCustomEntry.has(l.name), + ); + return baseLanguagesCache = [...overrides, ...fallback]; } function togglePreviewDisplay(previewable: boolean): void { @@ -85,13 +121,20 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn const previewableExts = new Set(config.previewableExtensions || []); const lineWrapExts = config.lineWrapExtensions || []; const cm = await importCodemirror(); + const markdown = cm.linguistLanguages.find((l) => l.name === 'Markdown'); + const dockerfile = cm.linguistLanguages.find((l) => l.name === 'Dockerfile'); const languageDescriptions: LanguageDescription[] = [ - ...cm.languageData.languages.filter((l: LanguageDescription) => l.name !== 'Markdown'), + ...buildBaseLanguages(cm), cm.language.LanguageDescription.of({ - name: 'Markdown', extensions: ['md', 'markdown', 'mkd'], + name: 'Markdown', extensions: markdown?.extensions ?? ['md', 'markdown', 'mkd'], load: async () => (await import('@codemirror/lang-markdown')).markdown({codeLanguages: languageDescriptions}), }), + cm.language.LanguageDescription.of({ + name: 'Dockerfile', extensions: dockerfile?.extensions ?? ['dockerfile', 'containerfile'], + filename: /^(Containerfile|Dockerfile)(\..+)?$/i, + load: async () => new cm.language.LanguageSupport(cm.language.StreamLanguage.define((await import('@codemirror/legacy-modes/mode/dockerfile')).dockerFile)), + }), cm.language.LanguageDescription.of({ name: 'Elixir', extensions: ['ex', 'exs'], load: async () => (await import('codemirror-lang-elixir')).elixir(), @@ -105,7 +148,7 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn load: async () => (await import('@replit/codemirror-lang-svelte')).svelte(), }), cm.language.LanguageDescription.of({ - name: 'Makefile', filename: /^(GNUm|M|m)akefile$/, + name: 'Makefile', extensions: ['mk', 'mak', 'make'], filename: /^(GNU|BSD)?[Mm]akefile(\..+)?$/, load: async () => new cm.language.LanguageSupport(cm.language.StreamLanguage.define((await import('@codemirror/legacy-modes/mode/shell')).shell)), }), cm.language.LanguageDescription.of({