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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Easily provide code reviews directly within [pi](https://pi.dev/).
6
6
 
7
- <img width="1947" height="1103" alt="image" src="https://github.com/user-attachments/assets/3b1e1c51-4c77-4430-8915-1f7d481b64cb" />
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-review",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local diff review TUI extension for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,7 +11,7 @@ export function parseDiffSource(args: string): DiffSource {
11
11
  };
12
12
  }
13
13
 
14
- const gitArgs = trimmed.split(/\s+/).filter(Boolean);
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
- const source = parseDiffSource(args);
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 ${source.label}: ${message}`, "error");
22
+ ctx.ui.notify(`Unable to read diff: ${message}`, "error");
22
23
  return;
23
24
  }
24
25
 
@@ -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) {