Mostly refactor prompt input (search/file saving) to use input loop shared with normal functionality
Some checks failed
timw4mail/scroll/pipeline/head There was a failure building this commit

This commit is contained in:
Timothy Warren 2024-06-28 16:45:55 -04:00
parent d405880ce8
commit 9afeed41cd
11 changed files with 432 additions and 294 deletions

View File

@ -1,16 +1,18 @@
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
import { IFileIO } from '../common/runtime.ts'; import { IFileIO } from '../common/runtime.ts';
import { appendFile } from 'node:fs/promises'; import { appendFile } from 'node:fs/promises';
const BunFileIO: IFileIO = { const BunFileIO: IFileIO = {
openFile: async (path: string): Promise<string> => { openFile: async (path: string): Promise<string> => {
const file = await Bun.file(path); const file = await globalThis.Bun.file(path);
return await file.text(); return await file.text();
}, },
appendFile: async function (path: string, contents: string): Promise<void> { appendFile: async function (path: string, contents: string): Promise<void> {
return await appendFile(path, contents); return await appendFile(path, contents);
}, },
saveFile: async function (path: string, contents: string): Promise<void> { saveFile: async function (path: string, contents: string): Promise<void> {
await Bun.write(path, contents); await globalThis.Bun.write(path, contents);
return; return;
}, },
}; };

View File

@ -1,3 +1,4 @@
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
/** /**
* The main entrypoint when using Bun as the runtime * The main entrypoint when using Bun as the runtime
*/ */

View File

@ -1,3 +1,4 @@
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
/** /**
* Wrap the runtime-specific hook into stdin * Wrap the runtime-specific hook into stdin
*/ */

View File

@ -1,3 +1,4 @@
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
/** /**
* Adapt the bun test interface to the shared testing interface * Adapt the bun test interface to the shared testing interface
*/ */

View File

@ -8,108 +8,170 @@ import {
ctrlKey, ctrlKey,
isControl, isControl,
maxAdd, maxAdd,
none,
posSub, posSub,
readKey,
some, some,
strlen,
truncate, truncate,
} from './fns.ts'; } from './fns.ts';
import { getRuntime, log, LogLevel } from './runtime.ts'; import { log, LogLevel } from './runtime.ts';
import { ITerminalSize, Position } from './types.ts'; import { ITerminalSize, Position } from './types.ts';
export enum EditorMode {
Normal = 'Normal',
Find = 'Find',
Save = 'Save',
Prompt = 'Prompt',
}
class Prompt {
private constructor(
public basePrompt: string,
private callback: (query: string, char: string) => void,
public key: string = '',
public answer: string = '',
) {}
public static from(
basePrompt: string,
callback: (query: string, char: string) => void = (
query: string,
key: string,
) => {
log('Default prompt callback', LogError.Warning);
},
): Prompt {
return new Prompt(basePrompt, callback);
}
public backspace(): void {
this.answer = truncate(this.answer, strlen(this.answer) - 1);
}
public append(char: string): void {
this.answer += char;
}
public cb(): void {
this.callback(this.answer, this.key);
}
public render(): string {
if (this.basePrompt.includes('%s')) {
return this.basePrompt.replace('%s', this.answer);
} else {
return `${this.basePrompt}${this.answer}`;
}
}
}
class Editor { class Editor {
/**
* How to handle the stdin stream
* @private
*/
public mode: EditorMode = EditorMode.Normal;
/** /**
* The document being edited * The document being edited
* @private * @private
*/ */
#document: Document; public document: Document;
/** /**
* The output buffer for the terminal * The output buffer for the terminal
* @private * @private
*/ */
#buffer: Buffer; protected buffer: Buffer;
/** /**
* The size of the screen in rows/columns * The size of the screen in rows/columns
* @private * @private
*/ */
#screen: ITerminalSize; protected screen: ITerminalSize;
/** /**
* The current location of the mouse cursor * The current location of the mouse cursor
* @private * @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 * @private
*/ */
#renderX: number = 0; protected renderX: number = 0;
/** /**
* The name of the currently open file * The name of the currently open file
* @private * @private
*/ */
#filename: string = ''; protected filename: string = '';
/**
* Current input prompt state
*/
public _prompt: Prompt | null = null;
/** /**
* A message to display at the bottom of the screen * A message to display at the bottom of the screen
* @private * @private
*/ */
#statusMessage: string = ''; public statusMessage: string = '';
/** /**
* Timeout for status messages * Timeout for status messages
* @private * @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 * @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(): Row | null { private get currentRow(): Row | null {
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)'); await this.document.save(this.filename);
if (filename === null) { this.setStatusMessage(`${this.filename} was saved to disk.`);
this.setStatusMessage('Save aborted'); return;
return;
}
this.#filename = filename;
} }
await this.#document.save(this.#filename); this.prompt('Save as: %s (ESC to cancel)', (name: string, key: string) => {
this.setStatusMessage(`${this.#filename} was saved to disk.`); if (key === KeyCommand.Enter) {
this.mode = EditorMode.Normal;
this.filename = name;
return this.save();
}
if (name === null || key === KeyCommand.Escape) {
this.mode = EditorMode.Normal;
this.setStatusMessage('Save aborted');
}
});
} }
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
@ -121,204 +183,96 @@ class Editor {
* @param input - the decoded chunk of stdin * @param input - the decoded chunk of stdin
*/ */
public async processKeyPress(input: string): Promise<boolean> { public async processKeyPress(input: string): Promise<boolean> {
switch (input) { switch (this.mode) {
// ---------------------------------------------------------------------- case EditorMode.Find:
// Ctrl-key chords log(this, LogLevel.Debug);
// ---------------------------------------------------------------------- // this._prompt = Prompt.from('Search: %s (Use ESC/Arrows/Enter)');
case ctrlKey('f'): this.find();
await this.find(); this.processPromptKeyPress(input);
break; // this.find();
return true;
case ctrlKey('s'): case EditorMode.Save:
await this.save(); log(this, LogLevel.Debug);
break; this.save();
this.processPromptKeyPress(input);
// this.save();
return true;
case ctrlKey('q'): case EditorMode.Prompt:
if (this.#quitTimes > 0 && this.#document.dirty) { return this.processPromptKeyPress(input);
this.setStatusMessage(
'WARNING!!! File has unsaved changes. ' +
`Press Ctrl-Q ${this.#quitTimes} more times to quit.`,
);
this.#quitTimes--;
return true;
}
await this.clearScreen();
return false;
// ---------------------------------------------------------------------- case EditorMode.Normal: // fall through
// Movement keys default:
// ---------------------------------------------------------------------- return this.processNormalKeyPress(input);
case KeyCommand.Home:
this.#cursor.x = 0;
break;
case KeyCommand.End:
if (this.currentRow !== null) {
this.#cursor.x = this.currentRow.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;
// ----------------------------------------------------------------------
// Text manipulation keys
// ----------------------------------------------------------------------
case KeyCommand.Enter:
this.#document.insertNewline(this.#cursor);
this.#cursor.x = 0;
this.#cursor.y++;
break;
case KeyCommand.Delete:
this.#document.delete(this.#cursor);
break;
case KeyCommand.Backspace:
{
if (this.#cursor.x > 0 || this.#cursor.y > 0) {
this.moveCursor(KeyCommand.ArrowLeft);
this.#document.delete(this.#cursor);
}
}
break;
// ----------------------------------------------------------------------
// Direct input
// ----------------------------------------------------------------------
default: {
if (!this.shouldFilter(input)) {
this.#document.insert(this.#cursor, input);
this.#cursor.x++;
}
}
} }
if (this.#quitTimes < SCROLL_QUIT_TIMES) { await this.refreshScreen();
this.#quitTimes = SCROLL_QUIT_TIMES;
this.setStatusMessage('');
}
return true;
} }
public async prompt( public prompt(
p: string, p: string,
callback?: (query: string, char: string) => void, callback: (query: string, char: string) => void,
): Promise<string | null> { ): string | null {
const { term } = await getRuntime(); if (this._prompt === null) {
this._prompt = Prompt.from(p, callback);
let res = '';
const maybeCallback = (query: string, char: string) => {
if (callback !== undefined) {
callback(query, char);
}
};
outer: while (true) {
if (p.includes('%s')) {
this.setStatusMessage(p.replace('%s', res));
} else {
this.setStatusMessage(`${p}${res}`);
}
await this.refreshScreen();
for await (const chunk of term.inputLoop()) {
const char = readKey(chunk);
if (none(char)) {
continue;
}
switch (char) {
// Remove the last character from the prompt input
case KeyCommand.Backspace:
case KeyCommand.Delete:
res = truncate(res, res.length - 1);
maybeCallback(res, char);
continue outer;
// End the prompt
case KeyCommand.Escape:
this.setStatusMessage('');
maybeCallback(res, char);
return null;
// Return the input and end the prompt
case KeyCommand.Enter:
if (res.length > 0) {
this.setStatusMessage('');
maybeCallback(res, char);
return res;
}
break;
// Add to the prompt result
default:
if (!isControl(char)) {
res += char;
}
}
maybeCallback(res, char);
}
} }
switch (this._prompt.key) {
// End the prompt
case KeyCommand.Escape:
this.mode = EditorMode.Normal;
this.setStatusMessage('');
return null;
// Return the input and end the prompt
case KeyCommand.Enter:
this.mode = EditorMode.Normal;
if (this._prompt.answer.length > 0) {
this.setStatusMessage('');
return this._prompt.answer;
}
break;
default:
// Nothing to do here
}
return this._prompt.answer ?? null;
} }
/** /**
* Find text within the document. This is roughly equivalent to the * Find text within the document. This is roughly equivalent to the
* `editorFindCallback` function in the kilo tutorial. * `editorFindCallback` function in the kilo tutorial.
*/ */
public async find(): Promise<void> { public find(): void {
const savedCursor = Position.from(this.#cursor); const savedCursor = Position.from(this.cursor);
const savedOffset = Position.from(this.#offset); const savedOffset = Position.from(this.offset);
const query = await this.prompt( this.prompt(
'Search: %s (Use ESC/Arrows/Enter)', 'Search: %s (Use ESC/Arrows/Enter)',
(query: string, key: string) => { (query: string, key: string) => {
if (key === KeyCommand.Enter || key === KeyCommand.Escape) { if (key === KeyCommand.Enter || key === KeyCommand.Escape) {
this.mode = EditorMode.Normal;
if (key === KeyCommand.Escape) { if (key === KeyCommand.Escape) {
this.#document.resetFind(); this.document.resetFind();
// Return to document position before search
// when you cancel the search (press the escape key)
if (query === null) {
this.cursor = Position.from(savedCursor);
this.offset = Position.from(savedOffset);
}
} }
return null; return null;
} }
if (some(query) && query.length > 0) { if (query.length > 0) {
const pos = this.#document.find(query, key); const pos = this.document.find(query, key);
if (pos !== null) { if (pos !== null) {
// We have a match here // We have a match here
this.#cursor = pos; this.cursor = pos;
this.scroll(); this.scroll();
} else { } else {
this.setStatusMessage('Not found'); this.setStatusMessage('Not found');
@ -326,13 +280,6 @@ class Editor {
} }
}, },
); );
// Return to document position before search
// when you cancel the search (press the escape key)
if (query === null) {
this.#cursor = Position.from(savedCursor);
this.#offset = Position.from(savedOffset);
}
} }
/** /**
@ -362,63 +309,61 @@ class Editor {
private moveCursor(char: string): void { private moveCursor(char: string): void {
switch (char) { switch (char) {
case KeyCommand.ArrowLeft: case KeyCommand.ArrowLeft:
if (this.#cursor.x > 0) { if (this.cursor.x > 0) {
this.#cursor.x--; this.cursor.x--;
} else if (this.#cursor.y > 0) { } else if (this.cursor.y > 0) {
this.#cursor.y--; this.cursor.y--;
this.#cursor.x = (this.currentRow !== null) this.cursor.x = (this.currentRow !== null) ? this.currentRow.size : 0;
? this.currentRow.size
: 0;
} }
break; break;
case KeyCommand.ArrowRight: case KeyCommand.ArrowRight:
if ( if (
this.currentRow !== null && this.#cursor.x < this.currentRow.size this.currentRow !== null && this.cursor.x < this.currentRow.size
) { ) {
this.#cursor.x++; this.cursor.x++;
} else if ( } else if (
this.currentRow !== null && this.currentRow !== null &&
this.#cursor.x === this.currentRow.size this.cursor.x === this.currentRow.size
) { ) {
this.#cursor.y++; this.cursor.y++;
this.#cursor.x = 0; this.cursor.x = 0;
} }
break; break;
case KeyCommand.ArrowUp: case KeyCommand.ArrowUp:
if (this.#cursor.y > 0) { if (this.cursor.y > 0) {
this.#cursor.y--; this.cursor.y--;
} }
break; break;
case KeyCommand.ArrowDown: case KeyCommand.ArrowDown:
if (this.#cursor.y < this.numRows) { if (this.cursor.y < this.numRows) {
this.#cursor.y++; this.cursor.y++;
} }
break; break;
} }
const rowLen = this.currentRow?.size ?? 0; const rowLen = this.currentRow?.size ?? 0;
if (this.#cursor.x > rowLen) { if (this.cursor.x > rowLen) {
this.#cursor.x = rowLen; this.cursor.x = rowLen;
} }
} }
private scroll(): void { private scroll(): void {
this.#renderX = 0; this.renderX = 0;
if (this.currentRow !== null) { if (this.currentRow !== null) {
this.#renderX = this.currentRow.cxToRx(this.#cursor.x); this.renderX = this.currentRow.cxToRx(this.cursor.x);
} }
if (this.#cursor.y < this.#offset.y) { if (this.cursor.y < this.offset.y) {
this.#offset.y = this.#cursor.y; this.offset.y = this.cursor.y;
} }
if (this.#cursor.y >= this.#offset.y + this.#screen.rows) { if (this.cursor.y >= this.offset.y + this.screen.rows) {
this.#offset.y = this.#cursor.y - this.#screen.rows + 1; this.offset.y = this.cursor.y - this.screen.rows + 1;
} }
if (this.#renderX < this.#offset.x) { if (this.renderX < this.offset.x) {
this.#offset.x = this.#renderX; this.offset.x = this.renderX;
} }
if (this.#renderX >= this.#offset.x + this.#screen.cols) { if (this.renderX >= this.offset.x + this.screen.cols) {
this.#offset.x = this.#renderX - this.#screen.cols + 1; this.offset.x = this.renderX - this.screen.cols + 1;
} }
} }
@ -428,8 +373,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();
} }
/** /**
@ -437,106 +382,281 @@ 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> { public 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 row = this.#document.row(y); const row = this.document.row(y);
if (row === null) { if (row === null) {
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);
} }
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, 20)} - ${this.numRows} lines ${modified}`;
const rStatus = `${this.#cursor.y + 1}/${this.numRows}`; const rStatus = `${this.cursor.y + 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);
} }
} }
// --------------------------------------------------------------------------
// Terminal input parsing
// --------------------------------------------------------------------------
private async processNormalKeyPress(input: string): Promise<boolean> {
switch (input) {
// ----------------------------------------------------------------------
// Ctrl-key chords
// ----------------------------------------------------------------------
case ctrlKey('f'):
this.mode = EditorMode.Find;
// break;
return this.processKeyPress(input);
case ctrlKey('s'):
this.mode = EditorMode.Save;
return this.processKeyPress(input);
// await this.save();
// break;
case ctrlKey('q'):
if (this.quitTimes > 0 && this.document.dirty) {
this.setStatusMessage(
'WARNING!!! File has unsaved changes. ' +
`Press Ctrl-Q ${this.quitTimes} more times to quit.`,
);
this.quitTimes--;
return true;
}
await this.clearScreen();
return false;
// ----------------------------------------------------------------------
// Movement keys
// ----------------------------------------------------------------------
case KeyCommand.Home:
this.cursor.x = 0;
break;
case KeyCommand.End:
if (this.currentRow !== null) {
this.cursor.x = this.currentRow.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;
// ----------------------------------------------------------------------
// Text manipulation keys
// ----------------------------------------------------------------------
case KeyCommand.Enter:
this.document.insertNewline(this.cursor);
this.cursor.x = 0;
this.cursor.y++;
break;
case KeyCommand.Delete:
this.document.delete(this.cursor);
break;
case KeyCommand.Backspace:
{
if (this.cursor.x > 0 || this.cursor.y > 0) {
this.moveCursor(KeyCommand.ArrowLeft);
this.document.delete(this.cursor);
}
}
break;
// ----------------------------------------------------------------------
// Direct input
// ----------------------------------------------------------------------
default: {
if (!this.shouldFilter(input)) {
this.document.insert(this.cursor, input);
this.cursor.x++;
}
}
}
if (this.quitTimes < SCROLL_QUIT_TIMES) {
this.quitTimes = SCROLL_QUIT_TIMES;
this.setStatusMessage('');
}
return true;
}
private async processPromptKeyPress(char: string): Promise<boolean> {
log(char, LogLevel.Debug);
log(this, LogLevel.Debug);
if (this._prompt === null) {
log('Prompt should not be null here', LogLevel.Warning);
this.mode = EditorMode.Normal;
return true;
}
this.setStatusMessage(this._prompt.render());
await this.refreshScreen();
this._prompt.key = char;
switch (char) {
// Remove the last character from the prompt input
case KeyCommand.Backspace:
case KeyCommand.Delete:
this._prompt.backspace();
this._prompt.cb();
break;
// End the prompt
case KeyCommand.Escape:
this.mode = EditorMode.Normal;
this.setStatusMessage('');
this._prompt.cb();
break;
// Return the input and end the prompt
case KeyCommand.Enter:
this.mode = EditorMode.Normal;
if (this._prompt.answer.length > 0) {
this.setStatusMessage('');
this._prompt.cb();
}
break;
// Add to the prompt result
default:
if (!isControl(char)) {
this._prompt.append(char);
}
}
// this.setStatusMessage(this._prompt.render());
// await this.refreshScreen();
return true;
}
} }
export default Editor; export default Editor;

View File

@ -51,9 +51,6 @@ export async function main() {
if (!shouldLoop) { if (!shouldLoop) {
return; return;
} }
// Render output
await editor.refreshScreen();
} }
} }
} }

View File

@ -30,7 +30,7 @@ let scrollRuntime: IRuntime | null = null;
export function log(s: unknown, level: LogLevel = LogLevel.Notice): void { export function log(s: unknown, level: LogLevel = LogLevel.Notice): void {
getRuntime().then(({ file }) => { getRuntime().then(({ file }) => {
const raw = typeof s === 'string' ? s : JSON.stringify(s, null, 2); const raw = JSON.stringify(s, null, 2);
const output = `${level}: ${raw}\n`; const output = `${level}: ${raw}\n`;
const outputFile = (level === LogLevel.Error) const outputFile = (level === LogLevel.Error)
@ -85,13 +85,16 @@ export async function getRuntime(): Promise<IRuntime> {
const pkg = await import(path); const pkg = await import(path);
if ('default' in pkg) { if ('default' in pkg) {
scrollRuntime = pkg.default; scrollRuntime = pkg.default;
if (scrollRuntime !== null) {
return Promise.resolve(scrollRuntime);
}
} }
if (scrollRuntime !== null) {
return Promise.resolve(scrollRuntime);
}
return Promise.reject('Missing default import');
} }
return Promise.reject('Missing default import'); return Promise.resolve(scrollRuntime);
} }
/** /**

View File

@ -1,3 +1,7 @@
if (!('Deno' in globalThis)) {
throw new Error('This module requires Deno to run');
}
import { IFileIO } from '../common/runtime.ts'; import { IFileIO } from '../common/runtime.ts';
const DenoFileIO: IFileIO = { const DenoFileIO: IFileIO = {

View File

@ -1,3 +1,6 @@
if (!('Deno' in globalThis)) {
throw new Error('This module requires Deno to run');
}
/** /**
* The main entrypoint when using Deno as the runtime * The main entrypoint when using Deno as the runtime
*/ */

View File

@ -1,3 +1,6 @@
if (!('Deno' in globalThis)) {
throw new Error('This module requires Deno to run');
}
import { readKey } from '../common/fns.ts'; import { readKey } from '../common/fns.ts';
import { ITerminal, ITerminalSize } from '../common/types.ts'; import { ITerminal, ITerminalSize } from '../common/types.ts';

View File

@ -1,3 +1,6 @@
if (!('Deno' in globalThis)) {
throw new Error('This module requires Deno to run');
}
import { ITestBase } from '../common/types.ts'; import { ITestBase } from '../common/types.ts';
import { stdAssert } from './deps.ts'; import { stdAssert } from './deps.ts';
const { const {