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 grammar. Unknown → plain text.
24const js = () => javascript();
25const ts = () => javascript({ typescript: true });
26const EXT_TO_LANG = {
27 js, mjs: js, cjs: js, jsx: () => javascript({ jsx: true }),
28 ts, mts: ts, cts: ts, tsx: () => javascript({ jsx: true, typescript: true }),
29 json, jsonc: json,
30 html, htm: html,
31 css, scss: css, less: css,
32 md: markdown, markdown,
33 py: python, pyw: python,
34 rs: rust,
35 c: cpp, h: cpp, cpp, cc: cpp, cxx: cpp, hpp: cpp, hh: cpp,
36 xml, svg: xml,
37 sql,
38 java,
39 php,
40};
41
42// Note: there is deliberately no extension list for "is this text" here. The
43// server sniffs each file's leading bytes and reports a FileKind, so that
44// question is answered once, from content, before the client ever sees it.
45// This file only maps an extension to a *grammar*, which content sniffing
46// cannot do (a .h is C or C++).
47
48function langExtension(filename) {
49 const base = (filename || "").toLowerCase();
50 const dot = base.lastIndexOf(".");
51 const ext = dot >= 0 ? base.slice(dot + 1) : base;
52 return Object.hasOwn(EXT_TO_LANG, ext) ? EXT_TO_LANG[ext]() : null;
53}
54
55function isDark() {
56 // The app's forced theme (class on <html>) wins over the OS setting.
57 const cls = document.documentElement.className;
58 if (cls.includes("dark")) return true;
59 if (cls.includes("light")) return false;
60 return (
61 window.matchMedia &&
62 window.matchMedia("(prefers-color-scheme: dark)").matches
63 );
64}
65
66// A subtle theme that follows the app's light/dark look for plain text.
67const appTheme = EditorView.theme({
68 "&": { fontSize: "13px", height: "100%" },
69 ".cm-scroller": { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
70 "&.cm-focused": { outline: "none" },
71});
72
73// Positional arguments throughout, so the Rust side can bind these directly
74// with #[wasm_bindgen(js_namespace = __fbng_cm)] instead of building an
75// options object.
76window.__fbng_cm = {
77 // Create an editor in `container`. Returns an opaque view handle.
78 create(container, value, filename, editable, onUpdate) {
79 const extensions = [appTheme, basicSetup];
80 if (isDark()) extensions.push(oneDark);
81 const le = langExtension(filename);
82 if (le) extensions.push(le);
83
84 if (!editable) {
85 extensions.push(EditorState.readOnly.of(true), EditorView.editable.of(false));
86 }
87
88 if (typeof onUpdate === "function") {
89 extensions.push(
90 EditorView.updateListener.of((update) => {
91 if (update.docChanged) onUpdate();
92 })
93 );
94 }
95
96 return new EditorView({
97 state: EditorState.create({ doc: value || "", extensions }),
98 parent: container,
99 });
100 },
101
102 destroy(view) {
103 try { view.destroy(); } catch (_) { /* already destroyed */ }
104 },
105
106 setValue(view, text) {
107 const len = view.state.doc.length;
108 view.dispatch({ changes: { from: 0, to: len, insert: text } });
109 },
110
111 getValue(view) {
112 return view.state.doc.toString();
113 },
114
115 focus(view) {
116 view.focus();
117 },
118};
119