miller/src/app/editor.ts

196 lines
5.5 KiB
TypeScript

import {
Transaction,
Compartment,
Extension,
StateEffect,
StateField,
EditorState,
} from "@codemirror/state";
import {
EditorView,
keymap,
lineNumbers,
highlightSpecialChars,
highlightActiveLine,
highlightActiveLineGutter,
drawSelection,
dropCursor,
rectangularSelection,
crosshairCursor,
showPanel,
} from "@codemirror/view";
import { defaultKeymap, undo, redo } from "@codemirror/commands";
import { oneDark } from "@codemirror/theme-one-dark";
import {
LanguageDescription,
foldGutter,
indentOnInput,
bracketMatching,
foldKeymap,
indentUnit,
} from "@codemirror/language";
import { languages } from "@codemirror/language-data";
import { autocompletion, closeBrackets } from "@codemirror/autocomplete";
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import van from "vanjs-core";
import { Displayable } from "./displayable";
import { OpenFile } from "./filestate";
const fixedHeightEditor = EditorView.theme({
"&": {
height: "100%",
minHeight: "1em",
resize: "horizontal",
overflow: "auto",
width: "768px",
minWidth: "8em",
flex: "none",
fontSize: "16px",
scrollMargin: "100px",
},
".cm-scroller": { overflow: "auto scroll" },
});
const FileStatusEffect = StateEffect.define<string | null>();
const FileStatusField = StateField.define<string | null>({
create: () => null,
update(value, tr): string | null {
for (const effect of tr.effects) {
if (effect.is(FileStatusEffect)) {
value = effect.value;
}
}
return value;
},
provide: (f) =>
showPanel.from(f, (state) => (state ? fileStatusPanel : null)),
});
function fileStatusPanel(view: EditorView) {
const dom = document.createElement("div");
dom.textContent = view.state.field(FileStatusField);
dom.style =
"padding: 2px 10px; font-size: 12px; color: white; background: #900;";
return { top: true, dom };
}
export class Editor extends Displayable {
view: EditorView;
file: OpenFile;
private wordWrapCompartment = new Compartment();
private languageCompartment = new Compartment();
dispatch(tr: Transaction, inhibitSync = false) {
this.view.update([tr]);
if (!inhibitSync) {
this.file.dispatch({ changes: tr.changes }, this);
}
}
constructor(file: OpenFile) {
super();
this.file = file;
const kmap = keymap.of([
...defaultKeymap,
...searchKeymap,
...foldKeymap,
{ key: "Mod-z", run: () => undo(file.target) },
{ key: "Mod-shift-z", run: () => redo(file.target) },
{
key: "Ctrl-s",
run: () => {
file.saveFile();
return true;
},
},
{
key: "Alt-z",
run: () => {
this.toggleExt(
this.wordWrapCompartment,
EditorView.lineWrapping,
);
return true;
},
},
]);
this.view = new EditorView({
doc: file.rootState.val.doc,
dispatch: (trs) => this.dispatch(trs),
extensions: [
oneDark,
fixedHeightEditor,
kmap,
FileStatusField,
this.wordWrapCompartment.of(EditorView.lineWrapping),
this.languageCompartment.of([]),
lineNumbers(),
highlightSpecialChars(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightActiveLineGutter(),
highlightSelectionMatches(),
indentUnit.of(" "),
// lintKeymap,
],
});
van.derive(() => {
LanguageDescription.matchFilename(languages, file.filePath.val)
?.load()
.then((Lang) => {
// const eff = StateEffect.appendConfig.of(Lang);
const eff = this.languageCompartment.reconfigure(Lang);
this.view.dispatch({ effects: [eff] });
});
});
van.derive(() => {
const effects = FileStatusEffect.of(
file.diskDiscrepancyMessage.val,
);
const tr = this.view.state.update({ effects });
this.dispatch(tr, true);
});
}
get dom() {
return this.view.dom;
}
focus() {
this.view.dom.scrollIntoView({ behavior: "smooth" });
this.view.focus();
}
title(): string {
return this.file.filePath.val + (this.file.isDirty() ? "*" : "");
}
close() {
if (this.deleteFn) {
this.file.removeEditor(this, this.deleteFn);
return true;
}
return false;
}
toggleExt(compartment: Compartment, extension: Extension) {
const on = compartment.get(this.view.state) == extension;
this.view.dispatch({
effects: compartment.reconfigure(on ? [] : extension),
});
}
}