scroll/src/common/editor.ts

304 lines
6.8 KiB
JavaScript
Raw Normal View History

import Ansi, { KeyCommand } from './ansi.ts';
2023-11-08 18:07:34 -05:00
import Buffer from './buffer.ts';
2023-11-20 14:21:42 -05:00
import Document, { Row } from './document.ts';
2023-11-20 15:39:27 -05:00
import {
IPoint,
ITerminalSize,
logToFile,
maxAdd,
truncate,
VERSION,
} from './mod.ts';
2023-11-20 14:21:42 -05:00
import { ctrlKey, posSub } from './utils.ts';
2023-11-08 11:11:19 -05:00
export class Editor {
/**
* The output buffer for the terminal
* @private
*/
2023-11-08 11:11:19 -05:00
#buffer: Buffer;
/**
* The size of the screen in rows/columns
* @private
*/
2023-11-10 08:36:18 -05:00
#screen: ITerminalSize;
/**
* The current location of the mouse cursor
* @private
*/
2023-11-10 08:36:18 -05:00
#cursor: IPoint;
2023-11-20 11:12:22 -05:00
/**
* The current scrolling offset
*/
#offset: IPoint;
/**
* The document being edited
* @private
*/
2023-11-13 14:46:04 -05:00
#document: Document;
/**
* The scrolling offset for the rendered row
* @private
*/
#render: IPoint;
2023-11-20 15:39:27 -05:00
/**
* The name of the currently open file
* @private
*/
#filename: string;
2023-11-20 14:21:42 -05:00
constructor(terminalSize: ITerminalSize) {
2023-11-08 11:11:19 -05:00
this.#buffer = new Buffer();
2023-11-10 08:36:18 -05:00
this.#screen = terminalSize;
2023-11-20 15:39:27 -05:00
this.#screen.rows -= 1;
2023-11-10 08:36:18 -05:00
this.#cursor = {
x: 0,
y: 0,
};
2023-11-20 11:12:22 -05:00
this.#offset = {
x: 0,
y: 0,
};
this.#render = {
x: 0,
y: 0,
};
this.#document = Document.empty();
2023-11-20 15:39:27 -05:00
this.#filename = '';
}
private get currentRow(): Row | null {
return this.#document.row(this.#cursor.y);
}
public async open(filename: string): Promise<Editor> {
await this.#document.open(filename);
2023-11-20 15:39:27 -05:00
this.#filename = filename;
return this;
2023-11-08 11:11:19 -05:00
}
2023-11-10 08:36:18 -05:00
// --------------------------------------------------------------------------
// Command/input mapping
// --------------------------------------------------------------------------
2023-11-08 11:11:19 -05:00
/**
* Determine what to do based on input
* @param input - the decoded chunk of stdin
*/
public processKeyPress(input: string): boolean {
switch (input) {
case ctrlKey('q'):
2023-11-09 12:32:41 -05:00
this.clearScreen().then(() => {});
2023-11-08 11:11:19 -05:00
return false;
case KeyCommand.Home:
this.#cursor.x = 0;
break;
case KeyCommand.End:
if (this.currentRow !== null) {
this.#cursor.x = this.currentRow.size - 1;
}
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.#document.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;
2023-11-10 08:36:18 -05:00
}
return true;
}
private moveCursor(char: string): void {
switch (char) {
case KeyCommand.ArrowLeft:
if (this.#cursor.x > 0) {
this.#cursor.x--;
2023-11-20 14:21:42 -05:00
} else if (this.#cursor.y > 0) {
this.#cursor.y--;
this.#cursor.x = (this.currentRow !== null)
? this.currentRow.size - 1
: 0;
}
2023-11-10 08:36:18 -05:00
break;
case KeyCommand.ArrowRight:
2023-11-20 14:21:42 -05:00
if (
this.currentRow !== null && this.#cursor.x < this.currentRow.size - 1
) {
this.#cursor.x++;
2023-11-20 14:21:42 -05:00
} else if (
this.currentRow !== null &&
this.#cursor.x === this.currentRow.size - 1
) {
this.#cursor.y++;
this.#cursor.x = 0;
}
2023-11-10 08:36:18 -05:00
break;
case KeyCommand.ArrowUp:
if (this.#cursor.y > 0) {
this.#cursor.y--;
}
2023-11-10 08:36:18 -05:00
break;
case KeyCommand.ArrowDown:
2023-11-20 14:21:42 -05:00
if (this.#cursor.y < this.#document.numRows) {
this.#cursor.y++;
}
2023-11-10 08:36:18 -05:00
break;
2023-11-08 11:11:19 -05:00
}
2023-11-20 14:21:42 -05:00
const rowLen = this.currentRow?.size ?? 0;
if (this.#cursor.x > rowLen) {
this.#cursor.x = rowLen;
}
2023-11-08 11:11:19 -05:00
}
2023-11-20 11:12:22 -05:00
private scroll(): void {
this.#render.x = 0;
if (this.currentRow !== null) {
this.#render.x = this.currentRow.cxToRx(this.#cursor.x);
}
2023-11-20 11:12:22 -05:00
if (this.#cursor.y < this.#offset.y) {
this.#offset.y = this.#cursor.y;
}
if (this.#cursor.y >= this.#offset.y + this.#screen.rows) {
this.#offset.y = this.#cursor.y - this.#screen.rows + 1;
}
if (this.#render.x < this.#offset.x) {
this.#offset.x = this.#render.x;
2023-11-20 14:21:42 -05:00
}
if (this.#render.x >= this.#offset.x + this.#screen.cols) {
this.#offset.x = this.#render.x - this.#screen.cols + 1;
2023-11-20 14:21:42 -05:00
}
2023-11-20 11:12:22 -05:00
}
2023-11-10 08:36:18 -05:00
// --------------------------------------------------------------------------
2023-11-09 12:05:30 -05:00
// Terminal Output / Drawing
2023-11-10 08:36:18 -05:00
// --------------------------------------------------------------------------
2023-11-09 12:05:30 -05:00
2023-11-08 11:11:19 -05:00
/**
* Clear the screen and write out the buffer
*/
public async refreshScreen(): Promise<void> {
2023-11-20 11:12:22 -05:00
this.scroll();
2023-11-09 12:05:30 -05:00
this.#buffer.append(Ansi.HideCursor);
this.#buffer.append(Ansi.ResetCursor);
this.drawRows();
2023-11-20 15:39:27 -05:00
this.drawStatusBar();
2023-11-10 08:36:18 -05:00
this.#buffer.append(
2023-11-20 14:21:42 -05:00
Ansi.moveCursor(
this.#cursor.y - this.#offset.y,
this.#render.x - this.#offset.x,
2023-11-20 14:21:42 -05:00
),
2023-11-10 08:36:18 -05:00
);
2023-11-09 12:05:30 -05:00
this.#buffer.append(Ansi.ShowCursor);
await this.#buffer.flush();
}
2023-11-08 11:11:19 -05:00
2023-11-09 12:32:41 -05:00
private async clearScreen(): Promise<void> {
this.#buffer.append(Ansi.ClearScreen);
this.#buffer.append(Ansi.ResetCursor);
await this.#buffer.flush();
}
2023-11-09 12:05:30 -05:00
private drawRows(): void {
2023-11-13 14:46:04 -05:00
for (let y = 0; y < this.#screen.rows; y++) {
this.#buffer.append(Ansi.ClearLine);
2023-11-20 14:21:42 -05:00
const filerow = y + this.#offset.y;
2023-11-20 11:12:22 -05:00
if (filerow >= this.#document.numRows) {
2023-11-20 14:21:42 -05:00
this.drawPlaceholderRow(filerow);
2023-11-13 14:46:04 -05:00
} else {
2023-11-20 14:21:42 -05:00
this.drawFileRow(filerow);
}
2023-11-20 15:39:27 -05:00
this.#buffer.appendLine();
2023-11-13 14:46:04 -05:00
}
}
private drawFileRow(y: number): void {
const row = this.#document.row(y);
2023-11-16 16:00:03 -05:00
if (row === null) {
logToFile(`Warning: trying to draw non-existent row '${y}'`);
return this.drawPlaceholderRow(y);
}
2023-11-20 14:21:42 -05:00
const len = Math.min(
posSub(row.rsize, this.#offset.x),
2023-11-20 14:21:42 -05:00
this.#screen.cols,
);
this.#buffer.append(row.rstring(this.#offset.x), len);
2023-11-13 14:46:04 -05:00
}
private drawPlaceholderRow(y: number): void {
if (y === Math.trunc(this.#screen.rows / 2) && this.#document.isEmpty()) {
2023-11-13 14:46:04 -05:00
const message = `Kilo editor -- version ${VERSION}`;
const messageLen = (message.length > this.#screen.cols)
? this.#screen.cols
: message.length;
let padding = Math.trunc((this.#screen.cols - messageLen) / 2);
if (padding > 0) {
2023-11-09 12:05:30 -05:00
this.#buffer.append('~');
2023-11-13 14:46:04 -05:00
padding -= 1;
2023-11-09 12:05:30 -05:00
2023-11-13 14:46:04 -05:00
this.#buffer.append(' '.repeat(padding));
2023-11-09 12:05:30 -05:00
}
2023-11-13 14:46:04 -05:00
this.#buffer.append(message, messageLen);
2023-11-13 14:46:04 -05:00
} else {
this.#buffer.append('~');
}
}
2023-11-20 15:39:27 -05:00
private drawStatusBar(): void {
this.#buffer.append(Ansi.InvertColor);
const name = (this.#filename !== '') ? this.#filename : '[No Name]';
const status = `${truncate(name, 20)} - ${this.#document.numRows} lines`;
const rstatus = `${this.#cursor.y + 1}/${this.#document.numRows}`;
let len = Math.min(status.length, this.#screen.cols);
this.#buffer.append(status, len);
while (len < this.#screen.cols) {
if (this.#screen.cols - len === rstatus.length) {
this.#buffer.append(rstatus);
break;
} else {
this.#buffer.append(' ');
len++;
}
}
this.#buffer.append(Ansi.ResetFormatting);
}
}