import Row from './row.ts'; import { arrayInsert, maxAdd, minSub } from './fns.ts'; import Option, { None, Some } from './option.ts'; import { getRuntime } from './runtime.ts'; import { Position, SearchDirection } from './types.ts'; export class Document { #rows: Row[]; /** * Has the document been modified? */ public dirty: boolean; private constructor() { this.#rows = []; this.dirty = false; } get numRows(): number { return this.#rows.length; } public static default(): Document { return new Document(); } public isEmpty(): boolean { return this.#rows.length === 0; } /** * Open a file for editing */ 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.dirty = false; return this; } /** * Save the current document */ public async save(filename: string): Promise { const { file } = await getRuntime(); await file.saveFile(filename, this.rowsToString()); this.dirty = false; } public find( q: string, at: Position, direction: SearchDirection = SearchDirection.Forward, ): Option { if (at.y >= this.numRows) { return None; } const position = Position.from(at); const start = (direction === SearchDirection.Forward) ? at.y : 0; const end = (direction === SearchDirection.Forward) ? this.numRows : maxAdd(at.y, 1, this.numRows); for (let y = start; y < end; y++) { if (this.row(position.y).isNone()) { return None; } const maybeMatch = this.#rows[y].find(q, position.x, direction); if (maybeMatch.isSome()) { position.x = maybeMatch.unwrap(); return Some(position); } if (direction === SearchDirection.Forward) { position.y = maxAdd(position.y, 1, this.numRows - 1); position.x = 0; } else { position.y = minSub(position.y, 1, 0); console.assert(position.y < this.numRows); position.x = this.#rows[position.y].size - 1; } } return None; } 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].update(None); } this.dirty = true; } /** * Insert a new line, splitting and/or creating a new row as needed */ public insertNewline(at: Position): void { if (at.y > this.numRows) { return; } // Just add a simple blank line if (at.y === this.numRows) { this.#rows.push(Row.default()); this.dirty = true; return; } // Split the current row, and insert a new // row with the leftovers const newRow = this.#rows[at.y].split(at.x); newRow.update(None); this.#rows = arrayInsert(this.#rows, at.y + 1, newRow); this.dirty = true; } /** * Remove a character from the document, merging * adjacent lines if necessary */ public delete(at: Position): void { const len = this.numRows; if (at.y >= len) { return; } const maybeRow = this.row(at.y); if (maybeRow.isNone()) { return; } const row = maybeRow.unwrap(); const mergeNextRow = at.x === row.size && at.y + 1 < len; 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.update(None); this.dirty = true; } public row(i: number): Option { if (i >= this.numRows) { return None; } return Option.from(this.#rows[i]); } public insertRow(at: number = this.numRows, s: string = ''): void { this.#rows = arrayInsert(this.#rows, at, Row.from(s)); this.#rows[at].update(None); this.dirty = true; } public highlight(searchMatch: Option): void { this.#rows.forEach((row) => { row.update(searchMatch); }); } /** * 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;