pi-diff-review 0.1.14 → 0.1.15

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
@@ -68,6 +68,7 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
68
68
  - `t` toggles inline comments/explanations
69
69
  - `v` toggles the diff between unified and side-by-side split rendering
70
70
  - `?` toggles an AI-generated explanation for the current hunk
71
+ - `/` searches diff lines; `n/N` moves between matches while a search is active
71
72
  - `J/K` to extend a highlighted selection into a comment range
72
73
  - `esc` clears the active selection, or exits review when no selection is active
73
74
  - `n/p` to jump hunks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-review",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Local diff review TUI extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,6 +54,10 @@ export class ReviewComponent {
54
54
  private navigation: ReviewNavigationState;
55
55
  private editMode = false;
56
56
  private editingCommentKey?: string;
57
+ private searchMode = false;
58
+ private searchQuery = "";
59
+ private draftSearchQuery = "";
60
+ private searchMessage = "";
57
61
 
58
62
  private inlineAnnotationsVisible = true;
59
63
  private visibleExplanationKeys = new Set<string>();
@@ -179,8 +183,15 @@ export class ReviewComponent {
179
183
  return;
180
184
  }
181
185
 
186
+ if (this.searchMode) {
187
+ this.handleSearchInput(data);
188
+ return;
189
+ }
190
+
182
191
  if (matchesKey(data, "escape")) {
183
- if (this.hasSelection()) {
192
+ if (this.searchQuery) {
193
+ this.clearSearch();
194
+ } else if (this.hasSelection()) {
184
195
  this.clearSelection();
185
196
  } else {
186
197
  this.done({ action: "cancel" });
@@ -203,6 +214,10 @@ export class ReviewComponent {
203
214
  this.toggleExplanationPane();
204
215
  return;
205
216
  }
217
+ if (data === "/") {
218
+ this.startSearchMode();
219
+ return;
220
+ }
206
221
  if (matchesKey(data, "ctrl+d")) {
207
222
  this.move(this.getPageMoveAmount());
208
223
  return;
@@ -236,7 +251,15 @@ export class ReviewComponent {
236
251
  return;
237
252
  }
238
253
  if (data === "n") {
239
- this.jumpHunk(1);
254
+ if (this.searchQuery) {
255
+ this.jumpSearch(1);
256
+ } else {
257
+ this.jumpHunk(1);
258
+ }
259
+ return;
260
+ }
261
+ if (data === "N" && this.searchQuery) {
262
+ this.jumpSearch(-1);
240
263
  return;
241
264
  }
242
265
  if (data === "p") {
@@ -277,7 +300,7 @@ export class ReviewComponent {
277
300
  ? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • editing inline comment • Enter save • Esc/Ctrl+C cancel`
278
301
  : this.hasSelection()
279
302
  ? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • J/K extend • Esc clear selection • c comment range • C overall comment • Enter submit`
280
- : `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • inline ${this.inlineAnnotationsVisible ? "shown" : "hidden"} • j/k move • g/G top/bottom • ctrl-u/d page • t annotations • v unified/split • ? explain • J/K extend • c comment • C overall • x delete • n/p hunk • Enter submit • q quit`,
303
+ : `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • inline ${this.inlineAnnotationsVisible ? "shown" : "hidden"} • j/k move • g/G top/bottom • ctrl-u/d page • / search • t annotations • v unified/split • ? explain • J/K extend • c comment • C overall • x delete • ${this.searchQuery ? "n/N search" : "n/p hunk"} • Enter submit • q quit`,
281
304
  ),
282
305
  width,
283
306
  ),
@@ -846,6 +869,13 @@ export class ReviewComponent {
846
869
  }
847
870
 
848
871
  private getFooterText(selectedLine?: ReviewLine): string {
872
+ if (this.searchMode) {
873
+ return `Search: /${this.draftSearchQuery} • Enter jump • Esc cancel`;
874
+ }
875
+
876
+ const searchStatus = this.getSearchStatusText();
877
+ if (searchStatus) return searchStatus;
878
+
849
879
  const selection = this.getSelectionBounds();
850
880
  if (selection) {
851
881
  const count = selection.end - selection.start + 1;
@@ -856,6 +886,21 @@ export class ReviewComponent {
856
886
  return `Selected: ${selectedLine ? formatLocation(selectedLine) : "(no selection)"}`;
857
887
  }
858
888
 
889
+ private getSearchStatusText(): string {
890
+ if (this.searchMessage) return this.searchMessage;
891
+ if (!this.searchQuery) return "";
892
+
893
+ const matches = this.getSearchMatchIndexes(this.searchQuery);
894
+ if (matches.length === 0) return `No matches for /${this.searchQuery}`;
895
+
896
+ const current = matches.findIndex((index) => index === this.selected);
897
+ const position =
898
+ current >= 0
899
+ ? `${current + 1}/${matches.length}`
900
+ : `${matches.length} matches`;
901
+ return `Search /${this.searchQuery} • ${position} • n next • N previous • Esc clear search`;
902
+ }
903
+
859
904
  private jumpHunk(direction: 1 | -1): void {
860
905
  let index = this.selected + direction;
861
906
  while (index >= 0 && index < this.lines.length) {
@@ -919,6 +964,93 @@ export class ReviewComponent {
919
964
  this.tui.requestRender(true);
920
965
  }
921
966
 
967
+ private startSearchMode(): void {
968
+ this.searchMode = true;
969
+ this.draftSearchQuery = this.searchQuery;
970
+ this.searchMessage = "";
971
+ this.tui.requestRender();
972
+ }
973
+
974
+ private handleSearchInput(data: string): void {
975
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
976
+ this.searchMode = false;
977
+ this.draftSearchQuery = "";
978
+ this.tui.requestRender();
979
+ return;
980
+ }
981
+
982
+ if (matchesKey(data, "enter")) {
983
+ const query = this.draftSearchQuery.trim();
984
+ this.searchMode = false;
985
+ this.draftSearchQuery = "";
986
+ this.searchQuery = query;
987
+ this.searchMessage = "";
988
+ if (query) {
989
+ this.jumpSearch(1);
990
+ } else {
991
+ this.tui.requestRender();
992
+ }
993
+ return;
994
+ }
995
+
996
+ if (matchesKey(data, "ctrl+u")) {
997
+ this.draftSearchQuery = "";
998
+ this.tui.requestRender();
999
+ return;
1000
+ }
1001
+
1002
+ if (matchesKey(data, "backspace") || data === "\u007f" || data === "\b") {
1003
+ this.draftSearchQuery = this.draftSearchQuery.slice(0, -1);
1004
+ this.tui.requestRender();
1005
+ return;
1006
+ }
1007
+
1008
+ if (data.length === 1 && data >= " " && data !== "\u007f") {
1009
+ this.draftSearchQuery += data;
1010
+ this.tui.requestRender();
1011
+ }
1012
+ }
1013
+
1014
+ private clearSearch(): void {
1015
+ this.searchMode = false;
1016
+ this.searchQuery = "";
1017
+ this.draftSearchQuery = "";
1018
+ this.searchMessage = "";
1019
+ this.tui.requestRender();
1020
+ }
1021
+
1022
+ private jumpSearch(direction: 1 | -1): void {
1023
+ const query = this.searchQuery.trim();
1024
+ if (!query) return;
1025
+
1026
+ const matches = this.getSearchMatchIndexes(query);
1027
+ if (matches.length === 0) {
1028
+ this.searchMessage = `No matches for /${query}`;
1029
+ this.tui.requestRender();
1030
+ return;
1031
+ }
1032
+
1033
+ this.searchMessage = "";
1034
+ const current =
1035
+ direction === 1
1036
+ ? matches.find((index) => index > this.selected)
1037
+ : [...matches].reverse().find((index) => index < this.selected);
1038
+ this.selected =
1039
+ current ?? (direction === 1 ? matches[0]! : matches[matches.length - 1]!);
1040
+ this.tui.requestRender();
1041
+ }
1042
+
1043
+ private getSearchMatchIndexes(query: string): number[] {
1044
+ const needle = query.toLocaleLowerCase();
1045
+ if (!needle) return [];
1046
+
1047
+ const matches: number[] = [];
1048
+ this.lines.forEach((line, index) => {
1049
+ if (line.text.toLocaleLowerCase().includes(needle)) matches.push(index);
1050
+ });
1051
+ return matches;
1052
+ }
1053
+
922
1054
  private getSplitDiffRows(): SplitDiffRow[] {
923
1055
  if (this.splitRows) return this.splitRows;
924
1056