Timothy J. Warren
b5856f063a
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good
255 lines
5.4 KiB
JavaScript
255 lines
5.4 KiB
JavaScript
import Row from './row.ts';
|
|
import { FileType } from './filetype/mod.ts';
|
|
import { arrayInsert, maxAdd, minSub } from './fns.ts';
|
|
import Option, { None, Some } from './option.ts';
|
|
import { getRuntime, logDebug, logWarning } from './runtime/mod.ts';
|
|
import { Position, SearchDirection } from './types.ts';
|
|
|
|
export class Document {
|
|
/**
|
|
* Each line of the current document
|
|
*/
|
|
#rows: Row[];
|
|
|
|
/**
|
|
* Has the document been modified?
|
|
*/
|
|
public dirty: boolean;
|
|
|
|
/**
|
|
* The meta-data for the file type of the current document
|
|
*/
|
|
public type: FileType;
|
|
|
|
private constructor() {
|
|
this.#rows = [];
|
|
this.dirty = false;
|
|
this.type = FileType.default();
|
|
}
|
|
|
|
public get fileType(): string {
|
|
return this.type.name;
|
|
}
|
|
|
|
public 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 = [];
|
|
}
|
|
|
|
this.type = FileType.from(filename);
|
|
|
|
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<void> {
|
|
const { file } = await getRuntime();
|
|
|
|
await file.saveFile(filename, this.rowsToString());
|
|
this.type = FileType.from(filename);
|
|
|
|
this.dirty = false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
public insert(at: Position, c: string): void {
|
|
if (at.y > this.numRows) {
|
|
return;
|
|
}
|
|
|
|
this.dirty = true;
|
|
|
|
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.type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert a new line, splitting and/or creating a new row as needed
|
|
*/
|
|
public insertNewline(at: Position): void {
|
|
if (at.y > this.numRows) {
|
|
return;
|
|
}
|
|
|
|
this.dirty = true;
|
|
|
|
// Just add a simple blank line
|
|
if (at.y === this.numRows) {
|
|
this.#rows.push(Row.default());
|
|
|
|
return;
|
|
}
|
|
|
|
// Split the current row, and insert a new
|
|
// row with the leftovers
|
|
const currentRow = this.#rows[at.y];
|
|
const newRow = currentRow.split(at.x, this.type);
|
|
currentRow.update(None, this.type);
|
|
newRow.update(None, this.type);
|
|
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
this.dirty = true;
|
|
|
|
const maybeRow = this.row(at.y);
|
|
if (maybeRow.isNone()) {
|
|
return;
|
|
}
|
|
|
|
const row = maybeRow.unwrap();
|
|
|
|
const mergeNextRow = at.x === row.size && this.row(at.y + 1).isSome();
|
|
|
|
logDebug('Document.delete', {
|
|
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.type);
|
|
this.deleteRow(at.y + 1);
|
|
} else {
|
|
row.delete(at.x);
|
|
}
|
|
|
|
row.update(None, this.type);
|
|
}
|
|
|
|
public row(i: number): Option<Row> {
|
|
if (i >= this.numRows || i < 0) {
|
|
return None;
|
|
}
|
|
|
|
return Option.from(this.#rows.at(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.type);
|
|
|
|
this.dirty = true;
|
|
}
|
|
|
|
public highlight(searchMatch: Option<string>): void {
|
|
this.#rows.forEach((row) => {
|
|
row.update(searchMatch, this.type);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete the specified row
|
|
* @param at - the index of the row to delete
|
|
*/
|
|
protected deleteRow(at: number): void {
|
|
this.#rows.splice(at, 1);
|
|
}
|
|
|
|
/**
|
|
* Convert the array of row objects into one string
|
|
*/
|
|
protected rowsToString(): string {
|
|
return this.#rows.map((r) => r.toString()).join('\n');
|
|
}
|
|
}
|
|
|
|
export default Document;
|