pi-diff-review 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/diff-source.ts +46 -1
- package/src/index.ts +4 -3
- package/src/review-component.ts +22 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Easily provide code reviews directly within [pi](https://pi.dev/).
|
|
6
6
|
|
|
7
|
-
<img width="
|
|
7
|
+
<img width="1095" height="853" alt="pi-diff-review-screenshot" src="https://github.com/user-attachments/assets/f1a39117-f2a6-4ee7-bf72-a299990ab1dd" />
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -27,6 +27,7 @@ pi install https://github.com/cmpadden/pi-diff-review
|
|
|
27
27
|
- `/diff` reviews the current unstaged `git diff`
|
|
28
28
|
- `/diff <git-diff-args>` passes arguments through to `git diff` (for example `/diff main...HEAD`)
|
|
29
29
|
- `j/k` or arrow keys to move
|
|
30
|
+
- `g/G` to jump to the top or bottom of the diff
|
|
30
31
|
- `ctrl-u` / `ctrl-d` to move up/down by half a page
|
|
31
32
|
- `t` toggles the diff between unified and side-by-side split rendering
|
|
32
33
|
- `J/K` to extend a highlighted selection into a comment range
|
package/package.json
CHANGED
package/src/diff-source.ts
CHANGED
|
@@ -11,7 +11,7 @@ export function parseDiffSource(args: string): DiffSource {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const gitArgs = trimmed
|
|
14
|
+
const gitArgs = tokenizeDiffArgs(trimmed);
|
|
15
15
|
return {
|
|
16
16
|
label: `git diff ${trimmed}`,
|
|
17
17
|
promptLabel: `\`git diff ${trimmed}\``,
|
|
@@ -19,6 +19,51 @@ export function parseDiffSource(args: string): DiffSource {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function tokenizeDiffArgs(input: string): string[] {
|
|
23
|
+
const args: string[] = [];
|
|
24
|
+
let current = "";
|
|
25
|
+
let quote: '"' | "'" | undefined;
|
|
26
|
+
let escaping = false;
|
|
27
|
+
|
|
28
|
+
for (const char of input) {
|
|
29
|
+
if (escaping) {
|
|
30
|
+
current += char;
|
|
31
|
+
escaping = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (char === "\\" && quote !== "'") {
|
|
36
|
+
escaping = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ((char === '"' || char === "'") && !quote) {
|
|
41
|
+
quote = char;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (char === quote) {
|
|
46
|
+
quote = undefined;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (/\s/.test(char) && !quote) {
|
|
51
|
+
if (current) {
|
|
52
|
+
args.push(current);
|
|
53
|
+
current = "";
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
current += char;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (escaping) current += "\\";
|
|
62
|
+
if (quote) throw new Error(`Unterminated ${quote} quote in git diff args`);
|
|
63
|
+
if (current) args.push(current);
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
|
|
22
67
|
export function getDiff(cwd: string, source: DiffSource): string {
|
|
23
68
|
return execFileSync(
|
|
24
69
|
"git",
|
package/src/index.ts
CHANGED
|
@@ -6,19 +6,20 @@ import { getDiff, parseDiffSource } from "./diff-source.ts";
|
|
|
6
6
|
import { parseDiff } from "./diff-parser.ts";
|
|
7
7
|
import { buildReviewPrompt } from "./prompt.ts";
|
|
8
8
|
import { ReviewComponent } from "./review-component.ts";
|
|
9
|
-
import type { ReviewComment, ReviewResult } from "./types.ts";
|
|
9
|
+
import type { DiffSource, ReviewComment, ReviewResult } from "./types.ts";
|
|
10
10
|
|
|
11
11
|
export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
12
12
|
pi.registerCommand("diff", {
|
|
13
13
|
description: "Review a git diff in a custom TUI (/diff [git diff args])",
|
|
14
14
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
15
|
-
|
|
15
|
+
let source: DiffSource;
|
|
16
16
|
let diffText: string;
|
|
17
17
|
try {
|
|
18
|
+
source = parseDiffSource(args);
|
|
18
19
|
diffText = getDiff(ctx.cwd, source);
|
|
19
20
|
} catch (error) {
|
|
20
21
|
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
-
ctx.ui.notify(`Unable to read
|
|
22
|
+
ctx.ui.notify(`Unable to read diff: ${message}`, "error");
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
24
25
|
|
package/src/review-component.ts
CHANGED
|
@@ -126,6 +126,14 @@ export class ReviewComponent {
|
|
|
126
126
|
this.move(-1);
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
|
+
if (data === "g") {
|
|
130
|
+
this.jumpToBoundary("start");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (data === "G") {
|
|
134
|
+
this.jumpToBoundary("end");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
129
137
|
if (data === "J") {
|
|
130
138
|
this.extendSelection(1);
|
|
131
139
|
return;
|
|
@@ -172,7 +180,7 @@ export class ReviewComponent {
|
|
|
172
180
|
? `${this.lines.length} lines • ${this.comments.size} comments • editing comment • Enter save • Esc/Ctrl+C cancel`
|
|
173
181
|
: this.hasSelection()
|
|
174
182
|
? `${this.lines.length} lines • ${this.comments.size} comments • J/K extend • Esc clear selection • c comment range • Enter submit`
|
|
175
|
-
: `${this.lines.length} lines • ${this.comments.size} comments • j/k move • ctrl-u/d page • t unified/split • J/K extend • c comment • x delete • n/p hunk • Enter submit • q quit`,
|
|
183
|
+
: `${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • j/k move • g/G top/bottom • ctrl-u/d page • t unified/split • J/K extend • c comment • x delete • n/p hunk • Enter submit • q quit`,
|
|
176
184
|
),
|
|
177
185
|
width,
|
|
178
186
|
),
|
|
@@ -344,6 +352,12 @@ export class ReviewComponent {
|
|
|
344
352
|
this.tui.requestRender();
|
|
345
353
|
}
|
|
346
354
|
|
|
355
|
+
private jumpToBoundary(boundary: "start" | "end"): void {
|
|
356
|
+
this.selected =
|
|
357
|
+
boundary === "start" ? 0 : Math.max(0, this.lines.length - 1);
|
|
358
|
+
this.clearSelection();
|
|
359
|
+
}
|
|
360
|
+
|
|
347
361
|
private toggleLayout(): void {
|
|
348
362
|
this.layout = this.layout === "side-by-side" ? "stacked" : "side-by-side";
|
|
349
363
|
this.tui.requestRender(true);
|
|
@@ -456,6 +470,13 @@ export class ReviewComponent {
|
|
|
456
470
|
};
|
|
457
471
|
}
|
|
458
472
|
|
|
473
|
+
private getPositionText(selectedLine?: ReviewLine): string {
|
|
474
|
+
const position = `${Math.min(this.selected + 1, this.lines.length)}/${this.lines.length}`;
|
|
475
|
+
return selectedLine?.filePath
|
|
476
|
+
? `${position} ${selectedLine.filePath}`
|
|
477
|
+
: position;
|
|
478
|
+
}
|
|
479
|
+
|
|
459
480
|
private getFooterText(selectedLine?: ReviewLine): string {
|
|
460
481
|
const selection = this.getSelectionBounds();
|
|
461
482
|
if (selection) {
|