import Row from './row.ts'; import { getRuntime } from './runtime.ts'; import { arrayInsert, Position } from './mod.ts'; export class Document { #filename: string | null; #rows: Row[]; dirty: boolean; private constructor() { this.#rows = []; this.#filename = null; this.dirty = false; } get numRows(): number { return this.#rows.length; } public static empty(): Document { return new Document(); } public isEmpty(): boolean { return this.#rows.length === 0; } public async open(filename: string): Promise { const { file } = await getRuntime(); // Clear any existing rows if (!this.isEmpty()) { this.#rows = []; } const rawFile = await file.openFile(filename); rawFile.split(/\r?\n/) .forEach((row) => this.insertRow(this.numRows, row)); this.#filename = filename; this.dirty = false; return this; } public async save() { if (this.#filename === null) { return; } const { file } = await getRuntime(); await file.saveFile(this.#filename, this.rowsToString()); this.dirty = false; } public insert(at: Position, c: string): void { if (at.y === this.numRows) { this.insertRow(this.numRows, c); } else { this.#rows[at.y].insertChar(at.x, c); this.#rows[at.y].updateRender(); } this.dirty = true; } public insertNewline(at: Position): void { if (at.y > this.numRows) { return; } if (at.y === this.numRows) { this.#rows.push(Row.default()); return; } const newRow = this.#rows[at.y].split(at.x); newRow.updateRender(); this.#rows = arrayInsert(this.#rows, at.y + 1, newRow); this.dirty = true; } public delete(at: Position): void { const len = this.numRows; if (at.y >= len) { return; } const row = this.row(at.y)!; const mergeNextRow = at.x === row.size && at.y < len - 1; const mergeIntoPrevRow = at.x === 0 && at.y > 0; // If we are at the end of a line, and press delete, // add the contents of the next row, and delete // the merged row object if (mergeNextRow) { // At the end of a line, pressing delete will merge // the next line into the current on const rowToAppend = this.#rows.at(at.y + 1)!.toString(); row.append(rowToAppend); this.deleteRow(at.y + 1); } else if (mergeIntoPrevRow) { // At the beginning of a line, merge the current line // into the previous Row const rowToAppend = row.toString(); this.#rows[at.y - 1].append(rowToAppend); this.deleteRow(at.y); } else { row.delete(at.x); } row.updateRender(); this.dirty = true; } public row(i: number): Row | null { return this.#rows[i] ?? null; } public insertRow(at: number = this.numRows, s: string = ''): void { this.#rows = arrayInsert(this.#rows, at, Row.from(s)); this.#rows[at].updateRender(); this.dirty = true; } /** * Delete the specified row * @param at - the index of the row to delete * @private */ private deleteRow(at: number): void { this.#rows.splice(at, 1); } /** * Convert the array of row objects into one string * @private */ private rowsToString(): string { return this.#rows.map((r) => r.toString()).join('\n'); } } export default Document;