2025-05-11 18:41:10 +03:00
|
|
|
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
2025-05-11 12:10:28 +03:00
|
|
|
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
|
2025-05-11 17:16:07 +03:00
|
|
|
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter } from "@codemirror/language";
|
2025-05-11 18:42:56 +03:00
|
|
|
import { Compartment, EditorState, type Extension } from "@codemirror/state";
|
2025-05-11 12:10:28 +03:00
|
|
|
import { highlightSelectionMatches } from "@codemirror/search";
|
2025-05-11 17:04:15 +03:00
|
|
|
import { vim } from "@replit/codemirror-vim";
|
2025-05-10 23:34:23 +03:00
|
|
|
import byMimeType from "./syntax_highlighting.js";
|
2025-05-11 17:39:10 +03:00
|
|
|
import smartIndentWithTab from "./extensions/custom_tab.js";
|
2025-05-11 20:19:40 +03:00
|
|
|
import type { ThemeDefinition } from "./color_themes.js";
|
|
|
|
|
2025-05-11 21:18:14 +03:00
|
|
|
export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
|
2025-05-10 20:07:53 +03:00
|
|
|
|
|
|
|
type ContentChangedListener = () => void;
|
|
|
|
|
2025-05-11 18:41:10 +03:00
|
|
|
export interface EditorConfig {
|
|
|
|
parent: HTMLElement;
|
2025-05-11 11:27:27 +03:00
|
|
|
placeholder?: string;
|
2025-05-11 12:07:54 +03:00
|
|
|
lineWrapping?: boolean;
|
2025-05-11 17:04:15 +03:00
|
|
|
vimKeybindings?: boolean;
|
2025-05-11 18:42:56 +03:00
|
|
|
readOnly?: boolean;
|
2025-05-10 20:07:53 +03:00
|
|
|
onContentChanged?: ContentChangedListener;
|
|
|
|
}
|
2025-05-10 19:10:30 +03:00
|
|
|
|
|
|
|
export default class CodeMirror extends EditorView {
|
2025-05-10 20:07:53 +03:00
|
|
|
|
|
|
|
private config: EditorConfig;
|
2025-05-10 23:34:23 +03:00
|
|
|
private languageCompartment: Compartment;
|
2025-05-11 12:23:09 +03:00
|
|
|
private historyCompartment: Compartment;
|
2025-05-11 20:19:40 +03:00
|
|
|
private themeCompartment: Compartment;
|
2025-05-10 20:07:53 +03:00
|
|
|
|
|
|
|
constructor(config: EditorConfig) {
|
2025-05-10 23:34:23 +03:00
|
|
|
const languageCompartment = new Compartment();
|
2025-05-11 12:23:09 +03:00
|
|
|
const historyCompartment = new Compartment();
|
2025-05-11 20:19:40 +03:00
|
|
|
const themeCompartment = new Compartment();
|
2025-05-11 12:23:09 +03:00
|
|
|
|
2025-05-11 17:04:15 +03:00
|
|
|
let extensions: Extension[] = [];
|
|
|
|
|
|
|
|
if (config.vimKeybindings) {
|
|
|
|
extensions.push(vim());
|
|
|
|
}
|
|
|
|
|
|
|
|
extensions = [
|
|
|
|
...extensions,
|
2025-05-10 23:34:23 +03:00
|
|
|
languageCompartment.of([]),
|
2025-05-11 21:18:14 +03:00
|
|
|
themeCompartment.of([
|
|
|
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
|
|
|
|
]),
|
2025-05-11 12:10:28 +03:00
|
|
|
highlightActiveLine(),
|
|
|
|
highlightSelectionMatches(),
|
2025-05-11 12:11:24 +03:00
|
|
|
bracketMatching(),
|
2025-05-11 11:37:52 +03:00
|
|
|
lineNumbers(),
|
2025-05-11 17:16:07 +03:00
|
|
|
foldGutter(),
|
2025-05-11 12:19:39 +03:00
|
|
|
indentUnit.of(" ".repeat(4)),
|
|
|
|
keymap.of([
|
|
|
|
...defaultKeymap,
|
|
|
|
...historyKeymap,
|
2025-05-11 17:39:10 +03:00
|
|
|
...smartIndentWithTab
|
2025-05-11 17:04:15 +03:00
|
|
|
])
|
|
|
|
]
|
2025-05-10 20:07:53 +03:00
|
|
|
|
2025-05-11 18:51:44 +03:00
|
|
|
if (!config.readOnly) {
|
|
|
|
// Logic specific to editable notes
|
|
|
|
if (config.placeholder) {
|
|
|
|
extensions.push(placeholder(config.placeholder));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.onContentChanged) {
|
|
|
|
extensions.push(EditorView.updateListener.of((v) => this.#onDocumentUpdated(v)));
|
|
|
|
}
|
2025-05-11 18:42:56 +03:00
|
|
|
|
2025-05-11 18:51:44 +03:00
|
|
|
extensions.push(historyCompartment.of(history()));
|
|
|
|
} else {
|
|
|
|
// Logic specific to read-only notes
|
|
|
|
extensions.push(EditorState.readOnly.of(true));
|
2025-05-11 11:27:27 +03:00
|
|
|
}
|
|
|
|
|
2025-05-11 12:07:54 +03:00
|
|
|
if (config.lineWrapping) {
|
|
|
|
extensions.push(EditorView.lineWrapping);
|
|
|
|
}
|
|
|
|
|
2025-05-10 19:10:30 +03:00
|
|
|
super({
|
2025-05-11 18:41:10 +03:00
|
|
|
parent: config.parent,
|
2025-05-10 20:07:53 +03:00
|
|
|
extensions
|
2025-05-10 19:10:30 +03:00
|
|
|
});
|
2025-05-10 20:07:53 +03:00
|
|
|
this.config = config;
|
2025-05-10 23:34:23 +03:00
|
|
|
this.languageCompartment = languageCompartment;
|
2025-05-11 12:23:09 +03:00
|
|
|
this.historyCompartment = historyCompartment;
|
2025-05-11 20:19:40 +03:00
|
|
|
this.themeCompartment = themeCompartment;
|
2025-05-10 20:07:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
#onDocumentUpdated(v: ViewUpdate) {
|
|
|
|
if (v.docChanged) {
|
|
|
|
this.config.onContentChanged?.();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getText() {
|
|
|
|
return this.state.doc.toString();
|
2025-05-10 19:10:30 +03:00
|
|
|
}
|
2025-05-10 19:22:57 +03:00
|
|
|
|
|
|
|
setText(content: string) {
|
|
|
|
this.dispatch({
|
|
|
|
changes: {
|
|
|
|
from: 0,
|
|
|
|
to: this.state.doc.length,
|
|
|
|
insert: content || "",
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2025-05-10 23:34:23 +03:00
|
|
|
|
2025-05-11 20:19:40 +03:00
|
|
|
async setTheme(theme: ThemeDefinition) {
|
2025-05-11 21:18:14 +03:00
|
|
|
const extension = await theme.load();
|
|
|
|
this.dispatch({
|
|
|
|
effects: [ this.themeCompartment.reconfigure([ extension ]) ]
|
|
|
|
});
|
2025-05-11 20:19:40 +03:00
|
|
|
}
|
|
|
|
|
2025-05-11 12:23:09 +03:00
|
|
|
/**
|
|
|
|
* Clears the history of undo/redo. Generally useful when changing to a new document.
|
|
|
|
*/
|
|
|
|
clearHistory() {
|
2025-05-11 18:51:44 +03:00
|
|
|
if (this.config.readOnly) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-05-11 12:23:09 +03:00
|
|
|
this.dispatch({
|
|
|
|
effects: [ this.historyCompartment.reconfigure([]) ]
|
|
|
|
});
|
|
|
|
this.dispatch({
|
|
|
|
effects: [ this.historyCompartment.reconfigure(history())]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-05-10 23:34:23 +03:00
|
|
|
async setMimeType(mime: string) {
|
2025-05-11 15:18:42 +03:00
|
|
|
let newExtension: Extension[] = [];
|
2025-05-10 23:34:23 +03:00
|
|
|
|
|
|
|
const correspondingSyntax = byMimeType[mime];
|
|
|
|
if (correspondingSyntax) {
|
2025-05-11 10:54:15 +03:00
|
|
|
const resolvedSyntax = await correspondingSyntax();
|
|
|
|
|
|
|
|
if ("token" in resolvedSyntax) {
|
|
|
|
const extension = StreamLanguage.define(resolvedSyntax);
|
|
|
|
newExtension.push(extension);
|
2025-05-11 15:18:42 +03:00
|
|
|
} else if (Array.isArray(resolvedSyntax)) {
|
|
|
|
newExtension = [ ...newExtension, ...resolvedSyntax ];
|
2025-05-11 10:54:15 +03:00
|
|
|
} else {
|
2025-05-11 13:09:36 +03:00
|
|
|
newExtension.push(resolvedSyntax);
|
2025-05-11 10:54:15 +03:00
|
|
|
}
|
2025-05-10 23:34:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
this.dispatch({
|
|
|
|
effects: this.languageCompartment.reconfigure(newExtension)
|
|
|
|
});
|
|
|
|
}
|
2025-05-10 19:10:30 +03:00
|
|
|
}
|