import Row from './row.ts'; import { arrayInsert } from './fns.ts'; import { getRuntime } from './runtime.ts'; import { Position, SearchDirection } from './types.ts'; import { KeyCommand } from './ansi.ts'; class Search { public lastMatch: number = -1; public current: number = -1; public direction: SearchDirection = SearchDirection.Forward; public parseInput(key: string) { switch (key) { case KeyCommand.ArrowRight: case KeyCommand.ArrowDown: this.direction = SearchDirection.Forward; break; case KeyCommand.ArrowLeft: case KeyCommand.ArrowUp: this.direction = SearchDirection.Backward; break; default: this.lastMatch = -1; this.direction = SearchDirection.Forward; } if (this.lastMatch === -1) { this.direction = SearchDirection.Forward; } this.current = this.lastMatch; } public getNextRow(rowCount: number): number { this.current += this.direction; if (this.current === -1) { this.current = rowCount - 1; } else if (this.current === rowCount) { this.current = 0; } return this.current; } } export class Document { #rows: Row[]; #search: Search; /** * Has the document been modified? */ public dirty: boolean; private constructor() { this.#rows = []; this.#search = new Search(); 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) { const { file } = await getRuntime(); await file.saveFile(filename, this.rowsToString()); this.dirty = false; } public resetFind() { this.#search = new Search(); } public find( q: string, key: string, ): Position | null { this.#search.parseInput(key); let i = 0; for (; i < this.numRows; i++) { const current = this.#search.getNextRow(this.numRows); const possible = this.#rows[current].find(q); if (possible !== null) { this.#search.lastMatch = current; return Position.at(possible, current); } } return null; } 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; } /** * 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 row = this.row(at.y)!; 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.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;