Files
llmx/codex-cli/tests/text-buffer.test.ts

265 lines
9.4 KiB
TypeScript
Raw Normal View History

import TextBuffer from "../src/text-buffer";
import { describe, it, expect } from "vitest";
describe("TextBuffer basic editing parity with Rust suite", () => {
/* ------------------------------------------------------------------ */
/* insert_char */
/* ------------------------------------------------------------------ */
it("insert_char / printable (single line)", () => {
// (col, char, expectedLine)
const cases: Array<[number, string, string]> = [
[0, "x", "xab"],
[1, "x", "axb"],
[2, "x", "abx"],
[1, "あ", "aあb"],
];
for (const [col, ch, want] of cases) {
const buf = new TextBuffer("ab");
buf.move("end"); // go to col 2
while (buf.getCursor()[1] > col) {
buf.move("left");
}
buf.insert(ch);
expect(buf.getText()).toBe(want);
expect(buf.getCursor()).toEqual([0, col + 1]);
}
});
/* ------------------------------------------------------------------ */
/* insert_char newline support */
/* ------------------------------------------------------------------ */
it("insert_char with a newline should split the line", () => {
const buf = new TextBuffer("ab");
// jump to end of first (and only) line
buf.move("end");
// Insert a raw \n character the Rust implementation splits the line
buf.insert("\n");
// We expect the text to be split into two separate lines
expect(buf.getLines()).toEqual(["ab", ""]);
expect(buf.getCursor()).toEqual([1, 0]);
});
/* ------------------------------------------------------------------ */
/* insert_str helpers */
/* ------------------------------------------------------------------ */
it("insert_str should insert multiline strings", () => {
const initial = ["ab", "cd", "ef"].join("\n");
const buf = new TextBuffer(initial);
// place cursor at (row:0, col:0)
// No move needed cursor starts at 0,0
buf.insertStr("x\ny");
const wantLines = ["x", "yab", "cd", "ef"];
expect(buf.getLines()).toEqual(wantLines);
expect(buf.getCursor()).toEqual([1, 1]);
});
/* ------------------------------------------------------------------ */
/* Undo / Redo */
/* ------------------------------------------------------------------ */
it("undo / redo history should revert edits", () => {
const buf = new TextBuffer("hello");
buf.move("end");
buf.insert("!"); // text becomes "hello!"
expect(buf.undo()).toBe(true);
expect(buf.getText()).toBe("hello");
expect(buf.redo()).toBe(true);
expect(buf.getText()).toBe("hello!");
});
/* ------------------------------------------------------------------ */
/* Selection model */
/* ------------------------------------------------------------------ */
it("copy & paste should operate on current selection", () => {
const buf = new TextBuffer("hello world");
buf.startSelection();
// Select the word "hello"
buf.move("right"); // h
buf.move("right"); // e
buf.move("right"); // l
buf.move("right"); // l
buf.move("right"); // o
buf.endSelection();
buf.copy();
// Move to end and paste
buf.move("end");
// add one space before pasting copied word
buf.insert(" ");
buf.paste();
expect(buf.getText()).toBe("hello world hello");
});
/* ------------------------------------------------------------------ */
/* Backspace behaviour */
/* ------------------------------------------------------------------ */
describe("backspace", () => {
it("deletes the character to the *left* of the caret within a line", () => {
const buf = new TextBuffer("abc");
// Move caret after the second character ( index 2 => after 'b' )
buf.move("right"); // -> a|bc (col 1)
buf.move("right"); // -> ab|c (col 2)
buf.backspace();
expect(buf.getLines()).toEqual(["ac"]);
expect(buf.getCursor()).toEqual([0, 1]);
});
it("merges with the previous line when invoked at column 0", () => {
const buf = new TextBuffer(["ab", "cd"].join("\n"));
// Place caret at the beginning of second line
buf.move("down"); // row = 1, col = 0
buf.backspace();
expect(buf.getLines()).toEqual(["abcd"]);
expect(buf.getCursor()).toEqual([0, 2]); // after 'b'
});
it("is a noop at the very beginning of the buffer", () => {
const buf = new TextBuffer("ab");
buf.backspace(); // caret starts at (0,0)
expect(buf.getLines()).toEqual(["ab"]);
expect(buf.getCursor()).toEqual([0, 0]);
});
});
/* ------------------------------------------------------------------ */
/* Vertical cursor movement we should preserve the preferred column */
/* ------------------------------------------------------------------ */
describe("up / down navigation keeps the preferred column", () => {
it("restores horizontal position when moving across shorter lines", () => {
// Three lines: long / short / long
const lines = ["abcdef", "x", "abcdefg"].join("\n");
const buf = new TextBuffer(lines);
// Place caret after the 5th char in first line (col = 5)
buf.move("end"); // col 6 (after 'f')
buf.move("left"); // col 5 (between 'e' and 'f')
// Move down twice through a short line and back to a long one
buf.move("down"); // should land on (1, 1) due to clamp
buf.move("down"); // desired: (2, 5)
expect(buf.getCursor()).toEqual([2, 5]);
});
});
/* ------------------------------------------------------------------ */
/* Left / Right arrow navigation across Unicode surrogate pairs */
/* ------------------------------------------------------------------ */
describe("left / right navigation", () => {
it("should treat multicodeunit emoji as a single character", () => {
// '🐶' is a surrogatepair (length 2) but one userperceived char.
const buf = new TextBuffer("🐶a");
// Move caret once to the right logically past the emoji.
buf.move("right");
// Insert another printable character
buf.insert("x");
// We expect the emoji to stay intact and the text to be 🐶xa
expect(buf.getLines()).toEqual(["🐶xa"]);
// Cursor should be after the inserted char (two visible columns along)
expect(buf.getCursor()).toEqual([0, 2]);
});
});
/* ------------------------------------------------------------------ */
/* HandleInput raw DEL bytes should map to backspace */
/* ------------------------------------------------------------------ */
it("handleInput should treat \x7f input as backspace", () => {
const buf = new TextBuffer("");
const vp = { width: 80, height: 25 };
// Type "hello" via printable input path
for (const ch of "hello") {
buf.handleInput(ch, {}, vp);
}
// Two DEL bytes terminal's backspace
buf.handleInput("\x7f", {}, vp);
buf.handleInput("\x7f", {}, vp);
expect(buf.getText()).toBe("hel");
expect(buf.getCursor()).toEqual([0, 3]);
});
/* ------------------------------------------------------------------ */
/* HandleInput `key.delete` should ALSO behave as backspace */
/* ------------------------------------------------------------------ */
it("handleInput should treat key.delete as backspace", () => {
const buf = new TextBuffer("");
const vp = { width: 80, height: 25 };
for (const ch of "hello") {
buf.handleInput(ch, {}, vp);
}
// Simulate the Delete (Mac backspace) key three times
buf.handleInput(undefined, { delete: true }, vp);
buf.handleInput(undefined, { delete: true }, vp);
buf.handleInput(undefined, { delete: true }, vp);
expect(buf.getText()).toBe("he");
expect(buf.getCursor()).toEqual([0, 2]);
});
/* ------------------------------------------------------------------ */
/* Cursor positioning semantics */
/* ------------------------------------------------------------------ */
describe("cursor movement & backspace semantics", () => {
it("typing should leave cursor after the last inserted character", () => {
const vp = { width: 80, height: 25 };
const buf = new TextBuffer("");
buf.handleInput("h", {}, vp);
expect(buf.getCursor()).toEqual([0, 1]);
for (const ch of "ello") {
buf.handleInput(ch, {}, vp);
}
expect(buf.getCursor()).toEqual([0, 5]); // after 'o'
});
it("arrowleft moves the caret to *between* characters (highlight next)", () => {
const vp = { width: 80, height: 25 };
const buf = new TextBuffer("");
for (const ch of "bar") {
buf.handleInput(ch, {}, vp);
} // cursor at col 3
buf.move("left"); // col 2 (right before 'r')
buf.move("left"); // col 1 (right before 'a')
expect(buf.getCursor()).toEqual([0, 1]);
// Character to the RIGHT of caret should be 'a'
const charRight = [...buf.getLines()[0]!][buf.getCursor()[1]];
expect(charRight).toBe("a");
// Backspace should delete the char to the *left* (i.e. 'b'), leaving "ar"
buf.backspace();
expect(buf.getLines()[0]).toBe("ar");
expect(buf.getCursor()).toEqual([0, 0]);
});
});
});