2023-11-10 18:22:09 -05:00
|
|
|
import Ansi, { KeyCommand } from './ansi.ts';
|
2023-11-08 18:07:34 -05:00
|
|
|
import Buffer from './buffer.ts';
|
2023-11-13 14:46:04 -05:00
|
|
|
import Document from './document.ts';
|
2023-11-14 15:53:45 -05:00
|
|
|
import { IPoint, ITerminalSize, VERSION } from './mod.ts';
|
|
|
|
import { ctrl_key } from './utils.ts';
|
2023-11-08 11:11:19 -05:00
|
|
|
|
2023-11-03 12:26:09 -04:00
|
|
|
export class Editor {
|
2023-11-14 15:53:45 -05:00
|
|
|
/**
|
|
|
|
* The output buffer for the terminal
|
|
|
|
* @private
|
|
|
|
*/
|
2023-11-08 11:11:19 -05:00
|
|
|
#buffer: Buffer;
|
2023-11-14 15:53:45 -05:00
|
|
|
/**
|
|
|
|
* The size of the screen in rows/columns
|
|
|
|
* @private
|
|
|
|
*/
|
2023-11-10 08:36:18 -05:00
|
|
|
#screen: ITerminalSize;
|
2023-11-14 15:53:45 -05:00
|
|
|
/**
|
|
|
|
* The current location of the mouse cursor
|
|
|
|
* @private
|
|
|
|
*/
|
2023-11-10 08:36:18 -05:00
|
|
|
#cursor: IPoint;
|
2023-11-14 15:53:45 -05:00
|
|
|
/**
|
|
|
|
* The document being edited
|
|
|
|
* @private
|
|
|
|
*/
|
2023-11-13 14:46:04 -05:00
|
|
|
#document: Document;
|
2023-11-14 15:53:45 -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;
|
|
|
|
this.#cursor = {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
};
|
2023-11-14 15:53:45 -05:00
|
|
|
|
|
|
|
this.#document = Document.empty();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async open(filename: string): Promise<Editor> {
|
|
|
|
await this.#document.open(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 ctrl_key('q'):
|
2023-11-09 12:32:41 -05:00
|
|
|
this.clearScreen().then(() => {});
|
2023-11-08 11:11:19 -05:00
|
|
|
return false;
|
|
|
|
|
2023-11-10 19:17:36 -05:00
|
|
|
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;
|
|
|
|
|
2023-11-10 18:22:09 -05:00
|
|
|
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) {
|
2023-11-10 18:22:09 -05:00
|
|
|
case KeyCommand.ArrowLeft:
|
2023-11-10 19:17:36 -05:00
|
|
|
if (this.#cursor.x > 0) {
|
|
|
|
this.#cursor.x--;
|
|
|
|
}
|
2023-11-10 08:36:18 -05:00
|
|
|
break;
|
2023-11-10 18:22:09 -05:00
|
|
|
case KeyCommand.ArrowRight:
|
2023-11-10 19:17:36 -05:00
|
|
|
if (this.#cursor.x < this.#screen.cols) {
|
|
|
|
this.#cursor.x++;
|
|
|
|
}
|
2023-11-10 08:36:18 -05:00
|
|
|
break;
|
2023-11-10 18:22:09 -05:00
|
|
|
case KeyCommand.ArrowUp:
|
2023-11-10 19:17:36 -05:00
|
|
|
if (this.#cursor.y > 0) {
|
|
|
|
this.#cursor.y--;
|
|
|
|
}
|
2023-11-10 08:36:18 -05:00
|
|
|
break;
|
2023-11-10 18:22:09 -05:00
|
|
|
case KeyCommand.ArrowDown:
|
2023-11-10 19:17:36 -05:00
|
|
|
if (this.#cursor.y < this.#screen.rows) {
|
|
|
|
this.#cursor.y++;
|
|
|
|
}
|
2023-11-10 08:36:18 -05:00
|
|
|
break;
|
2023-11-08 11:11:19 -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-09 12:05:30 -05:00
|
|
|
this.#buffer.append(Ansi.HideCursor);
|
|
|
|
this.#buffer.append(Ansi.ResetCursor);
|
2023-11-08 17:02:59 -05:00
|
|
|
this.drawRows();
|
2023-11-10 08:36:18 -05:00
|
|
|
this.#buffer.append(
|
2023-11-10 18:22:09 -05:00
|
|
|
Ansi.moveCursor(this.#cursor.y, this.#cursor.x),
|
2023-11-10 08:36:18 -05:00
|
|
|
);
|
2023-11-09 12:05:30 -05:00
|
|
|
this.#buffer.append(Ansi.ShowCursor);
|
2023-11-08 17:02:59 -05:00
|
|
|
|
2023-11-10 18:22:09 -05:00
|
|
|
await this.#buffer.flush();
|
2023-11-03 12:26:09 -04:00
|
|
|
}
|
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);
|
|
|
|
|
2023-11-10 18:22:09 -05:00
|
|
|
await this.#buffer.flush();
|
2023-11-08 17:02:59 -05:00
|
|
|
}
|
|
|
|
|
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++) {
|
2023-11-16 11:10:33 -05:00
|
|
|
if (this.#document.numRows < y) {
|
2023-11-13 14:46:04 -05:00
|
|
|
this.drawPlaceholderRow(y);
|
|
|
|
} else {
|
|
|
|
this.drawFileRow(y);
|
|
|
|
}
|
|
|
|
}
|
2023-11-09 13:08:00 -05:00
|
|
|
}
|
|
|
|
|
2023-11-14 15:53:45 -05:00
|
|
|
private drawFileRow(y: number): void {
|
|
|
|
const row = this.#document.row(y);
|
2023-11-13 14:46:04 -05:00
|
|
|
let len = row?.chars.length ?? 0;
|
|
|
|
if (len > this.#screen.cols) {
|
|
|
|
len = this.#screen.cols;
|
|
|
|
}
|
2023-11-09 13:08:00 -05:00
|
|
|
|
2023-11-14 15:53:45 -05:00
|
|
|
this.#buffer.append(row!.toString(), len);
|
|
|
|
this.#buffer.appendLine(Ansi.ClearLine);
|
2023-11-13 14:46:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private drawPlaceholderRow(y: number): void {
|
2023-11-14 15:53:45 -05:00
|
|
|
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
|
|
|
|
2023-11-14 15:53:45 -05:00
|
|
|
this.#buffer.append(message, messageLen);
|
2023-11-13 14:46:04 -05:00
|
|
|
} else {
|
|
|
|
this.#buffer.append('~');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#buffer.append(Ansi.ClearLine);
|
|
|
|
if (y < this.#screen.rows - 1) {
|
2023-11-14 15:53:45 -05:00
|
|
|
this.#buffer.appendLine();
|
2023-11-09 12:05:30 -05:00
|
|
|
}
|
2023-11-03 12:26:09 -04:00
|
|
|
}
|
|
|
|
}
|