diff --git a/src/common/all_test.ts b/src/common/all_test.ts index 77e7f22..1fe3b29 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -19,371 +19,423 @@ 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 BufferTest = { + 'new Buffer': () => { + const b = new Buffer(); + assertInstanceOf(b, Buffer); + assertEquals(b.strlen(), 0); + }, + '.appendLine': () => { + const b = new Buffer(); + + // Carriage return and line feed + b.appendLine(); + assertEquals(b.strlen(), 2); + + b.clear(); + assertEquals(b.strlen(), 0); + + b.appendLine('foo'); + assertEquals(b.strlen(), 5); + }, + '.append': () => { + const b = new Buffer(); + + b.append('foobar'); + assertEquals(b.strlen(), 6); + b.clear(); + + b.append('foobar', 3); + assertEquals(b.strlen(), 3); + }, + '.flush': async () => { + const b = new Buffer(); + b.appendLine('foobarbaz' + Ansi.ClearLine); + assertEquals(b.strlen(), 14); + + await b.flush(); + + assertEquals(b.strlen(), 0); + }, +}; + +// ---------------------------------------------------------------------------- + +const DocumentTest = { + '.default': () => { + const doc = Document.default(); + assertEquals(doc.numRows, 0); + assertTrue(doc.isEmpty()); + assertEquals(doc.row(0), null); + }, + '.insertRow': () => { + const doc = Document.default(); + doc.insertRow(undefined, 'foobar'); + assertEquals(doc.numRows, 1); + assertFalse(doc.isEmpty()); + assertInstanceOf(doc.row(0), Row); + }, + '.insert': () => { + const doc = Document.default(); + assertFalse(doc.dirty); + doc.insert(Position.at(0, 0), 'foobar'); + assertEquals(doc.numRows, 1); + assertTrue(doc.dirty); + + doc.insert(Position.at(2, 0), 'baz'); + assertEquals(doc.numRows, 1); + assertTrue(doc.dirty); + + doc.insert(Position.at(9, 0), 'buzz'); + assertEquals(doc.numRows, 1); + assertTrue(doc.dirty); + const row0 = doc.row(0); + assertEquals(row0?.toString(), 'foobazbarbuzz'); + assertEquals(row0?.rstring(), 'foobazbarbuzz'); + assertEquals(row0?.rsize, 13); + + doc.insert(Position.at(0, 1), 'Lorem Ipsum'); + assertEquals(doc.numRows, 2); + assertTrue(doc.dirty); + }, + '.delete': () => { + const doc = Document.default(); + doc.insert(Position.default(), 'foobar'); + doc.delete(Position.at(3, 0)); + assertEquals(doc.row(0)?.toString(), 'fooar'); + }, +}; + +// ---------------------------------------------------------------------------- + +const EditorTest = { + 'new Editor': () => { + const e = new Editor(defaultTerminalSize); + assertInstanceOf(e, Editor); + }, +}; + +// ---------------------------------------------------------------------------- + +const PositionTest = { + '.default': () => { + const p = Position.default(); + assertEquals(p.x, 0); + assertEquals(p.y, 0); + }, + '.at': () => { + const p = Position.at(5, 7); + assertEquals(p.x, 5); + assertEquals(p.y, 7); + }, + '.from': () => { + const p1 = Position.at(1, 2); + const p2 = Position.from(p1); + + p1.x = 2; + p1.y = 4; + + assertEquals(p1.x, 2); + assertEquals(p1.y, 4); + + assertEquals(p2.x, 1); + assertEquals(p2.y, 2); + }, +}; + +// ---------------------------------------------------------------------------- + +const RowTest = { + '.default': () => { + const row = Row.default(); + assertEquals(row.toString(), ''); + }, + '.from': () => { + // From string + const row = Row.from('xyz'); + assertEquals(row.toString(), 'xyz'); + + // From existing Row + assertEquals(Row.from(row).toString(), row.toString()); + + // From 'chars' + assertEquals(Row.from(['😺', '😸', '😹']).toString(), '😺😸😹'); + }, + '.append': () => { + const row = Row.from('foo'); + row.append('bar'); + assertEquals(row.toString(), 'foobar'); + }, + '.delete': () => { + const row = Row.from('foof'); + row.delete(3); + assertEquals(row.toString(), 'foo'); + + row.delete(4); + assertEquals(row.toString(), 'foo'); + }, + '.split': () => { + // When you split a row, it's from the cursor position + // (Kind of like if the string were one-indexed) + const row = Row.from('foobar'); + const row2 = Row.from('bar'); + assertEquals(row.split(3).toString(), row2.toString()); + }, + '.find': () => { + const normalRow = Row.from('For whom the bell tolls'); + assertEquals(normalRow.find('who'), 4); + assertNull(normalRow.find('foo')); + + const emojiRow = Row.from('😺😸😹'); + assertEquals(emojiRow.find('😹'), 2); + assertNull(emojiRow.find('🀰🏼')); + }, + '.byteIndexToCharIndex': () => { + // Each 'character' is two bytes + const row = Row.from('πŸ˜ΊπŸ˜ΈπŸ˜ΉπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'); + assertEquals(row.byteIndexToCharIndex(4), 2); + assertEquals(row.byteIndexToCharIndex(2), 1); + assertEquals(row.byteIndexToCharIndex(0), 0); + + // Return count on nonsense index + assertEquals(Fn.strlen(row.toString()), 10); + assertEquals(row.byteIndexToCharIndex(72), 10); + + const row2 = Row.from('foobar'); + assertEquals(row2.byteIndexToCharIndex(2), 2); + }, + '.charIndexToByteIndex': () => { + // Each 'character' is two bytes + const row = Row.from('πŸ˜ΊπŸ˜ΈπŸ˜ΉπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'); + assertEquals(row.charIndexToByteIndex(2), 4); + assertEquals(row.charIndexToByteIndex(1), 2); + assertEquals(row.charIndexToByteIndex(0), 0); + }, + '.cxToRx, .rxToCx': () => { + const row = Row.from('foo\tbar\tbaz'); + row.update(); + assertNotEquals(row.chars, row.rchars); + assertNotEquals(row.size, row.rsize); + assertEquals(row.size, 11); + assertEquals(row.rsize, row.size + (SCROLL_TAB_SIZE * 2) - 2); + + const cx = 11; + const aRx = row.cxToRx(cx); + const rx = 11; + const aCx = row.rxToCx(aRx); + assertEquals(aCx, cx); + assertEquals(aRx, rx); + }, +}; + +// ---------------------------------------------------------------------------- + +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; + + assertTrue(nullish(null)); + assertTrue(nullish(void 0)); + assertTrue(nullish(undefined)); + assertFalse(nullish(0)); + assertFalse(nullish(false)); + }, + 'arrayInsert() strings': () => { + const { arrayInsert } = Fn; + + 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); + + assertEquals(arrayInsert([], 0, 'foo'), ['foo']); + }, + 'arrayInsert() numbers': () => { + const { arrayInsert } = Fn; + + const a = [1, 3, 5]; + const b = [1, 3, 4, 5]; + assertEquals(arrayInsert(a, 2, 4), b); + + 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; + + // Invalid output + assertEquals(ord(''), 256); + + // Valid output + assertEquals(ord('a'), 97); + }, + 'strChars() properly splits strings into unicode characters': () => { + const { strChars } = Fn; + + 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), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); + }, +}; + +// ---------------------------------------------------------------------------- + const encoder = new TextEncoder(); + const testKeyMap = (codes: string[], expected: string) => { codes.forEach((code) => { assertEquals(Fn.readKey(encoder.encode(code)), expected); }); }; +const readKeyTest = { + 'empty input': () => { + assertEquals(Fn.readKey(new Uint8Array(0)), ''); + }, + 'passthrough': () => { + // Ignore unhandled escape sequences + assertEquals(Fn.readKey(encoder.encode('\x1b[]')), '\x1b[]'); + + // 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, + ); + + // And pass through whatever else + assertEquals(Fn.readKey(encoder.encode('foobaz')), 'foobaz'); + }, + + '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), +}; + +// ---------------------------------------------------------------------------- +// Test Suite Setup +// ---------------------------------------------------------------------------- + testSuite({ - 'ANSI utils': { - 'moveCursor()': () => { - assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H'); - }, - 'moveCursorForward()': () => { - assertEquals(Ansi.moveCursorForward(2), '\x1b[2C'); - }, - 'moveCursorDown()': () => { - assertEquals(Ansi.moveCursorDown(7), '\x1b[7B'); - }, - }, - Buffer: { - 'new Buffer': () => { - const b = new Buffer(); - assertInstanceOf(b, Buffer); - assertEquals(b.strlen(), 0); - }, - '.appendLine': () => { - const b = new Buffer(); - - // Carriage return and line feed - b.appendLine(); - assertEquals(b.strlen(), 2); - - b.clear(); - assertEquals(b.strlen(), 0); - - b.appendLine('foo'); - assertEquals(b.strlen(), 5); - }, - '.append': () => { - const b = new Buffer(); - - b.append('foobar'); - assertEquals(b.strlen(), 6); - b.clear(); - - b.append('foobar', 3); - assertEquals(b.strlen(), 3); - }, - '.flush': async () => { - const b = new Buffer(); - b.appendLine('foobarbaz' + Ansi.ClearLine); - assertEquals(b.strlen(), 14); - - await b.flush(); - - assertEquals(b.strlen(), 0); - }, - }, - Document: { - '.default': () => { - const doc = Document.default(); - assertEquals(doc.numRows, 0); - assertTrue(doc.isEmpty()); - assertEquals(doc.row(0), null); - }, - '.insertRow': () => { - const doc = Document.default(); - doc.insertRow(undefined, 'foobar'); - assertEquals(doc.numRows, 1); - assertFalse(doc.isEmpty()); - assertInstanceOf(doc.row(0), Row); - }, - '.insert': () => { - const doc = Document.default(); - assertFalse(doc.dirty); - doc.insert(Position.at(0, 0), 'foobar'); - assertEquals(doc.numRows, 1); - assertTrue(doc.dirty); - - doc.insert(Position.at(2, 0), 'baz'); - assertEquals(doc.numRows, 1); - assertTrue(doc.dirty); - - doc.insert(Position.at(9, 0), 'buzz'); - assertEquals(doc.numRows, 1); - assertTrue(doc.dirty); - const row0 = doc.row(0); - assertEquals(row0?.toString(), 'foobazbarbuzz'); - assertEquals(row0?.rstring(), 'foobazbarbuzz'); - assertEquals(row0?.rsize, 13); - - doc.insert(Position.at(0, 1), 'Lorem Ipsum'); - assertEquals(doc.numRows, 2); - assertTrue(doc.dirty); - }, - '.delete': () => { - const doc = Document.default(); - doc.insert(Position.default(), 'foobar'); - doc.delete(Position.at(3, 0)); - assertEquals(doc.row(0)?.toString(), 'fooar'); - }, - }, - Editor: { - 'new Editor': () => { - const e = new Editor(defaultTerminalSize); - assertInstanceOf(e, Editor); - }, - }, - Position: { - '.default': () => { - const p = Position.default(); - assertEquals(p.x, 0); - assertEquals(p.y, 0); - }, - '.at': () => { - const p = Position.at(5, 7); - assertEquals(p.x, 5); - assertEquals(p.y, 7); - }, - '.from': () => { - const p1 = Position.at(1, 2); - const p2 = Position.from(p1); - - p1.x = 2; - p1.y = 4; - - assertEquals(p1.x, 2); - assertEquals(p1.y, 4); - - assertEquals(p2.x, 1); - assertEquals(p2.y, 2); - }, - }, - Row: { - '.default': () => { - const row = Row.default(); - assertEquals(row.toString(), ''); - }, - '.from': () => { - // From string - const row = Row.from('xyz'); - assertEquals(row.toString(), 'xyz'); - - // From existing Row - assertEquals(Row.from(row).toString(), row.toString()); - - // From 'chars' - assertEquals(Row.from(['😺', '😸', '😹']).toString(), '😺😸😹'); - }, - '.append': () => { - const row = Row.from('foo'); - row.append('bar'); - assertEquals(row.toString(), 'foobar'); - }, - '.delete': () => { - const row = Row.from('foof'); - row.delete(3); - assertEquals(row.toString(), 'foo'); - - row.delete(4); - assertEquals(row.toString(), 'foo'); - }, - '.split': () => { - // When you split a row, it's from the cursor position - // (Kind of like if the string were one-indexed) - const row = Row.from('foobar'); - const row2 = Row.from('bar'); - assertEquals(row.split(3).toString(), row2.toString()); - }, - '.find': () => { - const normalRow = Row.from('For whom the bell tolls'); - assertEquals(normalRow.find('who'), 4); - assertNull(normalRow.find('foo')); - - const emojiRow = Row.from('😺😸😹'); - assertEquals(emojiRow.find('😹'), 2); - assertNull(emojiRow.find('🀰🏼')); - }, - '.byteIndexToCharIndex': () => { - // Each 'character' is two bytes - const row = Row.from('πŸ˜ΊπŸ˜ΈπŸ˜ΉπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'); - assertEquals(row.byteIndexToCharIndex(4), 2); - assertEquals(row.byteIndexToCharIndex(2), 1); - assertEquals(row.byteIndexToCharIndex(0), 0); - - // Return count on nonsense index - assertEquals(Fn.strlen(row.toString()), 10); - assertEquals(row.byteIndexToCharIndex(72), 10); - - const row2 = Row.from('foobar'); - assertEquals(row2.byteIndexToCharIndex(2), 2); - }, - '.charIndexToByteIndex': () => { - // Each 'character' is two bytes - const row = Row.from('πŸ˜ΊπŸ˜ΈπŸ˜ΉπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'); - assertEquals(row.charIndexToByteIndex(2), 4); - assertEquals(row.charIndexToByteIndex(1), 2); - assertEquals(row.charIndexToByteIndex(0), 0); - }, - '.cxToRx, .rxToCx': () => { - const row = Row.from('foo\tbar\tbaz'); - row.update(); - assertNotEquals(row.chars, row.rchars); - assertNotEquals(row.size, row.rsize); - assertEquals(row.size, 11); - assertEquals(row.rsize, row.size + (SCROLL_TAB_SIZE * 2) - 2); - - const cx = 11; - const aRx = row.cxToRx(cx); - const rx = 11; - const aCx = row.rxToCx(aRx); - assertEquals(aCx, cx); - assertEquals(aRx, rx); - }, - }, - 'fns': { - 'arrayInsert() strings': () => { - const { arrayInsert } = Fn; - - 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); - - assertEquals(arrayInsert([], 0, 'foo'), ['foo']); - }, - 'arrayInsert() numbers': () => { - const { arrayInsert } = Fn; - - const a = [1, 3, 5]; - const b = [1, 3, 4, 5]; - assertEquals(arrayInsert(a, 2, 4), b); - - 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; - - // Invalid output - assertEquals(ord(''), 256); - - // Valid output - assertEquals(ord('a'), 97); - }, - 'strChars() properly splits strings into unicode characters': () => { - const { strChars } = Fn; - - 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), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); - }, - }, - 'readKey()': { - 'empty input': () => { - assertEquals(Fn.readKey(new Uint8Array(0)), ''); - }, - 'passthrough': () => { - // Ignore unhandled escape sequences - assertEquals(Fn.readKey(encoder.encode('\x1b[]')), '\x1b[]'); - - // 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, - ); - - // And pass through whatever else - assertEquals(Fn.readKey(encoder.encode('foobaz')), 'foobaz'); - }, - - '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), - }, + 'ANSI utils': ANSITest, + Buffer: BufferTest, + Document: DocumentTest, + Editor: EditorTest, + Position: PositionTest, + Row: RowTest, + fns: fnTest, + 'readKey()': readKeyTest, }); diff --git a/src/common/document.ts b/src/common/document.ts index 75b5dcf..8d2cc7c 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -81,7 +81,7 @@ export class Document { const row = this.#rows[potential.y]; // Okay, we have to take the Javascript string index (potential.x), convert - // it to the Row character index, and then convert that to the Row render index + // it to the Row 'character' index, and then convert that to the Row render index // so that the highlighted color starts in the right place. const start = row.cxToRx(row.byteIndexToCharIndex(potential.x)); diff --git a/src/common/fns.ts b/src/common/fns.ts index 3180abc..14494a8 100644 --- a/src/common/fns.ts +++ b/src/common/fns.ts @@ -11,6 +11,20 @@ const decoder = new TextDecoder(); */ export const noop = () => {}; +/** + * Does a value exist? (not null or undefined) + */ +export function defined(v: unknown): boolean { + return v !== null && typeof v !== 'undefined'; +} + +/** + * Is the value null or undefined? + */ +export function nullish(v: unknown): boolean { + return v === null || typeof v === 'undefined'; +} + /** * Convert input from ANSI escape sequences into a form * that can be more easily mapped to editor commands diff --git a/src/common/row.ts b/src/common/row.ts index d66f409..6d56be7 100644 --- a/src/common/row.ts +++ b/src/common/row.ts @@ -107,6 +107,9 @@ export class Row { return this.byteIndexToCharIndex(byteCount); } + /** + * Convert the raw row offset to the equivalent offset for screen rendering + */ public cxToRx(cx: number): number { let rx = 0; let j; @@ -120,6 +123,9 @@ export class Row { return rx; } + /** + * Convert the screen rendering row offset to the file row offset + */ public rxToCx(rx: number): number { let curRx = 0; let cx = 0; @@ -137,6 +143,10 @@ export class Row { return cx; } + /** + * Convert the index of a JS string into the equivalent + * 'unicode character' index + */ public byteIndexToCharIndex(byteIndex: number): number { if (this.toString().length === this.chars.length) { return byteIndex; @@ -154,6 +164,10 @@ export class Row { return this.chars.length; } + /** + * Convert the 'unicode character' index into the equivalent + * JS string index + */ public charIndexToByteIndex(charIndex: number): number { if (charIndex === 0 || this.toString().length === this.chars.length) { return charIndex;