From 001d215b0eb61c4ba8696babc501f88d9300e299 Mon Sep 17 00:00:00 2001 From: Quinten Kock Date: Mon, 20 Oct 2025 18:02:37 +0200 Subject: [PATCH] Add basic multi-view support --- src/app/editor.ts | 29 ++++++++++++--- src/app/editorgrid.ts | 7 ++-- src/app/filestate.ts | 85 +++++++++++++++++++++++++++++++++++++++++++ src/app/renderer.ts | 14 +++++-- 4 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 src/app/filestate.ts diff --git a/src/app/editor.ts b/src/app/editor.ts index 93a8b61..0a56723 100644 --- a/src/app/editor.ts +++ b/src/app/editor.ts @@ -1,7 +1,10 @@ -import { basicSetup } from "codemirror"; -import { EditorView } from "@codemirror/view"; +import { Transaction } from "@codemirror/state"; +import { EditorView, keymap } from "@codemirror/view"; +import { defaultKeymap, undo, redo } from "@codemirror/commands"; import { oneDark } from "@codemirror/theme-one-dark"; +import { OpenFile } from "./filestate"; + const fixedHeightEditor = EditorView.theme({ "&": { height: "100%", @@ -11,17 +14,33 @@ const fixedHeightEditor = EditorView.theme({ width: "600px", minWidth: "8em", flex: "none", + fontSize: "16px", }, ".cm-scroller": { overflow: "auto scroll" }, }); export class Editor { view: EditorView; + file: OpenFile; - constructor() { + dispatch(tr: Transaction, inhibitSync = false) { + this.view.update([tr]); + if (!inhibitSync) { + this.file.dispatch({ changes: tr.changes }, this); + } + } + + constructor(file: OpenFile) { + this.file = file; + const kmap = keymap.of([ + ...defaultKeymap, + { key: "Mod-z", run: () => undo(file.target) }, + { key: "Mod-shift-z", run: () => redo(file.target) }, + ]); this.view = new EditorView({ - doc: "Start document", - extensions: [basicSetup, oneDark, fixedHeightEditor], + doc: file.rootState.doc, + dispatch: (trs) => this.dispatch(trs), + extensions: [oneDark, fixedHeightEditor, kmap], }); } diff --git a/src/app/editorgrid.ts b/src/app/editorgrid.ts index 31654a4..e299bdb 100644 --- a/src/app/editorgrid.ts +++ b/src/app/editorgrid.ts @@ -2,7 +2,7 @@ import van from "vanjs-core"; import * as vanX from "vanjs-ext"; const v = van.tags; -import { Editor } from "./editor"; +import { OpenFile } from "./filestate"; import * as u from "./utils"; const EditorWrapper = (editor: any, del: any, k: any) => @@ -20,9 +20,10 @@ const editors = vanX.reactive([[]]); const currentTab = van.state(0); van.derive(() => console.log("Setting tab to", currentTab.val)); -export function addEditor() { +export function addEditor(file: OpenFile) { console.log("Adding editor to tab ", currentTab.val, editors); - editors[currentTab.val].push(vanX.noreactive(new Editor())); + const editor = file.createEditor(); + editors[currentTab.val].push(vanX.noreactive(editor)); } export function addTab() { editors.push([]); diff --git a/src/app/filestate.ts b/src/app/filestate.ts new file mode 100644 index 0000000..1416007 --- /dev/null +++ b/src/app/filestate.ts @@ -0,0 +1,85 @@ +import { + EditorState, + EditorStateConfig, + TransactionSpec, + StateEffect, +} from "@codemirror/state"; +import { history } from "@codemirror/commands"; +import { Editor } from "./editor"; + +const openFiles: { [path: string]: OpenFile } = {}; + +export class OpenFile { + filePath: string; + editors: Editor[]; + rootState: EditorState; + + constructor(cfg: EditorStateConfig) { + this.filePath = null; + this.editors = []; + this.rootState = EditorState.create(cfg).update({ + effects: [StateEffect.appendConfig.of([history()])], + }).state; + } + + static async openFile(filePath?: string) { + const { content, path } = await window.electronAPI.readFile(filePath); + const file = new OpenFile({ doc: content }); + file.setPath(path); + return file; + } + + private setPath(path: string) { + delete openFiles[this.filePath]; + this.filePath = path; + openFiles[path] = this; + } + + async saveFile() { + if (this.filePath) { + await window.electronAPI.saveFile( + this.rootState.doc.toString(), + this.filePath, + ); + } else { + await this.saveAs(); + } + } + + async saveAs(filePath?: string) { + const { path } = await window.electronAPI.saveFile( + this.rootState.doc.toString(), + filePath, + ); + this.setPath(path); + } + + // Function to create and return a new EditorView for this file + createEditor(): Editor { + const editor = new Editor(this); + this.editors.push(editor); + return editor; + } + + dispatch(trs: TransactionSpec, origin?: Editor) { + console.log("Dispatching trs", trs, "to", this.editors, "from", origin); + console.log(this.rootState); + this.rootState = this.rootState.update(trs).state; + if (origin) { + const es = this.editors.filter((e) => e !== origin); + es.forEach((e) => e.dispatch(e.view.state.update(trs), true)); + } else { + this.editors.forEach((e) => + e.dispatch(e.view.state.update(trs), true), + ); + } + } + + get target() { + console.log("Getting target"); + return { + state: this.rootState, + dispatch: (tr: TransactionSpec) => this.dispatch(tr), + }; + } +} diff --git a/src/app/renderer.ts b/src/app/renderer.ts index e765d89..9436381 100644 --- a/src/app/renderer.ts +++ b/src/app/renderer.ts @@ -2,13 +2,17 @@ import "./index.css"; import van from "vanjs-core"; -import * as vanX from "vanjs-ext"; const v = van.tags; -import { Editor } from "./editor"; import { FolderTreeView } from "./foldernav"; import { EditorTabs, addTab, addEditor } from "./editorgrid"; import * as u from "./utils"; +import { OpenFile } from "./filestate"; + +function newFile() { + const file = new OpenFile({}); + addEditor(file); +} const app = v.div( { class: "h-screen max-h-screen w-screen max-w-screen flex" }, @@ -17,7 +21,7 @@ const app = v.div( class: "flex-none resize-x overflow-x-hidden overflow-y-scroll w-3xs min-w-32", }, u.InlineButton(addTab, "Add Tab", "+Tab"), - u.InlineButton(addEditor, "Add Editor", "+File"), + u.InlineButton(newFile, "Add Editor", "+File"), FolderTreeView, ), EditorTabs, @@ -25,4 +29,6 @@ const app = v.div( van.add(document.body, app); -addEditor(); +const file = new OpenFile({}); +addEditor(file); +addEditor(file);