mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): preserve indentation in rendered diffs (#9274)
## Problem
The edit tool's intra-line renderer can drop indentation from a removed
line when text is inserted before otherwise unchanged content.
For example, this input:
```diff
-83 await commitRecordingChunk({
+83 const result = await commitRecordingChunk({
```
was rendered as:
```diff
-83 await commitRecordingChunk({
+83 const result = await commitRecordingChunk({
```
`diffWords()` groups the shared leading spaces into the added segment
(`" const result = "`). The existing highlighting logic moved those
spaces outside inverse styling, but only appended them to the added
line.
## Solution
Extract whitespace shared by both lines before calling `diffWords()`,
then initialize both rendered lines with that prefix. This preserves
indentation without changing the existing word-level highlighting
behavior. The existing handling for whitespace that belongs to only one
side remains unchanged.
Added regression coverage for:
- insertion before indented content;
- replacement within indented content;
- keeping shared indentation outside inverse highlighting.
## Validation
- `npm run check`
- `./test.sh`
- targeted `packages/coding-agent/test/diff.test.ts` (3 tests)
This commit is contained in:
parent
66c6556771
commit
dca1f55527
2 changed files with 48 additions and 5 deletions
|
|
@ -20,14 +20,22 @@ function replaceTabs(text: string): string {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute word-level diff and render with inverse on changed parts.
|
* Compute word-level diff and render with inverse on changed parts.
|
||||||
* Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
|
* Separates common indentation before diffing so a leading insertion cannot consume it.
|
||||||
* Strips leading whitespace from inverse to avoid highlighting indentation.
|
|
||||||
*/
|
*/
|
||||||
function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
||||||
const wordDiff = Diff.diffWords(oldContent, newContent);
|
let commonIndentLength = 0;
|
||||||
|
while (
|
||||||
|
commonIndentLength < oldContent.length &&
|
||||||
|
oldContent[commonIndentLength] === newContent[commonIndentLength] &&
|
||||||
|
/\s/.test(oldContent[commonIndentLength])
|
||||||
|
) {
|
||||||
|
commonIndentLength++;
|
||||||
|
}
|
||||||
|
|
||||||
let removedLine = "";
|
const commonIndent = oldContent.slice(0, commonIndentLength);
|
||||||
let addedLine = "";
|
const wordDiff = Diff.diffWords(oldContent.slice(commonIndentLength), newContent.slice(commonIndentLength));
|
||||||
|
let removedLine = commonIndent;
|
||||||
|
let addedLine = commonIndent;
|
||||||
let isFirstRemoved = true;
|
let isFirstRemoved = true;
|
||||||
let isFirstAdded = true;
|
let isFirstAdded = true;
|
||||||
|
|
||||||
|
|
|
||||||
35
packages/coding-agent/test/diff.test.ts
Normal file
35
packages/coding-agent/test/diff.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
|
import { renderDiff } from "../src/modes/interactive/components/diff.ts";
|
||||||
|
import { initTheme, theme } from "../src/modes/interactive/theme/theme.ts";
|
||||||
|
import { stripAnsi } from "../src/utils/ansi.ts";
|
||||||
|
|
||||||
|
describe("renderDiff", () => {
|
||||||
|
beforeAll(() => initTheme("dark"));
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "text is inserted at the start of a line",
|
||||||
|
diff: "-83 await commitRecordingChunk({\n+83 const result = await commitRecordingChunk({",
|
||||||
|
expected: ["-83 await commitRecordingChunk({", "+83 const result = await commitRecordingChunk({"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indented text is replaced",
|
||||||
|
diff: "-12 const value = oldValue\n+12 const value = newValue",
|
||||||
|
expected: ["-12 const value = oldValue", "+12 const value = newValue"],
|
||||||
|
},
|
||||||
|
])("preserves indentation when $name", ({ diff, expected }) => {
|
||||||
|
const rendered = renderDiff(diff);
|
||||||
|
|
||||||
|
expect(stripAnsi(rendered).split("\n")).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not highlight shared indentation as an insertion", () => {
|
||||||
|
const rendered = renderDiff(
|
||||||
|
"-83 await commitRecordingChunk({\n+83 const result = await commitRecordingChunk({",
|
||||||
|
);
|
||||||
|
const inverseInsertion = theme.inverse("const result = ");
|
||||||
|
|
||||||
|
expect(rendered.indexOf("+83 ")).toBeLessThan(rendered.indexOf(inverseInsertion));
|
||||||
|
expect(rendered).toContain(inverseInsertion);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue