wrapper.js
⎇
Raw
1// CodeMirror 6 wrapper for filebrowser-ng.
2//
3// Bundled to web/cm6.js (an IIFE) by `bun run build` and exposed on
4// `window.__fbng_cm`. The Rust side drives it through this tiny surface, so
5// swapping the editor later only requires re-implementing these functions.
6
7import { EditorView, basicSetup } from "codemirror";
8import { EditorState } from "@codemirror/state";
9import { oneDark } from "@codemirror/theme-one-dark";
10import { javascript } from "@codemirror/lang-javascript";
11import { json } from "@codemirror/lang-json";
12import { html } from "@codemirror/lang-html";
13import { css } from "@codemirror/lang-css";
14import { markdown } from "@codemirror/lang-markdown";
15import { python } from "@codemirror/lang-python";
16import { rust } from "@codemirror/lang-rust";
17import { cpp } from "@codemirror/lang-cpp";
18import { xml } from "@codemirror/lang-xml";
19import { sql } from "@codemirror/lang-sql";
20import { java } from "@codemirror/lang-java";
21import { php } from "@codemirror/lang-php";
22
23// Map a lowercased file extension to a language id. Unknown → plain text.
24const EXT_TO_LANG = {
25 js: "javascript", mjs: "javascript", cjs: "javascript", jsx: "jsx",
26 ts: "typescript", mts: "typescript", cts: "typescript", tsx: "tsx",
27 json: "json", jsonc: "json",
28 html: "html", htm: "html",
29 css: "css", scss: "css", less: "css",
30 md: "markdown", markdown: "markdown",
31 py: "python", pyw: "python",
32 rs: "rust",
33 c: "cpp", h: "cpp", cpp: "cpp", cc: "cpp", cxx: "cpp", hpp: "cpp", hh: "cpp",
34 xml: "xml", svg: "xml",
35 sql: "sql",
36 java: "java",
37 php: "php",
38};
39
40// Note: there is deliberately no extension list for "is this text" here. The
41// server sniffs each file's leading bytes and reports a FileKind, so that
42// question is answered once, from content, before the client ever sees it.
43// This file only maps an extension to a *grammar*, which content sniffing
44// cannot do (a .h is C or C++).
45
46function langFromFilename(filename) {
47 const base = (filename || "").toLowerCase();
48 const dot = base.lastIndexOf(".");
49 const ext = dot >= 0 ? base.slice(dot + 1) : base;
50 return EXT_TO_LANG[ext] || "text";
51}
52
53function langExtension(langId) {
54 switch (langId) {
55 case "javascript": return javascript();
56 case "typescript": return javascript({ typescript: true });
57 case "jsx": return javascript({ jsx: true });
58 case "tsx": return javascript({ jsx: true, typescript: true });
59 case "json": return json();
60 case "html": return html();
61 case "css": return css();
62 case "markdown": return markdown();
63 case "python": return python();
64 case "rust": return rust();
65 case "cpp": return cpp();
66 case "xml": return xml();
67 case "sql": return sql();
68 case "java": return java();
69 case "php": return php();
70 default: return null;
71 }
72}
73
74function isDark() {
75 if (typeof window === "undefined") return false;
76 // The app's forced theme (class on <html>) wins over the OS setting.
77 const cls = document.documentElement.className;
78 if (cls.includes("dark")) return true;
79 if (cls.includes("light")) return false;
80 return (
81 window.matchMedia &&
82 window.matchMedia("(prefers-color-scheme: dark)").matches
83 );
84}
85
86// A subtle theme that follows the app's light/dark look for plain text.
87const appTheme = EditorView.theme({
88 "&": { fontSize: "13px", height: "100%" },
89 ".cm-scroller": { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
90 "&.cm-focused": { outline: "none" },
91});
92
93// Positional arguments throughout, so the Rust side can bind these directly
94// with #[wasm_bindgen(js_namespace = __fbng_cm)] instead of building an
95// options object.
96window.__fbng_cm = {
97 // Create an editor in `container`. Returns an opaque view handle.
98 create(container, value, filename, editable, onUpdate) {
99 const extensions = [appTheme, basicSetup];
100 if (isDark()) extensions.push(oneDark);
101 const le = langExtension(langFromFilename(filename));
102 if (le) extensions.push(le);
103
104 if (editable) {
105 // Ctrl/Cmd-S is intercepted by the host (see Rust keybinding).
106 extensions.push(EditorState.allowMultipleSelections.of(true));
107 } else {
108 extensions.push(EditorState.readOnly.of(true), EditorView.editable.of(false));
109 }
110
111 if (typeof onUpdate === "function") {
112 extensions.push(
113 EditorView.updateListener.of((update) => {
114 if (update.docChanged) onUpdate();
115 })
116 );
117 }
118
119 return new EditorView({
120 state: EditorState.create({ doc: value || "", extensions }),
121 parent: container,
122 });
123 },
124
125 destroy(view) {
126 try { view.destroy(); } catch (_) { /* already destroyed */ }
127 },
128
129 setValue(view, text) {
130 const len = view.state.doc.length;
131 view.dispatch({ changes: { from: 0, to: len, insert: text } });
132 },
133
134 getValue(view) {
135 return view.state.doc.toString();
136 },
137
138 focus(view) {
139 view.focus();
140 },
141};
142