scroll/src/common/document.ts

226 lines
4.6 KiB
JavaScript

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<Document> {
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;