mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 04:02:21 +00:00
feat(tui): implement word wrapping in Editor component
Previously, the Editor component used character-level (grapheme-level) wrapping, which broke words mid-character at line boundaries. This created an ugly visual experience when typing or pasting long text. Now the Editor uses word-aware wrapping: - Wraps at word boundaries when possible - Falls back to character-level wrapping for tokens wider than the available width (e.g., long URLs) - Strips leading whitespace at line starts - Preserves multiple spaces within lines Added wordWrapLine() helper function that tokenizes text into words and whitespace runs, then builds chunks that fit within the specified width while respecting word boundaries. Also updated buildVisualLineMap() to use the same word wrapping logic for consistent cursor navigation. Added tests for: - Word boundary wrapping - Leading whitespace stripping - Long token (URL) character-level fallback - Multiple space preservation - Edge cases (empty string, exact fit)
This commit is contained in:
parent
b123df5fab
commit
dde5f25170
3 changed files with 309 additions and 70 deletions
|
|
@ -576,4 +576,99 @@ describe("Editor component", () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Word wrapping", () => {
|
||||
it("wraps at word boundaries instead of mid-word", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
const width = 40;
|
||||
|
||||
editor.setText("Hello world this is a test of word wrapping functionality");
|
||||
const lines = editor.render(width);
|
||||
|
||||
// Get content lines (between borders)
|
||||
const contentLines = lines.slice(1, -1).map((l) => stripVTControlCharacters(l).trim());
|
||||
|
||||
// Should NOT break mid-word
|
||||
// Line 1 should end with a complete word
|
||||
assert.ok(!contentLines[0]!.endsWith("-"), "Line should not end with hyphen (mid-word break)");
|
||||
|
||||
// Each content line should be complete words
|
||||
for (const line of contentLines) {
|
||||
// Words at end of line should be complete (no partial words)
|
||||
const lastChar = line.trimEnd().slice(-1);
|
||||
assert.ok(lastChar === "" || /[\w.,!?;:]/.test(lastChar), `Line ends unexpectedly with: "${lastChar}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not start lines with leading whitespace after word wrap", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
const width = 20;
|
||||
|
||||
editor.setText("Word1 Word2 Word3 Word4 Word5 Word6");
|
||||
const lines = editor.render(width);
|
||||
|
||||
// Get content lines (between borders)
|
||||
const contentLines = lines.slice(1, -1);
|
||||
|
||||
// No line should start with whitespace (except for padding at the end)
|
||||
for (let i = 0; i < contentLines.length; i++) {
|
||||
const line = stripVTControlCharacters(contentLines[i]!);
|
||||
const trimmedStart = line.trimStart();
|
||||
// The line should either be all padding or start with a word character
|
||||
if (trimmedStart.length > 0) {
|
||||
assert.ok(!/^\s+\S/.test(line.trimEnd()), `Line ${i} starts with unexpected whitespace before content`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("breaks long words (URLs) at character level", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
const width = 30;
|
||||
|
||||
editor.setText("Check https://example.com/very/long/path/that/exceeds/width here");
|
||||
const lines = editor.render(width);
|
||||
|
||||
// All lines should fit within width
|
||||
for (let i = 1; i < lines.length - 1; i++) {
|
||||
const lineWidth = visibleWidth(lines[i]!);
|
||||
assert.strictEqual(lineWidth, width, `Line ${i} has width ${lineWidth}, expected ${width}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves multiple spaces within words on same line", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
const width = 50;
|
||||
|
||||
editor.setText("Word1 Word2 Word3");
|
||||
const lines = editor.render(width);
|
||||
|
||||
const contentLine = stripVTControlCharacters(lines[1]!).trim();
|
||||
// Multiple spaces should be preserved
|
||||
assert.ok(contentLine.includes("Word1 Word2"), "Multiple spaces should be preserved");
|
||||
});
|
||||
|
||||
it("handles empty string", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
const width = 40;
|
||||
|
||||
editor.setText("");
|
||||
const lines = editor.render(width);
|
||||
|
||||
// Should have border + empty content + border
|
||||
assert.strictEqual(lines.length, 3);
|
||||
});
|
||||
|
||||
it("handles single word that fits exactly", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
const width = 10;
|
||||
|
||||
editor.setText("1234567890");
|
||||
const lines = editor.render(width);
|
||||
|
||||
// Should have exactly 3 lines (top border, content, bottom border)
|
||||
assert.strictEqual(lines.length, 3);
|
||||
const contentLine = stripVTControlCharacters(lines[1]!);
|
||||
assert.ok(contentLine.includes("1234567890"), "Content should contain the word");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue