Files
llmx/codex-cli/tests/text-buffer-gaps.test.ts
Ilan Bigio 59a180ddec Initial commit
Signed-off-by: Ilan Bigio <ilan@openai.com>
2025-04-16 12:56:08 -04:00

251 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import TextBuffer from "../src/lib/text-buffer";
import { describe, it, expect } from "vitest";
// The purpose of this testsuite is NOT to make the implementation green today
// quite the opposite. We capture behaviours that are already covered by the
// reference Rust implementation (textarea.rs) but are *still missing* from the
// current TypeScript port. Every test is therefore marked with `.fails()` so
// that the suite passes while the functionality is absent. When a particular
// gap is closed the corresponding test will begin to succeed, causing Vitest to
// raise an error (a *good* error) that reminds us to remove the `.fails` flag.
/* -------------------------------------------------------------------------- */
/* Softtab insertion */
/* -------------------------------------------------------------------------- */
describe("softtab insertion (↹ => 4 spaces)", () => {
it.fails(
"inserts 4 spaces at caret position when hardtab mode is off",
() => {
const buf = new TextBuffer("");
// A literal "\t" character is treated as user pressing the Tab key. The
// Rust version expands it to softtabs by default.
buf.insert("\t");
expect(buf.getText()).toBe(" ");
expect(buf.getCursor()).toEqual([0, 4]);
},
);
});
/* -------------------------------------------------------------------------- */
/* Undo / Redo grouping & stack clearing */
/* -------------------------------------------------------------------------- */
describe("undo / redo advanced behaviour", () => {
it.fails(
"typing a word characterbycharacter should undo in one step",
() => {
const buf = new TextBuffer("");
for (const ch of "hello") {
buf.insert(ch);
}
// One single undo should revert the *whole* word, leaving empty buffer.
buf.undo();
expect(buf.getText()).toBe("");
expect(buf.getCursor()).toEqual([0, 0]);
},
);
});
/* -------------------------------------------------------------------------- */
/* Selection cut / delete selection */
/* -------------------------------------------------------------------------- */
describe("selection cut/delete", () => {
it.fails(
"cut() removes the selected range and yanks it into clipboard",
() => {
const buf = new TextBuffer("foo bar baz");
// Select the middle word "bar"
buf.move("wordRight"); // after "foo" + space => col 4
buf.startSelection();
buf.move("wordRight"); // after "bar" (col 8)
// @ts-expect-error method missing in current implementation
buf.cut();
// Text should now read "foo baz" (two spaces collapsed only if impl trims)
expect(buf.getText()).toBe("foo baz");
// Cursor should be at the start of the gap where text was removed
expect(buf.getCursor()).toEqual([0, 4]);
// And clipboard/yank buffer should contain the deleted word
// @ts-expect-error clipboard getter not exposed yet
expect(buf.getClipboard()).toBe("bar");
},
);
});
/* -------------------------------------------------------------------------- */
/* Wordwise forward deletion (Ctrl+Delete) */
/* -------------------------------------------------------------------------- */
describe("delete_next_word (Ctrl+Delete)", () => {
it.fails("removes everything until the next word boundary", () => {
const vp = { width: 80, height: 25 };
const buf = new TextBuffer("hello world!! next");
// Place caret at start of line (0,0). One Ctrl+Delete should wipe the
// word "hello" and the following space.
buf.handleInput(undefined, { delete: true, ctrl: true }, vp);
expect(buf.getText()).toBe("world!! next");
expect(buf.getCursor()).toEqual([0, 0]);
});
});
/* -------------------------------------------------------------------------- */
/* Configurable tab length */
/* -------------------------------------------------------------------------- */
describe("tab length configuration", () => {
it.fails("inserts the configured number of spaces when tabLen=2", () => {
// @ts-expect-error constructor currently has no config object
const buf = new TextBuffer("", { tabLen: 2 });
buf.insert("\t");
expect(buf.getText()).toBe(" "); // two spaces
expect(buf.getCursor()).toEqual([0, 2]);
});
});
/* -------------------------------------------------------------------------- */
/* Search subsystem */
/* -------------------------------------------------------------------------- */
describe("search / regex navigation", () => {
it.fails("search_forward jumps to the next match", () => {
const text = [
"alpha beta gamma",
"beta gamma alpha",
"gamma alpha beta",
].join("\n");
const buf = new TextBuffer(text);
// @ts-expect-error method missing
buf.setSearchPattern(/beta/);
// Cursor starts at 0,0. First search_forward should land on the first
// occurrence (row 0, col 6)
// @ts-expect-error method missing
buf.searchForward();
expect(buf.getCursor()).toEqual([0, 6]);
// Second invocation should wrap within viewport and find next occurrence
// (row 1, col 0)
// @ts-expect-error method missing
buf.searchForward();
expect(buf.getCursor()).toEqual([1, 0]);
});
});
/* -------------------------------------------------------------------------- */
/* Wordwise navigation accuracy */
/* -------------------------------------------------------------------------- */
describe("wordLeft / wordRight punctuation boundaries", () => {
it.fails("wordLeft stops after punctuation like hyphen (-)", () => {
const buf = new TextBuffer("hello-world");
// Place caret at end of line
buf.move("end");
// Perform a single wordLeft in Rust implementation this lands right
// *after* the hyphen, i.e. between '-' and 'w' (column index 6).
buf.move("wordLeft");
expect(buf.getCursor()).toEqual([0, 6]);
});
it.fails(
"wordRight stops after punctuation like underscore (_) which is not in JS boundary set",
() => {
const buf = new TextBuffer("foo_bar");
// From start, one wordRight should land right after the underscore (col 4)
buf.move("wordRight");
expect(buf.getCursor()).toEqual([0, 4]);
},
);
});
/* -------------------------------------------------------------------------- */
/* Wordwise deletion (Ctrl+Backspace) */
/* -------------------------------------------------------------------------- */
describe("word deletion shortcuts", () => {
it.fails("Ctrl+Backspace deletes the previous word", () => {
const vp = { width: 80, height: 25 };
const buf = new TextBuffer("hello world");
// Place caret after the last character
buf.move("end");
// Simulate Ctrl+Backspace (terminal usually sends backspace with ctrl flag)
buf.handleInput(undefined, { backspace: true, ctrl: true }, vp);
// The whole word "world" (and the preceding space) should be removed,
// leaving just "hello".
expect(buf.getText()).toBe("hello");
expect(buf.getCursor()).toEqual([0, 5]);
});
});
/* -------------------------------------------------------------------------- */
/* Paragraph navigation */
/* -------------------------------------------------------------------------- */
describe("paragraph navigation", () => {
it.fails("Jumping forward by paragraph stops after a blank line", () => {
const text = [
"first paragraph line 1",
"first paragraph line 2",
"", // blank line separates paragraphs
"second paragraph line 1",
].join("\n");
const buf = new TextBuffer(text);
// Start at very beginning
// (No method exposed yet once implemented we will call move("paragraphForward"))
// For now we imitate the call; test will fail until the command exists.
// @ts-expect-error method not implemented yet
buf.move("paragraphForward");
// Expect caret to land at start of the first line _after_ the blank one
expect(buf.getCursor()).toEqual([3, 0]);
});
});
/* -------------------------------------------------------------------------- */
/* Independent scrolling */
/* -------------------------------------------------------------------------- */
describe("viewport scrolling independent of cursor", () => {
it.fails("scrolls without moving the caret", () => {
const lines = Array.from({ length: 100 }, (_, i) => `line ${i}`);
const buf = new TextBuffer(lines.join("\n"));
const vp = { width: 10, height: 5 };
// Cursor stays at 0,0. We now ask the view to scroll down by one page.
// @ts-expect-error method not implemented yet
buf.scroll("pageDown", vp);
// Cursor must remain at (0,0) even though viewport origin changed.
expect(buf.getCursor()).toEqual([0, 0]);
// The first visible line should now be "line 5".
expect(buf.getVisibleLines(vp)[0]).toBe("line 5");
});
});