scroll/src/common/document.ts

238 lines
5.0 KiB
JavaScript
Raw Normal View History

2023-11-21 15:14:08 -05:00
import Row from './row.ts';
import { arrayInsert, maxAdd, minSub } from './fns.ts';
import Option, { None, Some } from './option.ts';
import { getRuntime, logDebug, logWarning } from './runtime.ts';
import { Position, SearchDirection } from './types.ts';
2023-11-13 14:46:04 -05:00
export class Document {
#rows: Row[];
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 = [];
2023-11-21 15:14:08 -05:00
this.dirty = false;
2023-11-13 14:46:04 -05:00
}
public get numRows(): number {
2023-11-13 14:46:04 -05:00
return this.#rows.length;
}
public static default(): Document {
return new Document();
2023-11-13 14:46:04 -05:00
}
public isEmpty(): boolean {
return this.#rows.length === 0;
}
2024-02-29 14:24:22 -05:00
/**
* 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 = [];
}
2023-11-13 14:46:04 -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;
return this;
2023-11-13 14:46:04 -05:00
}
2024-02-29 14:24:22 -05:00
/**
* Save the current document
*/
2024-07-02 16:27:18 -04:00
public async save(filename: string): Promise<void> {
2023-11-21 15:14:08 -05:00
const { file } = await getRuntime();
await file.saveFile(filename, this.rowsToString());
2023-11-21 15:14:08 -05:00
this.dirty = false;
}
2023-11-30 16:14:52 -05:00
public find(
q: string,
at: Position,
direction: SearchDirection = SearchDirection.Forward,
): Option<Position> {
if (at.y >= this.numRows) {
logWarning('Trying to search beyond the end of the current file', {
at,
document: this,
});
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()) {
logWarning('Invalid Search location', {
position,
document: this,
});
return None;
}
const maybeMatch = this.#rows[y].find(q, position.x, direction);
if (maybeMatch.isSome()) {
position.x = this.#rows[y].rxToCx(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;
2023-11-30 16:14:52 -05:00
}
2023-11-21 15:14:08 -05:00
public insert(at: Position, c: string): void {
2024-07-10 12:11:27 -04:00
if (at.y > this.numRows) {
return;
}
this.dirty = true;
2023-11-21 15:14:08 -05:00
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);
this.#rows[at.y].update(None);
2023-11-21 15:14:08 -05:00
}
}
2024-07-02 16:27:18 -04:00
/**
* Insert a new line, splitting and/or creating a new row as needed
*/
2023-11-22 17:09:41 -05:00
public insertNewline(at: Position): void {
if (at.y > this.numRows) {
return;
}
2024-07-10 12:11:27 -04:00
this.dirty = true;
2024-07-02 16:27:18 -04:00
// Just add a simple blank line
2023-11-22 17:09:41 -05:00
if (at.y === this.numRows) {
this.#rows.push(Row.default());
2024-07-10 12:11:27 -04:00
2023-11-22 17:09:41 -05:00
return;
}
2024-07-02 16:27:18 -04:00
// Split the current row, and insert a new
// row with the leftovers
2024-07-10 12:11:27 -04:00
const currentRow = this.#rows[at.y];
const newRow = currentRow.split(at.x);
currentRow.update(None);
newRow.update(None);
2023-11-22 17:09:41 -05:00
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
}
2024-02-29 14:24:22 -05:00
/**
* Remove a character from the document, merging
* adjacent lines if necessary
*/
2023-11-22 11:07:33 -05:00
public delete(at: Position): void {
const len = this.numRows;
if (at.y >= len) {
2023-11-22 11:07:33 -05:00
return;
}
this.dirty = true;
const maybeRow = this.row(at.y);
if (maybeRow.isNone()) {
return;
}
const row = maybeRow.unwrap();
2024-07-10 12:11:27 -04:00
const mergeNextRow = at.x === row.size && this.row(at.y + 1).isSome();
logDebug('Document.delete', {
2024-07-10 12:11:27 -04:00
method: 'Document.delete',
at,
mergeNextRow,
});
// 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 (This also works for pressing
// backspace at the beginning of a line: the cursor is
// moved to the end of the previous line)
if (mergeNextRow) {
// At the end of a line, pressing delete will merge
// the next line into the current one
const rowToAppend = this.#rows[at.y + 1].toString();
row.append(rowToAppend);
this.deleteRow(at.y + 1);
} else {
row.delete(at.x);
}
row.update(None);
2023-11-22 11:07:33 -05:00
}
public row(i: number): Option<Row> {
2024-07-10 12:11:27 -04:00
if (i >= this.numRows || i < 0) {
return None;
}
return Option.from(this.#rows.at(i));
2023-11-13 14:46:04 -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));
this.#rows[at].update(None);
2023-11-21 15:14:08 -05:00
this.dirty = true;
}
public highlight(searchMatch: Option<string>): void {
this.#rows.forEach((row) => {
row.update(searchMatch);
});
}
/**
* Delete the specified row
* @param at - the index of the row to delete
*/
protected 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
*/
protected rowsToString(): string {
2023-11-21 15:14:08 -05:00
return this.#rows.map((r) => r.toString()).join('\n');
}
2023-11-13 14:46:04 -05:00
}
export default Document;