pi-diff-review 0.1.20 → 0.1.21
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 +1 -1
- package/package.json +1 -1
- package/src/review/component.ts +44 -8
- package/src/review/search.ts +83 -20
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Open one or more files or folders with `/view`:
|
|
|
78
78
|
- `t` toggles inline comments/explanations
|
|
79
79
|
- `v` toggles the diff between unified and side-by-side split rendering
|
|
80
80
|
- `?` toggles an AI-generated explanation for the current hunk
|
|
81
|
-
- `/` searches diff
|
|
81
|
+
- `/` searches visible diff text, highlights matches, and `n/N` moves between them while a search is active
|
|
82
82
|
- `J/K` to extend a highlighted selection into a comment range
|
|
83
83
|
- `esc` clears the active selection, or exits review when no selection is active
|
|
84
84
|
- `n/p` to jump hunks
|
package/package.json
CHANGED
package/src/review/component.ts
CHANGED
|
@@ -1375,7 +1375,7 @@ export class ReviewComponent {
|
|
|
1375
1375
|
const lineNumber =
|
|
1376
1376
|
side === "left" ? line.oldLineNumber : line.newLineNumber;
|
|
1377
1377
|
const prefix = `${commentMark} ${lineNumberCell(lineNumber)} `;
|
|
1378
|
-
let styled = this.renderDiffRowContent(line, prefix);
|
|
1378
|
+
let styled = this.renderDiffRowContent(line, prefix, index);
|
|
1379
1379
|
|
|
1380
1380
|
styled = truncateToWidth(styled, width);
|
|
1381
1381
|
const selection = this.getSelectionBounds();
|
|
@@ -1428,14 +1428,18 @@ export class ReviewComponent {
|
|
|
1428
1428
|
return styled;
|
|
1429
1429
|
}
|
|
1430
1430
|
|
|
1431
|
-
private renderDiffRowContent(
|
|
1431
|
+
private renderDiffRowContent(
|
|
1432
|
+
line: ReviewLine,
|
|
1433
|
+
prefix: string,
|
|
1434
|
+
index: number,
|
|
1435
|
+
): string {
|
|
1432
1436
|
switch (line.kind) {
|
|
1433
1437
|
case "add":
|
|
1434
|
-
return `${this.theme.fg("toolDiffAdded", prefix)}${this.getHighlightedDisplayText(line)}`;
|
|
1438
|
+
return `${this.theme.fg("toolDiffAdded", prefix)}${this.getHighlightedDisplayText(line, index)}`;
|
|
1435
1439
|
case "remove":
|
|
1436
|
-
return `${this.theme.fg("toolDiffRemoved", prefix)}${this.getHighlightedDisplayText(line)}`;
|
|
1440
|
+
return `${this.theme.fg("toolDiffRemoved", prefix)}${this.getHighlightedDisplayText(line, index)}`;
|
|
1437
1441
|
case "context":
|
|
1438
|
-
return `${this.theme.fg("toolDiffContext", prefix)}${this.getHighlightedDisplayText(line)}`;
|
|
1442
|
+
return `${this.theme.fg("toolDiffContext", prefix)}${this.getHighlightedDisplayText(line, index)}`;
|
|
1439
1443
|
case "hunk":
|
|
1440
1444
|
return this.theme.fg("accent", `${prefix}${this.getDisplayText(line)}`);
|
|
1441
1445
|
default:
|
|
@@ -1443,15 +1447,21 @@ export class ReviewComponent {
|
|
|
1443
1447
|
}
|
|
1444
1448
|
}
|
|
1445
1449
|
|
|
1446
|
-
private getHighlightedDisplayText(line: ReviewLine): string {
|
|
1450
|
+
private getHighlightedDisplayText(line: ReviewLine, index: number): string {
|
|
1447
1451
|
const code = this.getDisplayText(line);
|
|
1448
1452
|
if (!code) return code;
|
|
1449
1453
|
|
|
1450
1454
|
const lang = line.filePath ? getLanguageFromPath(line.filePath) : undefined;
|
|
1451
|
-
const cacheKey = `${line.id}\0${lang ?? ""}\0${code}`;
|
|
1455
|
+
const cacheKey = `${line.id}\0${lang ?? ""}\0${this.search.getHighlightCacheKey()}\0${code}`;
|
|
1452
1456
|
const cached = this.highlightedLineCache.get(cacheKey);
|
|
1453
1457
|
if (cached != null) return cached;
|
|
1454
1458
|
|
|
1459
|
+
const searchHighlighted = this.getSearchHighlightedDisplayText(code, index);
|
|
1460
|
+
if (searchHighlighted) {
|
|
1461
|
+
this.highlightedLineCache.set(cacheKey, searchHighlighted);
|
|
1462
|
+
return searchHighlighted;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1455
1465
|
let highlighted = code;
|
|
1456
1466
|
try {
|
|
1457
1467
|
highlighted = highlightCode(code, lang)[0] ?? code;
|
|
@@ -1463,6 +1473,32 @@ export class ReviewComponent {
|
|
|
1463
1473
|
return highlighted;
|
|
1464
1474
|
}
|
|
1465
1475
|
|
|
1476
|
+
private getSearchHighlightedDisplayText(
|
|
1477
|
+
code: string,
|
|
1478
|
+
index: number,
|
|
1479
|
+
): string | undefined {
|
|
1480
|
+
const matches = this.search.getMatchesForLine(index);
|
|
1481
|
+
if (matches.length === 0) return undefined;
|
|
1482
|
+
|
|
1483
|
+
const activeMatch = this.search.getActiveMatch();
|
|
1484
|
+
let result = "";
|
|
1485
|
+
let cursor = 0;
|
|
1486
|
+
for (const match of matches) {
|
|
1487
|
+
result += code.slice(cursor, match.start);
|
|
1488
|
+
const text = code.slice(match.start, match.end);
|
|
1489
|
+
const isActive =
|
|
1490
|
+
activeMatch?.lineIndex === match.lineIndex &&
|
|
1491
|
+
activeMatch.start === match.start &&
|
|
1492
|
+
activeMatch.end === match.end;
|
|
1493
|
+
result += isActive
|
|
1494
|
+
? this.theme.bg("selectedBg", this.theme.fg("warning", text))
|
|
1495
|
+
: this.theme.bg("selectedBg", this.theme.fg("accent", text));
|
|
1496
|
+
cursor = match.end;
|
|
1497
|
+
}
|
|
1498
|
+
result += code.slice(cursor);
|
|
1499
|
+
return result;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1466
1502
|
private renderDiffLine(
|
|
1467
1503
|
line: ReviewLine,
|
|
1468
1504
|
index: number,
|
|
@@ -1474,7 +1510,7 @@ export class ReviewComponent {
|
|
|
1474
1510
|
const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
|
|
1475
1511
|
const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
|
|
1476
1512
|
const prefix = `${commentMark} ${numbers} `;
|
|
1477
|
-
let styled = this.renderDiffRowContent(line, prefix);
|
|
1513
|
+
let styled = this.renderDiffRowContent(line, prefix, index);
|
|
1478
1514
|
|
|
1479
1515
|
styled = truncateToWidth(styled, width);
|
|
1480
1516
|
const inSelection =
|
package/src/review/search.ts
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { ReviewLine } from "./types.ts";
|
|
3
3
|
|
|
4
|
+
function getSearchableLineText(line: ReviewLine): string {
|
|
5
|
+
return (
|
|
6
|
+
line.kind === "add" || line.kind === "remove" || line.kind === "context"
|
|
7
|
+
? line.text.slice(1)
|
|
8
|
+
: line.text
|
|
9
|
+
).toLocaleLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
export type SearchInputResult = {
|
|
5
13
|
selected?: number;
|
|
6
14
|
};
|
|
7
15
|
|
|
16
|
+
export type SearchMatch = {
|
|
17
|
+
lineIndex: number;
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
8
22
|
export class ReviewSearchState {
|
|
9
23
|
mode = false;
|
|
10
|
-
query = "";
|
|
11
24
|
draftQuery = "";
|
|
12
25
|
message = "";
|
|
26
|
+
private _query = "";
|
|
27
|
+
private activeMatchIndex = -1;
|
|
13
28
|
|
|
14
29
|
constructor(private readonly lines: ReviewLine[]) {}
|
|
15
30
|
|
|
31
|
+
get query(): string {
|
|
32
|
+
return this._query;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
set query(value: string) {
|
|
36
|
+
this._query = value;
|
|
37
|
+
this.activeMatchIndex = -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
start(): void {
|
|
17
41
|
this.mode = true;
|
|
18
42
|
this.draftQuery = this.query;
|
|
@@ -63,46 +87,85 @@ export class ReviewSearchState {
|
|
|
63
87
|
const query = this.query.trim();
|
|
64
88
|
if (!query) return {};
|
|
65
89
|
|
|
66
|
-
const matches = this.
|
|
90
|
+
const matches = this.getMatches(query);
|
|
67
91
|
if (matches.length === 0) {
|
|
92
|
+
this.activeMatchIndex = -1;
|
|
68
93
|
this.message = `No matches for /${query}`;
|
|
69
94
|
return {};
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
this.message = "";
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
let nextIndex: number;
|
|
99
|
+
if (this.activeMatchIndex >= 0 && this.activeMatchIndex < matches.length) {
|
|
100
|
+
nextIndex =
|
|
101
|
+
(this.activeMatchIndex + direction + matches.length) % matches.length;
|
|
102
|
+
} else {
|
|
103
|
+
if (direction === 1) {
|
|
104
|
+
nextIndex = matches.findIndex((match) => match.lineIndex >= selected);
|
|
105
|
+
} else {
|
|
106
|
+
nextIndex = -1;
|
|
107
|
+
for (let index = matches.length - 1; index >= 0; index--) {
|
|
108
|
+
if (matches[index]!.lineIndex <= selected) {
|
|
109
|
+
nextIndex = index;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (nextIndex < 0) nextIndex = direction === 1 ? 0 : matches.length - 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.activeMatchIndex = nextIndex;
|
|
118
|
+
return { selected: matches[nextIndex]!.lineIndex };
|
|
82
119
|
}
|
|
83
120
|
|
|
84
|
-
getStatusText(
|
|
121
|
+
getStatusText(_selected: number): string {
|
|
85
122
|
if (this.message) return this.message;
|
|
86
123
|
if (!this.query) return "";
|
|
87
124
|
|
|
88
|
-
const matches = this.
|
|
125
|
+
const matches = this.getMatches(this.query);
|
|
89
126
|
if (matches.length === 0) return `No matches for /${this.query}`;
|
|
90
127
|
|
|
91
|
-
const current = matches.findIndex((index) => index === selected);
|
|
92
128
|
const position =
|
|
93
|
-
|
|
94
|
-
? `${
|
|
129
|
+
this.activeMatchIndex >= 0 && this.activeMatchIndex < matches.length
|
|
130
|
+
? `${this.activeMatchIndex + 1}/${matches.length}`
|
|
95
131
|
: `${matches.length} matches`;
|
|
96
132
|
return `Search /${this.query} • ${position} • n next • N previous • Esc clear search`;
|
|
97
133
|
}
|
|
98
134
|
|
|
99
|
-
|
|
135
|
+
getMatchesForLine(lineIndex: number): SearchMatch[] {
|
|
136
|
+
if (!this.query.trim()) return [];
|
|
137
|
+
return this.getMatches(this.query).filter(
|
|
138
|
+
(match) => match.lineIndex === lineIndex,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getActiveMatch(): SearchMatch | undefined {
|
|
143
|
+
if (!this.query.trim()) return undefined;
|
|
144
|
+
const matches = this.getMatches(this.query);
|
|
145
|
+
return this.activeMatchIndex >= 0 && this.activeMatchIndex < matches.length
|
|
146
|
+
? matches[this.activeMatchIndex]
|
|
147
|
+
: undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getHighlightCacheKey(): string {
|
|
151
|
+
const active = this.getActiveMatch();
|
|
152
|
+
return active
|
|
153
|
+
? `${this.query}\0${active.lineIndex}:${active.start}-${active.end}`
|
|
154
|
+
: this.query;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private getMatches(query: string): SearchMatch[] {
|
|
100
158
|
const needle = query.toLocaleLowerCase();
|
|
101
159
|
if (!needle) return [];
|
|
102
160
|
|
|
103
|
-
const matches:
|
|
104
|
-
this.lines.forEach((line,
|
|
105
|
-
|
|
161
|
+
const matches: SearchMatch[] = [];
|
|
162
|
+
this.lines.forEach((line, lineIndex) => {
|
|
163
|
+
const haystack = getSearchableLineText(line);
|
|
164
|
+
let start = haystack.indexOf(needle);
|
|
165
|
+
while (start >= 0) {
|
|
166
|
+
matches.push({ lineIndex, start, end: start + needle.length });
|
|
167
|
+
start = haystack.indexOf(needle, start + needle.length);
|
|
168
|
+
}
|
|
106
169
|
});
|
|
107
170
|
return matches;
|
|
108
171
|
}
|