diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index fea3ab0e9..264b573b6 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -2882,12 +2882,6 @@ const typescriptLines = highlightCode("const x = 1;", "typescript"); // Detect language from a file path const lang = getLanguageFromPath("/path/to/file.rs"); // "rust" const fileLines = highlightCode(code, lang); - -// Uncommon grammars load lazily. In a tool renderer, request another render -// after the grammar and its dependencies are ready. -const lazyLines = highlightCode(code, lang, { - onLanguageReady: context.invalidate, -}); ``` ## Error Handling diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 7b656c0c9..3d05c2af4 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -393,7 +393,6 @@ export { getMarkdownTheme, getSelectListTheme, getSettingsListTheme, - type HighlightCodeOptions, highlightCode, initTheme, Theme, diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts index a5354c1ae..3f030586a 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts @@ -26,8 +26,6 @@ export class InteractiveThemeController { private autoSyncEnabled = false; private terminalColorSchemeUnsubscribe: (() => void) | undefined; private highlightLanguageLoadUnsubscribe: (() => void) | undefined; - private highlightLanguageRefreshTimer: ReturnType | undefined; - private highlightLanguageGlobalRefreshPending = false; constructor(ui: TUI, settingsManager: SettingsManager, showError: (message: string) => void, onChanged: () => void) { this.ui = ui; @@ -42,14 +40,8 @@ export class InteractiveThemeController { this.showError(`Failed to load syntax highlighting language "${event.language}": ${message}`); return; } - this.highlightLanguageGlobalRefreshPending ||= event.requiresGlobalRefresh; - if (this.highlightLanguageRefreshTimer !== undefined) return; - this.highlightLanguageRefreshTimer = setTimeout(() => { - this.highlightLanguageRefreshTimer = undefined; - if (this.highlightLanguageGlobalRefreshPending) this.ui.invalidate(); - this.highlightLanguageGlobalRefreshPending = false; - this.ui.requestRender(); - }, 0); + this.ui.invalidate(); + this.ui.requestRender(); }); this.bindTerminalColorSchemeListener(); } @@ -117,9 +109,6 @@ export class InteractiveThemeController { this.terminalColorSchemeUnsubscribe = undefined; this.highlightLanguageLoadUnsubscribe?.(); this.highlightLanguageLoadUnsubscribe = undefined; - if (this.highlightLanguageRefreshTimer !== undefined) clearTimeout(this.highlightLanguageRefreshTimer); - this.highlightLanguageRefreshTimer = undefined; - this.highlightLanguageGlobalRefreshPending = false; } getTerminalTheme(): TerminalTheme { diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 9e19f4758..31c416a97 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -17,8 +17,6 @@ import type { SourceInfo } from "../../../core/source-info.ts"; import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.ts"; import { highlight, requestHighlightLanguage } from "../../../utils/syntax-highlight.ts"; -export { getLanguageFromPath } from "../../../utils/syntax-highlight.ts"; - // ============================================================================ // Types & Schema // ============================================================================ @@ -1155,18 +1153,14 @@ function getCliHighlightTheme(t: Theme): CliHighlightTheme { return cachedCliHighlightTheme; } -export interface HighlightCodeOptions { - onLanguageReady?: () => void; -} - /** * Highlight code with syntax coloring based on file extension or language. * Returns array of highlighted lines. */ -export function highlightCode(code: string, lang?: string, options: HighlightCodeOptions = {}): string[] { +export function highlightCode(code: string, lang?: string): string[] { // Validate language before highlighting to avoid stderr spam from highlight.js. // Uncommon languages start loading in the background and render plainly once. - const validLang = lang ? requestHighlightLanguage(lang, options.onLanguageReady) : undefined; + const validLang = lang ? requestHighlightLanguage(lang) : undefined; // Skip highlighting when no valid language is specified. highlight.js's // auto-detection is unreliable and can misidentify prose as AppleScript, // LiveCodeServer, etc., coloring random English words as keywords. @@ -1185,6 +1179,77 @@ export function highlightCode(code: string, lang?: string, options: HighlightCod } } +/** + * Get language identifier from file path extension. + */ +export function getLanguageFromPath(filePath: string): string | undefined { + const ext = filePath.split(".").pop()?.toLowerCase(); + if (!ext) return undefined; + + const extToLang: Record = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + mjs: "javascript", + cjs: "javascript", + py: "python", + rb: "ruby", + rs: "rust", + go: "go", + java: "java", + kt: "kotlin", + swift: "swift", + c: "c", + h: "c", + cpp: "cpp", + cc: "cpp", + cxx: "cpp", + hpp: "cpp", + cs: "csharp", + php: "php", + sh: "bash", + bash: "bash", + zsh: "bash", + fish: "fish", + ps1: "powershell", + sql: "sql", + html: "html", + htm: "html", + css: "css", + scss: "scss", + sass: "sass", + less: "less", + json: "json", + yaml: "yaml", + yml: "yaml", + toml: "toml", + xml: "xml", + md: "markdown", + markdown: "markdown", + dockerfile: "dockerfile", + makefile: "makefile", + cmake: "cmake", + lua: "lua", + perl: "perl", + r: "r", + scala: "scala", + clj: "clojure", + ex: "elixir", + exs: "elixir", + erl: "erlang", + hs: "haskell", + ml: "ocaml", + vim: "vim", + graphql: "graphql", + proto: "protobuf", + tf: "hcl", + hcl: "hcl", + }; + + return extToLang[ext]; +} + export function getMarkdownTheme(): MarkdownTheme { return { heading: (text: string) => theme.fg("mdHeading", text), @@ -1201,7 +1266,7 @@ export function getMarkdownTheme(): MarkdownTheme { italic: (text: string) => theme.italic(text), underline: (text: string) => theme.underline(text), strikethrough: (text: string) => chalk.strikethrough(text), - highlightCode: (code, lang, onLanguageReady) => highlightCode(code, lang, { onLanguageReady }), + highlightCode, }; } diff --git a/packages/coding-agent/src/utils/highlight-languages.ts b/packages/coding-agent/src/utils/highlight-languages.ts index ffc3db5a8..06bccda2a 100644 --- a/packages/coding-agent/src/utils/highlight-languages.ts +++ b/packages/coding-agent/src/utils/highlight-languages.ts @@ -1,7 +1,7 @@ import type { LanguageDefinition } from "highlight.js/lib/core.js"; import bash from "highlight.js/lib/languages/bash.js"; import css from "highlight.js/lib/languages/css.js"; -import diff from "highlight.js/lib/languages/diff.js"; +import diffLanguage from "highlight.js/lib/languages/diff.js"; import javascript from "highlight.js/lib/languages/javascript.js"; import json from "highlight.js/lib/languages/json.js"; import markdown from "highlight.js/lib/languages/markdown.js"; @@ -12,545 +12,253 @@ import xml from "highlight.js/lib/languages/xml.js"; import yaml from "highlight.js/lib/languages/yaml.js"; type LanguageModule = { default: LanguageDefinition }; +type LanguageLoader = () => Promise; -export interface HighlightLanguage { - name: string; - extensions?: readonly string[]; - aliases?: readonly string[]; - dependencies?: readonly string[]; - definition?: LanguageDefinition; - load?: () => Promise; -} +export const HIGHLIGHT_LANGUAGE_DEFINITIONS: Record = { + xml: xml, + bash: bash, + css: css, + markdown: markdown, + diff: diffLanguage, + ruby: ruby, + javascript: javascript, + json: json, + python: python, + yaml: yaml, + typescript: typescript, +}; -// Registration, aliases, dependencies, lazy importers, and file-extension -// detection live in one registry. -// // Intentional exception to the no-dynamic-import rule: literal dynamic imports // let Bun embed every grammar in the standalone binary while keeping evaluation // lazy, and let Node load only the requested grammar. -export const HIGHLIGHT_LANGUAGES: readonly HighlightLanguage[] = [ - { name: "1c", load: () => import("highlight.js/lib/languages/1c.js") }, - { name: "abnf", load: () => import("highlight.js/lib/languages/abnf.js") }, - { name: "accesslog", load: () => import("highlight.js/lib/languages/accesslog.js") }, - { - name: "actionscript", - aliases: ["as"], - load: () => import("highlight.js/lib/languages/actionscript.js"), - }, - { name: "ada", load: () => import("highlight.js/lib/languages/ada.js") }, - { - name: "angelscript", - aliases: ["asc"], - load: () => import("highlight.js/lib/languages/angelscript.js"), - }, - { - name: "apache", - aliases: ["apacheconf"], - load: () => import("highlight.js/lib/languages/apache.js"), - }, - { - name: "applescript", - aliases: ["osascript"], - load: () => import("highlight.js/lib/languages/applescript.js"), - }, - { name: "arcade", load: () => import("highlight.js/lib/languages/arcade.js") }, - { name: "arduino", aliases: ["ino"], load: () => import("highlight.js/lib/languages/arduino.js") }, - { name: "armasm", aliases: ["arm"], load: () => import("highlight.js/lib/languages/armasm.js") }, - { - name: "xml", - extensions: ["xml"], - aliases: ["html", "xhtml", "rss", "atom", "xjb", "xsd", "xsl", "plist", "wsf", "svg"], - dependencies: ["css", "javascript"], - definition: xml, - }, - { name: "html", extensions: ["html", "htm"], dependencies: ["xml"] }, - { - name: "asciidoc", - aliases: ["adoc"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/asciidoc.js"), - }, - { name: "aspectj", load: () => import("highlight.js/lib/languages/aspectj.js") }, - { - name: "autohotkey", - aliases: ["ahk"], - load: () => import("highlight.js/lib/languages/autohotkey.js"), - }, - { name: "autoit", load: () => import("highlight.js/lib/languages/autoit.js") }, - { name: "avrasm", load: () => import("highlight.js/lib/languages/avrasm.js") }, - { name: "awk", load: () => import("highlight.js/lib/languages/awk.js") }, - { name: "axapta", aliases: ["x++"], load: () => import("highlight.js/lib/languages/axapta.js") }, - { name: "bash", extensions: ["sh", "bash", "zsh"], aliases: ["sh", "zsh"], definition: bash }, - { name: "basic", load: () => import("highlight.js/lib/languages/basic.js") }, - { name: "bnf", load: () => import("highlight.js/lib/languages/bnf.js") }, - { - name: "brainfuck", - aliases: ["bf"], - load: () => import("highlight.js/lib/languages/brainfuck.js"), - }, - { - name: "c-like", - aliases: ["c", "h", "cc", "c++", "h++", "hpp", "hh", "hxx", "cxx"], - load: () => import("highlight.js/lib/languages/c-like.js"), - }, - { name: "c", extensions: ["c", "h"], aliases: ["h"], load: () => import("highlight.js/lib/languages/c.js") }, - { name: "cal", load: () => import("highlight.js/lib/languages/cal.js") }, - { - name: "capnproto", - aliases: ["capnp"], - load: () => import("highlight.js/lib/languages/capnproto.js"), - }, - { name: "ceylon", load: () => import("highlight.js/lib/languages/ceylon.js") }, - { - name: "clean", - aliases: ["icl", "dcl"], - load: () => import("highlight.js/lib/languages/clean.js"), - }, - { - name: "clojure", - extensions: ["clj"], - aliases: ["clj"], - load: () => import("highlight.js/lib/languages/clojure.js"), - }, - { - name: "clojure-repl", - dependencies: ["clojure"], - load: () => import("highlight.js/lib/languages/clojure-repl.js"), - }, - { - name: "cmake", - extensions: ["cmake"], - aliases: ["cmake.in"], - load: () => import("highlight.js/lib/languages/cmake.js"), - }, - { - name: "coffeescript", - aliases: ["coffee", "cson", "iced"], - dependencies: ["javascript"], - load: () => import("highlight.js/lib/languages/coffeescript.js"), - }, - { name: "coq", load: () => import("highlight.js/lib/languages/coq.js") }, - { - name: "cos", - aliases: ["cls"], - dependencies: ["javascript", "sql", "xml"], - load: () => import("highlight.js/lib/languages/cos.js"), - }, - { - name: "cpp", - extensions: ["cpp", "cc", "cxx", "hpp"], - aliases: ["cc", "c++", "h++", "hpp", "hh", "hxx", "cxx"], - load: () => import("highlight.js/lib/languages/cpp.js"), - }, - { - name: "crmsh", - aliases: ["crm", "pcmk"], - load: () => import("highlight.js/lib/languages/crmsh.js"), - }, - { name: "crystal", aliases: ["cr"], load: () => import("highlight.js/lib/languages/crystal.js") }, - { - name: "csharp", - extensions: ["cs"], - aliases: ["cs", "c#"], - load: () => import("highlight.js/lib/languages/csharp.js"), - }, - { name: "csp", load: () => import("highlight.js/lib/languages/csp.js") }, - { name: "css", extensions: ["css"], definition: css }, - { name: "d", load: () => import("highlight.js/lib/languages/d.js") }, - { - name: "markdown", - extensions: ["md", "markdown"], - aliases: ["md", "mkdown", "mkd"], - dependencies: ["xml"], - definition: markdown, - }, - { - name: "dart", - dependencies: ["markdown"], - load: () => import("highlight.js/lib/languages/dart.js"), - }, - { - name: "delphi", - aliases: ["dpr", "dfm", "pas", "pascal", "freepascal", "lazarus", "lpr", "lfm"], - load: () => import("highlight.js/lib/languages/delphi.js"), - }, - { name: "diff", aliases: ["patch"], definition: diff }, - { - name: "django", - aliases: ["jinja"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/django.js"), - }, - { name: "dns", aliases: ["bind", "zone"], load: () => import("highlight.js/lib/languages/dns.js") }, - { - name: "dockerfile", - extensions: ["dockerfile"], - aliases: ["docker"], - dependencies: ["bash"], - load: () => import("highlight.js/lib/languages/dockerfile.js"), - }, - { name: "dos", aliases: ["bat", "cmd"], load: () => import("highlight.js/lib/languages/dos.js") }, - { name: "dsconfig", load: () => import("highlight.js/lib/languages/dsconfig.js") }, - { name: "dts", load: () => import("highlight.js/lib/languages/dts.js") }, - { - name: "dust", - aliases: ["dst"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/dust.js"), - }, - { name: "ebnf", load: () => import("highlight.js/lib/languages/ebnf.js") }, - { name: "elixir", extensions: ["ex", "exs"], load: () => import("highlight.js/lib/languages/elixir.js") }, - { name: "elm", load: () => import("highlight.js/lib/languages/elm.js") }, - { - name: "ruby", - extensions: ["rb"], - aliases: ["rb", "gemspec", "podspec", "thor", "irb"], - definition: ruby, - }, - { - name: "erb", - dependencies: ["ruby", "xml"], - load: () => import("highlight.js/lib/languages/erb.js"), - }, - { name: "erlang-repl", load: () => import("highlight.js/lib/languages/erlang-repl.js") }, - { - name: "erlang", - extensions: ["erl"], - aliases: ["erl"], - load: () => import("highlight.js/lib/languages/erlang.js"), - }, - { - name: "excel", - aliases: ["xlsx", "xls"], - load: () => import("highlight.js/lib/languages/excel.js"), - }, - { name: "fix", load: () => import("highlight.js/lib/languages/fix.js") }, - { name: "fish", extensions: ["fish"] }, - { name: "flix", load: () => import("highlight.js/lib/languages/flix.js") }, - { - name: "fortran", - aliases: ["f90", "f95"], - load: () => import("highlight.js/lib/languages/fortran.js"), - }, - { name: "fsharp", aliases: ["fs"], load: () => import("highlight.js/lib/languages/fsharp.js") }, - { name: "gams", aliases: ["gms"], load: () => import("highlight.js/lib/languages/gams.js") }, - { name: "gauss", aliases: ["gss"], load: () => import("highlight.js/lib/languages/gauss.js") }, - { name: "gcode", aliases: ["nc"], load: () => import("highlight.js/lib/languages/gcode.js") }, - { - name: "gherkin", - aliases: ["feature"], - load: () => import("highlight.js/lib/languages/gherkin.js"), - }, - { name: "glsl", load: () => import("highlight.js/lib/languages/glsl.js") }, - { name: "gml", load: () => import("highlight.js/lib/languages/gml.js") }, - { name: "go", extensions: ["go"], aliases: ["golang"], load: () => import("highlight.js/lib/languages/go.js") }, - { name: "golo", load: () => import("highlight.js/lib/languages/golo.js") }, - { name: "gradle", load: () => import("highlight.js/lib/languages/gradle.js") }, - { name: "graphql", extensions: ["graphql"] }, - { name: "groovy", load: () => import("highlight.js/lib/languages/groovy.js") }, - { - name: "haml", - dependencies: ["ruby"], - load: () => import("highlight.js/lib/languages/haml.js"), - }, - { - name: "handlebars", - aliases: ["hbs", "html.hbs", "html.handlebars", "htmlbars"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/handlebars.js"), - }, - { - name: "haskell", - extensions: ["hs"], - aliases: ["hs"], - load: () => import("highlight.js/lib/languages/haskell.js"), - }, - { name: "hcl", extensions: ["tf", "hcl"] }, - { name: "haxe", aliases: ["hx"], load: () => import("highlight.js/lib/languages/haxe.js") }, - { name: "hsp", load: () => import("highlight.js/lib/languages/hsp.js") }, - { - name: "htmlbars", - aliases: ["hbs", "html.hbs", "html.handlebars", "htmlbars"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/htmlbars.js"), - }, - { name: "http", aliases: ["https"], load: () => import("highlight.js/lib/languages/http.js") }, - { name: "hy", aliases: ["hylang"], load: () => import("highlight.js/lib/languages/hy.js") }, - { name: "inform7", aliases: ["i7"], load: () => import("highlight.js/lib/languages/inform7.js") }, - { name: "ini", aliases: ["toml"], load: () => import("highlight.js/lib/languages/ini.js") }, - { name: "irpf90", load: () => import("highlight.js/lib/languages/irpf90.js") }, - { name: "isbl", load: () => import("highlight.js/lib/languages/isbl.js") }, - { name: "java", extensions: ["java"], aliases: ["jsp"], load: () => import("highlight.js/lib/languages/java.js") }, - { - name: "javascript", - extensions: ["js", "jsx", "mjs", "cjs"], - aliases: ["js", "jsx", "mjs", "cjs"], - dependencies: ["css", "xml"], - definition: javascript, - }, - { - name: "jboss-cli", - aliases: ["wildfly-cli"], - load: () => import("highlight.js/lib/languages/jboss-cli.js"), - }, - { name: "json", extensions: ["json"], definition: json }, - { name: "julia", load: () => import("highlight.js/lib/languages/julia.js") }, - { - name: "julia-repl", - dependencies: ["julia"], - load: () => import("highlight.js/lib/languages/julia-repl.js"), - }, - { - name: "kotlin", - extensions: ["kt"], - aliases: ["kt", "kts"], - load: () => import("highlight.js/lib/languages/kotlin.js"), - }, - { - name: "lasso", - aliases: ["ls", "lassoscript"], - load: () => import("highlight.js/lib/languages/lasso.js"), - }, - { name: "latex", aliases: ["tex"], load: () => import("highlight.js/lib/languages/latex.js") }, - { name: "ldif", load: () => import("highlight.js/lib/languages/ldif.js") }, - { name: "leaf", load: () => import("highlight.js/lib/languages/leaf.js") }, - { name: "less", extensions: ["less"], load: () => import("highlight.js/lib/languages/less.js") }, - { name: "lisp", load: () => import("highlight.js/lib/languages/lisp.js") }, - { name: "livecodeserver", load: () => import("highlight.js/lib/languages/livecodeserver.js") }, - { - name: "livescript", - aliases: ["ls"], - dependencies: ["javascript"], - load: () => import("highlight.js/lib/languages/livescript.js"), - }, - { name: "llvm", load: () => import("highlight.js/lib/languages/llvm.js") }, - { name: "lsl", load: () => import("highlight.js/lib/languages/lsl.js") }, - { name: "lua", extensions: ["lua"], load: () => import("highlight.js/lib/languages/lua.js") }, - { - name: "makefile", - extensions: ["makefile"], - aliases: ["mk", "mak", "make"], - load: () => import("highlight.js/lib/languages/makefile.js"), - }, - { - name: "mathematica", - aliases: ["mma", "wl"], - load: () => import("highlight.js/lib/languages/mathematica.js"), - }, - { name: "matlab", load: () => import("highlight.js/lib/languages/matlab.js") }, - { name: "maxima", load: () => import("highlight.js/lib/languages/maxima.js") }, - { name: "mel", load: () => import("highlight.js/lib/languages/mel.js") }, - { - name: "mercury", - aliases: ["m", "moo"], - load: () => import("highlight.js/lib/languages/mercury.js"), - }, - { name: "mipsasm", aliases: ["mips"], load: () => import("highlight.js/lib/languages/mipsasm.js") }, - { name: "mizar", load: () => import("highlight.js/lib/languages/mizar.js") }, - { - name: "perl", - extensions: ["perl"], - aliases: ["pl", "pm"], - dependencies: ["mojolicious"], - load: () => import("highlight.js/lib/languages/perl.js"), - }, - { - name: "mojolicious", - dependencies: ["perl", "xml"], - load: () => import("highlight.js/lib/languages/mojolicious.js"), - }, - { name: "monkey", load: () => import("highlight.js/lib/languages/monkey.js") }, - { - name: "moonscript", - aliases: ["moon"], - load: () => import("highlight.js/lib/languages/moonscript.js"), - }, - { name: "n1ql", load: () => import("highlight.js/lib/languages/n1ql.js") }, - { name: "nginx", aliases: ["nginxconf"], load: () => import("highlight.js/lib/languages/nginx.js") }, - { name: "nim", load: () => import("highlight.js/lib/languages/nim.js") }, - { name: "nix", aliases: ["nixos"], load: () => import("highlight.js/lib/languages/nix.js") }, - { - name: "node-repl", - dependencies: ["javascript"], - load: () => import("highlight.js/lib/languages/node-repl.js"), - }, - { name: "nsis", load: () => import("highlight.js/lib/languages/nsis.js") }, - { - name: "objectivec", - aliases: ["mm", "objc", "obj-c", "obj-c++", "objective-c++"], - load: () => import("highlight.js/lib/languages/objectivec.js"), - }, - { name: "ocaml", extensions: ["ml"], aliases: ["ml"], load: () => import("highlight.js/lib/languages/ocaml.js") }, - { - name: "openscad", - aliases: ["scad"], - load: () => import("highlight.js/lib/languages/openscad.js"), - }, - { name: "oxygene", load: () => import("highlight.js/lib/languages/oxygene.js") }, - { - name: "parser3", - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/parser3.js"), - }, - { name: "pf", aliases: ["pf.conf"], load: () => import("highlight.js/lib/languages/pf.js") }, - { - name: "pgsql", - aliases: ["postgres", "postgresql"], - dependencies: ["bash", "java", "json", "lua", "perl", "php", "python", "r", "ruby", "scheme", "tcl", "xml"], - load: () => import("highlight.js/lib/languages/pgsql.js"), - }, - { - name: "php", - extensions: ["php"], - aliases: ["php3", "php4", "php5", "php6", "php7", "php8"], - load: () => import("highlight.js/lib/languages/php.js"), - }, - { - name: "php-template", - dependencies: ["php", "xml"], - load: () => import("highlight.js/lib/languages/php-template.js"), - }, - { - name: "plaintext", - aliases: ["text", "txt"], - load: () => import("highlight.js/lib/languages/plaintext.js"), - }, - { name: "pony", load: () => import("highlight.js/lib/languages/pony.js") }, - { - name: "powershell", - extensions: ["ps1"], - aliases: ["ps", "ps1"], - load: () => import("highlight.js/lib/languages/powershell.js"), - }, - { name: "processing", load: () => import("highlight.js/lib/languages/processing.js") }, - { name: "profile", load: () => import("highlight.js/lib/languages/profile.js") }, - { name: "prolog", load: () => import("highlight.js/lib/languages/prolog.js") }, - { name: "properties", load: () => import("highlight.js/lib/languages/properties.js") }, - { name: "protobuf", extensions: ["proto"], load: () => import("highlight.js/lib/languages/protobuf.js") }, - { name: "puppet", aliases: ["pp"], load: () => import("highlight.js/lib/languages/puppet.js") }, - { - name: "purebasic", - aliases: ["pb", "pbi"], - load: () => import("highlight.js/lib/languages/purebasic.js"), - }, - { name: "python", extensions: ["py"], aliases: ["py", "gyp", "ipython"], definition: python }, - { - name: "python-repl", - aliases: ["pycon"], - dependencies: ["python"], - load: () => import("highlight.js/lib/languages/python-repl.js"), - }, - { name: "q", aliases: ["k", "kdb"], load: () => import("highlight.js/lib/languages/q.js") }, - { - name: "qml", - aliases: ["qt"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/qml.js"), - }, - { name: "r", extensions: ["r"], load: () => import("highlight.js/lib/languages/r.js") }, - { name: "reasonml", aliases: ["re"], load: () => import("highlight.js/lib/languages/reasonml.js") }, - { name: "rib", load: () => import("highlight.js/lib/languages/rib.js") }, - { - name: "roboconf", - aliases: ["graph", "instances"], - load: () => import("highlight.js/lib/languages/roboconf.js"), - }, - { - name: "routeros", - aliases: ["mikrotik"], - load: () => import("highlight.js/lib/languages/routeros.js"), - }, - { name: "rsl", load: () => import("highlight.js/lib/languages/rsl.js") }, - { name: "ruleslanguage", load: () => import("highlight.js/lib/languages/ruleslanguage.js") }, - { name: "rust", extensions: ["rs"], aliases: ["rs"], load: () => import("highlight.js/lib/languages/rust.js") }, - { name: "sas", load: () => import("highlight.js/lib/languages/sas.js") }, - { name: "sass", extensions: ["sass"] }, - { name: "scala", extensions: ["scala"], load: () => import("highlight.js/lib/languages/scala.js") }, - { name: "scheme", load: () => import("highlight.js/lib/languages/scheme.js") }, - { name: "scilab", aliases: ["sci"], load: () => import("highlight.js/lib/languages/scilab.js") }, - { name: "scss", extensions: ["scss"], load: () => import("highlight.js/lib/languages/scss.js") }, - { - name: "shell", - aliases: ["console"], - dependencies: ["bash"], - load: () => import("highlight.js/lib/languages/shell.js"), - }, - { name: "smali", load: () => import("highlight.js/lib/languages/smali.js") }, - { - name: "smalltalk", - aliases: ["st"], - load: () => import("highlight.js/lib/languages/smalltalk.js"), - }, - { name: "sml", aliases: ["ml"], load: () => import("highlight.js/lib/languages/sml.js") }, - { name: "sqf", load: () => import("highlight.js/lib/languages/sqf.js") }, - { - name: "sql_more", - aliases: ["mysql", "oracle"], - load: () => import("highlight.js/lib/languages/sql_more.js"), - }, - { name: "sql", extensions: ["sql"], load: () => import("highlight.js/lib/languages/sql.js") }, - { name: "stan", aliases: ["stanfuncs"], load: () => import("highlight.js/lib/languages/stan.js") }, - { name: "stata", aliases: ["do", "ado"], load: () => import("highlight.js/lib/languages/stata.js") }, - { - name: "step21", - aliases: ["p21", "step", "stp"], - load: () => import("highlight.js/lib/languages/step21.js"), - }, - { name: "stylus", aliases: ["styl"], load: () => import("highlight.js/lib/languages/stylus.js") }, - { name: "subunit", load: () => import("highlight.js/lib/languages/subunit.js") }, - { name: "swift", extensions: ["swift"], load: () => import("highlight.js/lib/languages/swift.js") }, - { name: "taggerscript", load: () => import("highlight.js/lib/languages/taggerscript.js") }, - { - name: "yaml", - extensions: ["yaml", "yml"], - aliases: ["yml"], - dependencies: ["ruby"], - definition: yaml, - }, - { - name: "tap", - dependencies: ["yaml"], - load: () => import("highlight.js/lib/languages/tap.js"), - }, - { name: "tcl", aliases: ["tk"], load: () => import("highlight.js/lib/languages/tcl.js") }, - { name: "thrift", load: () => import("highlight.js/lib/languages/thrift.js") }, - { name: "toml", extensions: ["toml"], dependencies: ["ini"] }, - { name: "tp", load: () => import("highlight.js/lib/languages/tp.js") }, - { - name: "twig", - aliases: ["craftcms"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/twig.js"), - }, - { - name: "typescript", - extensions: ["ts", "tsx"], - aliases: ["ts", "tsx"], - dependencies: ["css", "xml"], - definition: typescript, - }, - { name: "vala", load: () => import("highlight.js/lib/languages/vala.js") }, - { name: "vbnet", aliases: ["vb"], load: () => import("highlight.js/lib/languages/vbnet.js") }, - { name: "vbscript", aliases: ["vbs"], load: () => import("highlight.js/lib/languages/vbscript.js") }, - { - name: "vbscript-html", - dependencies: ["vbscript", "xml"], - load: () => import("highlight.js/lib/languages/vbscript-html.js"), - }, - { - name: "verilog", - aliases: ["v", "sv", "svh"], - load: () => import("highlight.js/lib/languages/verilog.js"), - }, - { name: "vhdl", load: () => import("highlight.js/lib/languages/vhdl.js") }, - { name: "vim", extensions: ["vim"], load: () => import("highlight.js/lib/languages/vim.js") }, - { name: "x86asm", load: () => import("highlight.js/lib/languages/x86asm.js") }, - { name: "xl", aliases: ["tao"], load: () => import("highlight.js/lib/languages/xl.js") }, - { - name: "xquery", - aliases: ["xpath", "xq"], - dependencies: ["xml"], - load: () => import("highlight.js/lib/languages/xquery.js"), - }, - { name: "zephir", aliases: ["zep"], load: () => import("highlight.js/lib/languages/zephir.js") }, -]; +export const HIGHLIGHT_LANGUAGE_LOADERS: Record = { + "1c": () => import("highlight.js/lib/languages/1c.js"), + abnf: () => import("highlight.js/lib/languages/abnf.js"), + accesslog: () => import("highlight.js/lib/languages/accesslog.js"), + actionscript: () => import("highlight.js/lib/languages/actionscript.js"), + ada: () => import("highlight.js/lib/languages/ada.js"), + angelscript: () => import("highlight.js/lib/languages/angelscript.js"), + apache: () => import("highlight.js/lib/languages/apache.js"), + applescript: () => import("highlight.js/lib/languages/applescript.js"), + arcade: () => import("highlight.js/lib/languages/arcade.js"), + arduino: () => import("highlight.js/lib/languages/arduino.js"), + armasm: () => import("highlight.js/lib/languages/armasm.js"), + asciidoc: () => import("highlight.js/lib/languages/asciidoc.js"), + aspectj: () => import("highlight.js/lib/languages/aspectj.js"), + autohotkey: () => import("highlight.js/lib/languages/autohotkey.js"), + autoit: () => import("highlight.js/lib/languages/autoit.js"), + avrasm: () => import("highlight.js/lib/languages/avrasm.js"), + awk: () => import("highlight.js/lib/languages/awk.js"), + axapta: () => import("highlight.js/lib/languages/axapta.js"), + basic: () => import("highlight.js/lib/languages/basic.js"), + bnf: () => import("highlight.js/lib/languages/bnf.js"), + brainfuck: () => import("highlight.js/lib/languages/brainfuck.js"), + "c-like": () => import("highlight.js/lib/languages/c-like.js"), + c: () => import("highlight.js/lib/languages/c.js"), + cal: () => import("highlight.js/lib/languages/cal.js"), + capnproto: () => import("highlight.js/lib/languages/capnproto.js"), + ceylon: () => import("highlight.js/lib/languages/ceylon.js"), + clean: () => import("highlight.js/lib/languages/clean.js"), + clojure: () => import("highlight.js/lib/languages/clojure.js"), + "clojure-repl": () => import("highlight.js/lib/languages/clojure-repl.js"), + cmake: () => import("highlight.js/lib/languages/cmake.js"), + coffeescript: () => import("highlight.js/lib/languages/coffeescript.js"), + coq: () => import("highlight.js/lib/languages/coq.js"), + cos: () => import("highlight.js/lib/languages/cos.js"), + cpp: () => import("highlight.js/lib/languages/cpp.js"), + crmsh: () => import("highlight.js/lib/languages/crmsh.js"), + crystal: () => import("highlight.js/lib/languages/crystal.js"), + csharp: () => import("highlight.js/lib/languages/csharp.js"), + csp: () => import("highlight.js/lib/languages/csp.js"), + d: () => import("highlight.js/lib/languages/d.js"), + dart: () => import("highlight.js/lib/languages/dart.js"), + delphi: () => import("highlight.js/lib/languages/delphi.js"), + django: () => import("highlight.js/lib/languages/django.js"), + dns: () => import("highlight.js/lib/languages/dns.js"), + dockerfile: () => import("highlight.js/lib/languages/dockerfile.js"), + dos: () => import("highlight.js/lib/languages/dos.js"), + dsconfig: () => import("highlight.js/lib/languages/dsconfig.js"), + dts: () => import("highlight.js/lib/languages/dts.js"), + dust: () => import("highlight.js/lib/languages/dust.js"), + ebnf: () => import("highlight.js/lib/languages/ebnf.js"), + elixir: () => import("highlight.js/lib/languages/elixir.js"), + elm: () => import("highlight.js/lib/languages/elm.js"), + erb: () => import("highlight.js/lib/languages/erb.js"), + "erlang-repl": () => import("highlight.js/lib/languages/erlang-repl.js"), + erlang: () => import("highlight.js/lib/languages/erlang.js"), + excel: () => import("highlight.js/lib/languages/excel.js"), + fix: () => import("highlight.js/lib/languages/fix.js"), + flix: () => import("highlight.js/lib/languages/flix.js"), + fortran: () => import("highlight.js/lib/languages/fortran.js"), + fsharp: () => import("highlight.js/lib/languages/fsharp.js"), + gams: () => import("highlight.js/lib/languages/gams.js"), + gauss: () => import("highlight.js/lib/languages/gauss.js"), + gcode: () => import("highlight.js/lib/languages/gcode.js"), + gherkin: () => import("highlight.js/lib/languages/gherkin.js"), + glsl: () => import("highlight.js/lib/languages/glsl.js"), + gml: () => import("highlight.js/lib/languages/gml.js"), + go: () => import("highlight.js/lib/languages/go.js"), + golo: () => import("highlight.js/lib/languages/golo.js"), + gradle: () => import("highlight.js/lib/languages/gradle.js"), + groovy: () => import("highlight.js/lib/languages/groovy.js"), + haml: () => import("highlight.js/lib/languages/haml.js"), + handlebars: () => import("highlight.js/lib/languages/handlebars.js"), + haskell: () => import("highlight.js/lib/languages/haskell.js"), + haxe: () => import("highlight.js/lib/languages/haxe.js"), + hsp: () => import("highlight.js/lib/languages/hsp.js"), + htmlbars: () => import("highlight.js/lib/languages/htmlbars.js"), + http: () => import("highlight.js/lib/languages/http.js"), + hy: () => import("highlight.js/lib/languages/hy.js"), + inform7: () => import("highlight.js/lib/languages/inform7.js"), + ini: () => import("highlight.js/lib/languages/ini.js"), + irpf90: () => import("highlight.js/lib/languages/irpf90.js"), + isbl: () => import("highlight.js/lib/languages/isbl.js"), + java: () => import("highlight.js/lib/languages/java.js"), + "jboss-cli": () => import("highlight.js/lib/languages/jboss-cli.js"), + julia: () => import("highlight.js/lib/languages/julia.js"), + "julia-repl": () => import("highlight.js/lib/languages/julia-repl.js"), + kotlin: () => import("highlight.js/lib/languages/kotlin.js"), + lasso: () => import("highlight.js/lib/languages/lasso.js"), + latex: () => import("highlight.js/lib/languages/latex.js"), + ldif: () => import("highlight.js/lib/languages/ldif.js"), + leaf: () => import("highlight.js/lib/languages/leaf.js"), + less: () => import("highlight.js/lib/languages/less.js"), + lisp: () => import("highlight.js/lib/languages/lisp.js"), + livecodeserver: () => import("highlight.js/lib/languages/livecodeserver.js"), + livescript: () => import("highlight.js/lib/languages/livescript.js"), + llvm: () => import("highlight.js/lib/languages/llvm.js"), + lsl: () => import("highlight.js/lib/languages/lsl.js"), + lua: () => import("highlight.js/lib/languages/lua.js"), + makefile: () => import("highlight.js/lib/languages/makefile.js"), + mathematica: () => import("highlight.js/lib/languages/mathematica.js"), + matlab: () => import("highlight.js/lib/languages/matlab.js"), + maxima: () => import("highlight.js/lib/languages/maxima.js"), + mel: () => import("highlight.js/lib/languages/mel.js"), + mercury: () => import("highlight.js/lib/languages/mercury.js"), + mipsasm: () => import("highlight.js/lib/languages/mipsasm.js"), + mizar: () => import("highlight.js/lib/languages/mizar.js"), + perl: () => import("highlight.js/lib/languages/perl.js"), + mojolicious: () => import("highlight.js/lib/languages/mojolicious.js"), + monkey: () => import("highlight.js/lib/languages/monkey.js"), + moonscript: () => import("highlight.js/lib/languages/moonscript.js"), + n1ql: () => import("highlight.js/lib/languages/n1ql.js"), + nginx: () => import("highlight.js/lib/languages/nginx.js"), + nim: () => import("highlight.js/lib/languages/nim.js"), + nix: () => import("highlight.js/lib/languages/nix.js"), + "node-repl": () => import("highlight.js/lib/languages/node-repl.js"), + nsis: () => import("highlight.js/lib/languages/nsis.js"), + objectivec: () => import("highlight.js/lib/languages/objectivec.js"), + ocaml: () => import("highlight.js/lib/languages/ocaml.js"), + openscad: () => import("highlight.js/lib/languages/openscad.js"), + oxygene: () => import("highlight.js/lib/languages/oxygene.js"), + parser3: () => import("highlight.js/lib/languages/parser3.js"), + pf: () => import("highlight.js/lib/languages/pf.js"), + pgsql: () => import("highlight.js/lib/languages/pgsql.js"), + php: () => import("highlight.js/lib/languages/php.js"), + "php-template": () => import("highlight.js/lib/languages/php-template.js"), + plaintext: () => import("highlight.js/lib/languages/plaintext.js"), + pony: () => import("highlight.js/lib/languages/pony.js"), + powershell: () => import("highlight.js/lib/languages/powershell.js"), + processing: () => import("highlight.js/lib/languages/processing.js"), + profile: () => import("highlight.js/lib/languages/profile.js"), + prolog: () => import("highlight.js/lib/languages/prolog.js"), + properties: () => import("highlight.js/lib/languages/properties.js"), + protobuf: () => import("highlight.js/lib/languages/protobuf.js"), + puppet: () => import("highlight.js/lib/languages/puppet.js"), + purebasic: () => import("highlight.js/lib/languages/purebasic.js"), + "python-repl": () => import("highlight.js/lib/languages/python-repl.js"), + q: () => import("highlight.js/lib/languages/q.js"), + qml: () => import("highlight.js/lib/languages/qml.js"), + r: () => import("highlight.js/lib/languages/r.js"), + reasonml: () => import("highlight.js/lib/languages/reasonml.js"), + rib: () => import("highlight.js/lib/languages/rib.js"), + roboconf: () => import("highlight.js/lib/languages/roboconf.js"), + routeros: () => import("highlight.js/lib/languages/routeros.js"), + rsl: () => import("highlight.js/lib/languages/rsl.js"), + ruleslanguage: () => import("highlight.js/lib/languages/ruleslanguage.js"), + rust: () => import("highlight.js/lib/languages/rust.js"), + sas: () => import("highlight.js/lib/languages/sas.js"), + scala: () => import("highlight.js/lib/languages/scala.js"), + scheme: () => import("highlight.js/lib/languages/scheme.js"), + scilab: () => import("highlight.js/lib/languages/scilab.js"), + scss: () => import("highlight.js/lib/languages/scss.js"), + shell: () => import("highlight.js/lib/languages/shell.js"), + smali: () => import("highlight.js/lib/languages/smali.js"), + smalltalk: () => import("highlight.js/lib/languages/smalltalk.js"), + sml: () => import("highlight.js/lib/languages/sml.js"), + sqf: () => import("highlight.js/lib/languages/sqf.js"), + sql_more: () => import("highlight.js/lib/languages/sql_more.js"), + sql: () => import("highlight.js/lib/languages/sql.js"), + stan: () => import("highlight.js/lib/languages/stan.js"), + stata: () => import("highlight.js/lib/languages/stata.js"), + step21: () => import("highlight.js/lib/languages/step21.js"), + stylus: () => import("highlight.js/lib/languages/stylus.js"), + subunit: () => import("highlight.js/lib/languages/subunit.js"), + swift: () => import("highlight.js/lib/languages/swift.js"), + taggerscript: () => import("highlight.js/lib/languages/taggerscript.js"), + tap: () => import("highlight.js/lib/languages/tap.js"), + tcl: () => import("highlight.js/lib/languages/tcl.js"), + thrift: () => import("highlight.js/lib/languages/thrift.js"), + tp: () => import("highlight.js/lib/languages/tp.js"), + twig: () => import("highlight.js/lib/languages/twig.js"), + vala: () => import("highlight.js/lib/languages/vala.js"), + vbnet: () => import("highlight.js/lib/languages/vbnet.js"), + vbscript: () => import("highlight.js/lib/languages/vbscript.js"), + "vbscript-html": () => import("highlight.js/lib/languages/vbscript-html.js"), + verilog: () => import("highlight.js/lib/languages/verilog.js"), + vhdl: () => import("highlight.js/lib/languages/vhdl.js"), + vim: () => import("highlight.js/lib/languages/vim.js"), + x86asm: () => import("highlight.js/lib/languages/x86asm.js"), + xl: () => import("highlight.js/lib/languages/xl.js"), + xquery: () => import("highlight.js/lib/languages/xquery.js"), + zephir: () => import("highlight.js/lib/languages/zephir.js"), +}; + +const aliasPairs = ` +as=actionscript asc=angelscript apacheconf=apache osascript=applescript ino=arduino arm=armasm html=xml +xhtml=xml rss=xml atom=xml xjb=xml xsd=xml xsl=xml plist=xml wsf=xml svg=xml adoc=asciidoc ahk=autohotkey +x++=axapta sh=bash zsh=bash bf=brainfuck c=c-like h=c cc=cpp c++=cpp h++=cpp hpp=cpp hh=cpp hxx=cpp cxx=cpp +capnp=capnproto icl=clean dcl=clean clj=clojure cmake.in=cmake coffee=coffeescript cson=coffeescript +iced=coffeescript cls=cos crm=crmsh pcmk=crmsh cr=crystal cs=csharp c#=csharp md=markdown mkdown=markdown +mkd=markdown dpr=delphi dfm=delphi pas=delphi pascal=delphi freepascal=delphi lazarus=delphi lpr=delphi +lfm=delphi patch=diff jinja=django bind=dns zone=dns docker=dockerfile bat=dos cmd=dos dst=dust rb=ruby +gemspec=ruby podspec=ruby thor=ruby irb=ruby erl=erlang xlsx=excel xls=excel f90=fortran f95=fortran fs=fsharp +gms=gams gss=gauss nc=gcode feature=gherkin golang=go hbs=htmlbars html.hbs=htmlbars html.handlebars=htmlbars +htmlbars=htmlbars hs=haskell hx=haxe https=http hylang=hy i7=inform7 toml=ini jsp=java js=javascript +jsx=javascript mjs=javascript cjs=javascript wildfly-cli=jboss-cli kt=kotlin kts=kotlin ls=livescript +lassoscript=lasso tex=latex mk=makefile mak=makefile make=makefile mma=mathematica wl=mathematica m=mercury +moo=mercury mips=mipsasm pl=perl pm=perl moon=moonscript nginxconf=nginx nixos=nix mm=objectivec +objc=objectivec obj-c=objectivec obj-c++=objectivec objective-c++=objectivec ml=sml scad=openscad pf.conf=pf +postgres=pgsql postgresql=pgsql php3=php php4=php php5=php php6=php php7=php php8=php text=plaintext +txt=plaintext ps=powershell ps1=powershell pp=puppet pb=purebasic pbi=purebasic py=python gyp=python +ipython=python pycon=python-repl k=q kdb=q qt=qml re=reasonml graph=roboconf instances=roboconf +mikrotik=routeros rs=rust sci=scilab console=shell st=smalltalk mysql=sql_more oracle=sql_more stanfuncs=stan +do=stata ado=stata p21=step21 step=step21 stp=step21 styl=stylus yml=yaml tk=tcl craftcms=twig ts=typescript +tsx=typescript vb=vbnet vbs=vbscript v=verilog sv=verilog svh=verilog tao=xl xpath=xquery xq=xquery zep=zephir +` + .trim() + .split(/\s+/); + +export const HIGHLIGHT_LANGUAGE_ALIASES = new Map( + aliasPairs.map((pair) => { + const separator = pair.indexOf("="); + return [pair.slice(0, separator), pair.slice(separator + 1)] as const; + }), +); + +const dependencyPairs = ` +xml=css,javascript html=xml asciidoc=xml clojure-repl=clojure coffeescript=javascript cos=javascript,sql,xml +markdown=xml dart=markdown django=xml dockerfile=bash dust=xml erb=ruby,xml haml=ruby handlebars=xml htmlbars=xml +javascript=css,xml julia-repl=julia livescript=javascript perl=mojolicious mojolicious=perl,xml node-repl=javascript +parser3=xml pgsql=bash,java,json,lua,perl,php,python,r,ruby,scheme,tcl,xml php-template=php,xml python-repl=python +qml=xml shell=bash yaml=ruby tap=yaml toml=ini twig=xml typescript=css,xml vbscript-html=vbscript,xml xquery=xml +` + .trim() + .split(/\s+/); + +export const HIGHLIGHT_LANGUAGE_DEPENDENCIES = new Map( + dependencyPairs.map((pair) => { + const separator = pair.indexOf("="); + return [pair.slice(0, separator), pair.slice(separator + 1).split(",")] as const; + }), +); diff --git a/packages/coding-agent/src/utils/syntax-highlight.ts b/packages/coding-agent/src/utils/syntax-highlight.ts index 293084cc0..f27ad7600 100644 --- a/packages/coding-agent/src/utils/syntax-highlight.ts +++ b/packages/coding-agent/src/utils/syntax-highlight.ts @@ -1,5 +1,10 @@ import hljs, { type LanguageDefinition } from "highlight.js/lib/core.js"; -import { HIGHLIGHT_LANGUAGES, type HighlightLanguage } from "./highlight-languages.ts"; +import { + HIGHLIGHT_LANGUAGE_ALIASES, + HIGHLIGHT_LANGUAGE_DEFINITIONS, + HIGHLIGHT_LANGUAGE_DEPENDENCIES, + HIGHLIGHT_LANGUAGE_LOADERS, +} from "./highlight-languages.ts"; import { decodeHtmlEntityAt } from "./html.ts"; export type HighlightFormatter = (text: string) => string; @@ -12,64 +17,45 @@ export interface HighlightOptions { theme?: HighlightTheme; } -const LANGUAGE_BY_NAME = new Map(); -for (const language of HIGHLIGHT_LANGUAGES) { - LANGUAGE_BY_NAME.set(language.name, language); - for (const alias of language.aliases ?? []) LANGUAGE_BY_NAME.set(alias.toLowerCase(), language); -} - -const LANGUAGE_BY_EXTENSION = new Map( - HIGHLIGHT_LANGUAGES.flatMap(({ name, extensions = [] }) => - extensions.map((extension) => [extension, name] as const), - ), -); - -const registeredLanguages = new Set(); - -function registerLanguage(language: HighlightLanguage, definition: LanguageDefinition): void { - if (registeredLanguages.has(language.name)) return; - hljs.registerLanguage(language.name, definition); - registeredLanguages.add(language.name); -} - -for (const language of HIGHLIGHT_LANGUAGES) { - if (language.definition) registerLanguage(language, language.definition); +interface HighlightLanguage { + name: string; + definition: LanguageDefinition; } type HighlightLanguageLoadEvent = - | { language: string; requiresGlobalRefresh: boolean } + | { language: string; languages: HighlightLanguage[] } | { language: string; error: unknown }; type HighlightLanguageLoadListener = (event: HighlightLanguageLoadEvent) => void; -interface LanguageLoadState { - definition?: LanguageDefinition; - promise: Promise; -} - -interface LanguageRequestState { - promise: Promise; - callbacks: Set<() => void>; - requiresGlobalRefresh: boolean; - status: "pending" | "ready" | "failed"; -} - interface LanguageRegistryState { - loads: Map; + definitions: Map>; + requests: Map>; listeners: Set; - requests?: Map; } -// jiti can evaluate this module more than once. Sharing imported definitions -// and listeners lets extension-triggered loads refresh the host TUI. -const LANGUAGE_REGISTRY_KEY = Symbol.for("@earendil-works/pi-coding-agent:highlight-language-loaders-v3"); +// jiti can evaluate this module more than once. Share imported definitions and +// listeners so extension-triggered loads still refresh the host TUI. +const LANGUAGE_REGISTRY_KEY = Symbol.for("@earendil-works/pi-coding-agent:highlight-language-loaders-v4"); const globalRegistry = globalThis as Record; const languageRegistry: LanguageRegistryState = globalRegistry[LANGUAGE_REGISTRY_KEY] ?? { - loads: new Map(), + definitions: new Map(), + requests: new Map(), listeners: new Set(), }; globalRegistry[LANGUAGE_REGISTRY_KEY] = languageRegistry; -if (!languageRegistry.requests) languageRegistry.requests = new Map(); -const languageRequests = languageRegistry.requests; + +const registeredLanguages = new Set(); +for (const [name, definition] of Object.entries(HIGHLIGHT_LANGUAGE_DEFINITIONS)) { + hljs.registerLanguage(name, definition); + registeredLanguages.add(name); +} + +function getCanonicalLanguageName(name: string): string | undefined { + if (Object.hasOwn(HIGHLIGHT_LANGUAGE_DEFINITIONS, name) || Object.hasOwn(HIGHLIGHT_LANGUAGE_LOADERS, name)) { + return name; + } + return HIGHLIGHT_LANGUAGE_ALIASES.get(name); +} function notifyLanguageLoad(event: HighlightLanguageLoadEvent): void { if (languageRegistry.listeners.size > 0) { @@ -80,131 +66,93 @@ function notifyLanguageLoad(event: HighlightLanguageLoadEvent): void { } } -function getLanguageClosure(language: HighlightLanguage): HighlightLanguage[] { - const result: HighlightLanguage[] = []; - const visited = new Set(); - const visit = (current: HighlightLanguage) => { - if (visited.has(current.name)) return; - visited.add(current.name); - for (const dependencyName of current.dependencies ?? []) { - const dependency = LANGUAGE_BY_NAME.get(dependencyName); - if (dependency) visit(dependency); - } - result.push(current); - }; - visit(language); - return result; -} +function loadLanguageDefinition(name: string): Promise { + const eagerDefinition = HIGHLIGHT_LANGUAGE_DEFINITIONS[name]; + if (eagerDefinition) return Promise.resolve(eagerDefinition); -function ensureLanguagesRegistered(languages: HighlightLanguage[], requestedName: string): boolean { - for (const language of languages) { - const definition = language.definition ?? languageRegistry.loads.get(language.name)?.definition; - if (definition) registerLanguage(language, definition); - } - return ( - languages.every( - (language) => - registeredLanguages.has(language.name) || - (!language.definition && !language.load && supportsLanguage(language.name)), - ) && supportsLanguage(requestedName) - ); -} - -function loadLanguageDefinition(language: HighlightLanguage): Promise | undefined { - if (language.definition) return Promise.resolve(language.definition); - if (!language.load) return undefined; - - let state = languageRegistry.loads.get(language.name); - if (!state) { - const promise = Promise.resolve() - .then(language.load) + let promise = languageRegistry.definitions.get(name); + if (!promise) { + const loader = HIGHLIGHT_LANGUAGE_LOADERS[name]; + promise = Promise.resolve() + .then(loader) .then( - ({ default: definition }) => { - const currentState = languageRegistry.loads.get(language.name); - if (currentState) currentState.definition = definition; - return definition; - }, + ({ default: definition }) => definition, (error: unknown) => { - notifyLanguageLoad({ language: language.name, error }); + notifyLanguageLoad({ language: name, error }); throw error; }, ); - state = { promise }; - languageRegistry.loads.set(language.name, state); + languageRegistry.definitions.set(name, promise); } - return state.promise; + return promise; } -function getLanguageRequest(language: HighlightLanguage, languages: HighlightLanguage[]): LanguageRequestState { - let request = languageRequests.get(language.name); - if (request) return request; - - const callbacks = new Set<() => void>(); - const promises: Promise[] = []; - for (const current of languages) { - const promise = loadLanguageDefinition(current); - if (promise) promises.push(promise); +async function loadLanguageClosure(name: string, visited = new Set()): Promise { + if (visited.has(name)) return []; + visited.add(name); + const definition = await loadLanguageDefinition(name); + const languages: HighlightLanguage[] = []; + for (const dependency of HIGHLIGHT_LANGUAGE_DEPENDENCIES.get(name) ?? []) { + languages.push(...(await loadLanguageClosure(dependency, visited))); + } + languages.push({ name, definition }); + return languages; +} + +function registerLanguages(languages: HighlightLanguage[], requestedName: string): boolean { + for (const { name, definition } of languages) { + if (registeredLanguages.has(name)) continue; + hljs.registerLanguage(name, definition); + registeredLanguages.add(name); + } + return registeredLanguages.has(requestedName); +} + +function getLanguageRequest(name: string): Promise { + let request = languageRegistry.requests.get(name); + if (!request) { + request = loadLanguageClosure(name); + languageRegistry.requests.set(name, request); + void request.then( + (languages) => notifyLanguageLoad({ language: name, languages }), + () => {}, + ); } - const promise = Promise.all(promises).then(() => ensureLanguagesRegistered(languages, language.name)); - request = { promise, callbacks, requiresGlobalRefresh: false, status: "pending" }; - languageRequests.set(language.name, request); - void promise.then( - (ready) => { - request.status = ready ? "ready" : "failed"; - if (ready) { - for (const callback of callbacks) callback(); - notifyLanguageLoad({ language: language.name, requiresGlobalRefresh: request.requiresGlobalRefresh }); - } - callbacks.clear(); - }, - () => { - request.status = "failed"; - callbacks.clear(); - }, - ); return request; } export function onHighlightLanguageLoad(listener: HighlightLanguageLoadListener): () => void { - languageRegistry.listeners.add(listener); - return () => languageRegistry.listeners.delete(listener); + const localListener: HighlightLanguageLoadListener = (event) => { + if ("languages" in event) registerLanguages(event.languages, event.language); + listener(event); + }; + languageRegistry.listeners.add(localListener); + return () => languageRegistry.listeners.delete(localListener); } export async function loadHighlightLanguage(name: string): Promise { const normalizedName = name.toLowerCase(); - const language = LANGUAGE_BY_NAME.get(normalizedName); - if (!language) return supportsLanguage(normalizedName); - - const languages = getLanguageClosure(language); - if (ensureLanguagesRegistered(languages, language.name)) return true; - const request = getLanguageRequest(language, languages); - request.requiresGlobalRefresh = true; - return request.promise; + const canonicalName = getCanonicalLanguageName(normalizedName); + if (!canonicalName) return supportsLanguage(normalizedName); + if (registeredLanguages.has(canonicalName)) return true; + return registerLanguages(await getLanguageRequest(canonicalName), canonicalName); } /** Return the canonical grammar when ready, starting a background load when needed. */ -export function requestHighlightLanguage(name: string, onReady?: () => void): string | undefined { +export function requestHighlightLanguage(name: string): string | undefined { const normalizedName = name.toLowerCase(); - const language = LANGUAGE_BY_NAME.get(normalizedName); - if (!language) return supportsLanguage(normalizedName) ? normalizedName : undefined; - - const languages = getLanguageClosure(language); - if (ensureLanguagesRegistered(languages, language.name)) return language.name; - if (!languages.some((current) => current.load)) return undefined; - - const request = getLanguageRequest(language, languages); - if (request.status === "pending") { - if (onReady) request.callbacks.add(onReady); - else request.requiresGlobalRefresh = true; + const canonicalName = getCanonicalLanguageName(normalizedName); + if (!canonicalName) return supportsLanguage(normalizedName) ? normalizedName : undefined; + if (registeredLanguages.has(canonicalName)) { + return normalizedName === "html" || normalizedName === "toml" ? normalizedName : canonicalName; } + void getLanguageRequest(canonicalName).then( + (languages) => registerLanguages(languages, canonicalName), + () => {}, + ); return undefined; } -export function getLanguageFromPath(filePath: string): string | undefined { - const extension = filePath.split(".").pop()?.toLowerCase(); - return extension ? LANGUAGE_BY_EXTENSION.get(extension) : undefined; -} - const SPAN_CLOSE = ""; const HIGHLIGHT_CLASS_PREFIX = "hljs-"; diff --git a/packages/coding-agent/test/syntax-highlight.test.ts b/packages/coding-agent/test/syntax-highlight.test.ts index e8a8e2b59..94ff9d153 100644 --- a/packages/coding-agent/test/syntax-highlight.test.ts +++ b/packages/coding-agent/test/syntax-highlight.test.ts @@ -1,11 +1,10 @@ import { resetCapabilitiesCache, setCapabilities, type TUI } from "@earendil-works/pi-tui"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SettingsManager } from "../src/core/settings-manager.ts"; -import { highlightCode, initTheme } from "../src/modes/interactive/theme/theme.ts"; +import { getLanguageFromPath, highlightCode, initTheme } from "../src/modes/interactive/theme/theme.ts"; import { InteractiveThemeController } from "../src/modes/interactive/theme/theme-controller.ts"; -import { HIGHLIGHT_LANGUAGES } from "../src/utils/highlight-languages.ts"; +import { HIGHLIGHT_LANGUAGE_LOADERS } from "../src/utils/highlight-languages.ts"; import { - getLanguageFromPath, highlight, loadHighlightLanguage, onHighlightLanguageLoad, @@ -60,72 +59,31 @@ describe("syntax highlight renderer", () => { }); it("maps file extensions and preloads only common languages", () => { - expect(getLanguageFromPath("src/main.ts")).toBe("typescript"); - expect(getLanguageFromPath("public/index.html")).toBe("html"); - expect(getLanguageFromPath("Dockerfile")).toBe("dockerfile"); - expect(getLanguageFromPath("build.ps1")).toBe("powershell"); - expect(getLanguageFromPath("config.toml")).toBe("toml"); - expect(getLanguageFromPath("shell.fish")).toBe("fish"); - expect(getLanguageFromPath("style.sass")).toBe("sass"); - expect(getLanguageFromPath("schema.graphql")).toBe("graphql"); - expect(getLanguageFromPath("main.tf")).toBe("hcl"); - expect(getLanguageFromPath("config.hcl")).toBe("hcl"); - expect(supportsLanguage(getLanguageFromPath("src/main.ts")!)).toBe(true); - expect(supportsLanguage(getLanguageFromPath("public/index.html")!)).toBe(true); - expect(supportsLanguage(getLanguageFromPath("Dockerfile")!)).toBe(false); - expect(supportsLanguage(getLanguageFromPath("build.ps1")!)).toBe(false); - const handlebars = HIGHLIGHT_LANGUAGES.find(({ name }) => name === "handlebars"); - expect(handlebars?.definition).toBeUndefined(); - expect(handlebars?.load).toBeTypeOf("function"); + expect(["main.ts", "index.html", "Dockerfile", "build.ps1", "main.tf"].map(getLanguageFromPath)).toEqual([ + "typescript", + "html", + "dockerfile", + "powershell", + "hcl", + ]); + for (const common of ["typescript", "html"]) expect(supportsLanguage(common)).toBe(true); + for (const lazy of ["dockerfile", "powershell"]) expect(supportsLanguage(lazy)).toBe(false); for (const unsupported of ["fish", "graphql", "hcl", "sass"]) { expect(requestHighlightLanguage(unsupported)).toBeUndefined(); } }); - it("normalizes names and loads aliases through their canonical grammar", async () => { - expect(requestHighlightLanguage("ToMl")).toBeUndefined(); - expect(await loadHighlightLanguage("TOML")).toBe(true); - expect(supportsLanguage("ini")).toBe(true); - expect(supportsLanguage("toml")).toBe(true); - expect(requestHighlightLanguage("ToMl")).toBe("toml"); + it("loads one uncommon grammar with its aliases and dependencies", async () => { + const loaded: string[] = []; + const unsubscribe = onHighlightLanguageLoad((event) => { + if (!("error" in event)) loaded.push(event.language); + }); - expect(requestHighlightLanguage("Rust")).toBeUndefined(); - expect(await loadHighlightLanguage("RUST")).toBe(true); - expect(supportsLanguage("rust")).toBe(true); - expect(requestHighlightLanguage("Rust")).toBe("rust"); + expect(requestHighlightLanguage("BF")).toBeUndefined(); + expect(await loadHighlightLanguage("BF")).toBe(true); + expect(requestHighlightLanguage("bf")).toBe("brainfuck"); + expect(supportsLanguage("abnf")).toBe(false); - expect(await loadHighlightLanguage("PowerShell")).toBe(true); - expect(supportsLanguage("POWERSHELL")).toBe(true); - }); - - it("keeps duplicate aliases bound to their canonical grammar", async () => { - expect(await loadHighlightLanguage("ls")).toBe(true); - expect(requestHighlightLanguage("ls")).toBe("livescript"); - expect(await loadHighlightLanguage("lasso")).toBe(true); - expect(requestHighlightLanguage("ls")).toBe("livescript"); - - expect(await loadHighlightLanguage("ml")).toBe(true); - expect(requestHighlightLanguage("ml")).toBe("sml"); - expect(await loadHighlightLanguage("ocaml")).toBe(true); - expect(requestHighlightLanguage("ml")).toBe("sml"); - - expect(await loadHighlightLanguage("hbs")).toBe(true); - expect(requestHighlightLanguage("hbs")).toBe("htmlbars"); - expect(await loadHighlightLanguage("handlebars")).toBe(true); - expect(requestHighlightLanguage("hbs")).toBe("htmlbars"); - }); - - it("loads required sublanguage grammars", async () => { - expect(supportsLanguage("ruby")).toBe(true); - expect(await loadHighlightLanguage("ERB")).toBe(true); - expect( - highlight("<%= User.find(1) %>", { - language: "erb", - theme: { number: (text) => `[number:${text}]` }, - }), - ).toContain("[number:1]"); - - expect(supportsLanguage("php")).toBe(false); expect(await loadHighlightLanguage("php-template")).toBe(true); expect(supportsLanguage("php")).toBe(true); expect( @@ -134,37 +92,24 @@ describe("syntax highlight renderer", () => { theme: { keyword: (text) => `[keyword:${text}]` }, }), ).toContain("[keyword:echo]"); + expect(loaded).toEqual(["brainfuck", "php-template"]); + unsubscribe(); + expect(await loadHighlightLanguage("TOML")).toBe(true); + expect(requestHighlightLanguage("ToMl")).toBe("toml"); expect(await loadHighlightLanguage("mojolicious")).toBe(true); expect(supportsLanguage("perl")).toBe(true); - }); - - it("loads only the requested uncommon language", async () => { - expect(supportsLanguage("brainfuck")).toBe(false); - expect(supportsLanguage("abnf")).toBe(false); - let notifications = 0; - const unsubscribe = onHighlightLanguageLoad(() => notifications++); - - expect(requestHighlightLanguage("bf")).toBeUndefined(); - expect(await loadHighlightLanguage("bf")).toBe(true); - expect(supportsLanguage("brainfuck")).toBe(true); - expect(supportsLanguage("bf")).toBe(true); - expect(supportsLanguage("abnf")).toBe(false); - expect(notifications).toBe(1); - - expect(await loadHighlightLanguage("abnf")).toBe(true); - expect(supportsLanguage("abnf")).toBe(true); - expect(notifications).toBe(2); - unsubscribe(); + expect(await loadHighlightLanguage("ls")).toBe(true); + expect(await loadHighlightLanguage("lasso")).toBe(true); + expect(requestHighlightLanguage("ls")).toBe("livescript"); }); it("reports background load failures once without retrying", async () => { - const language = HIGHLIGHT_LANGUAGES.find(({ name }) => name === "accesslog"); - if (!language?.load) throw new Error("accesslog test grammar is missing"); - const originalLoad = language.load; + const originalLoad = HIGHLIGHT_LANGUAGE_LOADERS.accesslog; + if (!originalLoad) throw new Error("accesslog test grammar is missing"); const loadError = new Error("test grammar load failed"); let attempts = 0; - language.load = () => { + HIGHLIGHT_LANGUAGE_LOADERS.accesslog = () => { attempts++; return Promise.reject(loadError); }; @@ -185,14 +130,14 @@ describe("syntax highlight renderer", () => { expect(attempts).toBe(1); await expect(loadHighlightLanguage("accesslog")).rejects.toThrow("test grammar load failed"); } finally { - language.load = originalLoad; + HIGHLIGHT_LANGUAGE_LOADERS.accesslog = originalLoad; unsubscribe(); } }); }); describe("highlight language load lifecycle", () => { - it("invalidates only affected targets while retaining a global fallback", async () => { + it("refreshes the TUI when a grammar loads", async () => { const invalidate = vi.fn(); const requestRender = vi.fn(); const unsubscribeTerminalTheme = vi.fn(); @@ -203,62 +148,19 @@ describe("highlight language load lifecycle", () => { } as unknown as TUI; const settingsManager = { getThemeSetting: () => "dark" } as unknown as SettingsManager; const controller = new InteractiveThemeController(ui, settingsManager, vi.fn(), vi.fn()); - const arcade = HIGHLIGHT_LANGUAGES.find(({ name }) => name === "arcade"); - const oneC = HIGHLIGHT_LANGUAGES.find(({ name }) => name === "1c"); - const eager = HIGHLIGHT_LANGUAGES.find(({ name }) => name === "bash"); - if (!arcade?.load || !oneC?.load || !eager?.definition) throw new Error("highlight test grammars are missing"); - const originalArcadeLoad = arcade.load; - const originalOneCLoad = oneC.load; - arcade.load = () => Promise.resolve({ default: eager.definition! }); - oneC.load = () => Promise.resolve({ default: eager.definition! }); - try { - const loaded = new Set(); - let resolveLoads = (): void => {}; - const loadsFinished = new Promise((resolve) => { - resolveLoads = resolve; - }); - const unsubscribeLoads = onHighlightLanguageLoad((event) => { - if ("error" in event) return; - loaded.add(event.language); - if (loaded.has("arcade") && loaded.has("1c")) resolveLoads(); - }); - const invalidateArcade = vi.fn(); - const invalidateOneC = vi.fn(); + highlightCode("let value = 1", "arcade"); + await loadHighlightLanguage("arcade"); + expect(invalidate).toHaveBeenCalledOnce(); + expect(requestRender).toHaveBeenCalledOnce(); - highlightCode("let value = 1", "arcade", { onLanguageReady: invalidateArcade }); - highlightCode("let value = 1", "1c", { onLanguageReady: invalidateOneC }); - await loadsFinished; - unsubscribeLoads(); - - expect(invalidateArcade).toHaveBeenCalledOnce(); - expect(invalidateOneC).toHaveBeenCalledOnce(); - expect(invalidate).not.toHaveBeenCalled(); - expect(requestRender).not.toHaveBeenCalled(); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(invalidate).not.toHaveBeenCalled(); - expect(requestRender).toHaveBeenCalledOnce(); - requestRender.mockClear(); - - highlightCode("package main", "ada"); - await loadHighlightLanguage("ada"); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(invalidate).toHaveBeenCalledOnce(); - expect(requestRender).toHaveBeenCalledOnce(); - invalidate.mockClear(); - requestRender.mockClear(); - - controller.dispose(); - await loadHighlightLanguage("apache"); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(unsubscribeTerminalTheme).toHaveBeenCalledOnce(); - expect(invalidate).not.toHaveBeenCalled(); - expect(requestRender).not.toHaveBeenCalled(); - } finally { - arcade.load = originalArcadeLoad; - oneC.load = originalOneCLoad; - controller.dispose(); - } + invalidate.mockClear(); + requestRender.mockClear(); + controller.dispose(); + await loadHighlightLanguage("apache"); + expect(unsubscribeTerminalTheme).toHaveBeenCalledOnce(); + expect(invalidate).not.toHaveBeenCalled(); + expect(requestRender).not.toHaveBeenCalled(); }); }); diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 9f16c6716..608936c7b 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -212,7 +212,7 @@ export interface MarkdownTheme { italic: (text: string) => string; strikethrough: (text: string) => string; underline: (text: string) => string; - highlightCode?: (code: string, lang?: string, onLanguageReady?: () => void) => string[]; + highlightCode?: (code: string, lang?: string) => string[]; /** Prefix applied to each rendered code block line (default: " ") */ codeBlockIndent?: string; } @@ -241,7 +241,6 @@ export class Markdown implements Component { private theme: MarkdownTheme; private options: MarkdownOptions; private defaultStylePrefix?: string; - private readonly handleHighlightLanguageReady = (): void => this.invalidate(); // Cache for rendered output private cachedText?: string; @@ -522,11 +521,7 @@ export class Markdown implements Component { const indent = this.theme.codeBlockIndent ?? " "; lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`)); if (this.theme.highlightCode) { - const highlightedLines = this.theme.highlightCode( - token.text, - token.lang, - this.handleHighlightLanguageReady, - ); + const highlightedLines = this.theme.highlightCode(token.text, token.lang); for (const hlLine of highlightedLines) { lines.push(`${indent}${hlLine}`); } diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index c2513670e..d45b7716d 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -38,28 +38,6 @@ function stripAnsi(line: string): string { describe("Markdown component", () => { describe("Transforms", () => { - it("invalidates its cache when a code language finishes loading", () => { - const callbacks: Array<() => void> = []; - let highlightCalls = 0; - const markdown = new Markdown("```example\ncode\n```", 0, 0, { - ...defaultMarkdownTheme, - highlightCode: (_code, _lang, onLanguageReady) => { - highlightCalls++; - if (onLanguageReady) callbacks.push(onLanguageReady); - return ["code"]; - }, - }); - - markdown.render(80); - markdown.render(80); - assert.strictEqual(highlightCalls, 1); - - callbacks[0]?.(); - markdown.render(80); - assert.strictEqual(highlightCalls, 2); - assert.strictEqual(callbacks[0], callbacks[1]); - }); - it("caches transformed Markdown by source and available width", () => { const calls: Array<{ source: string; availableWidth: number }> = []; const markdown = new Markdown("source", 2, 0, defaultMarkdownTheme, undefined, {