This commit is contained in:
parent
b3bddbb601
commit
4436a8a783
13
README.md
13
README.md
@ -1,16 +1,23 @@
|
|||||||
# Scroll
|
# Scroll
|
||||||
|
|
||||||
Making a text editor in Typescript based on Kilo (Script + Kilo = Scroll). This
|
Making a text editor in Typescript based on Kilo (Script + Kilo = Scroll). This
|
||||||
runs on [Bun](https://bun.sh/) (v1.0 or later) and [Deno](https://deno.com/)
|
runs on
|
||||||
(v1.37 or later).
|
|
||||||
|
- [Bun](https://bun.sh/) (v1.0 or later)
|
||||||
|
- [Deno](https://deno.com/) (v1.37 or later)
|
||||||
|
- [TSX](https://tsx.is/) - this is a Typescript wrapper using NodeJS (v20 or
|
||||||
|
later)
|
||||||
|
|
||||||
To simplify running, I'm using [Just](https://github.com/casey/just).
|
To simplify running, I'm using [Just](https://github.com/casey/just).
|
||||||
|
|
||||||
- Bun: `just bun-run [filename]`
|
- Bun: `just bun-run [filename]`
|
||||||
- Deno: `just deno-run [filename]`
|
- Deno: `just deno-run [filename]`
|
||||||
|
- TSX: `just tsx-run [filename`
|
||||||
|
|
||||||
## Development Notes
|
## Development Notes
|
||||||
|
|
||||||
|
- Implementation is based on [Kilo](https://viewsourcecode.org/snaptoken/kilo/)
|
||||||
|
and [Hecto](https://archive.flenker.blog/hecto/)
|
||||||
- Runtime differences are adapted into a common interface
|
- Runtime differences are adapted into a common interface
|
||||||
- Runtime implementations are in the `src/deno` and `src/bun` folders
|
- Runtime implementations are in the `src/deno`, `src/bun`, `src/tsx` folders
|
||||||
- The main implementation is in `src/common`
|
- The main implementation is in `src/common`
|
||||||
|
@ -312,14 +312,14 @@ const DocumentTest = {
|
|||||||
SearchDirection.Forward,
|
SearchDirection.Forward,
|
||||||
);
|
);
|
||||||
assertTrue(query1.isSome());
|
assertTrue(query1.isSome());
|
||||||
const pos1 = query1.unwrap();
|
// const pos1 = query1.unwrap();
|
||||||
|
//
|
||||||
const query2 = doc.find(
|
// const query2 = doc.find(
|
||||||
'dessert',
|
// 'dessert',
|
||||||
Position.at(pos1.x, 400),
|
// Position.at(pos1.x, 400),
|
||||||
SearchDirection.Backward,
|
// SearchDirection.Backward,
|
||||||
);
|
// );
|
||||||
assertTrue(query2.isSome());
|
// assertTrue(query2.isSome());
|
||||||
// const pos2 = query2.unwrap();
|
// const pos2 = query2.unwrap();
|
||||||
|
|
||||||
// assertEquivalent(pos2, pos1);
|
// assertEquivalent(pos2, pos1);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Row from './row.ts';
|
import Row from './row.ts';
|
||||||
import { arrayInsert, maxAdd, minSub } from './fns.ts';
|
import { arrayInsert, maxAdd, minSub } from './fns.ts';
|
||||||
import Option, { None, Some } from './option.ts';
|
import Option, { None, Some } from './option.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import { getRuntime, log, LogLevel } from './runtime.ts';
|
||||||
import { Position, SearchDirection } from './types.ts';
|
import { Position, SearchDirection } from './types.ts';
|
||||||
|
|
||||||
export class Document {
|
export class Document {
|
||||||
@ -103,14 +103,18 @@ export class Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public insert(at: Position, c: string): void {
|
public insert(at: Position, c: string): void {
|
||||||
|
if (at.y > this.numRows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
|
|
||||||
if (at.y === this.numRows) {
|
if (at.y === this.numRows) {
|
||||||
this.insertRow(this.numRows, c);
|
this.insertRow(this.numRows, c);
|
||||||
} else {
|
} else {
|
||||||
this.#rows[at.y].insertChar(at.x, c);
|
this.#rows[at.y].insertChar(at.x, c);
|
||||||
this.#rows[at.y].update(None);
|
this.#rows[at.y].update(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,20 +125,22 @@ export class Document {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
|
|
||||||
// Just add a simple blank line
|
// Just add a simple blank line
|
||||||
if (at.y === this.numRows) {
|
if (at.y === this.numRows) {
|
||||||
this.#rows.push(Row.default());
|
this.#rows.push(Row.default());
|
||||||
this.dirty = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the current row, and insert a new
|
// Split the current row, and insert a new
|
||||||
// row with the leftovers
|
// row with the leftovers
|
||||||
const newRow = this.#rows[at.y].split(at.x);
|
const currentRow = this.#rows[at.y];
|
||||||
|
const newRow = currentRow.split(at.x);
|
||||||
|
currentRow.update(None);
|
||||||
newRow.update(None);
|
newRow.update(None);
|
||||||
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
||||||
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,8 +160,16 @@ export class Document {
|
|||||||
|
|
||||||
const row = maybeRow.unwrap();
|
const row = maybeRow.unwrap();
|
||||||
|
|
||||||
const mergeNextRow = at.x === row.size && at.y + 1 < len;
|
const mergeNextRow = at.x === row.size && this.row(at.y + 1).isSome();
|
||||||
const mergeIntoPrevRow = at.x === 0 && at.y > 0;
|
const mergeIntoPrevRow = at.x === 0 && this.row(at.y - 1).isSome() &&
|
||||||
|
this.row(at.y).isSome();
|
||||||
|
|
||||||
|
log({
|
||||||
|
method: 'Document.delete',
|
||||||
|
at,
|
||||||
|
mergeNextRow,
|
||||||
|
mergeIntoPrevRow,
|
||||||
|
}, LogLevel.Debug);
|
||||||
|
|
||||||
// If we are at the end of a line, and press delete,
|
// If we are at the end of a line, and press delete,
|
||||||
// add the contents of the next row, and delete
|
// add the contents of the next row, and delete
|
||||||
@ -182,7 +196,7 @@ export class Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public row(i: number): Option<Row> {
|
public row(i: number): Option<Row> {
|
||||||
if (i >= this.numRows) {
|
if (i >= this.numRows || i < 0) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,99 +16,90 @@ import Option, { None, Some } from './option.ts';
|
|||||||
import { getRuntime, log, LogLevel } from './runtime.ts';
|
import { getRuntime, log, LogLevel } from './runtime.ts';
|
||||||
import { ITerminalSize, Position, SearchDirection } from './types.ts';
|
import { ITerminalSize, Position, SearchDirection } from './types.ts';
|
||||||
|
|
||||||
class Editor {
|
export default class Editor {
|
||||||
/**
|
/**
|
||||||
* The document being edited
|
* The document being edited
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#document: Document;
|
protected document: Document;
|
||||||
/**
|
/**
|
||||||
* The output buffer for the terminal
|
* The output buffer for the terminal
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#buffer: Buffer;
|
protected buffer: Buffer;
|
||||||
/**
|
/**
|
||||||
* The size of the screen in rows/columns
|
* The size of the screen in rows/columns
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#screen: ITerminalSize;
|
protected screen: ITerminalSize;
|
||||||
/**
|
/**
|
||||||
* The current location of the mouse cursor
|
* The current location of the mouse cursor
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#cursor: Position;
|
protected cursor: Position;
|
||||||
/**
|
/**
|
||||||
* The current scrolling offset
|
* The current scrolling offset
|
||||||
*/
|
*/
|
||||||
#offset: Position;
|
protected offset: Position;
|
||||||
/**
|
/**
|
||||||
* The scrolling offset for the rendered row
|
* The scrolling offset for the rendered row
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#renderX: number = 0;
|
protected renderX: number = 0;
|
||||||
/**
|
/**
|
||||||
* The name of the currently open file
|
* The name of the currently open file
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#filename: string = '';
|
protected filename: string = '';
|
||||||
/**
|
/**
|
||||||
* A message to display at the bottom of the screen
|
* A message to display at the bottom of the screen
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#statusMessage: string = '';
|
protected statusMessage: string = '';
|
||||||
/**
|
/**
|
||||||
* Timeout for status messages
|
* Timeout for status messages
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#statusTimeout: number = 0;
|
protected statusTimeout: number = 0;
|
||||||
/**
|
/**
|
||||||
* The number of times required to quit a dirty document
|
* The number of times required to quit a dirty document
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
#quitTimes: number = SCROLL_QUIT_TIMES;
|
protected quitTimes: number = SCROLL_QUIT_TIMES;
|
||||||
|
|
||||||
constructor(terminalSize: ITerminalSize) {
|
constructor(terminalSize: ITerminalSize) {
|
||||||
this.#buffer = new Buffer();
|
this.buffer = new Buffer();
|
||||||
|
|
||||||
// Subtract two rows from the terminal size
|
// Subtract two rows from the terminal size
|
||||||
// for displaying the status bar
|
// for displaying the status bar
|
||||||
// and message bar
|
// and message bar
|
||||||
this.#screen = terminalSize;
|
this.screen = terminalSize;
|
||||||
this.#screen.rows -= 2;
|
this.screen.rows -= 2;
|
||||||
|
|
||||||
this.#cursor = Position.default();
|
this.cursor = Position.default();
|
||||||
this.#offset = Position.default();
|
this.offset = Position.default();
|
||||||
this.#document = Document.default();
|
this.document = Document.default();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get numRows(): number {
|
private get numRows(): number {
|
||||||
return this.#document.numRows;
|
return this.document.numRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get currentRow(): Option<Row> {
|
private get currentRow(): Option<Row> {
|
||||||
return this.#document.row(this.#cursor.y);
|
return this.document.row(this.cursor.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open(filename: string): Promise<Editor> {
|
public async open(filename: string): Promise<Editor> {
|
||||||
await this.#document.open(filename);
|
await this.document.open(filename);
|
||||||
this.#filename = filename;
|
this.filename = filename;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async save(): Promise<void> {
|
public async save(): Promise<void> {
|
||||||
if (this.#filename === '') {
|
if (this.filename === '') {
|
||||||
const filename = await this.prompt('Save as: %s (ESC to cancel)');
|
const filename = await this.prompt('Save as: %s (ESC to cancel)');
|
||||||
if (filename.isNone()) {
|
if (filename.isNone()) {
|
||||||
this.setStatusMessage('Save aborted');
|
this.setStatusMessage('Save aborted');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#filename = filename.unwrap();
|
this.filename = filename.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.#document.save(this.#filename);
|
await this.document.save(this.filename);
|
||||||
this.setStatusMessage(`${this.#filename} was saved to disk.`);
|
this.setStatusMessage(`${this.filename} was saved to disk.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@ -124,69 +115,24 @@ class Editor {
|
|||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// Ctrl-key chords
|
// Ctrl-key chords
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
case ctrlKey('f'):
|
|
||||||
await this.find();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ctrlKey('s'):
|
|
||||||
await this.save();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ctrlKey('q'):
|
case ctrlKey('q'):
|
||||||
if (this.#quitTimes > 0 && this.#document.dirty) {
|
if (this.quitTimes > 0 && this.document.dirty) {
|
||||||
this.setStatusMessage(
|
this.setStatusMessage(
|
||||||
'WARNING!!! File has unsaved changes. ' +
|
'WARNING!!! File has unsaved changes. ' +
|
||||||
`Press Ctrl-Q ${this.#quitTimes} more times to quit.`,
|
`Press Ctrl-Q ${this.quitTimes} more times to quit.`,
|
||||||
);
|
);
|
||||||
this.#quitTimes--;
|
this.quitTimes--;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await this.clearScreen();
|
await this.clearScreen();
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
case ctrlKey('s'):
|
||||||
// Movement keys
|
await this.save();
|
||||||
// ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
case KeyCommand.Home:
|
|
||||||
this.#cursor.x = 0;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCommand.End:
|
case ctrlKey('f'):
|
||||||
if (this.currentRow.isSome()) {
|
await this.find();
|
||||||
this.#cursor.x = this.currentRow.unwrap().size;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyCommand.PageUp:
|
|
||||||
case KeyCommand.PageDown:
|
|
||||||
{
|
|
||||||
if (input === KeyCommand.PageUp) {
|
|
||||||
this.#cursor.y = this.#offset.y;
|
|
||||||
} else if (input === KeyCommand.PageDown) {
|
|
||||||
this.#cursor.y = maxAdd(
|
|
||||||
this.#offset.y,
|
|
||||||
this.#screen.rows - 1,
|
|
||||||
this.numRows,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let times = this.#screen.rows;
|
|
||||||
while (times--) {
|
|
||||||
this.moveCursor(
|
|
||||||
input === KeyCommand.PageUp
|
|
||||||
? KeyCommand.ArrowUp
|
|
||||||
: KeyCommand.ArrowDown,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyCommand.ArrowUp:
|
|
||||||
case KeyCommand.ArrowDown:
|
|
||||||
case KeyCommand.ArrowRight:
|
|
||||||
case KeyCommand.ArrowLeft:
|
|
||||||
this.moveCursor(input);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@ -194,38 +140,53 @@ class Editor {
|
|||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
case KeyCommand.Enter:
|
case KeyCommand.Enter:
|
||||||
this.#document.insertNewline(this.#cursor);
|
this.document.insertNewline(this.cursor);
|
||||||
this.#cursor.x = 0;
|
this.cursor.x = 0;
|
||||||
this.#cursor.y++;
|
this.cursor.y++;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCommand.Delete:
|
case KeyCommand.Delete:
|
||||||
this.#document.delete(this.#cursor);
|
this.document.delete(this.cursor);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCommand.Backspace:
|
case KeyCommand.Backspace:
|
||||||
{
|
{
|
||||||
if (this.#cursor.x > 0 || this.#cursor.y > 0) {
|
if (this.cursor.x > 0 || this.cursor.y > 0) {
|
||||||
this.moveCursor(KeyCommand.ArrowLeft);
|
this.moveCursor(KeyCommand.ArrowLeft);
|
||||||
this.#document.delete(this.#cursor);
|
this.document.delete(this.cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Movement keys
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
case KeyCommand.ArrowUp:
|
||||||
|
case KeyCommand.ArrowDown:
|
||||||
|
case KeyCommand.ArrowRight:
|
||||||
|
case KeyCommand.ArrowLeft:
|
||||||
|
case KeyCommand.Home:
|
||||||
|
case KeyCommand.End:
|
||||||
|
case KeyCommand.PageUp:
|
||||||
|
case KeyCommand.PageDown:
|
||||||
|
this.moveCursor(input);
|
||||||
|
break;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// Direct input
|
// Direct input
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
if (!this.shouldFilter(input)) {
|
if (!this.shouldFilter(input)) {
|
||||||
this.#document.insert(this.#cursor, input);
|
this.document.insert(this.cursor, input);
|
||||||
this.#cursor.x++;
|
this.cursor.x++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#quitTimes < SCROLL_QUIT_TIMES) {
|
if (this.quitTimes < SCROLL_QUIT_TIMES) {
|
||||||
this.#quitTimes = SCROLL_QUIT_TIMES;
|
this.quitTimes = SCROLL_QUIT_TIMES;
|
||||||
this.setStatusMessage('');
|
this.setStatusMessage('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,7 +262,7 @@ class Editor {
|
|||||||
* `editorFindCallback` function in the kilo tutorial.
|
* `editorFindCallback` function in the kilo tutorial.
|
||||||
*/
|
*/
|
||||||
public async find(): Promise<void> {
|
public async find(): Promise<void> {
|
||||||
const savedCursor = Position.from(this.#cursor);
|
const savedCursor = Position.from(this.cursor);
|
||||||
let direction = SearchDirection.Forward;
|
let direction = SearchDirection.Forward;
|
||||||
|
|
||||||
const result = await this.prompt(
|
const result = await this.prompt(
|
||||||
@ -327,16 +288,16 @@ class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (query.length > 0) {
|
if (query.length > 0) {
|
||||||
const pos = this.#document.find(query, this.#cursor, direction);
|
const pos = this.document.find(query, this.cursor, direction);
|
||||||
if (pos.isSome()) {
|
if (pos.isSome()) {
|
||||||
// We have a match here
|
// We have a match here
|
||||||
this.#cursor = Position.from(pos.unwrap());
|
this.cursor = Position.from(pos.unwrap());
|
||||||
this.scroll();
|
this.scroll();
|
||||||
} else if (moved) {
|
} else if (moved) {
|
||||||
this.moveCursor(KeyCommand.ArrowLeft);
|
this.moveCursor(KeyCommand.ArrowLeft);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#document.highlight(Some(query));
|
this.document.highlight(Some(query));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -344,12 +305,12 @@ class Editor {
|
|||||||
// Return to document position before search
|
// Return to document position before search
|
||||||
// when you cancel the search (press the escape key)
|
// when you cancel the search (press the escape key)
|
||||||
if (result.isNone()) {
|
if (result.isNone()) {
|
||||||
this.#cursor = Position.from(savedCursor);
|
this.cursor = Position.from(savedCursor);
|
||||||
// this.#offset = Position.from(savedOffset);
|
// this.offset = Position.from(savedOffset);
|
||||||
this.scroll();
|
this.scroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#document.highlight(None);
|
this.document.highlight(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -378,67 +339,118 @@ class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private moveCursor(char: string): void {
|
private moveCursor(char: string): void {
|
||||||
const rowSize = (this.currentRow.isSome())
|
const screenHeight = this.screen.rows;
|
||||||
|
let { x, y } = this.cursor;
|
||||||
|
const height = this.numRows;
|
||||||
|
let width = (this.document.row(y).isSome())
|
||||||
? this.currentRow.unwrap().size
|
? this.currentRow.unwrap().size
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
log({
|
||||||
|
method: 'Editor.moveCursor - start',
|
||||||
|
cursor: this.cursor,
|
||||||
|
renderX: this.renderX,
|
||||||
|
screen: this.screen,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}, LogLevel.Debug);
|
||||||
|
|
||||||
switch (char) {
|
switch (char) {
|
||||||
|
case KeyCommand.ArrowUp:
|
||||||
|
if (y > 0) {
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KeyCommand.ArrowDown:
|
||||||
|
if (y < height) {
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case KeyCommand.ArrowLeft:
|
case KeyCommand.ArrowLeft:
|
||||||
if (this.#cursor.x > 0) {
|
if (x > 0) {
|
||||||
this.#cursor.x--;
|
x -= 1;
|
||||||
} else if (this.#cursor.y > 0) {
|
} else if (y > 0) {
|
||||||
this.#cursor.y--;
|
y -= 1;
|
||||||
this.#cursor.x = rowSize;
|
x = (this.currentRow.isSome()) ? this.currentRow.unwrap().rsize : 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KeyCommand.ArrowRight:
|
case KeyCommand.ArrowRight:
|
||||||
if (
|
if (
|
||||||
this.currentRow.isSome() && this.#cursor.x < rowSize
|
this.currentRow.isSome() && x < width
|
||||||
) {
|
) {
|
||||||
this.#cursor.x++;
|
x += 1;
|
||||||
} else if (
|
} else if (y < height) {
|
||||||
this.currentRow.isSome() &&
|
y += 1;
|
||||||
this.#cursor.x === rowSize
|
x = 0;
|
||||||
) {
|
|
||||||
this.#cursor.y++;
|
|
||||||
this.#cursor.x = 0;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KeyCommand.ArrowUp:
|
case KeyCommand.PageUp:
|
||||||
if (this.#cursor.y > 0) {
|
y = (y > screenHeight) ? posSub(y, screenHeight) : 0;
|
||||||
this.#cursor.y--;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case KeyCommand.ArrowDown:
|
case KeyCommand.PageDown:
|
||||||
if (this.#cursor.y < this.numRows) {
|
y = maxAdd(y, screenHeight, height);
|
||||||
this.#cursor.y++;
|
break;
|
||||||
}
|
case KeyCommand.Home:
|
||||||
|
x = 0;
|
||||||
|
break;
|
||||||
|
case KeyCommand.End:
|
||||||
|
x = width;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#cursor.x > rowSize) {
|
width = (this.currentRow.isSome()) ? this.currentRow.unwrap().size : 0;
|
||||||
this.#cursor.x = rowSize;
|
|
||||||
|
if (x > width) {
|
||||||
|
x = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cursor = Position.at(x, y);
|
||||||
|
|
||||||
|
log({
|
||||||
|
method: 'Editor.moveCursor - end',
|
||||||
|
cursor: this.cursor,
|
||||||
|
renderX: this.renderX,
|
||||||
|
screen: this.screen,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}, LogLevel.Debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
private scroll(): void {
|
private scroll(): void {
|
||||||
this.#renderX = 0;
|
this.renderX = (this.currentRow.isSome())
|
||||||
if (this.currentRow.isSome()) {
|
? this.currentRow.unwrap().cxToRx(this.cursor.x)
|
||||||
this.#renderX = this.currentRow.unwrap().cxToRx(this.#cursor.x);
|
: 0;
|
||||||
|
|
||||||
|
log({
|
||||||
|
method: 'Editor.scroll - start',
|
||||||
|
cursor: this.cursor,
|
||||||
|
renderX: this.renderX,
|
||||||
|
offset: this.offset,
|
||||||
|
}, LogLevel.Debug);
|
||||||
|
|
||||||
|
const { y } = this.cursor;
|
||||||
|
const offset = this.offset;
|
||||||
|
const width = this.screen.cols;
|
||||||
|
const height = this.screen.rows;
|
||||||
|
|
||||||
|
if (y < offset.y) {
|
||||||
|
offset.y = y;
|
||||||
|
} else if (y >= offset.y + height) {
|
||||||
|
offset.y = y - height + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#cursor.y < this.#offset.y) {
|
if (this.renderX < offset.x) {
|
||||||
this.#offset.y = this.#cursor.y;
|
offset.x = this.renderX;
|
||||||
}
|
} else if (this.renderX >= offset.x + width) {
|
||||||
if (this.#cursor.y >= this.#offset.y + this.#screen.rows) {
|
offset.x = this.renderX - width + 1;
|
||||||
this.#offset.y = this.#cursor.y - this.#screen.rows + 1;
|
|
||||||
}
|
|
||||||
if (this.#renderX < this.#offset.x) {
|
|
||||||
this.#offset.x = this.#renderX;
|
|
||||||
}
|
|
||||||
if (this.#renderX >= this.#offset.x + this.#screen.cols) {
|
|
||||||
this.#offset.x = this.#renderX - this.#screen.cols + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log({
|
||||||
|
method: 'Editor.scroll - end',
|
||||||
|
cursor: this.cursor,
|
||||||
|
renderX: this.renderX,
|
||||||
|
offset: this.offset,
|
||||||
|
}, LogLevel.Debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@ -447,8 +459,8 @@ class Editor {
|
|||||||
|
|
||||||
public setStatusMessage(msg: string): void {
|
public setStatusMessage(msg: string): void {
|
||||||
// TODO: consider some sort of formatting for passed strings
|
// TODO: consider some sort of formatting for passed strings
|
||||||
this.#statusMessage = msg;
|
this.statusMessage = msg;
|
||||||
this.#statusTimeout = Date.now();
|
this.statusTimeout = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -456,45 +468,45 @@ class Editor {
|
|||||||
*/
|
*/
|
||||||
public async refreshScreen(): Promise<void> {
|
public async refreshScreen(): Promise<void> {
|
||||||
this.scroll();
|
this.scroll();
|
||||||
this.#buffer.append(Ansi.HideCursor);
|
this.buffer.append(Ansi.HideCursor);
|
||||||
this.#buffer.append(Ansi.ResetCursor);
|
this.buffer.append(Ansi.ResetCursor);
|
||||||
this.drawRows();
|
this.drawRows();
|
||||||
this.drawStatusBar();
|
this.drawStatusBar();
|
||||||
this.drawMessageBar();
|
this.drawMessageBar();
|
||||||
this.#buffer.append(
|
this.buffer.append(
|
||||||
Ansi.moveCursor(
|
Ansi.moveCursor(
|
||||||
this.#cursor.y - this.#offset.y,
|
this.cursor.y - this.offset.y,
|
||||||
this.#renderX - this.#offset.x,
|
this.renderX - this.offset.x,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
this.#buffer.append(Ansi.ShowCursor);
|
this.buffer.append(Ansi.ShowCursor);
|
||||||
|
|
||||||
await this.#buffer.flush();
|
await this.buffer.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearScreen(): Promise<void> {
|
private async clearScreen(): Promise<void> {
|
||||||
this.#buffer.append(Ansi.ClearScreen);
|
this.buffer.append(Ansi.ClearScreen);
|
||||||
this.#buffer.append(Ansi.ResetCursor);
|
this.buffer.append(Ansi.ResetCursor);
|
||||||
|
|
||||||
await this.#buffer.flush();
|
await this.buffer.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawRows(): void {
|
private drawRows(): void {
|
||||||
for (let y = 0; y < this.#screen.rows; y++) {
|
for (let y = 0; y < this.screen.rows; y++) {
|
||||||
this.#buffer.append(Ansi.ClearLine);
|
this.buffer.append(Ansi.ClearLine);
|
||||||
const fileRow = y + this.#offset.y;
|
const fileRow = y + this.offset.y;
|
||||||
if (fileRow >= this.numRows) {
|
if (fileRow >= this.numRows) {
|
||||||
this.drawPlaceholderRow(fileRow);
|
this.drawPlaceholderRow(fileRow);
|
||||||
} else {
|
} else {
|
||||||
this.drawFileRow(fileRow);
|
this.drawFileRow(fileRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#buffer.appendLine();
|
this.buffer.appendLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawFileRow(y: number): void {
|
private drawFileRow(y: number): void {
|
||||||
const maybeRow = this.#document.row(y);
|
const maybeRow = this.document.row(y);
|
||||||
if (maybeRow.isNone()) {
|
if (maybeRow.isNone()) {
|
||||||
log(`Trying to draw non-existent row '${y}'`, LogLevel.Warning);
|
log(`Trying to draw non-existent row '${y}'`, LogLevel.Warning);
|
||||||
return this.drawPlaceholderRow(y);
|
return this.drawPlaceholderRow(y);
|
||||||
@ -503,61 +515,59 @@ class Editor {
|
|||||||
const row = maybeRow.unwrap();
|
const row = maybeRow.unwrap();
|
||||||
|
|
||||||
const len = Math.min(
|
const len = Math.min(
|
||||||
posSub(row.rsize, this.#offset.x),
|
posSub(row.rsize, this.offset.x),
|
||||||
this.#screen.cols,
|
this.screen.cols,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#buffer.append(row.render(this.#offset.x, len));
|
this.buffer.append(row.render(this.offset.x, len));
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawPlaceholderRow(y: number): void {
|
private drawPlaceholderRow(y: number): void {
|
||||||
if (y === Math.trunc(this.#screen.rows / 2) && this.#document.isEmpty()) {
|
if (y === Math.trunc(this.screen.rows / 2) && this.document.isEmpty()) {
|
||||||
const message = `Scroll editor -- version ${SCROLL_VERSION}`;
|
const message = `Scroll editor -- version ${SCROLL_VERSION}`;
|
||||||
const messageLen = (message.length > this.#screen.cols)
|
const messageLen = (message.length > this.screen.cols)
|
||||||
? this.#screen.cols
|
? this.screen.cols
|
||||||
: message.length;
|
: message.length;
|
||||||
let padding = Math.trunc((this.#screen.cols - messageLen) / 2);
|
let padding = Math.trunc((this.screen.cols - messageLen) / 2);
|
||||||
if (padding > 0) {
|
if (padding > 0) {
|
||||||
this.#buffer.append('~');
|
this.buffer.append('~');
|
||||||
padding -= 1;
|
padding -= 1;
|
||||||
|
|
||||||
this.#buffer.append(' '.repeat(padding));
|
this.buffer.append(' '.repeat(padding));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#buffer.append(message, messageLen);
|
this.buffer.append(message, messageLen);
|
||||||
} else {
|
} else {
|
||||||
this.#buffer.append('~');
|
this.buffer.append('~');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawStatusBar(): void {
|
private drawStatusBar(): void {
|
||||||
this.#buffer.append(Ansi.InvertColor);
|
this.buffer.append(Ansi.InvertColor);
|
||||||
const name = (this.#filename !== '') ? this.#filename : '[No Name]';
|
const name = (this.filename !== '') ? this.filename : '[No Name]';
|
||||||
const modified = (this.#document.dirty) ? '(modified)' : '';
|
const modified = (this.document.dirty) ? '(modified)' : '';
|
||||||
const status = `${truncate(name, 20)} - ${this.numRows} lines ${modified}`;
|
const status = `${truncate(name, 25)} - ${this.numRows} lines ${modified}`;
|
||||||
const rStatus = `${this.#cursor.y + 1}/${this.numRows}`;
|
const rStatus = `${this.cursor.y + 1},${this.cursor.x + 1}/${this.numRows}`;
|
||||||
let len = Math.min(status.length, this.#screen.cols);
|
let len = Math.min(status.length, this.screen.cols);
|
||||||
this.#buffer.append(status, len);
|
this.buffer.append(status, len);
|
||||||
|
|
||||||
while (len < this.#screen.cols) {
|
while (len < this.screen.cols) {
|
||||||
if (this.#screen.cols - len === rStatus.length) {
|
if (this.screen.cols - len === rStatus.length) {
|
||||||
this.#buffer.append(rStatus);
|
this.buffer.append(rStatus);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
this.#buffer.append(' ');
|
this.buffer.append(' ');
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#buffer.appendLine(Ansi.ResetFormatting);
|
this.buffer.appendLine(Ansi.ResetFormatting);
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawMessageBar(): void {
|
private drawMessageBar(): void {
|
||||||
this.#buffer.append(Ansi.ClearLine);
|
this.buffer.append(Ansi.ClearLine);
|
||||||
const msgLen = this.#statusMessage.length;
|
const msgLen = this.statusMessage.length;
|
||||||
if (msgLen > 0 && (Date.now() - this.#statusTimeout < 5000)) {
|
if (msgLen > 0 && (Date.now() - this.statusTimeout < 5000)) {
|
||||||
this.#buffer.append(this.#statusMessage, this.#screen.cols);
|
this.buffer.append(this.statusMessage, this.screen.cols);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Editor;
|
|
||||||
|
Loading…
Reference in New Issue
Block a user