Number diff lines, render task lists, and add word navigation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
co-authored by
Sisyphus
parent
940a1e7401
commit
3ef2d8a75a
@@ -50,6 +50,45 @@ test('the rendered diff marks additions and removals and counts them', () => {
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
test('changed lines carry their line numbers, old and new', () => {
|
||||
const before = ['one', 'two', 'three'].join('\n');
|
||||
const after = ['one', 'TWO', 'three'].join('\n');
|
||||
const app = render(<Diff before={before} after={after} />);
|
||||
|
||||
const frame = app.lastFrame() ?? '';
|
||||
// `two` was line 2 before and `TWO` is line 2 after: a diff without numbers
|
||||
// forces the reader to count them, which is exactly what the panel is for.
|
||||
expect(frame).toMatch(/2.*- two/);
|
||||
expect(frame).toMatch(/2.*\+ TWO/);
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
test('a gap names the line range it hides, so the reader can jump there', () => {
|
||||
const lines = Array.from({ length: 40 }, (_, i) => `line ${i + 1}`).join('\n');
|
||||
const app = render(<Diff before={lines} after={lines.replace('line 30', 'CHANGED')} />);
|
||||
|
||||
const frame = app.lastFrame() ?? '';
|
||||
expect(frame).toContain('CHANGED');
|
||||
// "27 unchanged lines" says how much; "lines 1-27" says where, which is what a
|
||||
// reader needs to open the file at the right place.
|
||||
expect(frame).toMatch(/lines \d+-\d+/);
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
test('added and removed lines count up separately through one diff', () => {
|
||||
const before = ['a', 'b'].join('\n');
|
||||
const after = ['a', 'B', 'c', 'd'].join('\n');
|
||||
const app = render(<Diff before={before} after={after} path="src/x.ts" />);
|
||||
|
||||
// The header is the part a reader scans for: +3 -1 answers "how big was this"
|
||||
// before the lines are read at all.
|
||||
const frame = app.lastFrame() ?? '';
|
||||
expect(frame).toContain('+3');
|
||||
expect(frame).toContain('-1');
|
||||
expect(frame).toContain('src/x.ts');
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
test('a very large diff is truncated with a notice', () => {
|
||||
const before = Array.from({ length: 200 }, (_, i) => `line ${i}`).join('\n');
|
||||
const after = Array.from({ length: 200 }, (_, i) => `changed ${i}`).join('\n');
|
||||
|
||||
@@ -134,6 +134,73 @@ test('ctrl-u clears to the start of the line', async () => {
|
||||
app.unmount();
|
||||
}, 20_000);
|
||||
|
||||
// Word-wise motion: the escape sequences a shell sends for ctrl-left/right.
|
||||
const CTRL_LEFT = '\u001B[1;5D';
|
||||
const CTRL_RIGHT = '\u001B[1;5C';
|
||||
|
||||
test('ctrl-left jumps the cursor back one word', async () => {
|
||||
const { app, session } = mount();
|
||||
await wait(150);
|
||||
await type(app, 'one two');
|
||||
await press(app, CTRL_LEFT);
|
||||
await type(app, 'X ');
|
||||
await press(app, '\r', 500);
|
||||
// Cursor was after "two"; a word jump puts it before it, so the X lands between.
|
||||
expect(session.messages[0]?.content).toBe('one X two');
|
||||
app.unmount();
|
||||
}, 20_000);
|
||||
|
||||
test('ctrl-right jumps the cursor forward one word over a gap', async () => {
|
||||
const { app, session } = mount();
|
||||
await wait(150);
|
||||
await press(app, 'one two', 150);
|
||||
// Back before "one", then one word forward: the jump crosses the word and stops
|
||||
// at its end, not one character along.
|
||||
await press(app, CTRL_LEFT);
|
||||
await press(app, CTRL_LEFT);
|
||||
await press(app, CTRL_RIGHT);
|
||||
await type(app, 'X');
|
||||
await press(app, '\r', 500);
|
||||
expect(session.messages[0]?.content).toBe('oneX two');
|
||||
app.unmount();
|
||||
}, 20_000);
|
||||
|
||||
test('a word jump over the line edge stays put', async () => {
|
||||
const { app, session } = mount();
|
||||
await wait(150);
|
||||
await type(app, 'abc');
|
||||
// Three jumps past the start: the cursor must clamp, not walk off the string.
|
||||
await press(app, CTRL_LEFT);
|
||||
await press(app, CTRL_LEFT);
|
||||
await type(app, 'X');
|
||||
await press(app, '\r', 500);
|
||||
expect(session.messages[0]?.content).toBe('Xabc');
|
||||
app.unmount();
|
||||
}, 20_000);
|
||||
|
||||
test('ctrl-d deletes forward from the cursor', async () => {
|
||||
const { app, session } = mount();
|
||||
await wait(150);
|
||||
await type(app, 'abcd');
|
||||
// Back to the start, then delete two characters forward.
|
||||
await press(app, '\u0001');
|
||||
await press(app, '\u0004');
|
||||
await press(app, '\u0004');
|
||||
await press(app, '\r', 500);
|
||||
expect(session.messages[0]?.content).toBe('cd');
|
||||
app.unmount();
|
||||
}, 20_000);
|
||||
|
||||
test('ctrl-d at the end of the line deletes nothing', async () => {
|
||||
const { app, session } = mount();
|
||||
await wait(150);
|
||||
await type(app, 'end');
|
||||
await press(app, '\u0004');
|
||||
await press(app, '\r', 500);
|
||||
expect(session.messages[0]?.content).toBe('end');
|
||||
app.unmount();
|
||||
}, 20_000);
|
||||
|
||||
test('a pasted multi-character chunk is inserted whole', async () => {
|
||||
const { app, session } = mount();
|
||||
await wait(150);
|
||||
|
||||
@@ -20,6 +20,24 @@ test('markdown renders headings, bullets, and code distinctly', () => {
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
test('a task list renders as checkboxes, not as literal brackets', () => {
|
||||
// Models write progress as markdown task lists. Rendering "- [x] done" literally
|
||||
// turns a status report into markup noise.
|
||||
const app = render(<Markdown text={'- [x] first\n- [ ] second'} />);
|
||||
const frame = app.lastFrame() ?? '';
|
||||
expect(frame).toContain('[x] first');
|
||||
expect(frame).toContain('[ ] second');
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
test('an ordered list keeps its numbers rather than becoming dashes', () => {
|
||||
const app = render(<Markdown text={'1. first step\n2. second step'} />);
|
||||
const frame = app.lastFrame() ?? '';
|
||||
expect(frame).toContain('1. first step');
|
||||
expect(frame).toContain('2. second step');
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
test('markdown strips the markup characters from the rendered output', () => {
|
||||
const app = render(<Markdown text={'Use **bold** and `code` here.'} />);
|
||||
const frame = app.lastFrame() ?? '';
|
||||
|
||||
Reference in New Issue
Block a user