scroll/src/common/editor.ts

189 lines
4.2 KiB
JavaScript

import Ansi, { KeyCommand } from './ansi.ts';
import Buffer from './buffer.ts';
import Document from './document.ts';
import { IPoint, ITerminalSize, VERSION } from './mod.ts';
import { ctrl_key } from './utils.ts';
export class Editor {
/**
* The output buffer for the terminal
* @private
*/
#buffer: Buffer;
/**
* The size of the screen in rows/columns
* @private
*/
#screen: ITerminalSize;
/**
* The current location of the mouse cursor
* @private
*/
#cursor: IPoint;
/**
* The document being edited
* @private
*/
#document: Document;
constructor(terminalSize: ITerminalSize) {
this.#buffer = new Buffer();
this.#screen = terminalSize;
this.#cursor = {
x: 0,
y: 0,
};
this.#document = Document.empty();
}
public async open(filename: string): Promise<Editor> {
await this.#document.open(filename);
return this;
}
// --------------------------------------------------------------------------
// Command/input mapping
// --------------------------------------------------------------------------
/**
* Determine what to do based on input
* @param input - the decoded chunk of stdin
*/
public processKeyPress(input: string): boolean {
switch (input) {
case ctrl_key('q'):
this.clearScreen().then(() => {});
return false;
case KeyCommand.Home:
this.#cursor.x = 0;
break;
case KeyCommand.End:
this.#cursor.x = this.#screen.cols - 1;
break;
case KeyCommand.PageUp:
case KeyCommand.PageDown:
{
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;
}
return true;
}
private moveCursor(char: string): void {
switch (char) {
case KeyCommand.ArrowLeft:
if (this.#cursor.x > 0) {
this.#cursor.x--;
}
break;
case KeyCommand.ArrowRight:
if (this.#cursor.x < this.#screen.cols) {
this.#cursor.x++;
}
break;
case KeyCommand.ArrowUp:
if (this.#cursor.y > 0) {
this.#cursor.y--;
}
break;
case KeyCommand.ArrowDown:
if (this.#cursor.y < this.#screen.rows) {
this.#cursor.y++;
}
break;
}
}
// --------------------------------------------------------------------------
// Terminal Output / Drawing
// --------------------------------------------------------------------------
/**
* Clear the screen and write out the buffer
*/
public async refreshScreen(): Promise<void> {
this.#buffer.append(Ansi.HideCursor);
this.#buffer.append(Ansi.ResetCursor);
this.drawRows();
this.#buffer.append(
Ansi.moveCursor(this.#cursor.y, this.#cursor.x),
);
this.#buffer.append(Ansi.ShowCursor);
await this.#buffer.flush();
}
private async clearScreen(): Promise<void> {
this.#buffer.append(Ansi.ClearScreen);
this.#buffer.append(Ansi.ResetCursor);
await this.#buffer.flush();
}
private drawRows(): void {
for (let y = 0; y < this.#screen.rows; y++) {
if (this.#document.numrows < y) {
this.drawPlaceholderRow(y);
} else {
this.drawFileRow(y);
}
}
}
private drawFileRow(y: number): void {
const row = this.#document.row(y);
let len = row?.chars.length ?? 0;
if (len > this.#screen.cols) {
len = this.#screen.cols;
}
this.#buffer.append(row!.toString(), len);
this.#buffer.appendLine(Ansi.ClearLine);
}
private drawPlaceholderRow(y: number): void {
if (y === Math.trunc(this.#screen.rows / 2) && this.#document.isEmpty()) {
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) {
this.#buffer.append('~');
padding -= 1;
this.#buffer.append(' '.repeat(padding));
}
this.#buffer.append(message, messageLen);
} else {
this.#buffer.append('~');
}
this.#buffer.append(Ansi.ClearLine);
if (y < this.#screen.rows - 1) {
this.#buffer.appendLine();
}
}
}