152 lines
4.6 KiB
TypeScript
152 lines
4.6 KiB
TypeScript
import {
|
|
EditorState,
|
|
EditorStateConfig,
|
|
TransactionSpec,
|
|
StateEffect,
|
|
Text,
|
|
Transaction,
|
|
} from "@codemirror/state";
|
|
import { history } from "@codemirror/commands";
|
|
import { Editor } from "./editor";
|
|
import van, { State } from "vanjs-core";
|
|
|
|
const openFiles: { [path: string]: OpenFile } = {};
|
|
|
|
export class OpenFile {
|
|
filePath: State<string>;
|
|
editors: Editor[];
|
|
rootState: State<EditorState>;
|
|
lastSaved?: State<Text>;
|
|
|
|
constructor(cfg: EditorStateConfig) {
|
|
this.filePath = van.state(null);
|
|
this.editors = [];
|
|
this.rootState = van.state(
|
|
EditorState.create(cfg).update({
|
|
effects: [StateEffect.appendConfig.of([history()])],
|
|
}).state,
|
|
);
|
|
this.lastSaved = van.state(this.rootState.val.doc);
|
|
}
|
|
|
|
static async openFile(filePath?: string): Promise<OpenFile> {
|
|
if (filePath && openFiles[filePath]) {
|
|
return openFiles[filePath];
|
|
}
|
|
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.val];
|
|
this.filePath.val = path;
|
|
openFiles[path] = this;
|
|
// TODO: what if openFiles[path] already exists?
|
|
}
|
|
|
|
async saveFile() {
|
|
if (this.filePath.val) {
|
|
await window.electronAPI.saveFile(
|
|
this.rootState.val.doc.toString(),
|
|
this.filePath.val,
|
|
);
|
|
this.lastSaved.val = this.rootState.val.doc;
|
|
} else {
|
|
await this.saveAs();
|
|
}
|
|
}
|
|
|
|
async saveAs(filePath?: string) {
|
|
const { path } = await window.electronAPI.saveFile(
|
|
this.rootState.val.doc.toString(),
|
|
filePath,
|
|
);
|
|
this.setPath(path);
|
|
this.lastSaved.val = this.rootState.val.doc;
|
|
}
|
|
|
|
// Function to create and return a new EditorView for this file
|
|
createEditor(): Editor {
|
|
const editor = new Editor(this);
|
|
this.editors.push(editor);
|
|
return editor;
|
|
}
|
|
|
|
// Function to remove an editor and clean up if no more editors exist
|
|
async removeEditor(editor: Editor, callback: () => void) {
|
|
const index = this.editors.indexOf(editor);
|
|
if (index == -1) return;
|
|
|
|
// If this is the last editor and the file is dirty, confirm before closing
|
|
if (this.editors.length === 1 && this.isDirty()) {
|
|
const confirmed = await this.confirmClose();
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Remove the editor from the list
|
|
this.editors.splice(index, 1);
|
|
|
|
// If no more editors, remove from openFiles dictionary
|
|
if (this.editors.length === 0) {
|
|
delete openFiles[this.filePath.val];
|
|
}
|
|
|
|
callback();
|
|
}
|
|
|
|
// Function to confirm closing of dirty file
|
|
private async confirmClose(): Promise<boolean> {
|
|
const fileName = this.filePath.val
|
|
? this.filePath.val.split("/").pop()
|
|
: "untitled";
|
|
const message = `Do you want to save the changes to ${fileName}?`;
|
|
const result = await window.electronAPI.showConfirmDialog(
|
|
message,
|
|
"Save Changes?",
|
|
["Save", "Don't Save", "Cancel"],
|
|
);
|
|
|
|
if (result === "Save") {
|
|
await this.saveFile();
|
|
return true;
|
|
} else if (result === "Don't Save") {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
dispatch(trs: TransactionSpec, origin?: Editor) {
|
|
const transaction = this.rootState.val.update(trs);
|
|
this.rootState.val = transaction.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) => {
|
|
const changes = transaction.changes;
|
|
const userEvent = transaction.annotation(Transaction.userEvent);
|
|
const annotations = userEvent
|
|
? [Transaction.userEvent.of(userEvent)]
|
|
: [];
|
|
e.dispatch(e.view.state.update({ changes, annotations }), true);
|
|
});
|
|
}
|
|
}
|
|
|
|
get target() {
|
|
return {
|
|
state: this.rootState.val,
|
|
dispatch: (tr: TransactionSpec) => this.dispatch(tr),
|
|
};
|
|
}
|
|
|
|
isDirty(): boolean {
|
|
return !this.lastSaved.val.eq(this.rootState.val.doc);
|
|
}
|
|
}
|