From f71239ded50cdbcb10fab6fc4a9f83945c0e81cc Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 26 Jun 2024 15:45:33 -0400 Subject: [PATCH] Minor refactoring, add some more comments and tests --- src/bun/mod.ts | 2 + src/common/all_test.ts | 374 ++++++++++++++++++++++------------------- src/common/document.ts | 4 +- src/common/editor.ts | 6 +- src/common/fns.ts | 4 +- src/common/position.ts | 2 + src/common/row.ts | 8 +- src/common/runtime.ts | 5 +- 8 files changed, 220 insertions(+), 185 deletions(-) diff --git a/src/bun/mod.ts b/src/bun/mod.ts index 32db2dc..743d89d 100644 --- a/src/bun/mod.ts +++ b/src/bun/mod.ts @@ -6,6 +6,8 @@ import { IRuntime, RunTimeType } from '../common/runtime.ts'; import BunTerminalIO from './terminal_io.ts'; import BunFileIO from './file_io.ts'; +import * as process from 'node:process'; + const BunRuntime: IRuntime = { name: RunTimeType.Bun, file: BunFileIO, diff --git a/src/common/all_test.ts b/src/common/all_test.ts index 1fe3b29..bb9fdea 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -1,12 +1,13 @@ +import Ansi, * as _Ansi from './ansi.ts'; import Buffer from './buffer.ts'; import Document from './document.ts'; import Editor from './editor.ts'; +import Position from './position.ts'; import Row from './row.ts'; -import { Ansi, KeyCommand } from './ansi.ts'; + +import * as Fn from './fns.ts'; import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts'; import { getTestRunner } from './runtime.ts'; -import { Position } from './types.ts'; -import * as Fn from './fns.ts'; const { assertEquals, @@ -19,16 +20,33 @@ const { testSuite, } = await getTestRunner(); -const ANSITest = { - 'moveCursor()': () => { - assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H'); - }, - 'moveCursorForward()': () => { - assertEquals(Ansi.moveCursorForward(2), '\x1b[2C'); - }, - 'moveCursorDown()': () => { - assertEquals(Ansi.moveCursorDown(7), '\x1b[7B'); - }, +// ---------------------------------------------------------------------------- + +const ANSITest = () => { + const { AnsiColor, Ground } = _Ansi; + + return { + 'color()': () => { + assertEquals(Ansi.color(AnsiColor.FgBlue), '\x1b[34m'); + }, + 'color256()': () => { + assertEquals(Ansi.color256(128, Ground.Back), '\x1b[48;5;128m'); + assertEquals(Ansi.color256(128, Ground.Fore), '\x1b[38;5;128m'); + }, + 'rgb()': () => { + assertEquals(Ansi.rgb(32, 64, 128, Ground.Back), '\x1b[48;2;32;64;128m'); + assertEquals(Ansi.rgb(32, 64, 128, Ground.Fore), '\x1b[38;2;32;64;128m'); + }, + 'moveCursor()': () => { + assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H'); + }, + 'moveCursorForward()': () => { + assertEquals(Ansi.moveCursorForward(2), '\x1b[2C'); + }, + 'moveCursorDown()': () => { + assertEquals(Ansi.moveCursorDown(7), '\x1b[7B'); + }, + }; }; // ---------------------------------------------------------------------------- @@ -244,185 +262,187 @@ const RowTest = { // ---------------------------------------------------------------------------- -const fnTest = { - 'defined()': () => { - const { defined } = Fn; - assertFalse(defined(null)); - assertFalse(defined(void 0)); - assertFalse(defined(undefined)); - assertTrue(defined(0)); - assertTrue(defined(false)); - }, - 'nullish()': () => { - const { nullish } = Fn; +const fnTest = () => { + const { + some, + none, + arrayInsert, + noop, + posSub, + minSub, + maxAdd, + ord, + strChars, + ctrlKey, + isControl, + isAscii, + isAsciiDigit, + strlen, + truncate, + } = Fn; - assertTrue(nullish(null)); - assertTrue(nullish(void 0)); - assertTrue(nullish(undefined)); - assertFalse(nullish(0)); - assertFalse(nullish(false)); - }, - 'arrayInsert() strings': () => { - const { arrayInsert } = Fn; + return { + 'some()': () => { + assertFalse(some(null)); + assertFalse(some(void 0)); + assertFalse(some(undefined)); + assertTrue(some(0)); + assertTrue(some(false)); + }, + 'none()': () => { + assertTrue(none(null)); + assertTrue(none(void 0)); + assertTrue(none(undefined)); + assertFalse(none(0)); + assertFalse(none(false)); + }, + 'arrayInsert() strings': () => { + const a = ['😺', '😸', '😹']; + const b = arrayInsert(a, 1, 'x'); + const c = ['😺', 'x', '😸', '😹']; + assertEquals(b, c); - const a = ['😺', '😸', '😹']; - const b = arrayInsert(a, 1, 'x'); - const c = ['😺', 'x', '😸', '😹']; - assertEquals(b, c); + const d = arrayInsert(c, 17, 'y'); + const e = ['😺', 'x', '😸', '😹', 'y']; + assertEquals(d, e); - const d = arrayInsert(c, 17, 'y'); - const e = ['😺', 'x', '😸', '😹', 'y']; - assertEquals(d, e); + assertEquals(arrayInsert([], 0, 'foo'), ['foo']); + }, + 'arrayInsert() numbers': () => { + const a = [1, 3, 5]; + const b = [1, 3, 4, 5]; + assertEquals(arrayInsert(a, 2, 4), b); - assertEquals(arrayInsert([], 0, 'foo'), ['foo']); - }, - 'arrayInsert() numbers': () => { - const { arrayInsert } = Fn; + const c = [1, 2, 3, 4, 5]; + assertEquals(arrayInsert(b, 1, 2), c); + }, + 'noop fn': () => { + assertExists(noop); + assertEquals(noop(), undefined); + }, + 'posSub()': () => { + assertEquals(posSub(14, 15), 0); + assertEquals(posSub(15, 1), 14); + }, + 'minSub()': () => { + assertEquals(minSub(13, 25, -1), -1); + assertEquals(minSub(25, 13, 0), 12); + }, + 'maxAdd()': () => { + assertEquals(maxAdd(99, 99, 75), 75); + assertEquals(maxAdd(25, 74, 101), 99); + }, + 'ord()': () => { + // Invalid output + assertEquals(ord(''), 256); - const a = [1, 3, 5]; - const b = [1, 3, 4, 5]; - assertEquals(arrayInsert(a, 2, 4), b); + // Valid output + assertEquals(ord('a'), 97); + }, + 'strChars() properly splits strings into unicode characters': () => { + assertEquals(strChars('😺😸😹'), ['😺', '😸', '😹']); + }, + 'ctrlKey()': () => { + const ctrl_a = ctrlKey('a'); + assertTrue(isControl(ctrl_a)); + assertEquals(ctrl_a, String.fromCodePoint(0x01)); - const c = [1, 2, 3, 4, 5]; - assertEquals(arrayInsert(b, 1, 2), c); - }, - 'noop fn': () => { - assertExists(Fn.noop); - assertEquals(Fn.noop(), undefined); - }, - 'posSub()': () => { - assertEquals(Fn.posSub(14, 15), 0); - assertEquals(Fn.posSub(15, 1), 14); - }, - 'minSub()': () => { - assertEquals(Fn.minSub(13, 25, -1), -1); - assertEquals(Fn.minSub(25, 13, 0), 12); - }, - 'maxAdd()': () => { - assertEquals(Fn.maxAdd(99, 99, 75), 75); - assertEquals(Fn.maxAdd(25, 74, 101), 99); - }, - 'ord()': () => { - const { ord } = Fn; + const invalid = ctrlKey('😺'); + assertFalse(isControl(invalid)); + assertEquals(invalid, '😺'); + }, + 'isAscii()': () => { + assertTrue(isAscii('asjyverkjhsdf1928374')); + assertFalse(isAscii('😺acalskjsdf')); + assertFalse(isAscii('ab😺ac')); + }, + 'isAsciiDigit()': () => { + assertTrue(isAsciiDigit('1234567890')); + assertFalse(isAsciiDigit('A1')); + assertFalse(isAsciiDigit('/')); + assertFalse(isAsciiDigit(':')); + }, + 'isControl()': () => { + assertFalse(isControl('abc')); + assertTrue(isControl(String.fromCodePoint(0x01))); + assertFalse(isControl('😺')); + }, + 'strlen()': () => { + // Ascii length + assertEquals(strlen('abc'), 'abc'.length); - // Invalid output - assertEquals(ord(''), 256); + // Get number of visible unicode characters + assertEquals(strlen('😺😸😹'), 3); + assertNotEquals('😺😸😹'.length, strlen('😺😸😹')); - // Valid output - assertEquals(ord('a'), 97); - }, - 'strChars() properly splits strings into unicode characters': () => { - const { strChars } = Fn; + // Skin tone modifier + base character + assertEquals(strlen('🀰🏼'), 2); + assertNotEquals('🀰🏼'.length, strlen('🀰🏼')); - assertEquals(strChars('😺😸😹'), ['😺', '😸', '😹']); - }, - 'ctrlKey()': () => { - const { ctrlKey, isControl } = Fn; - - const ctrl_a = ctrlKey('a'); - assertTrue(isControl(ctrl_a)); - assertEquals(ctrl_a, String.fromCodePoint(0x01)); - - const invalid = ctrlKey('😺'); - assertFalse(isControl(invalid)); - assertEquals(invalid, '😺'); - }, - 'isAscii()': () => { - const { isAscii } = Fn; - - assertTrue(isAscii('asjyverkjhsdf1928374')); - assertFalse(isAscii('😺acalskjsdf')); - assertFalse(isAscii('ab😺ac')); - }, - 'isAsciiDigit()': () => { - const { isAsciiDigit } = Fn; - - assertTrue(isAsciiDigit('1234567890')); - assertFalse(isAsciiDigit('A1')); - assertFalse(isAsciiDigit('/')); - assertFalse(isAsciiDigit(':')); - }, - 'isControl()': () => { - const { isControl } = Fn; - - assertFalse(isControl('abc')); - assertTrue(isControl(String.fromCodePoint(0x01))); - assertFalse(isControl('😺')); - }, - 'strlen()': () => { - const { strlen } = Fn; - - // Ascii length - assertEquals(strlen('abc'), 'abc'.length); - - // Get number of visible unicode characters - assertEquals(strlen('😺😸😹'), 3); - assertNotEquals('😺😸😹'.length, strlen('😺😸😹')); - - // Skin tone modifier + base character - assertEquals(strlen('🀰🏼'), 2); - assertNotEquals('🀰🏼'.length, strlen('🀰🏼')); - - // This has 4 sub-characters, and 3 zero-width-joiners - assertEquals(strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); - assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); - }, - 'truncate()': () => { - const { truncate } = Fn; - - assertEquals(truncate('😺😸😹', 1), '😺'); - assertEquals(truncate('😺😸😹', 5), '😺😸😹'); - assertEquals(truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); - }, + // This has 4 sub-characters, and 3 zero-width-joiners + assertEquals(strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); + assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); + }, + 'truncate()': () => { + assertEquals(truncate('😺😸😹', 1), '😺'); + assertEquals(truncate('😺😸😹', 5), '😺😸😹'); + assertEquals(truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); + }, + }; }; // ---------------------------------------------------------------------------- -const encoder = new TextEncoder(); +const readKeyTest = () => { + const { KeyCommand } = _Ansi; + const { readKey, ctrlKey } = Fn; -const testKeyMap = (codes: string[], expected: string) => { - codes.forEach((code) => { - assertEquals(Fn.readKey(encoder.encode(code)), expected); - }); -}; + const encoder = new TextEncoder(); -const readKeyTest = { - 'empty input': () => { - assertEquals(Fn.readKey(new Uint8Array(0)), ''); - }, - 'passthrough': () => { - // Ignore unhandled escape sequences - assertEquals(Fn.readKey(encoder.encode('\x1b[]')), '\x1b[]'); + const testKeyMap = (codes: string[], expected: string) => { + codes.forEach((code) => { + assertEquals(readKey(encoder.encode(code)), expected); + }); + }; - // Pass explicitly mapped values right through - assertEquals( - Fn.readKey(encoder.encode(KeyCommand.ArrowUp)), - KeyCommand.ArrowUp, - ); - assertEquals( - Fn.readKey(encoder.encode(KeyCommand.Home)), - KeyCommand.Home, - ); - assertEquals( - Fn.readKey(encoder.encode(KeyCommand.Delete)), - KeyCommand.Delete, - ); + return { + 'empty input': () => { + assertEquals(readKey(new Uint8Array(0)), ''); + }, + 'passthrough': () => { + // Ignore unhandled escape sequences + assertEquals(readKey(encoder.encode('\x1b[]')), '\x1b[]'); - // And pass through whatever else - assertEquals(Fn.readKey(encoder.encode('foobaz')), 'foobaz'); - }, + // Pass explicitly mapped values right through + assertEquals( + readKey(encoder.encode(KeyCommand.ArrowUp)), + KeyCommand.ArrowUp, + ); + assertEquals( + readKey(encoder.encode(KeyCommand.Home)), + KeyCommand.Home, + ); + assertEquals( + readKey(encoder.encode(KeyCommand.Delete)), + KeyCommand.Delete, + ); - 'Esc': () => testKeyMap(['\x1b', Fn.ctrlKey('l')], KeyCommand.Escape), - 'Backspace': () => - testKeyMap( - [Fn.ctrlKey('h'), '\x7f'], - KeyCommand.Backspace, - ), - 'Home': () => - testKeyMap(['\x1b[1~', '\x1b[7~', '\x1b[H', '\x1bOH'], KeyCommand.Home), - 'End': () => - testKeyMap(['\x1b[4~', '\x1b[8~', '\x1b[F', '\x1bOF'], KeyCommand.End), - 'Enter': () => testKeyMap(['\n', '\r', '\v'], KeyCommand.Enter), + // And pass through whatever else + assertEquals(readKey(encoder.encode('foobaz')), 'foobaz'); + }, + + 'Esc': () => testKeyMap(['\x1b', ctrlKey('l')], KeyCommand.Escape), + 'Backspace': () => + testKeyMap( + [ctrlKey('h'), '\x7f'], + KeyCommand.Backspace, + ), + 'Home': () => + testKeyMap(['\x1b[1~', '\x1b[7~', '\x1b[H', '\x1bOH'], KeyCommand.Home), + 'End': () => + testKeyMap(['\x1b[4~', '\x1b[8~', '\x1b[F', '\x1bOF'], KeyCommand.End), + 'Enter': () => testKeyMap(['\n', '\r', '\v'], KeyCommand.Enter), + }; }; // ---------------------------------------------------------------------------- @@ -430,12 +450,12 @@ const readKeyTest = { // ---------------------------------------------------------------------------- testSuite({ - 'ANSI utils': ANSITest, + 'ANSI utils': ANSITest(), Buffer: BufferTest, Document: DocumentTest, Editor: EditorTest, Position: PositionTest, Row: RowTest, - fns: fnTest, - 'readKey()': readKeyTest, + fns: fnTest(), + 'readKey()': readKeyTest(), }); diff --git a/src/common/document.ts b/src/common/document.ts index 8d2cc7c..5ab5856 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -1,5 +1,5 @@ import Row from './row.ts'; -import { arrayInsert, strlen } from './fns.ts'; +import { arrayInsert, some, strlen } from './fns.ts'; import { HighlightType } from './highlight.ts'; import { getRuntime } from './runtime.ts'; import { Position } from './types.ts'; @@ -76,7 +76,7 @@ export class Document { key: string, ): Position | null { const potential = this.#search.search(q, key); - if (potential !== null) { + if (some(potential) && potential instanceof Position) { // Update highlight of search match const row = this.#rows[potential.y]; diff --git a/src/common/editor.ts b/src/common/editor.ts index 258aa70..9058c46 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -8,8 +8,10 @@ import { ctrlKey, isControl, maxAdd, + none, posSub, readKey, + some, truncate, } from './fns.ts'; import { getRuntime, log, LogLevel } from './runtime.ts'; @@ -254,7 +256,7 @@ class Editor { await this.refreshScreen(); for await (const chunk of term.inputLoop()) { const char = readKey(chunk); - if (char === null) { + if (none(char)) { continue; } @@ -312,7 +314,7 @@ class Editor { return null; } - if (query !== null && query.length > 0) { + if (some(query) && query.length > 0) { const pos = this.#document.find(query, key); if (pos !== null) { // We have a match here diff --git a/src/common/fns.ts b/src/common/fns.ts index 14494a8..6975065 100644 --- a/src/common/fns.ts +++ b/src/common/fns.ts @@ -14,14 +14,14 @@ export const noop = () => {}; /** * Does a value exist? (not null or undefined) */ -export function defined(v: unknown): boolean { +export function some(v: unknown): boolean { return v !== null && typeof v !== 'undefined'; } /** * Is the value null or undefined? */ -export function nullish(v: unknown): boolean { +export function none(v: unknown): boolean { return v === null || typeof v === 'undefined'; } diff --git a/src/common/position.ts b/src/common/position.ts index d986033..06010c5 100644 --- a/src/common/position.ts +++ b/src/common/position.ts @@ -16,3 +16,5 @@ export class Position { return new Position(); } } + +export default Position; diff --git a/src/common/row.ts b/src/common/row.ts index 6d56be7..0a30dc2 100644 --- a/src/common/row.ts +++ b/src/common/row.ts @@ -1,5 +1,5 @@ import { SCROLL_TAB_SIZE } from './config.ts'; -import { arrayInsert, isAsciiDigit, strChars } from './fns.ts'; +import { arrayInsert, isAsciiDigit, some, strChars } from './fns.ts'; import { highlightToColor, HighlightType } from './highlight.ts'; import Ansi from './ansi.ts'; @@ -68,6 +68,9 @@ export class Row { } } + /** + * Truncate the current row, and return a new one at the specified index + */ public split(at: number): Row { const newRow = new Row(this.chars.slice(at)); this.chars = this.chars.slice(0, at); @@ -76,6 +79,9 @@ export class Row { return newRow; } + /** + * Remove a character at the specified index + */ public delete(at: number): void { if (at >= this.size) { return; diff --git a/src/common/runtime.ts b/src/common/runtime.ts index 9f94cea..96dcb2d 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime.ts @@ -85,10 +85,13 @@ export async function getRuntime(): Promise { const pkg = await import(path); if ('default' in pkg) { scrollRuntime = pkg.default; + if (scrollRuntime !== null) { + return Promise.resolve(scrollRuntime); + } } } - return Promise.resolve(scrollRuntime!); + return Promise.reject('Missing default import'); } /**