2023-11-21 15:14:08 -05:00
|
|
|
import Row from './row.ts';
|
2023-11-29 16:09:58 -05:00
|
|
|
import { arrayInsert } from './fns.ts';
|
2023-11-14 15:53:45 -05:00
|
|
|
import { getRuntime } from './runtime.ts';
|
2024-02-28 15:44:57 -05:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2023-11-13 14:46:04 -05:00
|
|
|
|
|
|
|
export class Document {
|
|
|
|
#rows: Row[];
|
2024-02-28 15:44:57 -05:00
|
|
|
#search: Search;
|
2023-11-30 16:14:52 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Has the document been modified?
|
|
|
|
*/
|
|
|
|
public dirty: boolean;
|
2023-11-13 14:46:04 -05:00
|
|
|
|
|
|
|
private constructor() {
|
|
|
|
this.#rows = [];
|
2024-02-28 15:44:57 -05:00
|
|
|
this.#search = new Search();
|
2023-11-21 15:14:08 -05:00
|
|
|
this.dirty = false;
|
2023-11-13 14:46:04 -05:00
|
|
|
}
|
|
|
|
|
2023-11-16 11:10:33 -05:00
|
|
|
get numRows(): number {
|
2023-11-13 14:46:04 -05:00
|
|
|
return this.#rows.length;
|
|
|
|
}
|
|
|
|
|
2023-11-27 10:25:30 -05:00
|
|
|
public static default(): Document {
|
2023-11-13 14:46:04 -05:00
|
|
|
return new Document();
|
|
|
|
}
|
|
|
|
|
2023-11-14 15:53:45 -05:00
|
|
|
public isEmpty(): boolean {
|
|
|
|
return this.#rows.length === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async open(filename: string): Promise<Document> {
|
|
|
|
const { file } = await getRuntime();
|
|
|
|
|
|
|
|
// Clear any existing rows
|
|
|
|
if (!this.isEmpty()) {
|
|
|
|
this.#rows = [];
|
|
|
|
}
|
2023-11-13 14:46:04 -05:00
|
|
|
|
2023-11-14 15:53:45 -05:00
|
|
|
const rawFile = await file.openFile(filename);
|
|
|
|
rawFile.split(/\r?\n/)
|
2023-11-22 17:09:41 -05:00
|
|
|
.forEach((row) => this.insertRow(this.numRows, row));
|
2023-11-13 14:46:04 -05:00
|
|
|
|
2023-11-21 15:14:08 -05:00
|
|
|
this.dirty = false;
|
|
|
|
|
2023-11-14 15:53:45 -05:00
|
|
|
return this;
|
2023-11-13 14:46:04 -05:00
|
|
|
}
|
|
|
|
|
2023-11-27 10:25:30 -05:00
|
|
|
public async save(filename: string) {
|
2023-11-21 15:14:08 -05:00
|
|
|
const { file } = await getRuntime();
|
|
|
|
|
2023-11-27 10:25:30 -05:00
|
|
|
await file.saveFile(filename, this.rowsToString());
|
2023-11-21 15:14:08 -05:00
|
|
|
|
|
|
|
this.dirty = false;
|
|
|
|
}
|
|
|
|
|
2024-02-28 15:44:57 -05:00
|
|
|
public resetFind() {
|
|
|
|
this.#search = new Search();
|
|
|
|
}
|
|
|
|
|
2023-11-30 16:14:52 -05:00
|
|
|
public find(
|
|
|
|
q: string,
|
2024-02-28 15:44:57 -05:00
|
|
|
key: string,
|
2023-11-30 16:14:52 -05:00
|
|
|
): Position | null {
|
2024-02-28 15:44:57 -05:00
|
|
|
this.#search.parseInput(key);
|
|
|
|
|
|
|
|
let i = 0;
|
2023-11-30 16:14:52 -05:00
|
|
|
for (; i < this.numRows; i++) {
|
2024-02-28 15:44:57 -05:00
|
|
|
const current = this.#search.getNextRow(this.numRows);
|
|
|
|
|
|
|
|
const possible = this.#rows[current].find(q);
|
2023-11-30 16:14:52 -05:00
|
|
|
if (possible !== null) {
|
2024-02-28 15:44:57 -05:00
|
|
|
this.#search.lastMatch = current;
|
|
|
|
return Position.at(possible, current);
|
2023-11-30 16:14:52 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-11-21 15:14:08 -05:00
|
|
|
public insert(at: Position, c: string): void {
|
|
|
|
if (at.y === this.numRows) {
|
2023-11-22 17:09:41 -05:00
|
|
|
this.insertRow(this.numRows, c);
|
2023-11-21 15:14:08 -05:00
|
|
|
} else {
|
|
|
|
this.#rows[at.y].insertChar(at.x, c);
|
2023-11-22 11:07:33 -05:00
|
|
|
this.#rows[at.y].updateRender();
|
2023-11-21 15:14:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
this.dirty = true;
|
|
|
|
}
|
|
|
|
|
2023-11-22 17:09:41 -05:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-11-22 11:07:33 -05:00
|
|
|
public delete(at: Position): void {
|
2023-11-22 15:11:32 -05:00
|
|
|
const len = this.numRows;
|
|
|
|
if (at.y >= len) {
|
2023-11-22 11:07:33 -05:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-11-22 15:11:32 -05:00
|
|
|
const row = this.row(at.y)!;
|
2023-11-30 11:15:37 -05:00
|
|
|
const mergeNextRow = at.x === row.size && at.y + 1 < len;
|
2023-11-22 15:11:32 -05:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-11-22 11:07:33 -05:00
|
|
|
row.updateRender();
|
2023-11-22 11:27:46 -05:00
|
|
|
|
|
|
|
this.dirty = true;
|
2023-11-22 11:07:33 -05:00
|
|
|
}
|
|
|
|
|
2023-11-14 15:53:45 -05:00
|
|
|
public row(i: number): Row | null {
|
2023-11-20 14:21:42 -05:00
|
|
|
return this.#rows[i] ?? null;
|
2023-11-13 14:46:04 -05:00
|
|
|
}
|
2023-11-14 15:53:45 -05:00
|
|
|
|
2023-11-22 17:09:41 -05:00
|
|
|
public insertRow(at: number = this.numRows, s: string = ''): void {
|
|
|
|
this.#rows = arrayInsert(this.#rows, at, Row.from(s));
|
2023-11-22 11:07:33 -05:00
|
|
|
this.#rows[at].updateRender();
|
2023-11-21 15:14:08 -05:00
|
|
|
|
|
|
|
this.dirty = true;
|
2023-11-20 15:14:36 -05:00
|
|
|
}
|
|
|
|
|
2023-11-22 15:11:32 -05:00
|
|
|
/**
|
|
|
|
* Delete the specified row
|
|
|
|
* @param at - the index of the row to delete
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private deleteRow(at: number): void {
|
|
|
|
this.#rows.splice(at, 1);
|
|
|
|
}
|
|
|
|
|
2023-11-22 17:09:41 -05:00
|
|
|
/**
|
|
|
|
* Convert the array of row objects into one string
|
|
|
|
* @private
|
|
|
|
*/
|
2023-11-21 15:14:08 -05:00
|
|
|
private rowsToString(): string {
|
|
|
|
return this.#rows.map((r) => r.toString()).join('\n');
|
|
|
|
}
|
2023-11-13 14:46:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export default Document;
|