@narumitw/pi-files 0.0.0-stage → 0.1.0
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +102 -2
- package/dist/chunks/chunk-3GQ7VS22.ts +273 -0
- package/dist/chunks/chunk-3GQ7VS22.ts.map +7 -0
- package/dist/chunks/chunk-AXW73HDJ.ts +31 -0
- package/dist/chunks/chunk-AXW73HDJ.ts.map +7 -0
- package/dist/chunks/chunk-F5QFC255.ts +101 -0
- package/dist/chunks/chunk-F5QFC255.ts.map +7 -0
- package/dist/chunks/chunk-NFNGJETD.ts +77 -0
- package/dist/chunks/chunk-NFNGJETD.ts.map +7 -0
- package/dist/chunks/chunk-OPGTWNJI.ts +258 -0
- package/dist/chunks/chunk-OPGTWNJI.ts.map +7 -0
- package/dist/chunks/chunk-WOTDNRTG.ts +242 -0
- package/dist/chunks/chunk-WOTDNRTG.ts.map +7 -0
- package/dist/chunks/file-context-explorer-WR6GQZHF.ts +1793 -0
- package/dist/chunks/file-context-explorer-WR6GQZHF.ts.map +7 -0
- package/dist/chunks/file-context-menu-VPLHUUQ6.ts +302 -0
- package/dist/chunks/file-context-menu-VPLHUUQ6.ts.map +7 -0
- package/dist/chunks/files-menu-MES4MQ4F.ts +119 -0
- package/dist/chunks/files-menu-MES4MQ4F.ts.map +7 -0
- package/dist/chunks/files-settings-NYXQU2HV.ts +295 -0
- package/dist/chunks/files-settings-NYXQU2HV.ts.map +7 -0
- package/dist/chunks/menu-P6IBMROH.ts +876 -0
- package/dist/chunks/menu-P6IBMROH.ts.map +7 -0
- package/dist/index.ts +1789 -0
- package/dist/index.ts.map +7 -0
- package/docs/edit-scope-reference.md +142 -0
- package/docs/file-context-reference.md +91 -0
- package/docs/migration.md +105 -0
- package/docs/verification.md +99 -0
- package/package.json +58 -4
- package/src/content-search-session.ts +204 -0
- package/src/content-search-ui.ts +159 -0
- package/src/content-search.ts +170 -0
- package/src/context-service.ts +515 -0
- package/src/external-editor.ts +143 -0
- package/src/file-browser-ui.ts +128 -0
- package/src/file-browser.ts +76 -0
- package/src/file-context-explorer.ts +992 -0
- package/src/file-context-menu.ts +360 -0
- package/src/file-context-preview-ui.ts +354 -0
- package/src/file-context-settings.ts +301 -0
- package/src/file-context.ts +321 -0
- package/src/file-search.ts +246 -0
- package/src/files-menu.ts +125 -0
- package/src/files-settings.ts +314 -0
- package/src/files-widget.ts +65 -0
- package/src/files.ts +148 -0
- package/src/git-context.ts +485 -0
- package/src/index.ts +1 -0
- package/src/scope/contracts.ts +243 -0
- package/src/scope/display.ts +27 -0
- package/src/scope/edit-scope.ts +435 -0
- package/src/scope/files.ts +118 -0
- package/src/scope/input.ts +207 -0
- package/src/scope/menu.ts +277 -0
- package/src/scope/paths.ts +142 -0
- package/src/scope/picker-keys.ts +154 -0
- package/src/scope/picker.ts +484 -0
- package/src/scope/policy.ts +55 -0
- package/src/scope/settings.ts +138 -0
- package/src/scope/state.ts +91 -0
- package/src/scope/ui.ts +106 -0
|
@@ -0,0 +1,1793 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
createFileQuoteSnapshot,
|
|
5
|
+
createPreviewQuote
|
|
6
|
+
} from "./chunk-WOTDNRTG.ts";
|
|
7
|
+
|
|
8
|
+
// src/file-context-explorer.ts
|
|
9
|
+
import {
|
|
10
|
+
Input as Input2,
|
|
11
|
+
Key as Key2,
|
|
12
|
+
matchesKey as matchesKey2,
|
|
13
|
+
truncateToWidth as truncateToWidth4,
|
|
14
|
+
visibleWidth as visibleWidth3
|
|
15
|
+
} from "@earendil-works/pi-tui";
|
|
16
|
+
|
|
17
|
+
// src/content-search-session.ts
|
|
18
|
+
import { Input, Key, matchesKey, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
19
|
+
|
|
20
|
+
// src/content-search.ts
|
|
21
|
+
var MAX_CONTENT_QUERY_LENGTH = 256;
|
|
22
|
+
var DEFAULT_MAX_RESULTS = 100;
|
|
23
|
+
async function searchProjectContents(files, loadFile, query, options = {}) {
|
|
24
|
+
const signal = options.signal;
|
|
25
|
+
signal?.throwIfAborted();
|
|
26
|
+
if (!query.trim() || query.length > MAX_CONTENT_QUERY_LENGTH) {
|
|
27
|
+
return { matches: [], truncated: false, skippedFiles: 0 };
|
|
28
|
+
}
|
|
29
|
+
const maxResults = Math.max(1, options.maxResults ?? DEFAULT_MAX_RESULTS);
|
|
30
|
+
const caseSensitive = options.caseSensitive ?? false;
|
|
31
|
+
const literalExpression = new RegExp(escapeRegExp(query), caseSensitive ? "g" : "gi");
|
|
32
|
+
const fuzzyQuery = options.fuzzy ? indexedCharacters(query, caseSensitive) : [];
|
|
33
|
+
const matches = [];
|
|
34
|
+
let truncated = false;
|
|
35
|
+
let skippedFiles = 0;
|
|
36
|
+
for (const path of files) {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
let file;
|
|
39
|
+
try {
|
|
40
|
+
file = await loadFile(path, signal);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (isAbortError(error) || signal?.aborted) throw error;
|
|
43
|
+
skippedFiles += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
for (let lineIndex = 0; lineIndex < file.lines.length; lineIndex += 1) {
|
|
48
|
+
const line = file.lines[lineIndex] ?? "";
|
|
49
|
+
const literalRanges = findLiteralRanges(line, literalExpression);
|
|
50
|
+
if (literalRanges.length > 0) {
|
|
51
|
+
if (matches.length < maxResults) {
|
|
52
|
+
matches.push({
|
|
53
|
+
path: file.path,
|
|
54
|
+
lineNumber: lineIndex + 1,
|
|
55
|
+
line,
|
|
56
|
+
ranges: literalRanges,
|
|
57
|
+
fuzzy: false
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
truncated = true;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!options.fuzzy) continue;
|
|
65
|
+
const fuzzyRanges = findSubsequenceRanges(line, fuzzyQuery, caseSensitive);
|
|
66
|
+
if (!fuzzyRanges) continue;
|
|
67
|
+
if (matches.length < maxResults) {
|
|
68
|
+
matches.push({
|
|
69
|
+
path: file.path,
|
|
70
|
+
lineNumber: lineIndex + 1,
|
|
71
|
+
line,
|
|
72
|
+
ranges: fuzzyRanges,
|
|
73
|
+
fuzzy: true
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
truncated = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { matches, truncated, skippedFiles };
|
|
81
|
+
}
|
|
82
|
+
function findLiteralRanges(line, expression) {
|
|
83
|
+
const ranges = [];
|
|
84
|
+
expression.lastIndex = 0;
|
|
85
|
+
for (let match = expression.exec(line); match; match = expression.exec(line)) {
|
|
86
|
+
const start = match.index;
|
|
87
|
+
ranges.push({ start, end: start + match[0].length });
|
|
88
|
+
}
|
|
89
|
+
return ranges;
|
|
90
|
+
}
|
|
91
|
+
function findSubsequenceRanges(line, queryCharacters, caseSensitive) {
|
|
92
|
+
const lineCharacters = indexedCharacters(line, caseSensitive);
|
|
93
|
+
const positions = [];
|
|
94
|
+
let queryIndex = 0;
|
|
95
|
+
for (let lineIndex = 0; lineIndex < lineCharacters.length && queryIndex < queryCharacters.length; lineIndex += 1) {
|
|
96
|
+
const lineCharacter = lineCharacters[lineIndex];
|
|
97
|
+
const queryCharacter = queryCharacters[queryIndex];
|
|
98
|
+
if (!lineCharacter || !queryCharacter || lineCharacter.comparable !== queryCharacter.comparable) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
positions.push({ start: lineCharacter.start, end: lineCharacter.end });
|
|
102
|
+
queryIndex += 1;
|
|
103
|
+
}
|
|
104
|
+
if (queryIndex !== queryCharacters.length) return void 0;
|
|
105
|
+
const ranges = [];
|
|
106
|
+
for (const position of positions) {
|
|
107
|
+
const previous = ranges.at(-1);
|
|
108
|
+
if (previous?.end === position.start) previous.end = position.end;
|
|
109
|
+
else ranges.push(position);
|
|
110
|
+
}
|
|
111
|
+
return ranges;
|
|
112
|
+
}
|
|
113
|
+
function indexedCharacters(value, caseSensitive) {
|
|
114
|
+
const characters = [];
|
|
115
|
+
for (let start = 0; start < value.length; ) {
|
|
116
|
+
const codePoint = value.codePointAt(start);
|
|
117
|
+
if (codePoint === void 0) break;
|
|
118
|
+
const character = String.fromCodePoint(codePoint);
|
|
119
|
+
const end = start + character.length;
|
|
120
|
+
characters.push({
|
|
121
|
+
start,
|
|
122
|
+
end,
|
|
123
|
+
comparable: caseSensitive ? character : character.toLowerCase()
|
|
124
|
+
});
|
|
125
|
+
start = end;
|
|
126
|
+
}
|
|
127
|
+
return characters;
|
|
128
|
+
}
|
|
129
|
+
function escapeRegExp(value) {
|
|
130
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
131
|
+
}
|
|
132
|
+
function isAbortError(error) {
|
|
133
|
+
return error instanceof Error && error.name === "AbortError";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/content-search-ui.ts
|
|
137
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
138
|
+
var CONTENT_SEARCH_CARD_ROWS = 3;
|
|
139
|
+
function contentSearchCardCapacity(availableRows) {
|
|
140
|
+
return Math.max(1, Math.floor((availableRows - 4) / CONTENT_SEARCH_CARD_ROWS));
|
|
141
|
+
}
|
|
142
|
+
function renderContentSearchScreen(options) {
|
|
143
|
+
const { theme, width, availableRows } = options;
|
|
144
|
+
const capacity = contentSearchCardCapacity(availableRows);
|
|
145
|
+
const cwdLabel = options.cwd ? ` \xB7 cwd: ${escapeTerminalControls(options.cwd)}` : "";
|
|
146
|
+
const title = theme.fg("accent", theme.bold(`File Context \xB7 Content Search${cwdLabel}`));
|
|
147
|
+
const modes = theme.fg(
|
|
148
|
+
"muted",
|
|
149
|
+
`Case: ${options.caseSensitive ? "on" : "off"} \xB7 Fuzzy: ${options.fuzzy ? "on" : "off"} \xB7 Alt+C case \xB7 Alt+F fuzzy`
|
|
150
|
+
);
|
|
151
|
+
const body = [];
|
|
152
|
+
if (options.error) {
|
|
153
|
+
body.push(theme.fg("error", ` Search failed: ${escapeTerminalControls(options.error)}`));
|
|
154
|
+
} else if (options.loading) {
|
|
155
|
+
body.push(theme.fg("warning", " Searching\u2026"));
|
|
156
|
+
} else if (!options.query.trim()) {
|
|
157
|
+
body.push(theme.fg("muted", " Type text to search project contents"));
|
|
158
|
+
} else if (options.matches.length === 0) {
|
|
159
|
+
body.push(
|
|
160
|
+
theme.fg(
|
|
161
|
+
"muted",
|
|
162
|
+
` No matches for "${escapeTerminalControls(options.query)}"${options.fuzzy ? "" : " \xB7 Alt+F enables fuzzy matching"}`
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
const visible = options.matches.slice(options.scrollOffset, options.scrollOffset + capacity);
|
|
167
|
+
for (let visibleIndex = 0; visibleIndex < visible.length; visibleIndex += 1) {
|
|
168
|
+
const index = options.scrollOffset + visibleIndex;
|
|
169
|
+
const match = visible[visibleIndex];
|
|
170
|
+
if (match) {
|
|
171
|
+
body.push(...renderContentSearchCard(match, index === options.selectedIndex, width, theme));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const count = `${options.matches.length}${options.truncated ? "+" : ""} matches`;
|
|
176
|
+
const skipped = options.skippedFiles > 0 ? ` \xB7 ${options.skippedFiles} skipped` : "";
|
|
177
|
+
const action = options.opening ? "Opening\u2026" : `${count}${skipped} \xB7 \u2191\u2193 navigate \xB7 Enter preview \xB7 Tab reference \xB7 Ctrl+F files \xB7 Esc cancel`;
|
|
178
|
+
return fitRows(
|
|
179
|
+
[
|
|
180
|
+
truncateToWidth(title, width, ""),
|
|
181
|
+
truncateToWidth(options.queryLine, width, ""),
|
|
182
|
+
truncateToWidth(modes, width, ""),
|
|
183
|
+
...body.map((line) => truncateToWidth(line, width, "")),
|
|
184
|
+
truncateToWidth(theme.fg(options.opening ? "warning" : "muted", action), width, "")
|
|
185
|
+
],
|
|
186
|
+
availableRows
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
function highlightContentRanges(line, ranges, theme) {
|
|
190
|
+
let result = "";
|
|
191
|
+
let index = 0;
|
|
192
|
+
for (const range of ranges) {
|
|
193
|
+
const start = Math.max(index, Math.min(line.length, range.start));
|
|
194
|
+
const end = Math.max(start, Math.min(line.length, range.end));
|
|
195
|
+
result += escapeTerminalControls(line.slice(index, start));
|
|
196
|
+
result += theme.fg("warning", theme.bold(escapeTerminalControls(line.slice(start, end))));
|
|
197
|
+
index = end;
|
|
198
|
+
}
|
|
199
|
+
return result + escapeTerminalControls(line.slice(index));
|
|
200
|
+
}
|
|
201
|
+
function renderContentSearchCard(match, selected, width, theme) {
|
|
202
|
+
if (width < 8) {
|
|
203
|
+
return [truncateToWidth(`${selected ? ">" : " "}${escapeTerminalControls(match.path)}`, width, "")];
|
|
204
|
+
}
|
|
205
|
+
const prefix = selected ? theme.fg("accent", "> ") : " ";
|
|
206
|
+
const cardWidth = Math.max(4, width - visibleWidth(prefix));
|
|
207
|
+
const innerWidth = Math.max(1, cardWidth - 2);
|
|
208
|
+
const borderColor = selected ? "borderAccent" : "borderMuted";
|
|
209
|
+
const border = (text) => theme.fg(borderColor, text);
|
|
210
|
+
const rawTitle = ` ${escapeTerminalControls(match.path)} \xB7 L${match.lineNumber} `;
|
|
211
|
+
const title = truncateToWidth(rawTitle, innerWidth, "");
|
|
212
|
+
const titleFill = "\u2500".repeat(Math.max(0, innerWidth - visibleWidth(title)));
|
|
213
|
+
const context = clipMatchContext(match, Math.max(1, innerWidth - 1));
|
|
214
|
+
const highlighted = highlightContentRanges(context.line, context.ranges, theme);
|
|
215
|
+
const content = truncateToWidth(` ${highlighted}`, innerWidth, "");
|
|
216
|
+
const contentFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
|
|
217
|
+
const contentLine = `${border("\u2502")}${content}${contentFill}${border("\u2502")}`;
|
|
218
|
+
return [
|
|
219
|
+
`${prefix}${border("\u256D")}${title}${border(`${titleFill}\u256E`)}`,
|
|
220
|
+
`${" ".repeat(visibleWidth(prefix))}${selected ? theme.bg("selectedBg", contentLine) : contentLine}`,
|
|
221
|
+
`${" ".repeat(visibleWidth(prefix))}${border(`\u2570${"\u2500".repeat(innerWidth)}\u256F`)}`
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
function clipMatchContext(match, maxCharacters) {
|
|
225
|
+
const firstStart = match.ranges[0]?.start ?? 0;
|
|
226
|
+
const lastEnd = match.ranges.at(-1)?.end ?? firstStart;
|
|
227
|
+
const matchSpan = Math.max(1, lastEnd - firstStart);
|
|
228
|
+
const before = Math.max(0, Math.floor((maxCharacters - Math.min(matchSpan, maxCharacters)) / 3));
|
|
229
|
+
const rawStart = Math.max(0, firstStart - before);
|
|
230
|
+
const prefix = rawStart > 0 ? "\u2026" : "";
|
|
231
|
+
const availableRawCharacters = Math.max(1, maxCharacters - prefix.length - 1);
|
|
232
|
+
let rawEnd = Math.min(match.line.length, rawStart + availableRawCharacters);
|
|
233
|
+
if (rawEnd < lastEnd) rawEnd = Math.min(match.line.length, lastEnd);
|
|
234
|
+
const suffix = rawEnd < match.line.length ? "\u2026" : "";
|
|
235
|
+
const line = `${prefix}${match.line.slice(rawStart, rawEnd)}${suffix}`;
|
|
236
|
+
const ranges = match.ranges.flatMap((range) => {
|
|
237
|
+
const start = Math.max(rawStart, range.start);
|
|
238
|
+
const end = Math.min(rawEnd, range.end);
|
|
239
|
+
return end > start ? [{ start: prefix.length + start - rawStart, end: prefix.length + end - rawStart }] : [];
|
|
240
|
+
});
|
|
241
|
+
return { line, ranges };
|
|
242
|
+
}
|
|
243
|
+
function fitRows(lines, height) {
|
|
244
|
+
if (lines.length <= height) return lines;
|
|
245
|
+
if (height <= 1) return lines.slice(0, 1);
|
|
246
|
+
return [...lines.slice(0, height - 1), lines.at(-1) ?? ""];
|
|
247
|
+
}
|
|
248
|
+
function escapeTerminalControls(text) {
|
|
249
|
+
return [...text].map((character) => {
|
|
250
|
+
if (character === " ") return " ";
|
|
251
|
+
const code = character.charCodeAt(0);
|
|
252
|
+
if (code <= 31 || code >= 127 && code <= 159) {
|
|
253
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
254
|
+
}
|
|
255
|
+
return character;
|
|
256
|
+
}).join("");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/content-search-session.ts
|
|
260
|
+
var ContentSearchSession = class {
|
|
261
|
+
constructor(options) {
|
|
262
|
+
this.options = options;
|
|
263
|
+
}
|
|
264
|
+
options;
|
|
265
|
+
input = new Input();
|
|
266
|
+
matches = [];
|
|
267
|
+
selectedIndex = 0;
|
|
268
|
+
scrollOffset = 0;
|
|
269
|
+
caseSensitive = false;
|
|
270
|
+
fuzzy = false;
|
|
271
|
+
truncated = false;
|
|
272
|
+
skippedFiles = 0;
|
|
273
|
+
loading = false;
|
|
274
|
+
request = 0;
|
|
275
|
+
controller;
|
|
276
|
+
error;
|
|
277
|
+
disposed = false;
|
|
278
|
+
set focused(value) {
|
|
279
|
+
this.input.focused = value;
|
|
280
|
+
}
|
|
281
|
+
activate() {
|
|
282
|
+
this.disposed = false;
|
|
283
|
+
if (this.input.getValue().trim()) this.startSearch();
|
|
284
|
+
}
|
|
285
|
+
deactivate() {
|
|
286
|
+
this.focused = false;
|
|
287
|
+
this.cancelSearch();
|
|
288
|
+
}
|
|
289
|
+
render(width, availableRows, opening, externalError) {
|
|
290
|
+
const capacity = contentSearchCardCapacity(availableRows);
|
|
291
|
+
this.keepSelectionVisible(capacity);
|
|
292
|
+
const queryLabel = this.options.theme.fg("muted", "Search: ");
|
|
293
|
+
const queryWidth = Math.max(1, width - visibleWidth2(queryLabel));
|
|
294
|
+
const queryLine = `${queryLabel}${this.input.render(queryWidth)[0] ?? ""}`;
|
|
295
|
+
return renderContentSearchScreen({
|
|
296
|
+
theme: this.options.theme,
|
|
297
|
+
width,
|
|
298
|
+
availableRows,
|
|
299
|
+
queryLine,
|
|
300
|
+
query: this.input.getValue(),
|
|
301
|
+
cwd: this.options.cwd,
|
|
302
|
+
matches: this.matches,
|
|
303
|
+
selectedIndex: this.selectedIndex,
|
|
304
|
+
scrollOffset: this.scrollOffset,
|
|
305
|
+
caseSensitive: this.caseSensitive,
|
|
306
|
+
fuzzy: this.fuzzy,
|
|
307
|
+
loading: this.loading,
|
|
308
|
+
opening,
|
|
309
|
+
truncated: this.truncated,
|
|
310
|
+
skippedFiles: this.skippedFiles,
|
|
311
|
+
error: externalError ?? this.error
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
handleInput(data) {
|
|
315
|
+
if (this.disposed) return;
|
|
316
|
+
if (matchesKey(data, Key.ctrl("f"))) {
|
|
317
|
+
this.options.onSwitchFiles();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (matchesKey(data, Key.escape)) {
|
|
321
|
+
this.options.onCancel();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (matchesKey(data, Key.alt("c"))) {
|
|
325
|
+
this.caseSensitive = !this.caseSensitive;
|
|
326
|
+
this.startSearch();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (matchesKey(data, Key.alt("f"))) {
|
|
330
|
+
this.fuzzy = !this.fuzzy;
|
|
331
|
+
this.startSearch();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (this.options.keybindings.matches(data, "tui.select.up")) {
|
|
335
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (this.options.keybindings.matches(data, "tui.select.down")) {
|
|
339
|
+
this.selectedIndex = Math.min(Math.max(0, this.matches.length - 1), this.selectedIndex + 1);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (this.options.keybindings.matches(data, "tui.select.pageUp")) {
|
|
343
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 10);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (this.options.keybindings.matches(data, "tui.select.pageDown")) {
|
|
347
|
+
this.selectedIndex = Math.min(Math.max(0, this.matches.length - 1), this.selectedIndex + 10);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (this.options.keybindings.matches(data, "tui.select.confirm")) {
|
|
351
|
+
const match = this.matches[this.selectedIndex];
|
|
352
|
+
if (match) this.options.onPreview(match);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (this.options.keybindings.matches(data, "tui.input.tab")) {
|
|
356
|
+
const match = this.matches[this.selectedIndex];
|
|
357
|
+
if (match) this.options.onReference(match.path);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const previousQuery = this.input.getValue();
|
|
361
|
+
this.input.handleInput(data);
|
|
362
|
+
if (this.input.getValue() !== previousQuery) this.startSearch();
|
|
363
|
+
}
|
|
364
|
+
invalidate() {
|
|
365
|
+
this.input.invalidate();
|
|
366
|
+
}
|
|
367
|
+
dispose() {
|
|
368
|
+
if (this.disposed) return;
|
|
369
|
+
this.disposed = true;
|
|
370
|
+
this.cancelSearch();
|
|
371
|
+
}
|
|
372
|
+
startSearch() {
|
|
373
|
+
this.cancelSearch();
|
|
374
|
+
const query = this.input.getValue();
|
|
375
|
+
this.matches = [];
|
|
376
|
+
this.selectedIndex = 0;
|
|
377
|
+
this.scrollOffset = 0;
|
|
378
|
+
this.truncated = false;
|
|
379
|
+
this.skippedFiles = 0;
|
|
380
|
+
this.error = void 0;
|
|
381
|
+
if (!query.trim()) return;
|
|
382
|
+
const request = this.request;
|
|
383
|
+
const controller = new AbortController();
|
|
384
|
+
this.controller = controller;
|
|
385
|
+
this.loading = true;
|
|
386
|
+
void searchProjectContents(this.options.files, this.options.loadFile, query, {
|
|
387
|
+
caseSensitive: this.caseSensitive,
|
|
388
|
+
fuzzy: this.fuzzy,
|
|
389
|
+
signal: controller.signal
|
|
390
|
+
}).then((result) => {
|
|
391
|
+
if (!this.isCurrent(request, controller)) return;
|
|
392
|
+
this.matches = result.matches;
|
|
393
|
+
this.truncated = result.truncated;
|
|
394
|
+
this.skippedFiles = result.skippedFiles;
|
|
395
|
+
}).catch((error) => {
|
|
396
|
+
if (this.isCurrent(request, controller) && !isAbortError2(error)) {
|
|
397
|
+
this.error = formatError(error);
|
|
398
|
+
}
|
|
399
|
+
}).finally(() => {
|
|
400
|
+
if (request === this.request) {
|
|
401
|
+
this.loading = false;
|
|
402
|
+
this.controller = void 0;
|
|
403
|
+
}
|
|
404
|
+
if (!this.disposed) this.options.tui.requestRender();
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
cancelSearch() {
|
|
408
|
+
this.request += 1;
|
|
409
|
+
this.controller?.abort();
|
|
410
|
+
this.controller = void 0;
|
|
411
|
+
this.loading = false;
|
|
412
|
+
}
|
|
413
|
+
isCurrent(request, controller) {
|
|
414
|
+
return !this.disposed && request === this.request && this.controller === controller && !controller.signal.aborted;
|
|
415
|
+
}
|
|
416
|
+
keepSelectionVisible(capacity) {
|
|
417
|
+
if (this.selectedIndex < this.scrollOffset) this.scrollOffset = this.selectedIndex;
|
|
418
|
+
if (this.selectedIndex >= this.scrollOffset + capacity) {
|
|
419
|
+
this.scrollOffset = this.selectedIndex - capacity + 1;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
function isAbortError2(error) {
|
|
424
|
+
return error instanceof Error && error.name === "AbortError";
|
|
425
|
+
}
|
|
426
|
+
function formatError(error) {
|
|
427
|
+
return error instanceof Error ? error.message : String(error);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/file-browser.ts
|
|
431
|
+
var ProjectFileBrowser = class {
|
|
432
|
+
files;
|
|
433
|
+
filesByPath;
|
|
434
|
+
constructor(paths) {
|
|
435
|
+
this.files = paths.map((path) => {
|
|
436
|
+
const displayPath = safeTerminalText(path);
|
|
437
|
+
return { path, displayPath, segments: displayPath.split("/") };
|
|
438
|
+
});
|
|
439
|
+
this.filesByPath = new Map(this.files.map((file) => [file.path, file]));
|
|
440
|
+
}
|
|
441
|
+
list(directory) {
|
|
442
|
+
const directorySegments = directory ? directory.split("/") : [];
|
|
443
|
+
const directories = /* @__PURE__ */ new Map();
|
|
444
|
+
const files = [];
|
|
445
|
+
for (const file of this.files) {
|
|
446
|
+
if (!startsWithSegments(file.segments, directorySegments)) continue;
|
|
447
|
+
const childIndex = directorySegments.length;
|
|
448
|
+
const label = file.segments[childIndex];
|
|
449
|
+
if (!label) continue;
|
|
450
|
+
if (file.segments.length > childIndex + 1) {
|
|
451
|
+
const path = [...directorySegments, label].join("/");
|
|
452
|
+
directories.set(path, { kind: "directory", path, label });
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
files.push({ kind: "file", path: file.path, label });
|
|
456
|
+
}
|
|
457
|
+
return [...[...directories.values()].sort(compareBrowserItems), ...files.sort(compareBrowserItems)];
|
|
458
|
+
}
|
|
459
|
+
searchResults(paths) {
|
|
460
|
+
return paths.flatMap((path) => {
|
|
461
|
+
const file = this.filesByPath.get(path);
|
|
462
|
+
return file ? [{ kind: "file", path: file.path, label: file.displayPath }] : [];
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
function parentProjectDirectory(directory) {
|
|
467
|
+
const separator = directory.lastIndexOf("/");
|
|
468
|
+
return separator < 0 ? "" : directory.slice(0, separator);
|
|
469
|
+
}
|
|
470
|
+
function safeTerminalText(text) {
|
|
471
|
+
return [...text].map((character) => {
|
|
472
|
+
if (character === " ") return " ";
|
|
473
|
+
const code = character.charCodeAt(0);
|
|
474
|
+
if (code <= 31 || code >= 127 && code <= 159) {
|
|
475
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
476
|
+
}
|
|
477
|
+
return character;
|
|
478
|
+
}).join("");
|
|
479
|
+
}
|
|
480
|
+
function startsWithSegments(path, prefix) {
|
|
481
|
+
return prefix.every((segment, index) => path[index] === segment);
|
|
482
|
+
}
|
|
483
|
+
function compareBrowserItems(left, right) {
|
|
484
|
+
return left.label < right.label ? -1 : left.label > right.label ? 1 : 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/file-browser-ui.ts
|
|
488
|
+
import { truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
|
|
489
|
+
function renderFileBrowser(options) {
|
|
490
|
+
const location = options.currentDirectory ? `/${options.currentDirectory}` : "/";
|
|
491
|
+
const title = options.theme.fg(
|
|
492
|
+
"accent",
|
|
493
|
+
options.theme.bold(`File Context \xB7 files \xB7 ${location}${options.repositoryLabel}`)
|
|
494
|
+
);
|
|
495
|
+
const visibleItems = options.items.slice(options.scrollOffset, options.scrollOffset + options.height);
|
|
496
|
+
const itemLines = visibleItems.map((item, visibleIndex) => {
|
|
497
|
+
const index = options.scrollOffset + visibleIndex;
|
|
498
|
+
const prefix = index === options.selectedIndex ? "> " : " ";
|
|
499
|
+
const status = item.kind === "file" ? options.statuses?.get(item.path)?.code ?? " " : " ";
|
|
500
|
+
const suffix = item.kind === "directory" ? "/" : "";
|
|
501
|
+
const line = `${prefix}${status} ${item.label}${suffix}`;
|
|
502
|
+
return truncateToWidth2(
|
|
503
|
+
index === options.selectedIndex ? options.theme.bg("selectedBg", options.theme.fg("text", line)) : line,
|
|
504
|
+
options.width,
|
|
505
|
+
""
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
if (itemLines.length === 0) {
|
|
509
|
+
itemLines.push(truncateToWidth2(options.theme.fg("muted", " No matching files"), options.width, ""));
|
|
510
|
+
}
|
|
511
|
+
const state = options.loading ? options.theme.fg("warning", "Loading\u2026") : options.error ? options.theme.fg("error", safeTerminalText(options.error)) : options.theme.fg("muted", formatFileBrowserHint(options));
|
|
512
|
+
return [
|
|
513
|
+
truncateToWidth2(title, options.width, ""),
|
|
514
|
+
truncateToWidth2(options.searchLine, options.width, ""),
|
|
515
|
+
...itemLines,
|
|
516
|
+
truncateToWidth2(state, options.width, "")
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
function formatFileBrowserHint(options) {
|
|
520
|
+
const selected = options.items[options.selectedIndex];
|
|
521
|
+
const countLabel = options.searchActive ? `${options.items.length} matching ${options.items.length === 1 ? "file" : "files"}` : `${options.items.length} ${options.items.length === 1 ? "item" : "items"}`;
|
|
522
|
+
const navigation = navigationHint(options.keybindings);
|
|
523
|
+
const confirm = bindingHint(options.keybindings, "tui.select.confirm", ["enter"]);
|
|
524
|
+
const tab = bindingHint(options.keybindings, "tui.input.tab", ["tab"]);
|
|
525
|
+
const cancel = uniqueKeys([
|
|
526
|
+
...bindingKeys(options.keybindings, "tui.select.cancel", ["escape"]),
|
|
527
|
+
formatHintKey("ctrl+c")
|
|
528
|
+
]).join("/");
|
|
529
|
+
return [
|
|
530
|
+
countLabel,
|
|
531
|
+
`${navigation} navigate`,
|
|
532
|
+
`${confirm} ${selected?.kind === "directory" ? "open folder" : "preview"}`,
|
|
533
|
+
...selected?.kind === "file" ? [`${tab} reference`] : [],
|
|
534
|
+
"Ctrl+F contents",
|
|
535
|
+
`${cancel} ${options.currentDirectory && !options.searchActive ? "back" : "cancel"}`
|
|
536
|
+
].join(" \xB7 ");
|
|
537
|
+
}
|
|
538
|
+
function navigationHint(keybindings) {
|
|
539
|
+
const up = bindingKeys(keybindings, "tui.select.up", ["up"]);
|
|
540
|
+
const down = bindingKeys(keybindings, "tui.select.down", ["down"]);
|
|
541
|
+
if (up.includes("\u2191") && down.includes("\u2193")) {
|
|
542
|
+
return uniqueKeys(["\u2191\u2193", ...up.filter((key) => key !== "\u2191"), ...down.filter((key) => key !== "\u2193")]).join("/");
|
|
543
|
+
}
|
|
544
|
+
return uniqueKeys([...up, ...down]).join("/");
|
|
545
|
+
}
|
|
546
|
+
function bindingHint(keybindings, binding, fallback) {
|
|
547
|
+
return bindingKeys(keybindings, binding, fallback).join("/");
|
|
548
|
+
}
|
|
549
|
+
function bindingKeys(keybindings, binding, fallback) {
|
|
550
|
+
const getKeys = keybindings.getKeys?.bind(keybindings);
|
|
551
|
+
const keys = getKeys?.(binding) ?? fallback;
|
|
552
|
+
return uniqueKeys(keys.map(formatHintKey));
|
|
553
|
+
}
|
|
554
|
+
function uniqueKeys(keys) {
|
|
555
|
+
return [...new Set(keys.filter(Boolean))];
|
|
556
|
+
}
|
|
557
|
+
function formatHintKey(key) {
|
|
558
|
+
const normalized = safeTerminalText(key).toLowerCase();
|
|
559
|
+
if (normalized === "up") return "\u2191";
|
|
560
|
+
if (normalized === "down") return "\u2193";
|
|
561
|
+
if (normalized === "left") return "\u2190";
|
|
562
|
+
if (normalized === "right") return "\u2192";
|
|
563
|
+
if (normalized === "return" || normalized === "enter") return "Enter";
|
|
564
|
+
if (normalized === "escape" || normalized === "esc") return "Esc";
|
|
565
|
+
if (normalized === "tab") return "Tab";
|
|
566
|
+
return normalized.split("+").map(
|
|
567
|
+
(part, index) => part === "ctrl" || part === "alt" || part === "shift" || index > 0 && part.length === 1 ? `${part[0]?.toUpperCase()}${part.slice(1)}` : part
|
|
568
|
+
).join("+");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/file-context-preview-ui.ts
|
|
572
|
+
import { truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
|
|
573
|
+
function renderFilePreview(options) {
|
|
574
|
+
const { file, theme, width } = options;
|
|
575
|
+
const range = selectionRange(options.anchor, options.cursor);
|
|
576
|
+
const selectedText = file.lines.slice(range.start, range.end + 1).join("\n");
|
|
577
|
+
const selectedBytes = Buffer.byteLength(selectedText, "utf8");
|
|
578
|
+
const estimatedTokens = Math.max(1, Math.ceil(selectedBytes / 4));
|
|
579
|
+
const externalEditorKey = bindingHint(options.keybindings, "app.editor.external", []);
|
|
580
|
+
const footer = options.error ? { lines: [escapeTerminalControls2(options.error)], primaryIndexes: [0] } : previewFooterLines(
|
|
581
|
+
width,
|
|
582
|
+
range.start + 1,
|
|
583
|
+
range.end + 1,
|
|
584
|
+
estimatedTokens,
|
|
585
|
+
selectedBytes,
|
|
586
|
+
options.selectedContext,
|
|
587
|
+
options.canContinue,
|
|
588
|
+
options.canEdit ? externalEditorKey : ""
|
|
589
|
+
);
|
|
590
|
+
const previewHeight = Math.max(1, options.availableRows - 1 - (options.blame ? 1 : 0) - footer.lines.length);
|
|
591
|
+
const scrollOffset = visiblePreviewOffset(options.scrollOffset, options.cursor, previewHeight);
|
|
592
|
+
const digits = String(Math.max(1, file.lines.length)).length;
|
|
593
|
+
const changedLines = new Set(options.fileGit?.hunks.flatMap((hunk) => hunk.changedLines) ?? []);
|
|
594
|
+
const deletedAtLines = new Set(
|
|
595
|
+
(options.fileGit?.hunks ?? []).filter((hunk) => hunk.changedLines.length === 0 && hunk.oldCount > 0).map((hunk) => Math.max(1, hunk.newStart))
|
|
596
|
+
);
|
|
597
|
+
const previewLines = file.lines.slice(scrollOffset, scrollOffset + previewHeight).map((rawLine, visibleIndex) => {
|
|
598
|
+
const index = scrollOffset + visibleIndex;
|
|
599
|
+
const selected = index >= range.start && index <= range.end;
|
|
600
|
+
const cursor = index === options.cursor ? ">" : " ";
|
|
601
|
+
const marker = changedLines.has(index + 1) ? "+" : deletedAtLines.has(index + 1) ? "-" : " ";
|
|
602
|
+
const number = String(index + 1).padStart(digits, " ");
|
|
603
|
+
const contentMatch = options.contentMatch?.path === file.path && options.contentMatch.lineNumber === index + 1 && options.contentMatch.line === rawLine ? options.contentMatch : void 0;
|
|
604
|
+
const content = contentMatch ? highlightContentRanges(rawLine, contentMatch.ranges, theme) : escapeTerminalControls2(rawLine);
|
|
605
|
+
const line = `${cursor}${marker}${number} \u2502 ${content}`;
|
|
606
|
+
const styled = selected ? theme.bg("selectedBg", theme.fg("text", line)) : index === options.cursor ? theme.fg("accent", line) : line;
|
|
607
|
+
return truncateToWidth3(styled, width, "");
|
|
608
|
+
});
|
|
609
|
+
if (previewLines.length === 0) {
|
|
610
|
+
previewLines.push(truncateToWidth3(theme.fg("muted", " Empty file"), width, ""));
|
|
611
|
+
}
|
|
612
|
+
const gitLabel = options.revision ? `${escapeTerminalControls2(options.revision.revision)}@${options.revision.commit.slice(0, 12)} \xB7 historical` : options.project ? `${escapeTerminalControls2(options.project.branch)}@${options.project.head.slice(0, 12)}${options.project.dirty ? " \xB7 dirty" : ""}${options.fileGit?.status ? ` \xB7 ${options.fileGit.status.label}` : " \xB7 clean"}` : "";
|
|
613
|
+
const blameLabel = options.blame ? `L${options.cursor + 1} \xB7 ${options.blame.committed ? options.blame.commit.slice(0, 12) : "uncommitted"} \xB7 ${escapeTerminalControls2(options.blame.author)} \xB7 ${escapeTerminalControls2(options.blame.summary)}` : "";
|
|
614
|
+
const titleLine = truncateToWidth3(
|
|
615
|
+
theme.fg("accent", theme.bold(`${escapeTerminalControls2(file.path)}${gitLabel ? ` \xB7 ${gitLabel}` : ""}`)),
|
|
616
|
+
width,
|
|
617
|
+
""
|
|
618
|
+
);
|
|
619
|
+
const blameLine = blameLabel ? truncateToWidth3(theme.fg("muted", blameLabel), width, "") : void 0;
|
|
620
|
+
const renderedFooter = footer.lines.map(
|
|
621
|
+
(line) => truncateToWidth3(theme.fg(options.error ? "error" : "muted", line), width, "")
|
|
622
|
+
);
|
|
623
|
+
return fitPreviewRows(
|
|
624
|
+
titleLine,
|
|
625
|
+
blameLine,
|
|
626
|
+
previewLines,
|
|
627
|
+
renderedFooter,
|
|
628
|
+
footer.primaryIndexes,
|
|
629
|
+
options.availableRows
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
function renderFilePreviewHelp(theme, keybindings, width, availableRows, canEdit) {
|
|
633
|
+
const externalEditorKey = bindingHint(keybindings, "app.editor.external", []);
|
|
634
|
+
const editDetailed = canEdit ? externalEditorKey ? `${externalEditorKey.padEnd(6, " ")} Edit the current worktree file in the external editor` : "Edit External editor action is unbound" : "Edit Historical revisions are read-only";
|
|
635
|
+
const editCompact = canEdit ? externalEditorKey ? `${externalEditorKey} edit worktree` : "External editor unbound" : "Historical revision \xB7 read-only";
|
|
636
|
+
const detailed = [
|
|
637
|
+
theme.fg("accent", theme.bold("Preview actions")),
|
|
638
|
+
"Enter Add selected lines and close File Context",
|
|
639
|
+
"A Add selected lines and keep browsing",
|
|
640
|
+
"Space Set or clear the range anchor; arrows extend the range",
|
|
641
|
+
"[ / ] Select the previous or next changed hunk",
|
|
642
|
+
"B Blame: show ownership for the current line when Git is available",
|
|
643
|
+
"H History: browse earlier versions of this file",
|
|
644
|
+
"R Revision: open a commit, branch, or tag",
|
|
645
|
+
"D Git diff: review and add one explicit changed hunk",
|
|
646
|
+
editDetailed,
|
|
647
|
+
"Esc Return to the preview without changing selected context"
|
|
648
|
+
];
|
|
649
|
+
const compact = [
|
|
650
|
+
theme.fg("accent", theme.bold("Preview actions")),
|
|
651
|
+
"Enter add \xB7 A keep browsing \xB7 Space range",
|
|
652
|
+
"\u2191\u2193 extend \xB7 [/] hunk \xB7 B blame",
|
|
653
|
+
"H history \xB7 R revision \xB7 D Git diff",
|
|
654
|
+
editCompact,
|
|
655
|
+
"Esc back without changing context"
|
|
656
|
+
];
|
|
657
|
+
const lines = availableRows < detailed.length ? compact : detailed;
|
|
658
|
+
return fitRows2(
|
|
659
|
+
lines.map((line) => truncateToWidth3(line, width, "")),
|
|
660
|
+
availableRows
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
function previewFooterLines(width, startLine, endLine, tokens, selectedBytes, state, canContinue, externalEditorKey) {
|
|
664
|
+
const nextCount = state ? state.count + 1 : void 0;
|
|
665
|
+
const nextBytes = state ? state.totalBytes + selectedBytes : void 0;
|
|
666
|
+
const selectedLines = endLine - startLine + 1;
|
|
667
|
+
const snippetOverLimit = state !== void 0 && (state.maximumSnippetLines !== void 0 && selectedLines > state.maximumSnippetLines || state.maximumSnippetBytes !== void 0 && selectedBytes > state.maximumSnippetBytes);
|
|
668
|
+
const aggregateOverLimit = state !== void 0 && nextCount !== void 0 && (nextCount > state.maximumCount || (nextBytes ?? 0) > state.maximumBytes);
|
|
669
|
+
const warning = snippetOverLimit ? "Snippet limit exceeded" : aggregateOverLimit ? "Next prompt limit exceeded" : void 0;
|
|
670
|
+
if (width < 74) {
|
|
671
|
+
const selection2 = `L${startLine}-${endLine} \xB7 ~${tokens} tok`;
|
|
672
|
+
const capacity2 = state ? warning ?? `Next ${nextCount}/${state.maximumCount}` : void 0;
|
|
673
|
+
const lines = [
|
|
674
|
+
[selection2, capacity2].filter(Boolean).join(" \xB7 "),
|
|
675
|
+
canContinue ? "Enter add \xB7 A keep browsing" : "Enter add & close",
|
|
676
|
+
"\u2191\u2193 move \xB7 Space range \xB7 Esc back",
|
|
677
|
+
[externalEditorKey ? `${externalEditorKey} edit` : "", "? actions"].filter(Boolean).join(" \xB7 "),
|
|
678
|
+
"B blame \xB7 H history \xB7 R revision",
|
|
679
|
+
"D diff \xB7 [/] hunk"
|
|
680
|
+
];
|
|
681
|
+
return { lines, primaryIndexes: [1, 3] };
|
|
682
|
+
}
|
|
683
|
+
const selection = `Lines ${startLine}-${endLine} \xB7 ~${tokens} tokens`;
|
|
684
|
+
const capacity = state ? snippetOverLimit ? "Snippet limit exceeded; shorten the range before adding" : aggregateOverLimit ? "Next prompt limit exceeded; shorten the range or review selected context" : `Next prompt: ${nextCount}/${state.maximumCount} snippets \xB7 ~${estimateTokens(nextBytes ?? 0)} tokens` : void 0;
|
|
685
|
+
const primaryActions = canContinue ? "Enter add & close \xB7 A add & continue \xB7 \u2191\u2193 move \xB7 Space range \xB7 Esc back" : "Enter add & close \xB7 \u2191\u2193 move \xB7 Space range \xB7 Esc back";
|
|
686
|
+
const editActions = [externalEditorKey ? `${externalEditorKey} edit` : "", "? actions"].filter(Boolean).join(" \xB7 ");
|
|
687
|
+
return {
|
|
688
|
+
lines: [
|
|
689
|
+
[selection, capacity].filter(Boolean).join(" \xB7 "),
|
|
690
|
+
primaryActions,
|
|
691
|
+
editActions,
|
|
692
|
+
"B blame \xB7 H history \xB7 R revision \xB7 D diff \xB7 [/] hunk"
|
|
693
|
+
],
|
|
694
|
+
primaryIndexes: [1, 2]
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function selectionRange(anchor, cursor) {
|
|
698
|
+
const rangeAnchor = anchor ?? cursor;
|
|
699
|
+
return {
|
|
700
|
+
start: Math.min(rangeAnchor, cursor),
|
|
701
|
+
end: Math.max(rangeAnchor, cursor)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function visiblePreviewOffset(current, cursor, height) {
|
|
705
|
+
if (cursor < current) return cursor;
|
|
706
|
+
if (cursor >= current + height) return cursor - height + 1;
|
|
707
|
+
return current;
|
|
708
|
+
}
|
|
709
|
+
function estimateTokens(bytes) {
|
|
710
|
+
return bytes === 0 ? 0 : Math.max(1, Math.ceil(bytes / 4));
|
|
711
|
+
}
|
|
712
|
+
function fitPreviewRows(title, blame, previewLines, footerLines, primaryFooterIndexes, height) {
|
|
713
|
+
let visibleTitle = title;
|
|
714
|
+
let visibleBlame = blame;
|
|
715
|
+
const visiblePreview = [...previewLines];
|
|
716
|
+
const visibleFooter = footerLines.map((line, index) => ({
|
|
717
|
+
line,
|
|
718
|
+
primary: primaryFooterIndexes.includes(index)
|
|
719
|
+
}));
|
|
720
|
+
const rowCount = () => (visibleTitle ? 1 : 0) + (visibleBlame ? 1 : 0) + visiblePreview.length + visibleFooter.length;
|
|
721
|
+
while (rowCount() > height) {
|
|
722
|
+
if (visibleBlame) {
|
|
723
|
+
visibleBlame = void 0;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (visiblePreview.length > 1) {
|
|
727
|
+
visiblePreview.pop();
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
let optionalFooter = -1;
|
|
731
|
+
for (let index = visibleFooter.length - 1; index >= 0; index -= 1) {
|
|
732
|
+
if (!visibleFooter[index]?.primary) {
|
|
733
|
+
optionalFooter = index;
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (optionalFooter >= 0) {
|
|
738
|
+
visibleFooter.splice(optionalFooter, 1);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (visibleTitle) {
|
|
742
|
+
visibleTitle = void 0;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (visiblePreview.length > 0) {
|
|
746
|
+
visiblePreview.pop();
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
return [
|
|
752
|
+
...visibleTitle ? [visibleTitle] : [],
|
|
753
|
+
...visibleBlame ? [visibleBlame] : [],
|
|
754
|
+
...visiblePreview,
|
|
755
|
+
...visibleFooter.map(({ line }) => line)
|
|
756
|
+
];
|
|
757
|
+
}
|
|
758
|
+
function fitRows2(lines, height) {
|
|
759
|
+
if (lines.length <= height) return lines;
|
|
760
|
+
if (height <= 1) return lines.slice(0, 1);
|
|
761
|
+
return [...lines.slice(0, height - 1), lines.at(-1) ?? ""];
|
|
762
|
+
}
|
|
763
|
+
function escapeTerminalControls2(text) {
|
|
764
|
+
return [...text].map((character) => {
|
|
765
|
+
if (character === " ") return " ";
|
|
766
|
+
const code = character.charCodeAt(0);
|
|
767
|
+
if (code <= 31 || code >= 127 && code <= 159) {
|
|
768
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
769
|
+
}
|
|
770
|
+
return character;
|
|
771
|
+
}).join("");
|
|
772
|
+
}
|
|
773
|
+
function formatHistoryDate(authorTime) {
|
|
774
|
+
const date = new Date(authorTime * 1e3);
|
|
775
|
+
return Number.isFinite(date.getTime()) ? date.toISOString().slice(0, 10) : "unknown-date";
|
|
776
|
+
}
|
|
777
|
+
function renderFileHistory(options) {
|
|
778
|
+
const { width, availableRows, theme, history, index: selectedIndex, error } = options;
|
|
779
|
+
const listHeight = Math.max(1, availableRows - 3);
|
|
780
|
+
const title = theme.fg("accent", theme.bold(`File history \xB7 ${safeTerminalText(options.path)}`));
|
|
781
|
+
const start = Math.max(0, selectedIndex - listHeight + 1);
|
|
782
|
+
const entries = history.slice(start, start + listHeight).map((entry, visibleIndex) => {
|
|
783
|
+
const index = start + visibleIndex;
|
|
784
|
+
const prefix = index === selectedIndex ? "> " : " ";
|
|
785
|
+
const date = formatHistoryDate(entry.authorTime);
|
|
786
|
+
const line = `${prefix}${entry.commit.slice(0, 12)} \xB7 ${date} \xB7 ${safeTerminalText(entry.author)} \xB7 ${safeTerminalText(entry.summary)}`;
|
|
787
|
+
return truncateToWidth3(index === selectedIndex ? theme.bg("selectedBg", theme.fg("text", line)) : line, width, "");
|
|
788
|
+
});
|
|
789
|
+
if (entries.length === 0) {
|
|
790
|
+
entries.push(truncateToWidth3(theme.fg("muted", " No file history"), width, ""));
|
|
791
|
+
}
|
|
792
|
+
const footer = error ? safeTerminalText(error) : "\u2191\u2193 navigate \xB7 Enter open revision \xB7 Esc preview";
|
|
793
|
+
return fitRows2(
|
|
794
|
+
[truncateToWidth3(title, width, ""), ...entries, truncateToWidth3(theme.fg("muted", footer), width, "")],
|
|
795
|
+
availableRows
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/file-search.ts
|
|
800
|
+
var MAX_SEARCH_QUERY_LENGTH = 256;
|
|
801
|
+
var MAX_SEARCH_QUERY_PARTS = 8;
|
|
802
|
+
var PATH_SCORE_PENALTY = 50;
|
|
803
|
+
var PREFIX_SCORE = 100;
|
|
804
|
+
var SUBSTRING_SCORE = 200;
|
|
805
|
+
var TYPO_SCORE = 300;
|
|
806
|
+
var SUBSEQUENCE_SCORE = 400;
|
|
807
|
+
var TOKENIZED_SCORE = 500;
|
|
808
|
+
var NO_MATCH = Number.POSITIVE_INFINITY;
|
|
809
|
+
var PATH_PART_SEPARATOR = /[/._\-\s]+/u;
|
|
810
|
+
var ProjectFileSearch = class {
|
|
811
|
+
files;
|
|
812
|
+
entries;
|
|
813
|
+
constructor(files) {
|
|
814
|
+
this.files = [...files];
|
|
815
|
+
this.entries = this.files.map((path, originalIndex) => {
|
|
816
|
+
const normalizedPath = safeTerminalText(path).toLowerCase();
|
|
817
|
+
const normalizedBasename = normalizedPath.slice(normalizedPath.lastIndexOf("/") + 1);
|
|
818
|
+
return {
|
|
819
|
+
path,
|
|
820
|
+
normalizedPath,
|
|
821
|
+
normalizedBasename,
|
|
822
|
+
basenameParts: splitPathParts(normalizedBasename),
|
|
823
|
+
pathParts: splitPathParts(normalizedPath),
|
|
824
|
+
originalIndex
|
|
825
|
+
};
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
search(query) {
|
|
829
|
+
const trimmedQuery = safeTerminalText(query).trim();
|
|
830
|
+
if (!trimmedQuery) return [...this.files];
|
|
831
|
+
if (trimmedQuery.length > MAX_SEARCH_QUERY_LENGTH) return [];
|
|
832
|
+
const normalizedQuery = trimmedQuery.toLowerCase();
|
|
833
|
+
const queryParts = splitPathParts(normalizedQuery);
|
|
834
|
+
const typoDistanceCache = /* @__PURE__ */ new Map();
|
|
835
|
+
return this.entries.flatMap((entry) => {
|
|
836
|
+
const score = scoreEntry(normalizedQuery, queryParts, entry, typoDistanceCache);
|
|
837
|
+
return Number.isFinite(score) ? [{ path: entry.path, score, originalIndex: entry.originalIndex }] : [];
|
|
838
|
+
}).sort((left, right) => left.score - right.score || left.originalIndex - right.originalIndex).map((result) => result.path);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
function scoreEntry(query, queryParts, entry, typoDistanceCache) {
|
|
842
|
+
const basenameScore = scoreField(query, queryParts, entry.normalizedBasename, entry.basenameParts, typoDistanceCache);
|
|
843
|
+
const pathScore = scoreField(query, queryParts, entry.normalizedPath, entry.pathParts, typoDistanceCache);
|
|
844
|
+
return Math.min(basenameScore, pathScore + PATH_SCORE_PENALTY);
|
|
845
|
+
}
|
|
846
|
+
function scoreField(query, queryParts, text, textParts, typoDistanceCache) {
|
|
847
|
+
const directScore = scoreText(query, text);
|
|
848
|
+
if (directScore < TYPO_SCORE) return directScore;
|
|
849
|
+
const typoScore = scoreTypo(query, textParts, typoDistanceCache);
|
|
850
|
+
if (Number.isFinite(typoScore)) return Math.min(directScore, typoScore);
|
|
851
|
+
if (Number.isFinite(directScore)) return directScore;
|
|
852
|
+
return scoreTokenized(queryParts, textParts, typoDistanceCache);
|
|
853
|
+
}
|
|
854
|
+
function scoreTokenized(queryParts, candidateParts, typoDistanceCache) {
|
|
855
|
+
if (queryParts.length < 2 || queryParts.length > MAX_SEARCH_QUERY_PARTS) return NO_MATCH;
|
|
856
|
+
let totalScore = TOKENIZED_SCORE;
|
|
857
|
+
for (const queryPart of queryParts) {
|
|
858
|
+
let bestDirectScore = NO_MATCH;
|
|
859
|
+
for (let index = 0; index < candidateParts.length; index += 1) {
|
|
860
|
+
bestDirectScore = Math.min(bestDirectScore, scoreText(queryPart, candidateParts[index] ?? "") + index * 2);
|
|
861
|
+
}
|
|
862
|
+
const bestPartScore = bestDirectScore < TYPO_SCORE ? bestDirectScore : Math.min(bestDirectScore, scoreTypo(queryPart, candidateParts, typoDistanceCache));
|
|
863
|
+
if (!Number.isFinite(bestPartScore)) return NO_MATCH;
|
|
864
|
+
totalScore += bestPartScore;
|
|
865
|
+
}
|
|
866
|
+
return totalScore;
|
|
867
|
+
}
|
|
868
|
+
function scoreText(query, text) {
|
|
869
|
+
if (query === text) return 0;
|
|
870
|
+
if (text.startsWith(query)) return PREFIX_SCORE + lengthPenalty(query, text);
|
|
871
|
+
const substringIndex = text.indexOf(query);
|
|
872
|
+
if (substringIndex >= 0) {
|
|
873
|
+
return SUBSTRING_SCORE + substringIndex * 2 + lengthPenalty(query, text);
|
|
874
|
+
}
|
|
875
|
+
return scoreSubsequence(query, text);
|
|
876
|
+
}
|
|
877
|
+
function scoreSubsequence(query, text) {
|
|
878
|
+
let queryIndex = 0;
|
|
879
|
+
let firstMatch = -1;
|
|
880
|
+
let previousMatch = -1;
|
|
881
|
+
let gaps = 0;
|
|
882
|
+
let boundaryMatches = 0;
|
|
883
|
+
let consecutiveMatches = 0;
|
|
884
|
+
for (let textIndex = 0; textIndex < text.length && queryIndex < query.length; textIndex += 1) {
|
|
885
|
+
if (text[textIndex] !== query[queryIndex]) continue;
|
|
886
|
+
if (firstMatch < 0) firstMatch = textIndex;
|
|
887
|
+
if (previousMatch >= 0) {
|
|
888
|
+
const gap = textIndex - previousMatch - 1;
|
|
889
|
+
gaps += gap;
|
|
890
|
+
if (gap === 0) consecutiveMatches += 1;
|
|
891
|
+
}
|
|
892
|
+
if (textIndex === 0 || PATH_PART_SEPARATOR.test(text[textIndex - 1] ?? "")) {
|
|
893
|
+
boundaryMatches += 1;
|
|
894
|
+
}
|
|
895
|
+
previousMatch = textIndex;
|
|
896
|
+
queryIndex += 1;
|
|
897
|
+
}
|
|
898
|
+
if (queryIndex !== query.length) return NO_MATCH;
|
|
899
|
+
return SUBSEQUENCE_SCORE + gaps * 3 + firstMatch * 2 + lengthPenalty(query, text) - boundaryMatches * 8 - consecutiveMatches;
|
|
900
|
+
}
|
|
901
|
+
function scoreTypo(query, parts, typoDistanceCache) {
|
|
902
|
+
const maxDistance = allowedTypoDistance(query.length);
|
|
903
|
+
if (maxDistance === 0) return NO_MATCH;
|
|
904
|
+
let bestScore = NO_MATCH;
|
|
905
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
906
|
+
const part = parts[index] ?? "";
|
|
907
|
+
if (Math.abs(query.length - part.length) > maxDistance) continue;
|
|
908
|
+
const distance = cachedTypoDistance(query, part, maxDistance, typoDistanceCache);
|
|
909
|
+
if (distance === void 0 || distance === 0) continue;
|
|
910
|
+
bestScore = Math.min(bestScore, TYPO_SCORE + distance * 20 + index * 2 + Math.abs(query.length - part.length));
|
|
911
|
+
}
|
|
912
|
+
return bestScore;
|
|
913
|
+
}
|
|
914
|
+
function cachedTypoDistance(query, part, maxDistance, cache) {
|
|
915
|
+
let queryCache = cache.get(query);
|
|
916
|
+
if (!queryCache) {
|
|
917
|
+
queryCache = /* @__PURE__ */ new Map();
|
|
918
|
+
cache.set(query, queryCache);
|
|
919
|
+
}
|
|
920
|
+
if (queryCache.has(part)) return queryCache.get(part);
|
|
921
|
+
const distance = boundedDamerauLevenshtein(query, part, maxDistance);
|
|
922
|
+
queryCache.set(part, distance);
|
|
923
|
+
return distance;
|
|
924
|
+
}
|
|
925
|
+
function boundedDamerauLevenshtein(left, right, maxDistance) {
|
|
926
|
+
if (Math.abs(left.length - right.length) > maxDistance) return void 0;
|
|
927
|
+
const unreachable = maxDistance + 1;
|
|
928
|
+
let previousPrevious = Array.from({ length: left.length + 1 }, () => unreachable);
|
|
929
|
+
let previous = Array.from({ length: left.length + 1 }, (_, index) => index <= maxDistance ? index : unreachable);
|
|
930
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
931
|
+
const current = Array.from({ length: left.length + 1 }, () => unreachable);
|
|
932
|
+
if (rightIndex <= maxDistance) current[0] = rightIndex;
|
|
933
|
+
const start = Math.max(1, rightIndex - maxDistance);
|
|
934
|
+
const end = Math.min(left.length, rightIndex + maxDistance);
|
|
935
|
+
for (let leftIndex = start; leftIndex <= end; leftIndex += 1) {
|
|
936
|
+
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
937
|
+
current[leftIndex] = Math.min(
|
|
938
|
+
(previous[leftIndex] ?? unreachable) + 1,
|
|
939
|
+
(current[leftIndex - 1] ?? unreachable) + 1,
|
|
940
|
+
(previous[leftIndex - 1] ?? unreachable) + substitutionCost
|
|
941
|
+
);
|
|
942
|
+
if (leftIndex > 1 && rightIndex > 1 && left[leftIndex - 1] === right[rightIndex - 2] && left[leftIndex - 2] === right[rightIndex - 1]) {
|
|
943
|
+
current[leftIndex] = Math.min(
|
|
944
|
+
current[leftIndex] ?? unreachable,
|
|
945
|
+
(previousPrevious[leftIndex - 2] ?? unreachable) + 1
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
previousPrevious = previous;
|
|
950
|
+
previous = current;
|
|
951
|
+
}
|
|
952
|
+
const distance = previous[left.length] ?? unreachable;
|
|
953
|
+
return distance <= maxDistance ? distance : void 0;
|
|
954
|
+
}
|
|
955
|
+
function allowedTypoDistance(queryLength) {
|
|
956
|
+
if (queryLength < 5) return 0;
|
|
957
|
+
return queryLength < 10 ? 1 : 2;
|
|
958
|
+
}
|
|
959
|
+
function splitPathParts(value) {
|
|
960
|
+
return value.split(PATH_PART_SEPARATOR).filter(Boolean);
|
|
961
|
+
}
|
|
962
|
+
function lengthPenalty(query, text) {
|
|
963
|
+
return Math.max(0, text.length - query.length) / 100;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/file-context-explorer.ts
|
|
967
|
+
var RESERVED_APP_ROWS = 3;
|
|
968
|
+
var EXPLORER_CHROME_ROWS = 4;
|
|
969
|
+
var DIFF_CHROME_ROWS = 3;
|
|
970
|
+
var FileQuoteExplorer = class {
|
|
971
|
+
constructor(options) {
|
|
972
|
+
this.options = options;
|
|
973
|
+
this.fileSearch = new ProjectFileSearch(options.files);
|
|
974
|
+
this.fileBrowser = new ProjectFileBrowser(options.files);
|
|
975
|
+
this.fileItems = this.fileBrowser.list("");
|
|
976
|
+
this.contentSearch = new ContentSearchSession({
|
|
977
|
+
tui: options.tui,
|
|
978
|
+
theme: options.theme,
|
|
979
|
+
keybindings: options.keybindings,
|
|
980
|
+
files: options.files,
|
|
981
|
+
cwd: options.cwd,
|
|
982
|
+
loadFile: options.loadFile,
|
|
983
|
+
onPreview: (match) => {
|
|
984
|
+
void this.openFile(match.path, {
|
|
985
|
+
returnMode: "contents",
|
|
986
|
+
cursorIndex: match.lineNumber - 1,
|
|
987
|
+
match
|
|
988
|
+
});
|
|
989
|
+
},
|
|
990
|
+
onReference: (path) => this.finish({ kind: "reference", path }),
|
|
991
|
+
onSwitchFiles: () => this.showFileSearch(),
|
|
992
|
+
onCancel: () => this.finish(this.rootExit("back"))
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
options;
|
|
996
|
+
search = new Input2();
|
|
997
|
+
contentSearch;
|
|
998
|
+
revisionInput = new Input2();
|
|
999
|
+
fileSearch;
|
|
1000
|
+
fileBrowser;
|
|
1001
|
+
fileItems;
|
|
1002
|
+
currentDirectory = "";
|
|
1003
|
+
directoryPositions = /* @__PURE__ */ new Map();
|
|
1004
|
+
selectedFileIndex = 0;
|
|
1005
|
+
fileScrollOffset = 0;
|
|
1006
|
+
mode = "files";
|
|
1007
|
+
previewReturnMode = "files";
|
|
1008
|
+
activeContentMatch;
|
|
1009
|
+
loadedFile;
|
|
1010
|
+
loadedGit;
|
|
1011
|
+
previewCursor = 0;
|
|
1012
|
+
previewAnchor;
|
|
1013
|
+
previewScrollOffset = 0;
|
|
1014
|
+
hunkIndex = -1;
|
|
1015
|
+
blame;
|
|
1016
|
+
history = [];
|
|
1017
|
+
historyIndex = 0;
|
|
1018
|
+
loadedRevision;
|
|
1019
|
+
diffHunkIndex = 0;
|
|
1020
|
+
diffScrollOffset = 0;
|
|
1021
|
+
detailRequest = 0;
|
|
1022
|
+
detailController;
|
|
1023
|
+
openRequest = 0;
|
|
1024
|
+
openController;
|
|
1025
|
+
editRequest = 0;
|
|
1026
|
+
editController;
|
|
1027
|
+
loading = false;
|
|
1028
|
+
error;
|
|
1029
|
+
finished = false;
|
|
1030
|
+
disposed = false;
|
|
1031
|
+
isFocused = false;
|
|
1032
|
+
get capturesViewportInput() {
|
|
1033
|
+
return this.mode !== "revision" && this.mode !== "preview-help";
|
|
1034
|
+
}
|
|
1035
|
+
get focused() {
|
|
1036
|
+
return this.isFocused;
|
|
1037
|
+
}
|
|
1038
|
+
set focused(value) {
|
|
1039
|
+
this.isFocused = value;
|
|
1040
|
+
this.search.focused = value && this.mode === "files";
|
|
1041
|
+
this.contentSearch.focused = value && this.mode === "contents";
|
|
1042
|
+
this.revisionInput.focused = value && this.mode === "revision";
|
|
1043
|
+
}
|
|
1044
|
+
render(width) {
|
|
1045
|
+
const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
|
|
1046
|
+
if (safeWidth === 0) return [];
|
|
1047
|
+
if (this.mode === "files") return this.renderFileList(safeWidth);
|
|
1048
|
+
if (this.mode === "contents") return this.renderContentSearch(safeWidth);
|
|
1049
|
+
if (this.mode === "history") return this.renderHistory(safeWidth);
|
|
1050
|
+
if (this.mode === "revision") return this.renderRevisionInput(safeWidth);
|
|
1051
|
+
if (this.mode === "diff") return this.renderDiff(safeWidth);
|
|
1052
|
+
if (this.mode === "preview-help") return this.renderPreviewHelp(safeWidth);
|
|
1053
|
+
return this.renderPreview(safeWidth);
|
|
1054
|
+
}
|
|
1055
|
+
handleInput(data) {
|
|
1056
|
+
if (this.finished || this.disposed) return;
|
|
1057
|
+
if (matchesKey2(data, Key2.ctrl("c"))) {
|
|
1058
|
+
this.finish(this.rootExit("close"));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (this.mode === "files") this.handleFileInput(data);
|
|
1062
|
+
else if (this.mode === "contents") this.handleContentInput(data);
|
|
1063
|
+
else if (this.mode === "history") this.handleHistoryInput(data);
|
|
1064
|
+
else if (this.mode === "revision") this.handleRevisionInput(data);
|
|
1065
|
+
else if (this.mode === "diff") this.handleDiffInput(data);
|
|
1066
|
+
else if (this.mode === "preview-help") this.handlePreviewHelpInput(data);
|
|
1067
|
+
else this.handlePreviewInput(data);
|
|
1068
|
+
if (!this.finished) this.options.tui.requestRender();
|
|
1069
|
+
}
|
|
1070
|
+
invalidate() {
|
|
1071
|
+
this.search.invalidate();
|
|
1072
|
+
this.contentSearch.invalidate();
|
|
1073
|
+
this.revisionInput.invalidate();
|
|
1074
|
+
}
|
|
1075
|
+
dispose() {
|
|
1076
|
+
if (this.disposed) return;
|
|
1077
|
+
this.disposed = true;
|
|
1078
|
+
this.contentSearch.dispose();
|
|
1079
|
+
this.cancelOpenRequest();
|
|
1080
|
+
this.cancelDetailRequest();
|
|
1081
|
+
this.cancelEditRequest();
|
|
1082
|
+
if (!this.finished) {
|
|
1083
|
+
this.finished = true;
|
|
1084
|
+
this.options.done(void 0);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
renderFileList(width) {
|
|
1088
|
+
const availableRows = Math.max(1, this.options.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
1089
|
+
const listHeight = Math.max(1, availableRows - EXPLORER_CHROME_ROWS);
|
|
1090
|
+
this.keepFileVisible(listHeight);
|
|
1091
|
+
const project = this.options.gitContext?.project;
|
|
1092
|
+
const repositoryLabel = project ? ` \xB7 ${safeTerminalText(project.branch)}@${project.head.slice(0, 12)}${project.dirty ? " \xB7 dirty" : ""}` : "";
|
|
1093
|
+
const queryLabel = this.options.theme.fg("muted", "Search: ");
|
|
1094
|
+
const queryWidth = Math.max(1, width - visibleWidth3(queryLabel));
|
|
1095
|
+
const searchLine = `${queryLabel}${this.search.render(queryWidth)[0] ?? ""}`;
|
|
1096
|
+
return fitRows2(
|
|
1097
|
+
renderFileBrowser({
|
|
1098
|
+
theme: this.options.theme,
|
|
1099
|
+
keybindings: this.options.keybindings,
|
|
1100
|
+
width,
|
|
1101
|
+
height: listHeight,
|
|
1102
|
+
items: this.fileItems,
|
|
1103
|
+
selectedIndex: this.selectedFileIndex,
|
|
1104
|
+
scrollOffset: this.fileScrollOffset,
|
|
1105
|
+
currentDirectory: this.currentDirectory,
|
|
1106
|
+
searchLine,
|
|
1107
|
+
searchActive: this.isFileSearchActive(),
|
|
1108
|
+
repositoryLabel,
|
|
1109
|
+
statuses: this.options.gitContext?.statuses,
|
|
1110
|
+
loading: this.loading,
|
|
1111
|
+
error: this.error
|
|
1112
|
+
}),
|
|
1113
|
+
availableRows
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
renderContentSearch(width) {
|
|
1117
|
+
const availableRows = Math.max(1, this.options.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
1118
|
+
return this.contentSearch.render(width, availableRows, this.loading, this.error);
|
|
1119
|
+
}
|
|
1120
|
+
renderPreview(width) {
|
|
1121
|
+
const availableRows = Math.max(1, this.options.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
1122
|
+
if (!this.loadedFile) return [this.options.theme.fg("warning", "Loading preview\u2026")];
|
|
1123
|
+
return renderFilePreview({
|
|
1124
|
+
theme: this.options.theme,
|
|
1125
|
+
keybindings: this.options.keybindings,
|
|
1126
|
+
width,
|
|
1127
|
+
availableRows,
|
|
1128
|
+
file: this.loadedFile,
|
|
1129
|
+
fileGit: this.loadedGit,
|
|
1130
|
+
project: this.options.gitContext?.project,
|
|
1131
|
+
revision: this.loadedRevision,
|
|
1132
|
+
contentMatch: this.activeContentMatch,
|
|
1133
|
+
blame: this.blame,
|
|
1134
|
+
cursor: this.previewCursor,
|
|
1135
|
+
anchor: this.previewAnchor,
|
|
1136
|
+
scrollOffset: this.previewScrollOffset,
|
|
1137
|
+
error: this.error,
|
|
1138
|
+
selectedContext: this.options.getSelectedContextState?.(),
|
|
1139
|
+
canContinue: this.options.onAddAndContinue !== void 0,
|
|
1140
|
+
canEdit: this.options.editFile !== void 0 && this.loadedRevision === void 0
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
renderPreviewHelp(width) {
|
|
1144
|
+
const availableRows = Math.max(1, this.options.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
1145
|
+
return renderFilePreviewHelp(
|
|
1146
|
+
this.options.theme,
|
|
1147
|
+
this.options.keybindings,
|
|
1148
|
+
width,
|
|
1149
|
+
availableRows,
|
|
1150
|
+
this.options.editFile !== void 0 && this.loadedRevision === void 0
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
renderRevisionInput(width) {
|
|
1154
|
+
const availableRows = Math.max(1, this.options.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
1155
|
+
const title = this.options.theme.fg(
|
|
1156
|
+
"accent",
|
|
1157
|
+
this.options.theme.bold(`Open Git revision \xB7 ${safeTerminalText(this.loadedFile?.path ?? "")}`)
|
|
1158
|
+
);
|
|
1159
|
+
const label = this.options.theme.fg("muted", "Revision: ");
|
|
1160
|
+
const inputWidth = Math.max(1, width - visibleWidth3(label));
|
|
1161
|
+
const input = `${label}${this.revisionInput.render(inputWidth)[0] ?? ""}`;
|
|
1162
|
+
const state = this.error ? this.options.theme.fg("error", safeTerminalText(this.error)) : this.loading ? this.options.theme.fg("warning", "Loading revision\u2026") : this.options.theme.fg("muted", "Enter open commit/branch/tag \xB7 Esc preview");
|
|
1163
|
+
return fitRows2(
|
|
1164
|
+
[truncateToWidth4(title, width, ""), truncateToWidth4(input, width, ""), truncateToWidth4(state, width, "")],
|
|
1165
|
+
availableRows
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
renderDiff(width) {
|
|
1169
|
+
const availableRows = Math.max(1, this.options.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
1170
|
+
const contentHeight = Math.max(1, availableRows - DIFF_CHROME_ROWS);
|
|
1171
|
+
const hunk = this.loadedGit?.hunks[this.diffHunkIndex];
|
|
1172
|
+
const path = safeTerminalText(this.loadedFile?.path ?? "");
|
|
1173
|
+
const title = this.options.theme.fg("accent", this.options.theme.bold(`Git diff \xB7 ${path} \xB7 HEAD \u2192 worktree`));
|
|
1174
|
+
const hunkLines = hunk?.lines ?? ["No changed hunks"];
|
|
1175
|
+
const maxScroll = Math.max(0, hunkLines.length - contentHeight);
|
|
1176
|
+
this.diffScrollOffset = Math.min(this.diffScrollOffset, maxScroll);
|
|
1177
|
+
const lines = hunkLines.slice(this.diffScrollOffset, this.diffScrollOffset + contentHeight).map(
|
|
1178
|
+
(line) => truncateToWidth4(
|
|
1179
|
+
line.startsWith("+") ? this.options.theme.fg("success", safeTerminalText(line)) : line.startsWith("-") ? this.options.theme.fg("error", safeTerminalText(line)) : safeTerminalText(line),
|
|
1180
|
+
width,
|
|
1181
|
+
""
|
|
1182
|
+
)
|
|
1183
|
+
);
|
|
1184
|
+
const text = hunk?.lines.join("\n") ?? "";
|
|
1185
|
+
const tokens = Math.max(1, Math.ceil(Buffer.byteLength(text, "utf8") / 4));
|
|
1186
|
+
const footer = this.error ? this.options.theme.fg("error", safeTerminalText(this.error)) : `~${tokens} tokens \xB7 Enter attach diff \xB7 Hunk ${hunk ? this.diffHunkIndex + 1 : 0}/${this.loadedGit?.hunks.length ?? 0} \xB7 rows ${hunkLines.length === 0 ? 0 : this.diffScrollOffset + 1}-${Math.min(hunkLines.length, this.diffScrollOffset + contentHeight)}/${hunkLines.length} \xB7 \u2191\u2193 scroll \xB7 [] navigate \xB7 Esc preview`;
|
|
1187
|
+
return fitRows2(
|
|
1188
|
+
[truncateToWidth4(title, width, ""), ...lines, truncateToWidth4(this.options.theme.fg("muted", footer), width, "")],
|
|
1189
|
+
availableRows
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
renderHistory(width) {
|
|
1193
|
+
return renderFileHistory({
|
|
1194
|
+
width,
|
|
1195
|
+
availableRows: Math.max(1, this.options.tui.terminal.rows - RESERVED_APP_ROWS),
|
|
1196
|
+
theme: this.options.theme,
|
|
1197
|
+
path: this.loadedFile?.path ?? "",
|
|
1198
|
+
history: this.history,
|
|
1199
|
+
index: this.historyIndex,
|
|
1200
|
+
error: this.error
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
handleFileInput(data) {
|
|
1204
|
+
if (this.options.keybindings.matches(data, "tui.select.cancel")) {
|
|
1205
|
+
this.cancelFileList();
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (matchesKey2(data, Key2.ctrl("f"))) {
|
|
1209
|
+
this.showContentSearch();
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (matchesKey2(data, Key2.escape)) {
|
|
1213
|
+
this.cancelFileList();
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (this.loading) return;
|
|
1217
|
+
if (this.options.keybindings.matches(data, "tui.select.up")) {
|
|
1218
|
+
this.selectedFileIndex = Math.max(0, this.selectedFileIndex - 1);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (this.options.keybindings.matches(data, "tui.select.down")) {
|
|
1222
|
+
this.selectedFileIndex = Math.min(Math.max(0, this.fileItems.length - 1), this.selectedFileIndex + 1);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (this.options.keybindings.matches(data, "tui.select.pageUp")) {
|
|
1226
|
+
this.selectedFileIndex = Math.max(0, this.selectedFileIndex - 10);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (this.options.keybindings.matches(data, "tui.select.pageDown")) {
|
|
1230
|
+
this.selectedFileIndex = Math.min(Math.max(0, this.fileItems.length - 1), this.selectedFileIndex + 10);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (this.options.keybindings.matches(data, "tui.select.confirm")) {
|
|
1234
|
+
this.openSelectedFileItem();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (this.options.keybindings.matches(data, "tui.input.tab")) {
|
|
1238
|
+
const item = this.fileItems[this.selectedFileIndex];
|
|
1239
|
+
if (item?.kind === "file") this.finish({ kind: "reference", path: item.path });
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (!this.isFileSearchActive() && this.currentDirectory && (matchesKey2(data, Key2.left) || matchesKey2(data, Key2.backspace))) {
|
|
1243
|
+
this.showParentDirectory();
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (!this.isFileSearchActive() && matchesKey2(data, Key2.right)) {
|
|
1247
|
+
const item = this.fileItems[this.selectedFileIndex];
|
|
1248
|
+
if (item?.kind === "directory") this.showDirectory(item.path);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const previousQuery = this.search.getValue();
|
|
1252
|
+
this.search.handleInput(data);
|
|
1253
|
+
const query = this.search.getValue();
|
|
1254
|
+
if (query !== previousQuery) {
|
|
1255
|
+
this.refreshFileItems(query);
|
|
1256
|
+
this.selectedFileIndex = 0;
|
|
1257
|
+
this.fileScrollOffset = 0;
|
|
1258
|
+
this.error = void 0;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
handleContentInput(data) {
|
|
1262
|
+
this.cancelOpenRequest();
|
|
1263
|
+
this.error = void 0;
|
|
1264
|
+
this.contentSearch.handleInput(data);
|
|
1265
|
+
}
|
|
1266
|
+
handlePreviewInput(data) {
|
|
1267
|
+
const loadedFile = this.loadedFile;
|
|
1268
|
+
if (!loadedFile) return;
|
|
1269
|
+
const lines = loadedFile.lines;
|
|
1270
|
+
if (this.options.keybindings.matches(data, "app.editor.external")) {
|
|
1271
|
+
if (this.loadedRevision) {
|
|
1272
|
+
this.error = "Historical revisions are read-only; return to the worktree preview to edit";
|
|
1273
|
+
} else if (this.options.editFile && !this.loading) {
|
|
1274
|
+
void this.editCurrentFile();
|
|
1275
|
+
} else if (!this.options.editFile) {
|
|
1276
|
+
this.error = "External editing is unavailable";
|
|
1277
|
+
}
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (matchesKey2(data, Key2.escape)) {
|
|
1281
|
+
this.returnToOrigin();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (data === "?") {
|
|
1285
|
+
this.cancelDetailRequest();
|
|
1286
|
+
this.mode = "preview-help";
|
|
1287
|
+
this.search.focused = false;
|
|
1288
|
+
this.contentSearch.focused = false;
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (data === "a" && this.options.onAddAndContinue) {
|
|
1292
|
+
try {
|
|
1293
|
+
const quote = this.createSelectedQuote();
|
|
1294
|
+
this.options.onAddAndContinue(quote);
|
|
1295
|
+
this.returnToOrigin();
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
this.error = formatError2(error);
|
|
1298
|
+
}
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (data === " ") {
|
|
1302
|
+
this.previewAnchor = this.previewAnchor === void 0 ? this.previewCursor : void 0;
|
|
1303
|
+
this.error = void 0;
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (data === "b") {
|
|
1307
|
+
void this.loadBlame();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (data === "h") {
|
|
1311
|
+
void this.loadHistory();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (data === "r") {
|
|
1315
|
+
this.mode = "revision";
|
|
1316
|
+
this.revisionInput.setValue("");
|
|
1317
|
+
this.revisionInput.focused = this.isFocused;
|
|
1318
|
+
this.error = void 0;
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (data === "d") {
|
|
1322
|
+
if (this.loadedRevision) {
|
|
1323
|
+
this.error = "Diff context is available from the worktree preview";
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if ((this.loadedGit?.hunks.length ?? 0) === 0) {
|
|
1327
|
+
this.error = "No changed hunks for this file";
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.diffHunkIndex = Math.max(0, this.hunkIndex);
|
|
1331
|
+
this.diffScrollOffset = 0;
|
|
1332
|
+
this.mode = "diff";
|
|
1333
|
+
this.error = void 0;
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (this.options.keybindings.matches(data, "tui.select.up")) {
|
|
1337
|
+
this.movePreviewCursor(Math.max(0, this.previewCursor - 1));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (this.options.keybindings.matches(data, "tui.select.down")) {
|
|
1341
|
+
this.movePreviewCursor(Math.min(Math.max(0, lines.length - 1), this.previewCursor + 1));
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (this.options.keybindings.matches(data, "tui.select.pageUp")) {
|
|
1345
|
+
this.movePreviewCursor(Math.max(0, this.previewCursor - 10));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (this.options.keybindings.matches(data, "tui.select.pageDown")) {
|
|
1349
|
+
this.movePreviewCursor(Math.min(Math.max(0, lines.length - 1), this.previewCursor + 10));
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (data === "]" || data === "[") {
|
|
1353
|
+
this.navigateHunk(data === "]" ? 1 : -1);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (this.options.keybindings.matches(data, "tui.select.confirm")) {
|
|
1357
|
+
try {
|
|
1358
|
+
const quote = this.createSelectedQuote();
|
|
1359
|
+
this.options.validateQuote?.(quote);
|
|
1360
|
+
this.finish({ kind: "quote", quote });
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
this.error = formatError2(error);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
handlePreviewHelpInput(data) {
|
|
1367
|
+
if (matchesKey2(data, Key2.escape)) this.mode = "preview";
|
|
1368
|
+
}
|
|
1369
|
+
handleHistoryInput(data) {
|
|
1370
|
+
if (matchesKey2(data, Key2.escape)) {
|
|
1371
|
+
this.cancelDetailRequest();
|
|
1372
|
+
this.mode = "preview";
|
|
1373
|
+
this.error = void 0;
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (this.options.keybindings.matches(data, "tui.select.up")) {
|
|
1377
|
+
this.historyIndex = Math.max(0, this.historyIndex - 1);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (this.options.keybindings.matches(data, "tui.select.down")) {
|
|
1381
|
+
this.historyIndex = Math.min(Math.max(0, this.history.length - 1), this.historyIndex + 1);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (this.options.keybindings.matches(data, "tui.select.confirm")) {
|
|
1385
|
+
const entry = this.history[this.historyIndex];
|
|
1386
|
+
if (entry) void this.loadRevision(entry.commit, entry.path);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
handleRevisionInput(data) {
|
|
1390
|
+
if (matchesKey2(data, Key2.escape)) {
|
|
1391
|
+
this.cancelDetailRequest();
|
|
1392
|
+
this.mode = "preview";
|
|
1393
|
+
this.revisionInput.focused = false;
|
|
1394
|
+
this.error = void 0;
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (this.loading) return;
|
|
1398
|
+
if (this.options.keybindings.matches(data, "tui.select.confirm")) {
|
|
1399
|
+
void this.loadRevision(this.revisionInput.getValue());
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
this.revisionInput.handleInput(data);
|
|
1403
|
+
}
|
|
1404
|
+
handleDiffInput(data) {
|
|
1405
|
+
if (matchesKey2(data, Key2.escape)) {
|
|
1406
|
+
this.mode = "preview";
|
|
1407
|
+
this.error = void 0;
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const hunks = this.loadedGit?.hunks ?? [];
|
|
1411
|
+
const hunkLines = hunks[this.diffHunkIndex]?.lines ?? [];
|
|
1412
|
+
if (this.options.keybindings.matches(data, "tui.select.up")) {
|
|
1413
|
+
this.diffScrollOffset = Math.max(0, this.diffScrollOffset - 1);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (this.options.keybindings.matches(data, "tui.select.down")) {
|
|
1417
|
+
this.diffScrollOffset = Math.min(Math.max(0, hunkLines.length - 1), this.diffScrollOffset + 1);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (this.options.keybindings.matches(data, "tui.select.pageUp")) {
|
|
1421
|
+
this.diffScrollOffset = Math.max(0, this.diffScrollOffset - 10);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (this.options.keybindings.matches(data, "tui.select.pageDown")) {
|
|
1425
|
+
this.diffScrollOffset = Math.min(Math.max(0, hunkLines.length - 1), this.diffScrollOffset + 10);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (data === "]" || data === "[") {
|
|
1429
|
+
if (hunks.length > 0) {
|
|
1430
|
+
const direction = data === "]" ? 1 : -1;
|
|
1431
|
+
this.diffHunkIndex = (this.diffHunkIndex + direction + hunks.length) % hunks.length;
|
|
1432
|
+
this.diffScrollOffset = 0;
|
|
1433
|
+
}
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (this.options.keybindings.matches(data, "tui.select.confirm")) {
|
|
1437
|
+
const hunk = hunks[this.diffHunkIndex];
|
|
1438
|
+
const loadedFile = this.loadedFile;
|
|
1439
|
+
const project = this.options.gitContext?.project;
|
|
1440
|
+
if (!hunk || !loadedFile || !project) return;
|
|
1441
|
+
try {
|
|
1442
|
+
const startLine = Math.max(1, hunk.newStart);
|
|
1443
|
+
const endLine = Math.max(startLine, hunk.newStart + hunk.newCount - 1);
|
|
1444
|
+
const quote = createFileQuoteSnapshot(loadedFile.path, startLine, endLine, hunk.lines.join("\n"), {
|
|
1445
|
+
head: project.head,
|
|
1446
|
+
branch: project.branch,
|
|
1447
|
+
status: this.loadedGit?.status?.label ?? "modified",
|
|
1448
|
+
blob: this.loadedGit?.blob,
|
|
1449
|
+
source: "git_diff",
|
|
1450
|
+
base: "HEAD"
|
|
1451
|
+
});
|
|
1452
|
+
this.options.validateQuote?.(quote);
|
|
1453
|
+
this.finish({ kind: "quote", quote });
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
this.error = formatError2(error);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async editCurrentFile() {
|
|
1460
|
+
const path = this.loadedFile?.path;
|
|
1461
|
+
const editFile = this.options.editFile;
|
|
1462
|
+
if (!path || !editFile || this.loadedRevision) return;
|
|
1463
|
+
this.cancelOpenRequest();
|
|
1464
|
+
this.cancelDetailRequest();
|
|
1465
|
+
this.cancelEditRequest();
|
|
1466
|
+
const request = this.editRequest;
|
|
1467
|
+
const controller = new AbortController();
|
|
1468
|
+
this.editController = controller;
|
|
1469
|
+
this.loading = true;
|
|
1470
|
+
this.error = void 0;
|
|
1471
|
+
this.options.tui.requestRender();
|
|
1472
|
+
try {
|
|
1473
|
+
await editFile(path, controller.signal);
|
|
1474
|
+
if (!this.isCurrentEditRequest(request, controller, path)) return;
|
|
1475
|
+
const gitContext = this.options.gitContext;
|
|
1476
|
+
const [loadedFile, loadedGit] = await Promise.all([
|
|
1477
|
+
this.options.loadFile(path, controller.signal),
|
|
1478
|
+
gitContext ? gitContext.refreshFileContext?.(path, controller.signal) ?? gitContext.getFileContext(path, controller.signal) : void 0
|
|
1479
|
+
]);
|
|
1480
|
+
if (!this.isCurrentEditRequest(request, controller, path)) return;
|
|
1481
|
+
const maximumIndex = Math.max(0, loadedFile.lines.length - 1);
|
|
1482
|
+
this.loadedFile = loadedFile;
|
|
1483
|
+
this.loadedGit = loadedGit;
|
|
1484
|
+
this.previewCursor = Math.min(this.previewCursor, maximumIndex);
|
|
1485
|
+
if (this.previewAnchor !== void 0) {
|
|
1486
|
+
this.previewAnchor = Math.min(this.previewAnchor, maximumIndex);
|
|
1487
|
+
}
|
|
1488
|
+
this.previewScrollOffset = Math.min(this.previewScrollOffset, maximumIndex);
|
|
1489
|
+
this.activeContentMatch = void 0;
|
|
1490
|
+
this.hunkIndex = -1;
|
|
1491
|
+
this.blame = void 0;
|
|
1492
|
+
this.history = [];
|
|
1493
|
+
this.error = void 0;
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
if (this.isCurrentEditRequest(request, controller, path) && !isAbortError3(error)) {
|
|
1496
|
+
this.error = formatError2(error);
|
|
1497
|
+
}
|
|
1498
|
+
} finally {
|
|
1499
|
+
if (request === this.editRequest && this.editController === controller) {
|
|
1500
|
+
this.loading = false;
|
|
1501
|
+
this.editController = void 0;
|
|
1502
|
+
}
|
|
1503
|
+
if (!this.finished && !this.disposed) this.options.tui.requestRender(true);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
async loadRevision(revision, historicalPath) {
|
|
1507
|
+
const path = this.loadedFile?.path;
|
|
1508
|
+
const gitContext = this.options.gitContext;
|
|
1509
|
+
if (!path || !gitContext) {
|
|
1510
|
+
this.error = "Git revision browsing is unavailable";
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const { request, signal } = this.beginDetailRequest();
|
|
1514
|
+
this.error = void 0;
|
|
1515
|
+
this.options.tui.requestRender();
|
|
1516
|
+
try {
|
|
1517
|
+
const loadedRevision = await gitContext.loadRevision(path, revision, historicalPath, signal);
|
|
1518
|
+
if (this.finished || request !== this.detailRequest) return;
|
|
1519
|
+
this.loadedRevision = loadedRevision;
|
|
1520
|
+
this.loadedFile = { path: loadedRevision.path, lines: loadedRevision.lines };
|
|
1521
|
+
this.activeContentMatch = void 0;
|
|
1522
|
+
this.previewCursor = 0;
|
|
1523
|
+
this.previewAnchor = void 0;
|
|
1524
|
+
this.previewScrollOffset = 0;
|
|
1525
|
+
this.blame = void 0;
|
|
1526
|
+
this.mode = "preview";
|
|
1527
|
+
this.revisionInput.focused = false;
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
if (request === this.detailRequest && !isAbortError3(error)) this.error = formatError2(error);
|
|
1530
|
+
} finally {
|
|
1531
|
+
this.finishDetailRequest(request);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
async loadBlame() {
|
|
1535
|
+
const path = this.loadedFile?.path;
|
|
1536
|
+
const gitContext = this.options.gitContext;
|
|
1537
|
+
if (!path || !gitContext) {
|
|
1538
|
+
this.error = "Git blame is unavailable";
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const { request, signal } = this.beginDetailRequest();
|
|
1542
|
+
const requestedLine = this.previewCursor + 1;
|
|
1543
|
+
this.error = void 0;
|
|
1544
|
+
this.options.tui.requestRender();
|
|
1545
|
+
try {
|
|
1546
|
+
const blame = await gitContext.getBlame(path, requestedLine, this.loadedRevision?.commit, signal);
|
|
1547
|
+
if (this.finished || request !== this.detailRequest || this.mode !== "preview" || requestedLine !== this.previewCursor + 1) {
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
this.blame = blame;
|
|
1551
|
+
if (!blame) this.error = "No blame information for this line";
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if (request === this.detailRequest && !isAbortError3(error)) this.error = formatError2(error);
|
|
1554
|
+
} finally {
|
|
1555
|
+
this.finishDetailRequest(request);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
async loadHistory() {
|
|
1559
|
+
const path = this.loadedFile?.path;
|
|
1560
|
+
const gitContext = this.options.gitContext;
|
|
1561
|
+
if (!path || !gitContext) {
|
|
1562
|
+
this.error = "Git history is unavailable";
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const { request, signal } = this.beginDetailRequest();
|
|
1566
|
+
this.error = void 0;
|
|
1567
|
+
this.options.tui.requestRender();
|
|
1568
|
+
try {
|
|
1569
|
+
const history = await gitContext.getHistory(path, signal);
|
|
1570
|
+
if (this.finished || request !== this.detailRequest || this.mode !== "preview") return;
|
|
1571
|
+
this.history = history;
|
|
1572
|
+
this.historyIndex = 0;
|
|
1573
|
+
this.mode = "history";
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
if (request === this.detailRequest && !isAbortError3(error)) this.error = formatError2(error);
|
|
1576
|
+
} finally {
|
|
1577
|
+
this.finishDetailRequest(request);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async openFile(path, options = {}) {
|
|
1581
|
+
this.cancelOpenRequest();
|
|
1582
|
+
const request = this.openRequest;
|
|
1583
|
+
const controller = new AbortController();
|
|
1584
|
+
this.openController = controller;
|
|
1585
|
+
this.loading = true;
|
|
1586
|
+
this.error = void 0;
|
|
1587
|
+
this.options.tui.requestRender();
|
|
1588
|
+
try {
|
|
1589
|
+
const [loadedFile, loadedGit] = await Promise.all([
|
|
1590
|
+
this.options.loadFile(path, controller.signal),
|
|
1591
|
+
this.options.gitContext?.getFileContext(path, controller.signal)
|
|
1592
|
+
]);
|
|
1593
|
+
if (!this.isCurrentOpenRequest(request, controller)) return;
|
|
1594
|
+
this.loadedFile = loadedFile;
|
|
1595
|
+
this.loadedGit = loadedGit;
|
|
1596
|
+
this.loadedRevision = void 0;
|
|
1597
|
+
this.mode = "preview";
|
|
1598
|
+
this.previewReturnMode = options.returnMode ?? "files";
|
|
1599
|
+
this.activeContentMatch = options.match;
|
|
1600
|
+
this.previewCursor = Math.max(0, Math.min(Math.max(0, loadedFile.lines.length - 1), options.cursorIndex ?? 0));
|
|
1601
|
+
this.previewAnchor = void 0;
|
|
1602
|
+
this.previewScrollOffset = Math.max(0, this.previewCursor - 1);
|
|
1603
|
+
this.hunkIndex = -1;
|
|
1604
|
+
this.blame = void 0;
|
|
1605
|
+
this.history = [];
|
|
1606
|
+
this.detailRequest += 1;
|
|
1607
|
+
this.error = void 0;
|
|
1608
|
+
this.search.focused = false;
|
|
1609
|
+
this.contentSearch.focused = false;
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
if (this.isCurrentOpenRequest(request, controller) && !isAbortError3(error)) {
|
|
1612
|
+
this.error = formatError2(error);
|
|
1613
|
+
}
|
|
1614
|
+
} finally {
|
|
1615
|
+
if (request === this.openRequest) {
|
|
1616
|
+
this.loading = false;
|
|
1617
|
+
this.openController = void 0;
|
|
1618
|
+
}
|
|
1619
|
+
if (!this.finished && !this.disposed) this.options.tui.requestRender();
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
navigateHunk(direction) {
|
|
1623
|
+
const hunks = this.loadedGit?.hunks ?? [];
|
|
1624
|
+
const lineCount = this.loadedFile?.lines.length ?? 0;
|
|
1625
|
+
if (hunks.length === 0 || lineCount === 0) {
|
|
1626
|
+
this.error = "No changed hunks for this file";
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
this.hunkIndex = this.hunkIndex < 0 ? direction > 0 ? 0 : hunks.length - 1 : (this.hunkIndex + direction + hunks.length) % hunks.length;
|
|
1630
|
+
const hunk = hunks[this.hunkIndex];
|
|
1631
|
+
const selectedLines = hunk.changedLines.length > 0 ? hunk.changedLines : [hunk.newStart];
|
|
1632
|
+
const start = Math.max(0, Math.min(...selectedLines) - 1);
|
|
1633
|
+
const end = Math.max(start, Math.min(lineCount - 1, Math.max(...selectedLines) - 1));
|
|
1634
|
+
this.previewAnchor = start;
|
|
1635
|
+
this.movePreviewCursor(end);
|
|
1636
|
+
this.error = void 0;
|
|
1637
|
+
}
|
|
1638
|
+
showContentSearch() {
|
|
1639
|
+
this.cancelOpenRequest();
|
|
1640
|
+
this.mode = "contents";
|
|
1641
|
+
this.search.focused = false;
|
|
1642
|
+
this.contentSearch.activate();
|
|
1643
|
+
this.contentSearch.focused = this.isFocused;
|
|
1644
|
+
this.error = void 0;
|
|
1645
|
+
}
|
|
1646
|
+
showFileSearch() {
|
|
1647
|
+
this.contentSearch.deactivate();
|
|
1648
|
+
this.cancelOpenRequest();
|
|
1649
|
+
this.mode = "files";
|
|
1650
|
+
this.search.focused = this.isFocused;
|
|
1651
|
+
this.error = void 0;
|
|
1652
|
+
}
|
|
1653
|
+
openSelectedFileItem() {
|
|
1654
|
+
const item = this.fileItems[this.selectedFileIndex];
|
|
1655
|
+
if (item?.kind === "directory") {
|
|
1656
|
+
this.showDirectory(item.path);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (item?.kind === "file") void this.openFile(item.path);
|
|
1660
|
+
}
|
|
1661
|
+
showDirectory(directory) {
|
|
1662
|
+
this.directoryPositions.set(this.currentDirectory, {
|
|
1663
|
+
index: this.selectedFileIndex,
|
|
1664
|
+
offset: this.fileScrollOffset
|
|
1665
|
+
});
|
|
1666
|
+
this.currentDirectory = directory;
|
|
1667
|
+
this.refreshFileItems("");
|
|
1668
|
+
const position = this.directoryPositions.get(directory);
|
|
1669
|
+
this.selectedFileIndex = Math.min(position?.index ?? 0, Math.max(0, this.fileItems.length - 1));
|
|
1670
|
+
this.fileScrollOffset = Math.min(position?.offset ?? 0, this.selectedFileIndex);
|
|
1671
|
+
this.error = void 0;
|
|
1672
|
+
}
|
|
1673
|
+
showParentDirectory() {
|
|
1674
|
+
this.cancelOpenRequest();
|
|
1675
|
+
this.currentDirectory = parentProjectDirectory(this.currentDirectory);
|
|
1676
|
+
this.refreshFileItems("");
|
|
1677
|
+
const position = this.directoryPositions.get(this.currentDirectory);
|
|
1678
|
+
this.selectedFileIndex = Math.min(position?.index ?? 0, Math.max(0, this.fileItems.length - 1));
|
|
1679
|
+
this.fileScrollOffset = Math.min(position?.offset ?? 0, this.selectedFileIndex);
|
|
1680
|
+
this.error = void 0;
|
|
1681
|
+
}
|
|
1682
|
+
refreshFileItems(query) {
|
|
1683
|
+
this.fileItems = this.isFileSearchActive(query) ? this.fileBrowser.searchResults(this.fileSearch.search(query)) : this.fileBrowser.list(this.currentDirectory);
|
|
1684
|
+
}
|
|
1685
|
+
isFileSearchActive(query = this.search.getValue()) {
|
|
1686
|
+
return safeTerminalText(query).trim().length > 0;
|
|
1687
|
+
}
|
|
1688
|
+
cancelFileList() {
|
|
1689
|
+
if (this.currentDirectory && !this.isFileSearchActive()) {
|
|
1690
|
+
this.showParentDirectory();
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
this.finish(this.rootExit("back"));
|
|
1694
|
+
}
|
|
1695
|
+
cancelOpenRequest() {
|
|
1696
|
+
this.openRequest += 1;
|
|
1697
|
+
this.openController?.abort();
|
|
1698
|
+
this.openController = void 0;
|
|
1699
|
+
this.loading = false;
|
|
1700
|
+
}
|
|
1701
|
+
isCurrentOpenRequest(request, controller) {
|
|
1702
|
+
return !this.finished && !this.disposed && request === this.openRequest && this.openController === controller && !controller.signal.aborted;
|
|
1703
|
+
}
|
|
1704
|
+
isCurrentEditRequest(request, controller, path) {
|
|
1705
|
+
return !this.finished && !this.disposed && this.mode === "preview" && this.loadedRevision === void 0 && this.loadedFile?.path === path && request === this.editRequest && this.editController === controller && !controller.signal.aborted;
|
|
1706
|
+
}
|
|
1707
|
+
beginDetailRequest() {
|
|
1708
|
+
this.cancelDetailRequest();
|
|
1709
|
+
const request = this.detailRequest;
|
|
1710
|
+
const controller = new AbortController();
|
|
1711
|
+
this.detailController = controller;
|
|
1712
|
+
this.loading = true;
|
|
1713
|
+
return { request, signal: controller.signal };
|
|
1714
|
+
}
|
|
1715
|
+
finishDetailRequest(request) {
|
|
1716
|
+
if (request === this.detailRequest) {
|
|
1717
|
+
this.loading = false;
|
|
1718
|
+
this.detailController = void 0;
|
|
1719
|
+
}
|
|
1720
|
+
if (!this.finished && !this.disposed) this.options.tui.requestRender();
|
|
1721
|
+
}
|
|
1722
|
+
cancelDetailRequest() {
|
|
1723
|
+
this.detailRequest += 1;
|
|
1724
|
+
this.detailController?.abort();
|
|
1725
|
+
this.detailController = void 0;
|
|
1726
|
+
this.loading = false;
|
|
1727
|
+
}
|
|
1728
|
+
cancelEditRequest() {
|
|
1729
|
+
this.editRequest += 1;
|
|
1730
|
+
this.editController?.abort();
|
|
1731
|
+
this.editController = void 0;
|
|
1732
|
+
this.loading = false;
|
|
1733
|
+
}
|
|
1734
|
+
movePreviewCursor(next) {
|
|
1735
|
+
if (next === this.previewCursor) return;
|
|
1736
|
+
this.cancelDetailRequest();
|
|
1737
|
+
this.previewCursor = next;
|
|
1738
|
+
this.blame = void 0;
|
|
1739
|
+
this.error = void 0;
|
|
1740
|
+
}
|
|
1741
|
+
createSelectedQuote() {
|
|
1742
|
+
if (!this.loadedFile) throw new Error("No file is open");
|
|
1743
|
+
return createPreviewQuote(
|
|
1744
|
+
this.loadedFile,
|
|
1745
|
+
this.previewAnchor ?? this.previewCursor,
|
|
1746
|
+
this.previewCursor,
|
|
1747
|
+
this.options.gitContext?.project,
|
|
1748
|
+
this.loadedGit,
|
|
1749
|
+
this.loadedRevision
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
returnToOrigin() {
|
|
1753
|
+
this.cancelDetailRequest();
|
|
1754
|
+
this.cancelOpenRequest();
|
|
1755
|
+
this.cancelEditRequest();
|
|
1756
|
+
this.mode = this.previewReturnMode;
|
|
1757
|
+
this.loadedFile = void 0;
|
|
1758
|
+
this.loadedGit = void 0;
|
|
1759
|
+
this.loadedRevision = void 0;
|
|
1760
|
+
this.previewAnchor = void 0;
|
|
1761
|
+
this.blame = void 0;
|
|
1762
|
+
this.error = void 0;
|
|
1763
|
+
this.search.focused = this.isFocused && this.mode === "files";
|
|
1764
|
+
this.contentSearch.focused = this.isFocused && this.mode === "contents";
|
|
1765
|
+
}
|
|
1766
|
+
finish(result) {
|
|
1767
|
+
this.finished = true;
|
|
1768
|
+
this.contentSearch.dispose();
|
|
1769
|
+
this.cancelOpenRequest();
|
|
1770
|
+
this.cancelDetailRequest();
|
|
1771
|
+
this.cancelEditRequest();
|
|
1772
|
+
this.options.done(result);
|
|
1773
|
+
}
|
|
1774
|
+
rootExit(kind) {
|
|
1775
|
+
return this.options.rootNavigation ? { kind } : void 0;
|
|
1776
|
+
}
|
|
1777
|
+
keepFileVisible(height) {
|
|
1778
|
+
if (this.selectedFileIndex < this.fileScrollOffset) this.fileScrollOffset = this.selectedFileIndex;
|
|
1779
|
+
if (this.selectedFileIndex >= this.fileScrollOffset + height) {
|
|
1780
|
+
this.fileScrollOffset = this.selectedFileIndex - height + 1;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
function isAbortError3(error) {
|
|
1785
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1786
|
+
}
|
|
1787
|
+
function formatError2(error) {
|
|
1788
|
+
return error instanceof Error ? error.message : String(error);
|
|
1789
|
+
}
|
|
1790
|
+
export {
|
|
1791
|
+
FileQuoteExplorer
|
|
1792
|
+
};
|
|
1793
|
+
//# sourceMappingURL=file-context-explorer-WR6GQZHF.ts.map
|