Add util functions for null/undefined checks, organize tests into sections
Some checks failed
timw4mail/scroll/pipeline/head There was a failure building this commit
Some checks failed
timw4mail/scroll/pipeline/head There was a failure building this commit
This commit is contained in:
parent
e6b53ef327
commit
8093683f92
@ -19,371 +19,423 @@ const {
|
|||||||
testSuite,
|
testSuite,
|
||||||
} = await getTestRunner();
|
} = 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 encoder = new TextEncoder();
|
||||||
|
|
||||||
const testKeyMap = (codes: string[], expected: string) => {
|
const testKeyMap = (codes: string[], expected: string) => {
|
||||||
codes.forEach((code) => {
|
codes.forEach((code) => {
|
||||||
assertEquals(Fn.readKey(encoder.encode(code)), expected);
|
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({
|
testSuite({
|
||||||
'ANSI utils': {
|
'ANSI utils': ANSITest,
|
||||||
'moveCursor()': () => {
|
Buffer: BufferTest,
|
||||||
assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
|
Document: DocumentTest,
|
||||||
},
|
Editor: EditorTest,
|
||||||
'moveCursorForward()': () => {
|
Position: PositionTest,
|
||||||
assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
|
Row: RowTest,
|
||||||
},
|
fns: fnTest,
|
||||||
'moveCursorDown()': () => {
|
'readKey()': readKeyTest,
|
||||||
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),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
@ -81,7 +81,7 @@ export class Document {
|
|||||||
const row = this.#rows[potential.y];
|
const row = this.#rows[potential.y];
|
||||||
|
|
||||||
// Okay, we have to take the Javascript string index (potential.x), convert
|
// 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.
|
// so that the highlighted color starts in the right place.
|
||||||
const start = row.cxToRx(row.byteIndexToCharIndex(potential.x));
|
const start = row.cxToRx(row.byteIndexToCharIndex(potential.x));
|
||||||
|
|
||||||
|
@ -11,6 +11,20 @@ const decoder = new TextDecoder();
|
|||||||
*/
|
*/
|
||||||
export const noop = () => {};
|
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
|
* Convert input from ANSI escape sequences into a form
|
||||||
* that can be more easily mapped to editor commands
|
* that can be more easily mapped to editor commands
|
||||||
|
@ -107,6 +107,9 @@ export class Row {
|
|||||||
return this.byteIndexToCharIndex(byteCount);
|
return this.byteIndexToCharIndex(byteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the raw row offset to the equivalent offset for screen rendering
|
||||||
|
*/
|
||||||
public cxToRx(cx: number): number {
|
public cxToRx(cx: number): number {
|
||||||
let rx = 0;
|
let rx = 0;
|
||||||
let j;
|
let j;
|
||||||
@ -120,6 +123,9 @@ export class Row {
|
|||||||
return rx;
|
return rx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the screen rendering row offset to the file row offset
|
||||||
|
*/
|
||||||
public rxToCx(rx: number): number {
|
public rxToCx(rx: number): number {
|
||||||
let curRx = 0;
|
let curRx = 0;
|
||||||
let cx = 0;
|
let cx = 0;
|
||||||
@ -137,6 +143,10 @@ export class Row {
|
|||||||
return cx;
|
return cx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the index of a JS string into the equivalent
|
||||||
|
* 'unicode character' index
|
||||||
|
*/
|
||||||
public byteIndexToCharIndex(byteIndex: number): number {
|
public byteIndexToCharIndex(byteIndex: number): number {
|
||||||
if (this.toString().length === this.chars.length) {
|
if (this.toString().length === this.chars.length) {
|
||||||
return byteIndex;
|
return byteIndex;
|
||||||
@ -154,6 +164,10 @@ export class Row {
|
|||||||
return this.chars.length;
|
return this.chars.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the 'unicode character' index into the equivalent
|
||||||
|
* JS string index
|
||||||
|
*/
|
||||||
public charIndexToByteIndex(charIndex: number): number {
|
public charIndexToByteIndex(charIndex: number): number {
|
||||||
if (charIndex === 0 || this.toString().length === this.chars.length) {
|
if (charIndex === 0 || this.toString().length === this.chars.length) {
|
||||||
return charIndex;
|
return charIndex;
|
||||||
|
Loading…
Reference in New Issue
Block a user