@xuhaojun/githunk 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/dist/githunk.js +1446 -313
  3. package/package.json +4 -3
package/dist/githunk.js CHANGED
@@ -1,6 +1,64 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli/args.ts
4
+ import { Command, CommanderError } from "commander";
5
+ import { readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ var cachedVersion;
9
+ function getCliVersion() {
10
+ if (cachedVersion !== undefined)
11
+ return cachedVersion;
12
+ let directory = dirname(fileURLToPath(import.meta.url));
13
+ for (let level = 0;level < 4; level += 1) {
14
+ try {
15
+ const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
16
+ if (manifest.name === "@xuhaojun/githunk" && typeof manifest.version === "string") {
17
+ cachedVersion = manifest.version;
18
+ return cachedVersion;
19
+ }
20
+ } catch {}
21
+ const parent = dirname(directory);
22
+ if (parent === directory)
23
+ break;
24
+ directory = parent;
25
+ }
26
+ cachedVersion = "0.0.0-dev";
27
+ return cachedVersion;
28
+ }
29
+ function parseCliArgs(argv) {
30
+ let stdout = "";
31
+ let stderr = "";
32
+ const program = new Command;
33
+ program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(getCliVersion(), "-V, --version", "output the version number").option("-p, --path <dir>", "path to the Git repository to open").argument("[path]", "path to the Git repository to open").exitOverride().configureOutput({
34
+ writeOut: (text) => {
35
+ stdout += text;
36
+ },
37
+ writeErr: (text) => {
38
+ stderr += text;
39
+ }
40
+ });
41
+ try {
42
+ program.parse([...argv], { from: "user" });
43
+ } catch (error) {
44
+ if (error instanceof CommanderError) {
45
+ if (error.code === "commander.helpDisplayed")
46
+ return { kind: "help", text: stdout };
47
+ if (error.code === "commander.version")
48
+ return { kind: "version", text: stdout };
49
+ const message = stderr.trim() === "" ? error.message : stderr.trim();
50
+ return { kind: "error", message, exitCode: error.exitCode };
51
+ }
52
+ throw error;
53
+ }
54
+ const options = program.opts();
55
+ const positional = program.args[0];
56
+ const startDirectory = options.path ?? positional;
57
+ return startDirectory === undefined ? { kind: "start" } : { kind: "start", startDirectory };
58
+ }
59
+
3
60
  // src/main.ts
61
+ import { resolve as resolve4 } from "node:path";
4
62
  import { createCliRenderer } from "@opentui/core";
5
63
 
6
64
  // src/domain/command.ts
@@ -775,6 +833,7 @@ function withCommitStatuses(commits, sets) {
775
833
 
776
834
  // src/git/commits.ts
777
835
  var LOG_FORMAT = "%H%n%h%n%P%n%an%n%aI%n%s%n%b";
836
+ var COMMITS_LIMIT = 300;
778
837
  function parseSummary(record) {
779
838
  const fields = record.split(`
780
839
  `);
@@ -802,8 +861,11 @@ function parseSummary(record) {
802
861
  function parseCommitLog(raw) {
803
862
  return raw.split("\x00").map(parseSummary).filter((summary) => summary !== undefined);
804
863
  }
805
- async function listCommits(runner, range, filter) {
806
- const args = ["log", "-z", "--topo-order", `--format=${LOG_FORMAT}`, range];
864
+ async function listCommits(runner, range, filter, options) {
865
+ const args = ["log", "-z", "--topo-order", `--format=${LOG_FORMAT}`];
866
+ if (options?.limit ?? true)
867
+ args.push(`-${COMMITS_LIMIT}`);
868
+ args.push(range);
807
869
  if (filter !== undefined && filter.length > 0)
808
870
  args.push("--", filter);
809
871
  const [result, statusSets] = await Promise.all([
@@ -916,7 +978,7 @@ function fingerprintWorkingTreeFile(target, filePatch) {
916
978
 
917
979
  // src/storage/local-state-file.ts
918
980
  import { mkdir, open, rename, stat, unlink, lstat, link, readFile } from "node:fs/promises";
919
- import { dirname, isAbsolute, join, resolve } from "node:path";
981
+ import { dirname as dirname2, isAbsolute, join as join2, resolve } from "node:path";
920
982
  import { randomUUID } from "node:crypto";
921
983
  async function assertNoSymlinkInPath(path, pathKind) {
922
984
  const absolute = resolve(path);
@@ -946,7 +1008,7 @@ class LocalStateFile {
946
1008
  this.pathKind = options.pathKind ?? "state";
947
1009
  }
948
1010
  get path() {
949
- return this.resolvedPath ?? join(this.runner.cwd, ".git", this.relativePath);
1011
+ return this.resolvedPath ?? join2(this.runner.cwd, ".git", this.relativePath);
950
1012
  }
951
1013
  async resolvePath() {
952
1014
  if (this.resolvedPath !== undefined)
@@ -954,7 +1016,7 @@ class LocalStateFile {
954
1016
  const output = (await this.runner.run(["rev-parse", "--git-path", this.relativePath], { readOnly: true })).stdout.trim();
955
1017
  if (output.length === 0)
956
1018
  throw new Error(`git returned an empty path for ${this.relativePath}`);
957
- this.resolvedPath = isAbsolute(output) ? output : join(this.runner.cwd, output);
1019
+ this.resolvedPath = isAbsolute(output) ? output : join2(this.runner.cwd, output);
958
1020
  return this.resolvedPath;
959
1021
  }
960
1022
  async readText() {
@@ -971,7 +1033,7 @@ class LocalStateFile {
971
1033
  async writeText(text) {
972
1034
  const path = await this.resolvePath();
973
1035
  await assertNoSymlinkInPath(path, this.pathKind);
974
- await mkdir(dirname(path), { recursive: true, mode: 448 });
1036
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
975
1037
  const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
976
1038
  await assertNoSymlinkInPath(temporary, this.pathKind);
977
1039
  const handle = await open(temporary, "wx", 384);
@@ -984,7 +1046,7 @@ class LocalStateFile {
984
1046
  }
985
1047
  await rename(temporary, path);
986
1048
  try {
987
- const directory = await open(dirname(path), "r");
1049
+ const directory = await open(dirname2(path), "r");
988
1050
  try {
989
1051
  await directory.sync();
990
1052
  } finally {
@@ -1008,7 +1070,7 @@ class LocalStateFile {
1008
1070
  async createTextExclusive(text) {
1009
1071
  const path = await this.resolvePath();
1010
1072
  await assertNoSymlinkInPath(path, this.pathKind);
1011
- await mkdir(dirname(path), { recursive: true, mode: 448 });
1073
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
1012
1074
  const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
1013
1075
  await assertNoSymlinkInPath(temporary, this.pathKind);
1014
1076
  const handle = await open(temporary, "wx", 384);
@@ -1037,7 +1099,7 @@ class LocalStateFile {
1037
1099
  return;
1038
1100
  });
1039
1101
  try {
1040
- const directory = await open(dirname(path), "r");
1102
+ const directory = await open(dirname2(path), "r");
1041
1103
  try {
1042
1104
  await directory.sync();
1043
1105
  } finally {
@@ -2072,7 +2134,7 @@ async function listReflog(runner, options = {}) {
2072
2134
 
2073
2135
  // src/git/worktrees.ts
2074
2136
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
2075
- import { dirname as dirname2, join as join2 } from "node:path";
2137
+ import { dirname as dirname3, join as join3 } from "node:path";
2076
2138
  function finalizeEntry(entry) {
2077
2139
  return {
2078
2140
  path: entry.path,
@@ -2174,7 +2236,7 @@ async function resolveRepositoryPaths(runner) {
2174
2236
  const worktreeGitDirPath = lines[1] ?? "";
2175
2237
  const repoGitDirPath = lines[2] ?? "";
2176
2238
  const isSubmodule = (lines[3] ?? "").length > 0;
2177
- const repoPath = worktreeGitDirPath === repoGitDirPath || isSubmodule ? worktreePath : dirname2(repoGitDirPath);
2239
+ const repoPath = worktreeGitDirPath === repoGitDirPath || isSubmodule ? worktreePath : dirname3(repoGitDirPath);
2178
2240
  return { worktreePath, worktreeGitDirPath, repoPath, repoGitDirPath };
2179
2241
  }
2180
2242
  async function isPathMissing(path) {
@@ -2207,11 +2269,11 @@ async function readTrimmedFile(path) {
2207
2269
  }
2208
2270
  async function inProgressBranch(gitDir) {
2209
2271
  for (const directory of ["rebase-merge", "rebase-apply"]) {
2210
- const headName = await readTrimmedFile(join2(gitDir, directory, "head-name"));
2272
+ const headName = await readTrimmedFile(join3(gitDir, directory, "head-name"));
2211
2273
  if (headName !== undefined)
2212
2274
  return headName.replace(/^refs\/heads\//, "");
2213
2275
  }
2214
- return await readTrimmedFile(join2(gitDir, "BISECT_START"));
2276
+ return await readTrimmedFile(join3(gitDir, "BISECT_START"));
2215
2277
  }
2216
2278
  async function listWorktrees(runner) {
2217
2279
  const repositoryPaths = await resolveRepositoryPaths(runner);
@@ -2278,7 +2340,7 @@ function worktreeRemovalRequiresForce(error) {
2278
2340
 
2279
2341
  // src/git/submodules.ts
2280
2342
  import { readFile as readFile3 } from "node:fs/promises";
2281
- import { join as join3, resolve as resolve2 } from "node:path";
2343
+ import { join as join4, resolve as resolve2 } from "node:path";
2282
2344
 
2283
2345
  // src/domain/submodule.ts
2284
2346
  function submoduleFullName(submodule) {
@@ -2333,7 +2395,7 @@ function parseGitModules(raw) {
2333
2395
  }
2334
2396
  async function readGitModules(directory) {
2335
2397
  try {
2336
- return await readFile3(join3(directory, ".gitmodules"), "utf8");
2398
+ return await readFile3(join4(directory, ".gitmodules"), "utf8");
2337
2399
  } catch (error) {
2338
2400
  if (error instanceof Error && "code" in error) {
2339
2401
  const code = error.code;
@@ -2344,7 +2406,7 @@ async function readGitModules(directory) {
2344
2406
  }
2345
2407
  }
2346
2408
  async function collectSubmodules(worktreePath, parentModule, visited) {
2347
- const directory = parentModule === undefined ? worktreePath : join3(worktreePath, submoduleFullPath(parentModule));
2409
+ const directory = parentModule === undefined ? worktreePath : join4(worktreePath, submoduleFullPath(parentModule));
2348
2410
  const resolved = resolve2(directory);
2349
2411
  if (visited.has(resolved))
2350
2412
  return [];
@@ -2474,6 +2536,7 @@ class AppController {
2474
2536
  loadCommitDetails;
2475
2537
  loadCommitFile;
2476
2538
  generation = 0;
2539
+ limitCommits = true;
2477
2540
  pullRequestRefreshGeneration = 0;
2478
2541
  currentState;
2479
2542
  reviewDatabase = emptyWorkingTreeReviewDatabase();
@@ -2494,7 +2557,7 @@ class AppController {
2494
2557
  return loadWorkingTree(runner, target2.scope, snapshotOptions ?? {});
2495
2558
  });
2496
2559
  this.loadBranchesListing = options instanceof GitRunner ? () => listBranches(options) : options.loadBranches ?? options.branchesLoader ?? (runner !== undefined ? () => listBranches(runner) : async () => ({ detached: true, localBranches: [], remotes: [] }));
2497
- this.loadCommitList = options instanceof GitRunner ? (range, filter) => listCommits(options, range, filter) : options.loadCommits ?? options.commitsLoader ?? (runner === undefined ? async () => [] : (range, filter) => listCommits(runner, range, filter));
2560
+ this.loadCommitList = options instanceof GitRunner ? (range, filter, listOptions) => listCommits(options, range, filter, listOptions) : options.loadCommits ?? options.commitsLoader ?? (runner === undefined ? async () => [] : (range, filter, listOptions) => listCommits(runner, range, filter, listOptions));
2498
2561
  this.loadCommitDetails = options instanceof GitRunner ? (oid) => loadCommit(options, oid) : options.loadCommit ?? options.commitLoader ?? (runner === undefined ? async () => {
2499
2562
  throw new Error("Commit details require a GitRunner");
2500
2563
  } : (oid) => loadCommit(runner, oid));
@@ -2732,6 +2795,7 @@ class AppController {
2732
2795
  await this.switchLocalBranch(branch);
2733
2796
  }
2734
2797
  async switchLocalBranch(branch) {
2798
+ this.limitCommits = true;
2735
2799
  this.logAction(LOG_ACTIONS.checkoutBranch);
2736
2800
  await this.runBranchMutation(() => this.requireRunnerOperation((runner) => switchLocal(runner, branch)));
2737
2801
  }
@@ -3103,7 +3167,20 @@ class AppController {
3103
3167
  return this.loadCommitDetails(oid);
3104
3168
  }
3105
3169
  async loadBranchCommits(branch) {
3106
- return this.loadCommitList(`refs/heads/${branch}`);
3170
+ return this.loadCommitList(`refs/heads/${branch}`, undefined, { limit: this.limitCommits });
3171
+ }
3172
+ async expandCommits() {
3173
+ if (!this.limitCommits)
3174
+ return false;
3175
+ this.limitCommits = false;
3176
+ const history = await this.loadCommitHistory("HEAD");
3177
+ this.currentState = {
3178
+ ...this.currentState,
3179
+ commits: history.commits,
3180
+ ...this.commandLogSnapshot(),
3181
+ ...history.warning === undefined ? {} : { banner: history.warning }
3182
+ };
3183
+ return true;
3107
3184
  }
3108
3185
  async loadCommitFileInspection(oid, path) {
3109
3186
  return this.loadCommitFile(oid, path);
@@ -3402,7 +3479,7 @@ class AppController {
3402
3479
  }
3403
3480
  async loadCommitHistory(range) {
3404
3481
  try {
3405
- return { commits: await this.loadCommitList(range) };
3482
+ return { commits: await this.loadCommitList(range, undefined, { limit: this.limitCommits }) };
3406
3483
  } catch (error) {
3407
3484
  const warning = error instanceof GitCommandError ? error.record.stderr || error.message : error instanceof Error ? error.message : String(error);
3408
3485
  return { commits: this.currentState.commits ?? [], warning };
@@ -4408,6 +4485,13 @@ function syncVerticalScrollbar(bar, text, viewportHeight) {
4408
4485
  bar.viewportSize = scrollbarViewportOverrides.get(bar) ?? Math.max(0, Math.floor(text.height));
4409
4486
  bar.scrollPosition = text.scrollY;
4410
4487
  }
4488
+ function clearScrollbarViewportOverride(text) {
4489
+ const bar = scrollbars.get(text);
4490
+ if (bar === undefined)
4491
+ return;
4492
+ scrollbarViewportOverrides.delete(bar);
4493
+ syncVerticalScrollbar(bar, text);
4494
+ }
4411
4495
  function createPane(renderer, id, title, content, selectable = false, options = {}) {
4412
4496
  const tabsConfig = options.tabs;
4413
4497
  const BoxClass = tabsConfig === undefined ? BoxRenderable2 : PaneTabsBoxRenderable;
@@ -4892,36 +4976,62 @@ function computeColumnLayout(rows, width) {
4892
4976
  const widths = indexes.map((j) => j === flexIndex ? flexWidth : rawWidths[j]);
4893
4977
  return { indexes, widths };
4894
4978
  }
4895
- function renderColumns(row, layout) {
4896
- const chunks = [];
4979
+ function layoutListRowSegments(row, layout) {
4980
+ const segments = [];
4897
4981
  for (let i = 0;i < layout.indexes.length; i++) {
4898
4982
  const column = row.columns[layout.indexes[i]];
4899
4983
  const cellWidth = layout.widths[i];
4900
4984
  const isLast = i === layout.indexes.length - 1;
4901
4985
  if (i > 0)
4902
- chunks.push(plainChunk2(" "));
4986
+ segments.push({ text: " " });
4903
4987
  const text = column?.text ?? "";
4904
4988
  const truncated = truncateToWidth(text, cellWidth);
4905
4989
  if (truncated.length > 0) {
4906
- if (column?.segments !== undefined && truncated === text) {
4990
+ if (column?.segments !== undefined) {
4991
+ let remaining = [...truncated].length;
4907
4992
  for (const segment of column.segments) {
4993
+ if (remaining <= 0)
4994
+ break;
4908
4995
  if (segment.text.length === 0)
4909
4996
  continue;
4910
- chunks.push(styleToChunk(segment.text, column.style, segment.color));
4997
+ const chars = [...segment.text];
4998
+ if (chars.length <= remaining) {
4999
+ segments.push({
5000
+ text: segment.text,
5001
+ ...column.style === undefined ? {} : { style: column.style },
5002
+ ...segment.color === undefined ? {} : { color: segment.color }
5003
+ });
5004
+ remaining -= chars.length;
5005
+ } else {
5006
+ segments.push({
5007
+ text: chars.slice(0, remaining).join(""),
5008
+ ...column.style === undefined ? {} : { style: column.style },
5009
+ ...segment.color === undefined ? {} : { color: segment.color }
5010
+ });
5011
+ remaining = 0;
5012
+ break;
5013
+ }
4911
5014
  }
4912
5015
  } else {
4913
- chunks.push(styleToChunk(truncated, column?.style, column?.color));
5016
+ segments.push({
5017
+ text: truncated,
5018
+ ...column?.style === undefined ? {} : { style: column.style },
5019
+ ...column?.color === undefined ? {} : { color: column.color }
5020
+ });
4914
5021
  }
4915
5022
  }
4916
5023
  if (!isLast) {
4917
5024
  const pad = cellWidth - visualLength(truncated);
4918
5025
  if (pad > 0)
4919
- chunks.push(plainChunk2(" ".repeat(pad)));
5026
+ segments.push({ text: " ".repeat(pad) });
4920
5027
  }
4921
5028
  }
4922
- while (chunks.length > 0 && chunks[chunks.length - 1].text.trim().length === 0)
4923
- chunks.pop();
4924
- return chunks;
5029
+ while (segments.length > 0 && segments[segments.length - 1].text.trim().length === 0)
5030
+ segments.pop();
5031
+ return segments;
5032
+ }
5033
+ function renderColumns(row, layout) {
5034
+ return layoutListRowSegments(row, layout).map((segment) => styleToChunk(segment.text, segment.style, segment.color));
4925
5035
  }
4926
5036
  function highlightChunk(chunk, selectedBg) {
4927
5037
  const current = chunk.fg;
@@ -5017,6 +5127,393 @@ function renderListRows(state, focused, width, hoveredId) {
5017
5127
  return new StyledText2(allChunks);
5018
5128
  }
5019
5129
 
5130
+ // src/ui/panes/list-text.ts
5131
+ import { parseColor } from "@opentui/core";
5132
+
5133
+ // src/domain/diff/cell-width.ts
5134
+ var WIDE_RANGES = [
5135
+ [4352, 4447],
5136
+ [11904, 42191],
5137
+ [44032, 55203],
5138
+ [63744, 64255],
5139
+ [65072, 65135],
5140
+ [65280, 65376],
5141
+ [65504, 65510],
5142
+ [127744, 129535],
5143
+ [129648, 129791],
5144
+ [131072, 262141]
5145
+ ];
5146
+ function isWide(grapheme) {
5147
+ const code = grapheme.codePointAt(0);
5148
+ if (code === undefined)
5149
+ return false;
5150
+ for (const [low, high] of WIDE_RANGES) {
5151
+ if (code >= low && code <= high)
5152
+ return true;
5153
+ }
5154
+ return false;
5155
+ }
5156
+ function cellWidth(value) {
5157
+ let ascii = true;
5158
+ for (let index = 0;index < value.length; index++) {
5159
+ if (value.charCodeAt(index) > 127) {
5160
+ ascii = false;
5161
+ break;
5162
+ }
5163
+ }
5164
+ if (ascii)
5165
+ return value.length;
5166
+ let width = 0;
5167
+ for (const codePoint of value)
5168
+ width += isWide(codePoint) ? 2 : 1;
5169
+ return width;
5170
+ }
5171
+
5172
+ // src/ui/panes/pane-text.ts
5173
+ function internalsOf(text) {
5174
+ const candidate = text;
5175
+ const buffer = candidate.textBuffer;
5176
+ const style = candidate._textBufferSyntaxStyle;
5177
+ if (buffer === undefined || style === undefined)
5178
+ return;
5179
+ if (typeof buffer.setText !== "function" || typeof buffer.addHighlight !== "function" || typeof buffer.clearAllHighlights !== "function")
5180
+ return;
5181
+ if (typeof buffer.clearLineHighlights !== "function")
5182
+ return;
5183
+ if (typeof style.registerStyle !== "function" || typeof candidate.updateTextInfo !== "function")
5184
+ return;
5185
+ return candidate;
5186
+ }
5187
+ function paneTextBuffer(text) {
5188
+ const internals = internalsOf(text);
5189
+ if (internals === undefined)
5190
+ return;
5191
+ internals._hasManualStyledText = true;
5192
+ return {
5193
+ setText(value) {
5194
+ internals.textBuffer.setText(value);
5195
+ internals.updateTextInfo();
5196
+ },
5197
+ addHighlight(row, highlight) {
5198
+ internals.textBuffer.addHighlight(row, highlight);
5199
+ },
5200
+ clearRow(row) {
5201
+ internals.textBuffer.clearLineHighlights(row);
5202
+ },
5203
+ clearAllHighlights() {
5204
+ internals.textBuffer.clearAllHighlights();
5205
+ },
5206
+ registerStyle(name, definition) {
5207
+ return internals._textBufferSyntaxStyle.registerStyle(name, definition);
5208
+ },
5209
+ refresh() {
5210
+ internals.updateTextInfo();
5211
+ }
5212
+ };
5213
+ }
5214
+ function onPaneLifecyclePass(text, callback) {
5215
+ const host = text;
5216
+ const previous = host.onLifecyclePass;
5217
+ host.onLifecyclePass = () => {
5218
+ previous?.call(text);
5219
+ callback();
5220
+ };
5221
+ }
5222
+
5223
+ // src/ui/panes/viewport-highlights.ts
5224
+ var MARGIN_LINES = 32;
5225
+ var LINE_END_COLS = 1e6;
5226
+ function createViewportHighlights(text, spec) {
5227
+ const { buffer, paintLine, scrollY: logicalScrollY } = spec;
5228
+ let content;
5229
+ let installed = "";
5230
+ let active = false;
5231
+ let rowSources;
5232
+ let rowSourcesWidth = -1;
5233
+ let appliedScrollY = -1;
5234
+ let appliedHeight = -1;
5235
+ let painted;
5236
+ const paint = (force) => {
5237
+ if (!active)
5238
+ return;
5239
+ const height = Math.max(1, Math.floor(text.height));
5240
+ const scrollY = Math.max(0, Math.floor(logicalScrollY?.(content) ?? text.scrollY));
5241
+ const width = Math.max(1, Math.floor(text.width));
5242
+ if (!force && painted !== undefined && appliedScrollY === scrollY && appliedHeight === height && rowSourcesWidth === width)
5243
+ return;
5244
+ if (rowSources === undefined || rowSourcesWidth !== width) {
5245
+ rowSources = text.lineInfo.lineSources;
5246
+ rowSourcesWidth = width;
5247
+ }
5248
+ const sources = rowSources;
5249
+ const lastRow = Math.max(0, Math.min(scrollY + height - 1, sources.length - 1));
5250
+ const firstLine = sources[Math.min(scrollY, lastRow)] ?? scrollY;
5251
+ const lastLine = sources[lastRow] ?? lastRow;
5252
+ const from = Math.max(0, firstLine - MARGIN_LINES);
5253
+ const to = lastLine + MARGIN_LINES;
5254
+ const previous = painted;
5255
+ if (force || previous === undefined || previous.to < from || previous.from > to) {
5256
+ buffer.clearAllHighlights();
5257
+ for (let line = from;line <= to; line++)
5258
+ paintLine(line, content);
5259
+ } else {
5260
+ for (let line = previous.from;line < from; line++)
5261
+ buffer.clearRow(line);
5262
+ for (let line = to + 1;line <= previous.to; line++)
5263
+ buffer.clearRow(line);
5264
+ for (let line = from;line < previous.from; line++)
5265
+ paintLine(line, content);
5266
+ for (let line = previous.to + 1;line <= to; line++)
5267
+ paintLine(line, content);
5268
+ }
5269
+ painted = { from, to };
5270
+ appliedScrollY = scrollY;
5271
+ appliedHeight = height;
5272
+ };
5273
+ onPaneLifecyclePass(text, () => paint(false));
5274
+ return {
5275
+ install(full, next) {
5276
+ content = next;
5277
+ active = true;
5278
+ const changed = installed !== full;
5279
+ if (changed) {
5280
+ installed = full;
5281
+ buffer.setText(full);
5282
+ rowSources = undefined;
5283
+ painted = undefined;
5284
+ }
5285
+ paint(changed);
5286
+ },
5287
+ repaint() {
5288
+ paint(true);
5289
+ },
5290
+ release() {
5291
+ if (!active)
5292
+ return;
5293
+ buffer.clearAllHighlights();
5294
+ active = false;
5295
+ installed = "";
5296
+ rowSources = undefined;
5297
+ rowSourcesWidth = -1;
5298
+ appliedScrollY = -1;
5299
+ appliedHeight = -1;
5300
+ painted = undefined;
5301
+ }
5302
+ };
5303
+ }
5304
+
5305
+ // src/ui/panes/list-text.ts
5306
+ var painters = new WeakMap;
5307
+ function resolveSegment(text, style, color) {
5308
+ let fg3;
5309
+ let dim2;
5310
+ if (color !== undefined) {
5311
+ fg3 = parseColor(color);
5312
+ } else {
5313
+ switch (style) {
5314
+ case "dim":
5315
+ dim2 = true;
5316
+ break;
5317
+ case "cyan":
5318
+ fg3 = ANSI_CYAN;
5319
+ break;
5320
+ case "green":
5321
+ fg3 = ANSI_GREEN;
5322
+ break;
5323
+ case "yellow":
5324
+ fg3 = ANSI_YELLOW;
5325
+ break;
5326
+ case "magenta":
5327
+ fg3 = ANSI_MAGENTA;
5328
+ break;
5329
+ case "default":
5330
+ case undefined:
5331
+ break;
5332
+ }
5333
+ }
5334
+ return {
5335
+ text,
5336
+ ...fg3 === undefined ? {} : { fg: fg3 },
5337
+ ...dim2 === undefined ? {} : { dim: dim2 }
5338
+ };
5339
+ }
5340
+ function styleKey(fg3, bold3, dim2, bg2) {
5341
+ const fgKey = fg3 === undefined ? "" : fg3.toInts().join(",");
5342
+ const bgKey = bg2 === undefined ? "" : bg2.toInts().join(",");
5343
+ return `${fgKey}|${bold3 ? 1 : 0}|${dim2 ? 1 : 0}|${bgKey}`;
5344
+ }
5345
+ function styleIdFor(record, fg3, bold3, dim2, bg2) {
5346
+ const key = styleKey(fg3, bold3, dim2, bg2);
5347
+ const cached = record.styleIds.get(key);
5348
+ if (cached !== undefined)
5349
+ return cached;
5350
+ const id = record.buffer.registerStyle(`githunk.list.${record.styleIds.size}`, {
5351
+ ...fg3 === undefined ? {} : { fg: fg3 },
5352
+ ...bg2 === undefined ? {} : { bg: bg2 },
5353
+ ...bold3 ? { bold: bold3 } : {},
5354
+ ...dim2 ? { dim: dim2 } : {}
5355
+ });
5356
+ record.styleIds.set(key, id);
5357
+ return id;
5358
+ }
5359
+ function rowVisual(state, focused, hoveredId, rowIndexById, range, displayRow) {
5360
+ if (displayRow.kind !== "item")
5361
+ return 0;
5362
+ if (focused && displayRow.id === state.selectedId)
5363
+ return 1;
5364
+ if (range !== undefined) {
5365
+ const index = rowIndexById.get(displayRow.id);
5366
+ if (index !== undefined && index >= range.startIndex && index <= range.endIndex)
5367
+ return 2;
5368
+ }
5369
+ if (displayRow.id === hoveredId)
5370
+ return 3;
5371
+ return 0;
5372
+ }
5373
+ function layoutFor(state, safeWidth) {
5374
+ const displayRows = state.displayRows;
5375
+ const rowMap = new Map(state.rows.map((row) => [row.id, row]));
5376
+ const visibleRows = displayRows.flatMap((dr) => {
5377
+ if (dr.kind !== "item")
5378
+ return [];
5379
+ const row = rowMap.get(dr.id);
5380
+ return row === undefined ? [] : [row];
5381
+ });
5382
+ const layout = computeColumnLayout(visibleRows, safeWidth);
5383
+ const rowTexts = [];
5384
+ const rowSegments = [];
5385
+ for (const dr of displayRows) {
5386
+ if (dr.kind !== "item") {
5387
+ const truncated = [...dr.text].slice(0, safeWidth).join("");
5388
+ rowTexts.push(truncated);
5389
+ rowSegments.push(truncated.length === 0 ? [] : [{ text: truncated }]);
5390
+ continue;
5391
+ }
5392
+ const row = rowMap.get(dr.id);
5393
+ const laidOut = row === undefined ? [] : layoutListRowSegments(row, layout).map((segment) => resolveSegment(segment.text, segment.style, segment.color));
5394
+ const joined = laidOut.map((segment) => segment.text).join("");
5395
+ const pad = Math.max(0, safeWidth - cellWidth(joined));
5396
+ rowTexts.push(pad === 0 ? joined : `${joined}${" ".repeat(pad)}`);
5397
+ rowSegments.push(laidOut);
5398
+ }
5399
+ return { rows: state.rows, width: safeWidth, rowTexts, rowSegments, joined: rowTexts.join(`
5400
+ `) };
5401
+ }
5402
+ function visualsFor(state, focused, hoveredId) {
5403
+ const rowIndexById = new Map(state.rows.map((row, index) => [row.id, index]));
5404
+ const rangeActive = focused && isListRangeActive(state);
5405
+ const range = rangeActive ? getListSelectionRange(state) : undefined;
5406
+ return state.displayRows.map((dr) => rowVisual(state, focused, hoveredId, rowIndexById, range, dr));
5407
+ }
5408
+ function paintRow(record, line) {
5409
+ const snap = record.ref.snap;
5410
+ if (snap === undefined)
5411
+ return;
5412
+ const segments = snap.rowSegments[line];
5413
+ const visual = snap.visuals[line];
5414
+ if (segments === undefined || visual === undefined)
5415
+ return;
5416
+ const { buffer } = record;
5417
+ buffer.clearRow(line);
5418
+ const bg2 = visual === 1 || visual === 2 ? SELECTED_LINE_BG : visual === 3 ? HOVER_LINE_BG : undefined;
5419
+ let column = 0;
5420
+ for (const segment of segments) {
5421
+ const cells = cellWidth(segment.text);
5422
+ if (cells > 0) {
5423
+ if (visual === 1) {
5424
+ buffer.addHighlight(line, {
5425
+ start: column,
5426
+ end: column + cells,
5427
+ styleId: styleIdFor(record, segment.fg === undefined ? undefined : brightenAnsiForeground(segment.fg), true, segment.dim === true, bg2)
5428
+ });
5429
+ } else if (bg2 !== undefined) {
5430
+ buffer.addHighlight(line, {
5431
+ start: column,
5432
+ end: column + cells,
5433
+ styleId: styleIdFor(record, segment.fg, false, segment.dim === true, bg2)
5434
+ });
5435
+ } else if (segment.fg !== undefined || segment.dim === true) {
5436
+ buffer.addHighlight(line, {
5437
+ start: column,
5438
+ end: column + cells,
5439
+ styleId: styleIdFor(record, segment.fg, false, segment.dim === true, undefined)
5440
+ });
5441
+ }
5442
+ }
5443
+ column += cells;
5444
+ }
5445
+ if (bg2 !== undefined) {
5446
+ buffer.addHighlight(line, { start: column, end: LINE_END_COLS, styleId: styleIdFor(record, undefined, false, false, bg2) });
5447
+ }
5448
+ }
5449
+ function ensurePainter(text, buffer) {
5450
+ const existing = painters.get(text);
5451
+ if (existing !== undefined)
5452
+ return existing;
5453
+ const ref = { snap: undefined };
5454
+ const record = {
5455
+ buffer,
5456
+ viewport: undefined,
5457
+ ref,
5458
+ styleIds: new Map,
5459
+ cache: undefined,
5460
+ joined: "",
5461
+ visuals: []
5462
+ };
5463
+ record.viewport = createViewportHighlights(text, {
5464
+ buffer,
5465
+ paintLine: (line) => paintRow(record, line)
5466
+ });
5467
+ painters.set(text, record);
5468
+ return record;
5469
+ }
5470
+ function installListText(text, content) {
5471
+ const buffer = paneTextBuffer(text);
5472
+ if (buffer === undefined) {
5473
+ text.content = renderListRows(content.state, content.focused, content.width, content.hoveredId);
5474
+ return;
5475
+ }
5476
+ const record = ensurePainter(text, buffer);
5477
+ const safeWidth = Math.max(0, Math.floor(content.width));
5478
+ const previousCache = record.cache;
5479
+ const cache = previousCache !== undefined && previousCache.rows === content.state.rows && previousCache.width === safeWidth ? previousCache : layoutFor(content.state, safeWidth);
5480
+ record.cache = cache;
5481
+ const visuals = visualsFor(content.state, content.focused, content.hoveredId);
5482
+ record.ref.snap = { rowSegments: cache.rowSegments, visuals };
5483
+ if (cache.joined !== record.joined) {
5484
+ record.joined = cache.joined;
5485
+ record.visuals = visuals;
5486
+ record.viewport.install(cache.joined, record.ref);
5487
+ return;
5488
+ }
5489
+ if (cache !== previousCache) {
5490
+ record.visuals = visuals;
5491
+ record.viewport.repaint();
5492
+ record.buffer.refresh();
5493
+ return;
5494
+ }
5495
+ const previous = record.visuals;
5496
+ record.visuals = visuals;
5497
+ let repainted = false;
5498
+ const lines = Math.max(previous.length, visuals.length);
5499
+ for (let line = 0;line < lines; line++) {
5500
+ if (previous[line] !== visuals[line]) {
5501
+ paintRow(record, line);
5502
+ repainted = true;
5503
+ }
5504
+ }
5505
+ if (repainted)
5506
+ record.buffer.refresh();
5507
+ }
5508
+ function releaseListText(text) {
5509
+ const record = painters.get(text);
5510
+ if (record === undefined)
5511
+ return;
5512
+ record.viewport.release();
5513
+ record.buffer.refresh();
5514
+ painters.delete(text);
5515
+ }
5516
+
5020
5517
  // src/ui/panes/branches-pane.ts
5021
5518
  function localBranchRows(model, filter = "", options = {}) {
5022
5519
  const listing = model.branches;
@@ -5067,8 +5564,7 @@ function createBranchesPane(renderer, model) {
5067
5564
  const rows = localBranchRows(model);
5068
5565
  const displayRows = rows.length === 0 ? [{ kind: "message", text: "No branches" }] : undefined;
5069
5566
  const state = createListState(rows, displayRows);
5070
- const content = renderListRows(state, false, 80);
5071
- pane.update(content);
5567
+ installListText(pane.text, { state, width: 80, focused: false });
5072
5568
  pane.syncScrollbar();
5073
5569
  return pane;
5074
5570
  }
@@ -5389,47 +5885,6 @@ function commitGraphRows(commits, getColor) {
5389
5885
 
5390
5886
  // src/ui/author-style.ts
5391
5887
  import { createHash as createHash2 } from "node:crypto";
5392
-
5393
- // src/ui/cell-width.ts
5394
- var WIDE_RANGES = [
5395
- [4352, 4447],
5396
- [11904, 42191],
5397
- [44032, 55203],
5398
- [63744, 64255],
5399
- [65072, 65135],
5400
- [65280, 65376],
5401
- [65504, 65510],
5402
- [127744, 129535],
5403
- [129648, 129791],
5404
- [131072, 262141]
5405
- ];
5406
- function isWide(grapheme) {
5407
- const code = grapheme.codePointAt(0);
5408
- if (code === undefined)
5409
- return false;
5410
- for (const [low, high] of WIDE_RANGES) {
5411
- if (code >= low && code <= high)
5412
- return true;
5413
- }
5414
- return false;
5415
- }
5416
- function cellWidth(value) {
5417
- let ascii = true;
5418
- for (let index = 0;index < value.length; index++) {
5419
- if (value.charCodeAt(index) > 127) {
5420
- ascii = false;
5421
- break;
5422
- }
5423
- }
5424
- if (ascii)
5425
- return value.length;
5426
- let width = 0;
5427
- for (const codePoint of value)
5428
- width += isWide(codePoint) ? 2 : 1;
5429
- return width;
5430
- }
5431
-
5432
- // src/ui/author-style.ts
5433
5888
  var initialsCache = new Map;
5434
5889
  var colorCache = new Map;
5435
5890
  function randInt(bytes, max) {
@@ -5518,6 +5973,7 @@ function reflogRows(model, filter = "") {
5518
5973
 
5519
5974
  // src/ui/panes/commits-pane.ts
5520
5975
  var paneStates = new WeakMap;
5976
+ var COMMIT_THRESHOLD = 200;
5521
5977
  function formatRelativeTime(authoredAt, now) {
5522
5978
  const date = new Date(authoredAt);
5523
5979
  if (Number.isNaN(date.getTime()))
@@ -5559,20 +6015,22 @@ function buildCommitRows(commits, now, filter = "") {
5559
6015
  const shortHash2 = commit.oid.length >= 8 ? commit.oid.slice(0, 8) : commit.shortOid;
5560
6016
  const initials = authorInitials(commit.authorName).padEnd(AUTHOR_COLUMN_WIDTH, " ");
5561
6017
  const relative = formatRelativeTime(commit.authoredAt, now);
6018
+ const graphText = graph?.text ?? "";
6019
+ const graphSegments = graph?.segments ?? [];
6020
+ const subjectSegments = commit.subject.length === 0 ? [] : [{ text: commit.subject }];
5562
6021
  return {
5563
6022
  id: commit.oid,
5564
6023
  columns: [
5565
6024
  { text: shortHash2, priority: 1, color: commitHashColor(commit.status) },
5566
- { text: initials, priority: 3, color: authorColor(commit.authorName) },
5567
- { text: graph?.text ?? "", priority: 0, segments: graph?.segments ?? [] },
5568
- { text: commit.subject, priority: 2, flex: true },
5569
- { text: relative, priority: 4, style: "dim" }
6025
+ { text: initials, priority: 2, color: authorColor(commit.authorName) },
6026
+ { text: `${graphText}${commit.subject}`, priority: 2, flex: true, segments: [...graphSegments, ...subjectSegments] },
6027
+ { text: relative, priority: 0, style: "dim" }
5570
6028
  ]
5571
6029
  };
5572
6030
  });
5573
6031
  if (filter.length === 0)
5574
6032
  return rows;
5575
- return [...filterItems(filter, rows, (row) => `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? row.id}`)];
6033
+ return [...filterItems(filter, rows, (row) => `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? row.id}`)];
5576
6034
  }
5577
6035
  function createCommitsPane(renderer, model) {
5578
6036
  const pane = createPane(renderer, "commits", "", "No commit selected", false, {
@@ -5586,6 +6044,7 @@ function updateCommitsPane(pane, model) {
5586
6044
  if (commits.length === 0) {
5587
6045
  const empty = createListState([]);
5588
6046
  paneStates.set(pane, empty);
6047
+ releaseListText(pane.text);
5589
6048
  pane.update(model.loading ? "Loading…" : "No commits");
5590
6049
  return;
5591
6050
  }
@@ -5599,8 +6058,7 @@ function updateCommitsPane(pane, model) {
5599
6058
  state = withPrev;
5600
6059
  }
5601
6060
  paneStates.set(pane, state);
5602
- const content = renderListRows(state, false, 80);
5603
- pane.update(content);
6061
+ installListText(pane.text, { state, width: 80, focused: false });
5604
6062
  pane.box.bottomTitle = undefined;
5605
6063
  }
5606
6064
 
@@ -5769,150 +6227,23 @@ function parseAnsi(input) {
5769
6227
  out.push(char);
5770
6228
  row++;
5771
6229
  column = 0;
5772
- runStart = 0;
5773
- index += 1;
5774
- continue;
5775
- }
5776
- const codePoint = input.codePointAt(index);
5777
- const size = codePoint > 65535 ? 2 : 1;
5778
- out.push(input.slice(index, index + size));
5779
- column += 1;
5780
- index += size;
5781
- }
5782
- closeRun();
5783
- return { text: out.join(""), spans };
5784
- }
5785
-
5786
- // src/ui/panes/command-log-pane.ts
5787
- import { BoxRenderable as BoxRenderable3, TextRenderable as TextRenderable2 } from "@opentui/core";
5788
-
5789
- // src/ui/panes/pane-text.ts
5790
- function internalsOf(text) {
5791
- const candidate = text;
5792
- const buffer = candidate.textBuffer;
5793
- const style = candidate._textBufferSyntaxStyle;
5794
- if (buffer === undefined || style === undefined)
5795
- return;
5796
- if (typeof buffer.setText !== "function" || typeof buffer.addHighlight !== "function" || typeof buffer.clearAllHighlights !== "function")
5797
- return;
5798
- if (typeof buffer.clearLineHighlights !== "function")
5799
- return;
5800
- if (typeof style.registerStyle !== "function" || typeof candidate.updateTextInfo !== "function")
5801
- return;
5802
- return candidate;
5803
- }
5804
- function paneTextBuffer(text) {
5805
- const internals = internalsOf(text);
5806
- if (internals === undefined)
5807
- return;
5808
- internals._hasManualStyledText = true;
5809
- return {
5810
- setText(value) {
5811
- internals.textBuffer.setText(value);
5812
- internals.updateTextInfo();
5813
- },
5814
- addHighlight(row, highlight) {
5815
- internals.textBuffer.addHighlight(row, highlight);
5816
- },
5817
- clearRow(row) {
5818
- internals.textBuffer.clearLineHighlights(row);
5819
- },
5820
- clearAllHighlights() {
5821
- internals.textBuffer.clearAllHighlights();
5822
- },
5823
- registerStyle(name, definition) {
5824
- return internals._textBufferSyntaxStyle.registerStyle(name, definition);
5825
- }
5826
- };
5827
- }
5828
- function onPaneLifecyclePass(text, callback) {
5829
- const host = text;
5830
- const previous = host.onLifecyclePass;
5831
- host.onLifecyclePass = () => {
5832
- previous?.call(text);
5833
- callback();
5834
- };
5835
- }
5836
-
5837
- // src/ui/panes/viewport-highlights.ts
5838
- var MARGIN_LINES = 32;
5839
- var LINE_END_COLS = 1e6;
5840
- function createViewportHighlights(text, spec) {
5841
- const { buffer, paintLine } = spec;
5842
- let content;
5843
- let installed = "";
5844
- let active = false;
5845
- let rowSources;
5846
- let rowSourcesWidth = -1;
5847
- let appliedScrollY = -1;
5848
- let appliedHeight = -1;
5849
- let painted;
5850
- const paint = (force) => {
5851
- if (!active)
5852
- return;
5853
- const height = Math.max(1, Math.floor(text.height));
5854
- const scrollY = Math.max(0, Math.floor(text.scrollY));
5855
- const width = Math.max(1, Math.floor(text.width));
5856
- if (!force && painted !== undefined && appliedScrollY === scrollY && appliedHeight === height && rowSourcesWidth === width)
5857
- return;
5858
- if (rowSources === undefined || rowSourcesWidth !== width) {
5859
- rowSources = text.lineInfo.lineSources;
5860
- rowSourcesWidth = width;
5861
- }
5862
- const sources = rowSources;
5863
- const lastRow = Math.max(0, Math.min(scrollY + height - 1, sources.length - 1));
5864
- const firstLine = sources[Math.min(scrollY, lastRow)] ?? scrollY;
5865
- const lastLine = sources[lastRow] ?? lastRow;
5866
- const from = Math.max(0, firstLine - MARGIN_LINES);
5867
- const to = lastLine + MARGIN_LINES;
5868
- const previous = painted;
5869
- if (previous === undefined || previous.to < from || previous.from > to) {
5870
- buffer.clearAllHighlights();
5871
- for (let line = from;line <= to; line++)
5872
- paintLine(line, content);
5873
- } else {
5874
- for (let line = previous.from;line < from; line++)
5875
- buffer.clearRow(line);
5876
- for (let line = to + 1;line <= previous.to; line++)
5877
- buffer.clearRow(line);
5878
- for (let line = from;line < previous.from; line++)
5879
- paintLine(line, content);
5880
- for (let line = previous.to + 1;line <= to; line++)
5881
- paintLine(line, content);
5882
- }
5883
- painted = { from, to };
5884
- appliedScrollY = scrollY;
5885
- appliedHeight = height;
5886
- };
5887
- onPaneLifecyclePass(text, () => paint(false));
5888
- return {
5889
- install(full, next) {
5890
- content = next;
5891
- active = true;
5892
- const changed = installed !== full;
5893
- if (changed) {
5894
- installed = full;
5895
- buffer.setText(full);
5896
- rowSources = undefined;
5897
- painted = undefined;
5898
- }
5899
- paint(changed);
5900
- },
5901
- release() {
5902
- if (!active)
5903
- return;
5904
- buffer.clearAllHighlights();
5905
- active = false;
5906
- installed = "";
5907
- rowSources = undefined;
5908
- rowSourcesWidth = -1;
5909
- appliedScrollY = -1;
5910
- appliedHeight = -1;
5911
- painted = undefined;
6230
+ runStart = 0;
6231
+ index += 1;
6232
+ continue;
5912
6233
  }
5913
- };
6234
+ const codePoint = input.codePointAt(index);
6235
+ const size = codePoint > 65535 ? 2 : 1;
6236
+ out.push(input.slice(index, index + size));
6237
+ column += 1;
6238
+ index += size;
6239
+ }
6240
+ closeRun();
6241
+ return { text: out.join(""), spans };
5914
6242
  }
5915
6243
 
6244
+ // src/ui/panes/command-log-pane.ts
6245
+ import { BoxRenderable as BoxRenderable3, TextRenderable as TextRenderable2 } from "@opentui/core";
6246
+
5916
6247
  // src/ui/panes/command-log-text.ts
5917
6248
  var STYLE_DEFINITIONS = {
5918
6249
  action: { fg: ANSI_YELLOW },
@@ -5947,7 +6278,7 @@ function registerStyles(buffer) {
5947
6278
  }
5948
6279
  return ids;
5949
6280
  }
5950
- var painters = new WeakMap;
6281
+ var painters2 = new WeakMap;
5951
6282
  function installCommandLogText(text, lines) {
5952
6283
  const full = lines.map((line) => line.spans.map((span) => span.text).join("")).join(`
5953
6284
  `);
@@ -5956,7 +6287,7 @@ function installCommandLogText(text, lines) {
5956
6287
  text.content = full;
5957
6288
  return;
5958
6289
  }
5959
- let painter = painters.get(text);
6290
+ let painter = painters2.get(text);
5960
6291
  if (painter === undefined) {
5961
6292
  const styleIds = registerStyles(buffer);
5962
6293
  painter = createViewportHighlights(text, {
@@ -5970,7 +6301,7 @@ function installCommandLogText(text, lines) {
5970
6301
  }
5971
6302
  }
5972
6303
  });
5973
- painters.set(text, painter);
6304
+ painters2.set(text, painter);
5974
6305
  }
5975
6306
  painter.install(full, lines);
5976
6307
  }
@@ -6452,7 +6783,7 @@ function createFilesPane(renderer, model) {
6452
6783
  const initialRows = filesTreeRows(createFilesTreeState(model), model);
6453
6784
  const displayRows = initialRows.length === 0 ? [{ kind: "message", text: NO_CHANGED_FILES }] : undefined;
6454
6785
  const state = createListState(initialRows, displayRows);
6455
- pane.update(renderListRows(state, false, 80));
6786
+ installListText(pane.text, { state, width: 80, focused: false });
6456
6787
  return pane;
6457
6788
  }
6458
6789
  function anyStagedChanges(model) {
@@ -6737,14 +7068,14 @@ function changedIndexesInDiffLineRange(document, state) {
6737
7068
 
6738
7069
  // src/ui/panes/ansi-text.ts
6739
7070
  import { StyledText as StyledText3, bold as boldChunk, dim as dimChunk, fg as fgChunk } from "@opentui/core";
6740
- var painters2 = new WeakMap;
6741
- function styleKey(span) {
7071
+ var painters3 = new WeakMap;
7072
+ function styleKey2(span) {
6742
7073
  const color = span.fg;
6743
7074
  const colorKey = color === undefined ? "-" : `${color.intent}:${color.slot}:${color.toInts().join(",")}`;
6744
7075
  return `${colorKey}|${span.bold === true ? "b" : "-"}${span.dim === true ? "d" : "-"}`;
6745
7076
  }
6746
- function styleIdFor(buffer, styleIds, span) {
6747
- const key = styleKey(span);
7077
+ function styleIdFor2(buffer, styleIds, span) {
7078
+ const key = styleKey2(span);
6748
7079
  const existing = styleIds.get(key);
6749
7080
  if (existing !== undefined)
6750
7081
  return existing;
@@ -6826,7 +7157,7 @@ function installAnsiText(text, content) {
6826
7157
  }
6827
7158
  const { text: full, firstBodyRow } = joined(content);
6828
7159
  const spansByRow = groupByRow(content.spans, firstBodyRow);
6829
- let painter = painters2.get(text);
7160
+ let painter = painters3.get(text);
6830
7161
  if (painter === undefined) {
6831
7162
  const styleIds = new Map;
6832
7163
  painter = {
@@ -6838,17 +7169,17 @@ function installAnsiText(text, content) {
6838
7169
  if (spans === undefined)
6839
7170
  return;
6840
7171
  for (const span of spans) {
6841
- buffer.addHighlight(row, { start: span.start, end: span.end, styleId: styleIdFor(buffer, styleIds, span) });
7172
+ buffer.addHighlight(row, { start: span.start, end: span.end, styleId: styleIdFor2(buffer, styleIds, span) });
6842
7173
  }
6843
7174
  }
6844
7175
  })
6845
7176
  };
6846
- painters2.set(text, painter);
7177
+ painters3.set(text, painter);
6847
7178
  }
6848
7179
  painter.highlights.install(full, spansByRow);
6849
7180
  }
6850
7181
  function releaseAnsiText(text) {
6851
- painters2.get(text)?.highlights.release();
7182
+ painters3.get(text)?.highlights.release();
6852
7183
  }
6853
7184
 
6854
7185
  // src/ui/panes/diff-text.ts
@@ -6937,7 +7268,7 @@ function statSpansForRow(value, spans) {
6937
7268
  function preambleSpansForRow(value, spans) {
6938
7269
  return spans === undefined ? [plainChunk3(value)] : statSpansForRow(value, spans);
6939
7270
  }
6940
- var painters3 = new WeakMap;
7271
+ var painters4 = new WeakMap;
6941
7272
  function registerStyles2(buffer) {
6942
7273
  const ids = {};
6943
7274
  for (const [name, definition] of Object.entries(STYLE_DEFINITIONS2)) {
@@ -6975,7 +7306,7 @@ function styledChunk(style, value) {
6975
7306
  }
6976
7307
  function paintAsChunks2(text, content) {
6977
7308
  const { text: full, firstDiffRow } = joined2(content);
6978
- const preambleSpans = statSpansForPreamble(content.preamble);
7309
+ const preambleSpans = content.preambleSpans ?? statSpansForPreamble(content.preamble);
6979
7310
  const rows = full.split(`
6980
7311
  `);
6981
7312
  const chunks = [];
@@ -7008,13 +7339,15 @@ function installDiffText(text, content) {
7008
7339
  const paint = {
7009
7340
  displayLines: content.displayLines,
7010
7341
  firstDiffRow,
7011
- preambleSpans: statSpansForPreamble(content.preamble)
7342
+ preambleSpans: content.preambleSpans ?? statSpansForPreamble(content.preamble),
7343
+ ...content.highlightScrollY === undefined ? {} : { highlightScrollY: content.highlightScrollY }
7012
7344
  };
7013
- let painter = painters3.get(text);
7345
+ let painter = painters4.get(text);
7014
7346
  if (painter === undefined) {
7015
7347
  const styleIds = registerStyles2(buffer);
7016
7348
  painter = createViewportHighlights(text, {
7017
7349
  buffer,
7350
+ scrollY: (current) => current.highlightScrollY?.() ?? text.scrollY,
7018
7351
  paintLine: (row, current) => {
7019
7352
  const preambleSpans = current.preambleSpans.get(row);
7020
7353
  if (preambleSpans !== undefined) {
@@ -7031,12 +7364,508 @@ function installDiffText(text, content) {
7031
7364
  buffer.addHighlight(row, { start: display.gutterCols, end: LINE_END_COLS, styleId: styleIds[display.style] });
7032
7365
  }
7033
7366
  });
7034
- painters3.set(text, painter);
7367
+ painters4.set(text, painter);
7035
7368
  }
7036
7369
  painter.install(full, paint);
7037
7370
  }
7038
7371
  function releaseDiffText(text) {
7039
- painters3.get(text)?.release();
7372
+ painters4.get(text)?.release();
7373
+ }
7374
+
7375
+ // src/domain/diff/virtual.ts
7376
+ var VIRTUAL_DIFF_LINE_THRESHOLD = 1e4;
7377
+ function isSourceLine(line) {
7378
+ return line.kind === "context" || line.kind === "addition" || line.kind === "deletion";
7379
+ }
7380
+ function styleFor2(line) {
7381
+ if (line.kind === "addition")
7382
+ return "addition";
7383
+ if (line.kind === "deletion")
7384
+ return "deletion";
7385
+ if (line.kind === "hunk-header")
7386
+ return "hunk-header";
7387
+ if (line.kind === "metadata" || line.kind === "no-newline")
7388
+ return "metadata";
7389
+ return "plain";
7390
+ }
7391
+ function lineNumberWidth2(document) {
7392
+ let largest = 1;
7393
+ for (const line of document.lines) {
7394
+ largest = Math.max(largest, line.oldLine ?? 0, line.newLine ?? 0);
7395
+ }
7396
+ return String(largest).length;
7397
+ }
7398
+ function linePrefix(line, width) {
7399
+ if (!isSourceLine(line))
7400
+ return "";
7401
+ const old = line.oldLine === undefined ? "" : String(line.oldLine);
7402
+ const next = line.newLine === undefined ? "" : String(line.newLine);
7403
+ return `${old.padStart(width, " ")} ${next.padStart(width, " ")} `;
7404
+ }
7405
+ function withoutLineEnding2(raw) {
7406
+ if (raw.endsWith(`\r
7407
+ `))
7408
+ return raw.slice(0, -2);
7409
+ if (raw.endsWith(`
7410
+ `) || raw.endsWith("\r"))
7411
+ return raw.slice(0, -1);
7412
+ return raw;
7413
+ }
7414
+ function normalizePreamble(preamble) {
7415
+ if (preamble.length === 0)
7416
+ return { text: "", rows: [], starts: [], ends: [] };
7417
+ const text = preamble.endsWith(`
7418
+ `) ? preamble : `${preamble}
7419
+ `;
7420
+ const rows = text.slice(0, -1).split(`
7421
+ `).map(withoutLineEnding2);
7422
+ const starts = [];
7423
+ const ends = [];
7424
+ let start = 0;
7425
+ for (let row = 0;row < rows.length; row += 1) {
7426
+ const newline = text.indexOf(`
7427
+ `, start);
7428
+ starts.push(start);
7429
+ ends.push(newline < 0 ? text.length : newline + 1);
7430
+ start = newline < 0 ? text.length : newline + 1;
7431
+ }
7432
+ return { text, rows, starts, ends };
7433
+ }
7434
+ function boundedIndex(value, length) {
7435
+ if (!Number.isFinite(value))
7436
+ return value < 0 ? 0 : length;
7437
+ return Math.min(length, Math.max(0, Math.floor(value)));
7438
+ }
7439
+ function createVirtualDiffLayout(document, preamble) {
7440
+ const normalized = normalizePreamble(preamble);
7441
+ const width = lineNumberWidth2(document);
7442
+ const prefixes = document.lines.map((line) => linePrefix(line, width));
7443
+ const displayStarts = new Array(document.lines.length);
7444
+ let displayLength = normalized.text.length;
7445
+ let contentWidth = 0;
7446
+ for (const row of normalized.rows)
7447
+ contentWidth = Math.max(contentWidth, cellWidth(row));
7448
+ for (let index = 0;index < document.lines.length; index += 1) {
7449
+ const line = document.lines[index];
7450
+ const prefix = prefixes[index];
7451
+ const text = `${prefix}${withoutLineEnding2(line.raw)}`;
7452
+ displayStarts[index] = displayLength;
7453
+ displayLength += prefix.length + line.raw.length;
7454
+ contentWidth = Math.max(contentWidth, cellWidth(text));
7455
+ }
7456
+ const totalRows = normalized.rows.length + document.lines.length;
7457
+ const bodyRow = (lineIndex) => {
7458
+ const line = document.lines[lineIndex];
7459
+ const prefix = prefixes[lineIndex];
7460
+ return {
7461
+ text: `${prefix}${withoutLineEnding2(line.raw)}`,
7462
+ gutterCols: prefix.length,
7463
+ style: styleFor2(line),
7464
+ lineIndex,
7465
+ rawStartUtf16: line.startUtf16,
7466
+ rawEndUtf16: line.endUtf16,
7467
+ displayStartUtf16: displayStarts[lineIndex],
7468
+ displayEndUtf16: displayStarts[lineIndex] + prefix.length + line.raw.length
7469
+ };
7470
+ };
7471
+ const rowAt = (row) => {
7472
+ if (!Number.isSafeInteger(row) || row < 0 || row >= totalRows)
7473
+ return;
7474
+ if (row < normalized.rows.length) {
7475
+ const text = normalized.rows[row];
7476
+ return {
7477
+ text,
7478
+ gutterCols: 0,
7479
+ style: "plain",
7480
+ displayStartUtf16: normalized.starts[row],
7481
+ displayEndUtf16: normalized.ends[row]
7482
+ };
7483
+ }
7484
+ return bodyRow(row - normalized.rows.length);
7485
+ };
7486
+ const window = (scrollTop, viewportHeight, overscan) => {
7487
+ const viewport = Math.max(0, Math.floor(Number.isFinite(viewportHeight) ? viewportHeight : 0));
7488
+ if (totalRows === 0 || viewport === 0)
7489
+ return [0, -1];
7490
+ const margin = Math.max(0, Math.floor(Number.isFinite(overscan) ? overscan : 0));
7491
+ const maxScroll = Math.max(0, totalRows - viewport);
7492
+ const top = Math.min(maxScroll, Math.max(0, Math.floor(Number.isFinite(scrollTop) ? scrollTop : 0)));
7493
+ return [Math.max(0, top - margin), Math.min(totalRows - 1, top + viewport - 1 + margin)];
7494
+ };
7495
+ const displayOffsetsForLines = (startIndex, endIndex) => {
7496
+ let start = boundedIndex(startIndex, document.lines.length);
7497
+ let end = boundedIndex(endIndex, document.lines.length);
7498
+ if (end < start)
7499
+ [start, end] = [end, start];
7500
+ const rawStartUtf16 = start < document.lines.length ? document.lines[start].startUtf16 : document.text.length;
7501
+ const rawEndUtf16 = end < document.lines.length ? document.lines[end].startUtf16 : document.text.length;
7502
+ const displayStartUtf16 = start < document.lines.length ? displayStarts[start] : displayLength;
7503
+ const displayEndUtf16 = end < document.lines.length ? displayStarts[end] : displayLength;
7504
+ return { rawStartUtf16, rawEndUtf16, displayStartUtf16, displayEndUtf16 };
7505
+ };
7506
+ const rawOffsetAt = (row, column) => {
7507
+ const value = rowAt(row);
7508
+ if (value?.lineIndex === undefined || value.rawStartUtf16 === undefined || value.rawEndUtf16 === undefined)
7509
+ return;
7510
+ const line = document.lines[value.lineIndex];
7511
+ const target = Math.max(0, Math.floor(Number.isFinite(column) ? column : 0));
7512
+ if (target <= value.gutterCols)
7513
+ return line.startUtf16;
7514
+ const body = withoutLineEnding2(line.raw);
7515
+ const bodyColumn = target - value.gutterCols;
7516
+ let cells = 0;
7517
+ let utf16 = 0;
7518
+ for (const codePoint of body) {
7519
+ const widthInCells = cellWidth(codePoint);
7520
+ if (bodyColumn <= cells)
7521
+ return line.startUtf16 + utf16;
7522
+ utf16 += codePoint.length;
7523
+ cells += widthInCells;
7524
+ if (bodyColumn <= cells)
7525
+ return line.startUtf16 + utf16;
7526
+ }
7527
+ return line.endUtf16;
7528
+ };
7529
+ return { preambleRows: normalized.rows.length, totalRows, contentWidth, rowAt, window, displayOffsetsForLines, rawOffsetAt };
7530
+ }
7531
+
7532
+ // src/ui/panes/virtual-main-pane.ts
7533
+ var ACCESSORS = ["scrollY", "scrollHeight", "maxScrollY", "scrollX", "scrollWidth", "maxScrollX"];
7534
+ var virtualPanes = new WeakMap;
7535
+ var VIRTUAL_MAIN_OVERSCAN_MIN = 10;
7536
+ function prototypeDescriptor(text, name) {
7537
+ let prototype = Object.getPrototypeOf(text);
7538
+ while (prototype !== null) {
7539
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
7540
+ if (descriptor !== undefined)
7541
+ return descriptor;
7542
+ prototype = Object.getPrototypeOf(prototype);
7543
+ }
7544
+ return;
7545
+ }
7546
+ function isFiniteNonNegative(value) {
7547
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
7548
+ }
7549
+ function documentSelection(document, startUtf16, endUtf16) {
7550
+ const line = document.lines.find((entry) => startUtf16 >= entry.startUtf16 && startUtf16 <= entry.endUtf16) ?? document.lines[0];
7551
+ return {
7552
+ valid: true,
7553
+ startUtf16,
7554
+ endUtf16,
7555
+ ...line === undefined ? {} : {
7556
+ fileIndex: line.fileIndex,
7557
+ ...line.hunkIndex === undefined ? {} : { hunkIndex: line.hunkIndex }
7558
+ },
7559
+ active: true
7560
+ };
7561
+ }
7562
+ function dimensions(text) {
7563
+ return {
7564
+ height: Math.max(1, Math.floor(isFiniteNonNegative(text.height))),
7565
+ width: Math.max(0, Math.floor(isFiniteNonNegative(text.width)))
7566
+ };
7567
+ }
7568
+ function padDisplayRow(value, width) {
7569
+ return `${value}${" ".repeat(Math.max(0, width - cellWidth(value)))}`;
7570
+ }
7571
+ function withoutLineEnding3(raw) {
7572
+ if (raw.endsWith(`\r
7573
+ `))
7574
+ return raw.slice(0, -2);
7575
+ if (raw.endsWith(`
7576
+ `) || raw.endsWith("\r"))
7577
+ return raw.slice(0, -1);
7578
+ return raw;
7579
+ }
7580
+ function rawDisplayCells(raw, relativeUtf16, includeLineEnding) {
7581
+ const body = withoutLineEnding3(raw);
7582
+ const prefixLength = Math.min(body.length, Math.max(0, Math.floor(relativeUtf16)));
7583
+ const cells = cellWidth(body.slice(0, prefixLength));
7584
+ return cells + (includeLineEnding && prefixLength >= body.length && body.length < raw.length ? 1 : 0);
7585
+ }
7586
+ function installAccessors(pane, state, rerender) {
7587
+ const text = pane.text;
7588
+ for (const name of ACCESSORS) {
7589
+ const descriptor = state.originalDescriptors.get(name);
7590
+ if (descriptor === undefined)
7591
+ continue;
7592
+ const nextDescriptor = {
7593
+ configurable: true,
7594
+ enumerable: descriptor.enumerable ?? false,
7595
+ get: () => {
7596
+ if (!state.active)
7597
+ return descriptor.get?.call(text);
7598
+ const layout = state.layout;
7599
+ if (name === "scrollY")
7600
+ return state.scrollY;
7601
+ if (name === "scrollX")
7602
+ return state.scrollX;
7603
+ if (name === "scrollHeight")
7604
+ return layout?.totalRows ?? descriptor.get?.call(text) ?? 0;
7605
+ if (name === "scrollWidth")
7606
+ return layout?.contentWidth ?? descriptor.get?.call(text) ?? 0;
7607
+ const viewport = name === "maxScrollY" ? state.viewportHeight : state.viewportWidth;
7608
+ const size = name === "maxScrollY" ? layout?.totalRows ?? 0 : layout?.contentWidth ?? 0;
7609
+ return Math.max(0, size - viewport);
7610
+ },
7611
+ ...name === "scrollY" || name === "scrollX" ? {
7612
+ set: (value) => {
7613
+ if (!state.active) {
7614
+ descriptor.set?.call(text, value);
7615
+ return;
7616
+ }
7617
+ const numeric = typeof value === "number" ? value : Number(value);
7618
+ if (name === "scrollY") {
7619
+ const max = Math.max(0, (state.layout?.totalRows ?? 0) - state.viewportHeight);
7620
+ const next = Math.min(max, Math.floor(isFiniteNonNegative(numeric)));
7621
+ if (state.scrollY === next)
7622
+ return;
7623
+ state.scrollY = next;
7624
+ rerender();
7625
+ } else {
7626
+ const max = Math.max(0, (state.layout?.contentWidth ?? 0) - state.viewportWidth);
7627
+ const next = Math.min(max, Math.floor(isFiniteNonNegative(numeric)));
7628
+ state.scrollX = next;
7629
+ descriptor.set?.call(text, next);
7630
+ pane.text.requestRender();
7631
+ }
7632
+ }
7633
+ } : {}
7634
+ };
7635
+ Object.defineProperty(text, name, nextDescriptor);
7636
+ }
7637
+ }
7638
+ function createAdapter(pane) {
7639
+ const text = pane.text;
7640
+ const originalOwnDescriptors = new Map(ACCESSORS.map((name) => [name, Object.getOwnPropertyDescriptor(text, name)]));
7641
+ const originalDescriptors = new Map(ACCESSORS.map((name) => [name, Object.getOwnPropertyDescriptor(text, name) ?? prototypeDescriptor(text, name)]));
7642
+ const state = {
7643
+ active: false,
7644
+ document: undefined,
7645
+ layout: undefined,
7646
+ preamble: "",
7647
+ scrollY: 0,
7648
+ scrollX: 0,
7649
+ viewportHeight: Math.max(1, Math.floor(text.height)),
7650
+ viewportWidth: Math.max(0, Math.floor(text.width)),
7651
+ rawSelection: undefined,
7652
+ renderedWindow: undefined,
7653
+ preambleSpans: new Map,
7654
+ originalDescriptors,
7655
+ originalOwnDescriptors
7656
+ };
7657
+ const restoreAccessors = () => {
7658
+ const target = text;
7659
+ for (const name of ACCESSORS) {
7660
+ delete target[name];
7661
+ const own = state.originalOwnDescriptors.get(name);
7662
+ if (own !== undefined)
7663
+ Object.defineProperty(target, name, own);
7664
+ }
7665
+ };
7666
+ const visibleSelection = () => {
7667
+ const selection = state.rawSelection;
7668
+ const layout = state.layout;
7669
+ const window = state.renderedWindow;
7670
+ if (selection === undefined || layout === undefined || window === undefined)
7671
+ return;
7672
+ let start;
7673
+ let end;
7674
+ let localOffset = 0;
7675
+ for (let row = window[0];row <= window[1]; row += 1) {
7676
+ const current = layout.rowAt(row);
7677
+ if (current === undefined)
7678
+ continue;
7679
+ const rowStart = localOffset;
7680
+ localOffset += layout.contentWidth + (row < window[1] ? 1 : 0);
7681
+ if (current.lineIndex === undefined || current.rawStartUtf16 === undefined || current.rawEndUtf16 === undefined)
7682
+ continue;
7683
+ const line = state.document?.lines[current.lineIndex];
7684
+ if (line === undefined)
7685
+ continue;
7686
+ const overlapStart = Math.max(selection.startUtf16, current.rawStartUtf16);
7687
+ const overlapEnd = Math.min(selection.endUtf16, current.rawEndUtf16);
7688
+ if (overlapStart >= overlapEnd)
7689
+ continue;
7690
+ const displayStart = rowStart + (overlapStart === current.rawStartUtf16 ? 0 : current.gutterCols + rawDisplayCells(line.raw, overlapStart - current.rawStartUtf16, false));
7691
+ const displayEnd = rowStart + current.gutterCols + rawDisplayCells(line.raw, overlapEnd - current.rawStartUtf16, overlapEnd > current.rawStartUtf16 + withoutLineEnding3(line.raw).length);
7692
+ start = start === undefined ? displayStart : Math.min(start, displayStart);
7693
+ end = end === undefined ? displayEnd : Math.max(end, displayEnd);
7694
+ }
7695
+ return start === undefined || end === undefined ? undefined : { start, end };
7696
+ };
7697
+ const paintSelection = () => {
7698
+ const selected = visibleSelection();
7699
+ const surface = text;
7700
+ if (selected === undefined)
7701
+ surface.resetSelection?.();
7702
+ else
7703
+ surface.setSelection?.(selected.start, selected.end);
7704
+ };
7705
+ const renderWindow = () => {
7706
+ if (!state.active || state.layout === undefined)
7707
+ return;
7708
+ const current = dimensions(text);
7709
+ state.viewportHeight = current.height;
7710
+ state.viewportWidth = current.width;
7711
+ const max = Math.max(0, state.layout.totalRows - state.viewportHeight);
7712
+ if (state.scrollY > max)
7713
+ state.scrollY = max;
7714
+ const maxX = Math.max(0, state.layout.contentWidth - state.viewportWidth);
7715
+ if (state.scrollX > maxX)
7716
+ state.scrollX = maxX;
7717
+ const overscan = Math.max(VIRTUAL_MAIN_OVERSCAN_MIN, state.viewportHeight);
7718
+ const window = state.layout.window(state.scrollY, state.viewportHeight, overscan);
7719
+ const localScrollY = state.scrollY - window[0];
7720
+ state.renderedWindow = window;
7721
+ const rows = [];
7722
+ const displays = [];
7723
+ const preambleRows = [];
7724
+ const preambleSpans = new Map;
7725
+ const first = window[0];
7726
+ const last = window[1];
7727
+ if (last >= first && first < state.layout.preambleRows) {
7728
+ const preambleLast = Math.min(last, state.layout.preambleRows - 1);
7729
+ for (let row = first;row <= preambleLast; row += 1) {
7730
+ const value = state.layout.rowAt(row);
7731
+ if (value !== undefined)
7732
+ preambleRows.push(padDisplayRow(value.text, state.layout.contentWidth));
7733
+ const spans = state.preambleSpans.get(row);
7734
+ if (spans !== undefined)
7735
+ preambleSpans.set(row - first, spans);
7736
+ }
7737
+ }
7738
+ const bodyFirst = Math.max(first, state.layout.preambleRows);
7739
+ for (let row = bodyFirst;row <= last; row += 1) {
7740
+ const value = state.layout.rowAt(row);
7741
+ if (value === undefined || value.lineIndex === undefined)
7742
+ continue;
7743
+ rows.push(padDisplayRow(value.text, state.layout.contentWidth));
7744
+ displays.push({ gutterCols: value.gutterCols, style: value.style });
7745
+ }
7746
+ const preamble = preambleRows.length === 0 ? "" : `${preambleRows.join(`
7747
+ `)}
7748
+ `;
7749
+ installDiffText(text, { preamble, body: rows.join(`
7750
+ `), displayLines: displays, highlightScrollY: () => localScrollY, preambleSpans });
7751
+ const originalScrollY = state.originalDescriptors.get("scrollY")?.set;
7752
+ originalScrollY?.call(text, localScrollY);
7753
+ const originalScrollX = state.originalDescriptors.get("scrollX")?.set;
7754
+ originalScrollX?.call(text, state.scrollX);
7755
+ paintSelection();
7756
+ pane.syncScrollbar(state.viewportHeight);
7757
+ text.requestRender?.();
7758
+ };
7759
+ const rerender = () => renderWindow();
7760
+ const adapter = {
7761
+ install(document, preamble) {
7762
+ if (!state.active) {
7763
+ state.scrollY = isFiniteNonNegative(Number(text.scrollY));
7764
+ state.scrollX = isFiniteNonNegative(Number(text.scrollX));
7765
+ }
7766
+ state.active = true;
7767
+ state.document = document;
7768
+ state.layout = createVirtualDiffLayout(document, preamble);
7769
+ state.preamble = preamble;
7770
+ state.preambleSpans = statSpansForPreamble(preamble);
7771
+ const current = dimensions(text);
7772
+ state.viewportHeight = current.height;
7773
+ state.viewportWidth = current.width;
7774
+ installAccessors(pane, state, rerender);
7775
+ text.wrapMode = "none";
7776
+ renderWindow();
7777
+ },
7778
+ deactivate() {
7779
+ if (!state.active)
7780
+ return;
7781
+ const originalY = state.originalDescriptors.get("scrollY")?.set;
7782
+ const originalX = state.originalDescriptors.get("scrollX")?.set;
7783
+ originalY?.call(text, state.scrollY);
7784
+ originalX?.call(text, state.scrollX);
7785
+ state.active = false;
7786
+ state.document = undefined;
7787
+ state.layout = undefined;
7788
+ state.rawSelection = undefined;
7789
+ state.renderedWindow = undefined;
7790
+ releaseDiffText(text);
7791
+ state.preambleSpans = new Map;
7792
+ restoreAccessors();
7793
+ clearScrollbarViewportOverride(text);
7794
+ text.wrapMode = "char";
7795
+ },
7796
+ isActive: () => state.active,
7797
+ layout: () => state.layout,
7798
+ lineOffsets: (startIndex, endIndex) => state.layout?.displayOffsetsForLines(startIndex, endIndex),
7799
+ visualRowRange: (startIndex, endIndex) => {
7800
+ if (state.layout === undefined)
7801
+ return;
7802
+ const start = state.layout.preambleRows + Math.max(0, startIndex);
7803
+ const end = state.layout.preambleRows + Math.max(startIndex, endIndex);
7804
+ return { startRow: start, endRow: end };
7805
+ },
7806
+ setLineSelection(startUtf16, endUtf16) {
7807
+ if (!state.active || state.document === undefined)
7808
+ return;
7809
+ state.rawSelection = documentSelection(state.document, startUtf16, endUtf16);
7810
+ paintSelection();
7811
+ },
7812
+ setPointerSelection(startRow, startColumn, endRow, endColumn) {
7813
+ if (!state.active || state.layout === undefined || state.document === undefined)
7814
+ return;
7815
+ const start = state.layout.rawOffsetAt(Math.max(0, Math.floor(state.scrollY + startRow)), Math.max(0, Math.floor(state.scrollX + startColumn)));
7816
+ const end = state.layout.rawOffsetAt(Math.max(0, Math.floor(state.scrollY + endRow)), Math.max(0, Math.floor(state.scrollX + endColumn)));
7817
+ if (start === undefined || end === undefined) {
7818
+ state.rawSelection = undefined;
7819
+ paintSelection();
7820
+ return;
7821
+ }
7822
+ const selection = documentSelection(state.document, Math.min(start, end), Math.max(start, end));
7823
+ state.rawSelection = selection;
7824
+ paintSelection();
7825
+ return selection;
7826
+ },
7827
+ selection: () => state.rawSelection,
7828
+ resetSelection() {
7829
+ state.rawSelection = undefined;
7830
+ text.resetSelection?.();
7831
+ },
7832
+ clampScroll() {
7833
+ if (!state.active || state.layout === undefined) {
7834
+ const originalY = state.originalDescriptors.get("scrollY")?.set;
7835
+ const originalX = state.originalDescriptors.get("scrollX")?.set;
7836
+ originalY?.call(text, text.scrollY);
7837
+ originalX?.call(text, text.scrollX);
7838
+ return;
7839
+ }
7840
+ const maxY = Math.max(0, state.layout.totalRows - state.viewportHeight);
7841
+ const maxX = Math.max(0, state.layout.contentWidth - state.viewportWidth);
7842
+ state.scrollY = Math.min(maxY, Math.max(0, state.scrollY));
7843
+ state.scrollX = Math.min(maxX, Math.max(0, state.scrollX));
7844
+ renderWindow();
7845
+ }
7846
+ };
7847
+ onPaneLifecyclePass(text, () => {
7848
+ if (!state.active || state.layout === undefined)
7849
+ return;
7850
+ const current = dimensions(text);
7851
+ if (current.height !== state.viewportHeight || current.width !== state.viewportWidth)
7852
+ renderWindow();
7853
+ });
7854
+ return adapter;
7855
+ }
7856
+ function createVirtualMainPane(pane) {
7857
+ const existing = virtualPanes.get(pane);
7858
+ if (existing !== undefined)
7859
+ return existing;
7860
+ const adapter = createAdapter(pane);
7861
+ virtualPanes.set(pane, adapter);
7862
+ return adapter;
7863
+ }
7864
+ function virtualMainPaneFor(pane) {
7865
+ return virtualPanes.get(pane);
7866
+ }
7867
+ function isVirtualDiffDocument(document) {
7868
+ return document.lines.length > VIRTUAL_DIFF_LINE_THRESHOLD;
7040
7869
  }
7041
7870
 
7042
7871
  // src/ui/panes/main-pane.ts
@@ -7071,6 +7900,7 @@ function createMainPane(renderer, _model) {
7071
7900
  const pane = createPane(renderer, "main", "0 Main", "", true);
7072
7901
  pane.text.selectionBg = SELECTED_LINE_BG;
7073
7902
  ensureMainTextSelectionSurface(pane);
7903
+ createVirtualMainPane(pane);
7074
7904
  pane.box.title = "0 Main";
7075
7905
  paneTitles.set(pane, "0 Main");
7076
7906
  return pane;
@@ -7119,6 +7949,9 @@ function mainDiffVisualRowRange(pane, startIndex, endIndex) {
7119
7949
  const document = documents.get(pane);
7120
7950
  if (document === undefined)
7121
7951
  return;
7952
+ const virtual = virtualMainPaneFor(pane);
7953
+ if (virtual?.isActive())
7954
+ return virtual.visualRowRange(startIndex, endIndex);
7122
7955
  const content = installedContents.get(pane);
7123
7956
  const preambleRows = (normalizedPreamble(content?.preamble ?? "").match(/\n/g) ?? []).length;
7124
7957
  const firstSource = preambleRows + Math.max(0, startIndex);
@@ -7143,7 +7976,16 @@ function getMainDiffLineSelection(pane) {
7143
7976
  if (document === undefined || state === undefined || state.rangeMode === "none")
7144
7977
  return;
7145
7978
  const range = diffLineSelectionRange(state);
7146
- const offsets = mainDiffLineOffsets(document, range.startIndex, range.endIndex, installedContents.get(pane)?.preamble ?? "");
7979
+ const virtual = virtualMainPaneFor(pane);
7980
+ const offsets = virtual?.isActive() ? (() => {
7981
+ const value = virtual.lineOffsets(range.startIndex, range.endIndex + 1);
7982
+ return value === undefined ? undefined : {
7983
+ startUtf16: value.rawStartUtf16,
7984
+ endUtf16: value.rawEndUtf16,
7985
+ displayStartUtf16: value.displayStartUtf16,
7986
+ displayEndUtf16: value.displayEndUtf16
7987
+ };
7988
+ })() : mainDiffLineOffsets(document, range.startIndex, range.endIndex, installedContents.get(pane)?.preamble ?? "");
7147
7989
  if (offsets === undefined)
7148
7990
  return;
7149
7991
  return {
@@ -7154,16 +7996,42 @@ function getMainDiffLineSelection(pane) {
7154
7996
  };
7155
7997
  }
7156
7998
  function applyMainDiffLineVisualSelection(pane, resetWhenInactive = true) {
7157
- const text = pane.text;
7999
+ const virtual = virtualMainPaneFor(pane);
7158
8000
  const selection = getMainDiffLineSelection(pane);
7159
- if (selection !== undefined) {
8001
+ if (virtual?.isActive()) {
8002
+ if (selection !== undefined)
8003
+ virtual.setLineSelection(selection.startUtf16, selection.endUtf16);
8004
+ else if (resetWhenInactive)
8005
+ virtual.resetSelection();
8006
+ return;
8007
+ }
8008
+ const text = pane.text;
8009
+ if (selection !== undefined)
7160
8010
  text.setSelection?.(selection.displayStartUtf16, selection.displayEndUtf16);
7161
- } else if (resetWhenInactive) {
8011
+ else if (resetWhenInactive)
7162
8012
  text.resetSelection?.();
7163
- }
8013
+ }
8014
+ function getMainPointerSelection(pane) {
8015
+ return virtualMainPaneFor(pane)?.selection();
7164
8016
  }
7165
8017
  function setMainDiffLineRangeState(pane, state) {
7166
8018
  lineRanges.set(pane, state);
8019
+ const virtual = virtualMainPaneFor(pane);
8020
+ if (state.rangeMode === "none") {
8021
+ if (virtual?.isActive())
8022
+ virtual.resetSelection();
8023
+ else
8024
+ applyMainDiffLineVisualSelection(pane);
8025
+ return;
8026
+ }
8027
+ const document = documents.get(pane);
8028
+ const range = diffLineSelectionRange(state);
8029
+ const start = document?.lines[range.startIndex];
8030
+ const end = document?.lines[range.endIndex];
8031
+ if (virtual?.isActive() && start !== undefined && end !== undefined) {
8032
+ virtual.setLineSelection(start.startUtf16, end.endUtf16);
8033
+ return;
8034
+ }
7167
8035
  applyMainDiffLineVisualSelection(pane);
7168
8036
  }
7169
8037
  function getMainDocument(pane) {
@@ -7243,11 +8111,17 @@ function buildPlainContent(content) {
7243
8111
  return "No content";
7244
8112
  }
7245
8113
  function clampMainScroll(pane) {
8114
+ const virtual = virtualMainPaneFor(pane);
8115
+ if (virtual?.isActive()) {
8116
+ virtual.clampScroll();
8117
+ return;
8118
+ }
7246
8119
  pane.text.scrollY = Math.max(0, Math.min(pane.text.maxScrollY, pane.text.scrollY));
7247
8120
  pane.text.scrollX = Math.max(0, Math.min(pane.text.maxScrollX, pane.text.scrollX));
7248
8121
  pane.syncScrollbar();
7249
8122
  }
7250
8123
  function updatePlain(pane, value) {
8124
+ virtualMainPaneFor(pane)?.deactivate();
7251
8125
  releaseDiffText(pane.text);
7252
8126
  releaseAnsiText(pane.text);
7253
8127
  pane.update(value);
@@ -7255,20 +8129,25 @@ function updatePlain(pane, value) {
7255
8129
  function installMainContent(pane, content, tooSmall) {
7256
8130
  const previousContent = installedContents.get(pane);
7257
8131
  const previousText = renderedTexts.get(pane);
7258
- const nextText = renderedTextFor(content);
8132
+ const virtualDocument = content.document !== undefined && isVirtualDiffDocument(content.document);
8133
+ const nextText = virtualDocument ? undefined : renderedTextFor(content);
7259
8134
  const previousIdentity = previousContent === undefined ? undefined : `${previousContent.source}:${previousContent.stableId}`;
7260
8135
  const nextIdentity = `${content.source}:${content.stableId}`;
7261
8136
  const sameIdentity = previousIdentity !== undefined && previousIdentity === nextIdentity;
7262
- const identicalText = previousText !== undefined && previousText === nextText;
8137
+ const identicalText = virtualDocument ? previousContent?.document?.text === content.document?.text && previousContent?.preamble === content.preamble : previousText !== undefined && previousText === nextText;
7263
8138
  const previousWasDocument = documents.has(pane);
7264
8139
  const replacingDocument = previousWasDocument && content.document === undefined;
7265
8140
  const enteringDocument = !previousWasDocument && content.document !== undefined;
7266
8141
  const leavingAnsi = previousContent?.ansi !== undefined && content.ansi === undefined;
7267
8142
  installedContents.set(pane, content);
7268
- renderedTexts.set(pane, nextText);
8143
+ if (nextText === undefined)
8144
+ renderedTexts.delete(pane);
8145
+ else
8146
+ renderedTexts.set(pane, nextText);
7269
8147
  paneTitles.set(pane, `0 Main — ${content.label}`);
7270
8148
  const previousRange = lineRanges.get(pane);
7271
8149
  const clearSelection = () => {
8150
+ virtualMainPaneFor(pane)?.resetSelection();
7272
8151
  const view = pane.text;
7273
8152
  if (view !== null && typeof view === "object" && "resetSelection" in view) {
7274
8153
  const reset = view.resetSelection;
@@ -7339,6 +8218,16 @@ function installMainContent(pane, content, tooSmall) {
7339
8218
  } else {
7340
8219
  cursorTargets.delete(pane);
7341
8220
  }
8221
+ const virtual = virtualMainPaneFor(pane);
8222
+ if (virtualDocument && virtual !== undefined) {
8223
+ releaseAnsiText(pane.text);
8224
+ virtual.install(doc, content.preamble ?? "");
8225
+ renderedTexts.delete(pane);
8226
+ if (previousRange?.rangeMode !== "none")
8227
+ applyMainDiffLineVisualSelection(pane);
8228
+ return;
8229
+ }
8230
+ virtual?.deactivate();
7342
8231
  pane.text.wrapMode = "char";
7343
8232
  releaseAnsiText(pane.text);
7344
8233
  if (enteringDocument || !sameIdentity || !identicalText) {
@@ -7353,6 +8242,7 @@ function installMainContent(pane, content, tooSmall) {
7353
8242
  return;
7354
8243
  }
7355
8244
  if (content.ansi !== undefined) {
8245
+ virtualMainPaneFor(pane)?.deactivate();
7356
8246
  documents.delete(pane);
7357
8247
  if (replacingDocument || !sameIdentity || !identicalText)
7358
8248
  clearSelection();
@@ -7364,6 +8254,7 @@ function installMainContent(pane, content, tooSmall) {
7364
8254
  pane.syncScrollbar();
7365
8255
  return;
7366
8256
  }
8257
+ virtualMainPaneFor(pane)?.deactivate();
7367
8258
  documents.delete(pane);
7368
8259
  if (replacingDocument || leavingAnsi || !sameIdentity || !identicalText)
7369
8260
  clearSelection();
@@ -7415,8 +8306,7 @@ function createStashPane(renderer, model) {
7415
8306
  const rows = stashRows(model);
7416
8307
  const displayRows = stashDisplayRows(model, rows);
7417
8308
  const state = createListState(rows, displayRows);
7418
- const content = renderListRows(state, false, 80);
7419
- pane.update(content);
8309
+ installListText(pane.text, { state, width: 80, focused: false });
7420
8310
  return pane;
7421
8311
  }
7422
8312
  function selectedStashEntryFromState(state, model) {
@@ -9336,6 +10226,7 @@ class RootView {
9336
10226
  lastSplitterPress;
9337
10227
  activeSplitterDrag;
9338
10228
  gestureOwner;
10229
+ mainPointerAnchor;
9339
10230
  pendingClick;
9340
10231
  hoveredListRow;
9341
10232
  model;
@@ -9399,6 +10290,7 @@ class RootView {
9399
10290
  onInspectStash;
9400
10291
  onBrowseRemote;
9401
10292
  loadBranchCommits;
10293
+ onExpandCommits;
9402
10294
  onInspectBranch;
9403
10295
  onCheckoutRemoteTracking;
9404
10296
  onFilterBranches;
@@ -9492,6 +10384,7 @@ class RootView {
9492
10384
  this.loadTagInspection = options.loadTagInspection;
9493
10385
  this.loadRefLogInspection = options.loadRefLogInspection;
9494
10386
  this.loadBranchCommits = options.loadBranchCommits;
10387
+ this.onExpandCommits = options.onExpandCommits;
9495
10388
  this.onPreviewError = options.onPreviewError;
9496
10389
  this.onCommitMessage = options.onCommitMessage;
9497
10390
  this.onAmendMessage = options.onAmendMessage;
@@ -9624,6 +10517,10 @@ class RootView {
9624
10517
  this.syncPreviewForFocus(focus);
9625
10518
  };
9626
10519
  this.handleResize = () => {
10520
+ if (this.gestureOwner?.kind === "main-selection") {
10521
+ virtualMainPaneFor(this.panes.main)?.resetSelection();
10522
+ this.cancelGesture();
10523
+ }
9627
10524
  this.recomputeLayout();
9628
10525
  };
9629
10526
  this.handleKey = (key) => {
@@ -9672,9 +10569,13 @@ class RootView {
9672
10569
  this.applyLayout();
9673
10570
  }
9674
10571
  update(model, options = {}) {
10572
+ if (this.gestureOwner?.kind === "main-selection") {
10573
+ virtualMainPaneFor(this.panes.main)?.resetSelection();
10574
+ this.cancelGesture();
10575
+ }
9675
10576
  this.branchActionGeneration += 1;
9676
10577
  this.invalidateBranchCommitsRequest();
9677
- if (!options.preserveRemoteCheckout) {
10578
+ if (!options.preserveRemoteCheckout && !options.preserveFilterInput) {
9678
10579
  this.branchFilterActive = false;
9679
10580
  this.branchFilter = "";
9680
10581
  this.filterInput.clear();
@@ -9902,7 +10803,11 @@ class RootView {
9902
10803
  };
9903
10804
  }
9904
10805
  cancelGesture() {
10806
+ if (this.gestureOwner?.kind === "main-selection") {
10807
+ virtualMainPaneFor(this.panes.main)?.resetSelection();
10808
+ }
9905
10809
  this.gestureOwner = undefined;
10810
+ this.mainPointerAnchor = undefined;
9906
10811
  this.activeSplitterDrag = undefined;
9907
10812
  }
9908
10813
  get activeBranchesTab() {
@@ -10201,14 +11106,15 @@ class RootView {
10201
11106
  const focused = this.focusManager.active === "branches";
10202
11107
  const activeView = this.activeListView("branches");
10203
11108
  if (activeView === undefined) {
11109
+ releaseListText(pane.text);
10204
11110
  pane.update("");
10205
11111
  return;
10206
11112
  }
10207
11113
  const { state } = activeView;
10208
11114
  const win = this.geometry.windows.branches;
10209
11115
  const width = sidePaneListWidth(win, state);
10210
- const content = renderListRows(state, focused, width, this.hoveredIdFor(activeView));
10211
- pane.update(content);
11116
+ const hoveredId = this.hoveredIdFor(activeView);
11117
+ installListText(pane.text, { state, width, focused, ...hoveredId === undefined ? {} : { hoveredId } });
10212
11118
  pane.syncScrollbar(sidePaneViewportHeight(win));
10213
11119
  }
10214
11120
  get filesTitleStyled() {
@@ -10268,13 +11174,15 @@ class RootView {
10268
11174
  pane.setTabs?.({ tabs: tabsInput.tabs, activeIndex: tabsInput.activeIndex, focused: tabsInput.focused });
10269
11175
  const activeView = this.activeListView("files");
10270
11176
  if (activeView === undefined) {
11177
+ releaseListText(pane.text);
10271
11178
  pane.update("");
10272
11179
  return;
10273
11180
  }
10274
11181
  const { state } = activeView;
10275
11182
  const win = this.geometry.windows.files;
10276
11183
  const width = sidePaneListWidth(win, state);
10277
- pane.update(renderListRows(state, tabsInput.focused, width, this.hoveredIdFor(activeView)));
11184
+ const hoveredId = this.hoveredIdFor(activeView);
11185
+ installListText(pane.text, { state, width, focused: tabsInput.focused, ...hoveredId === undefined ? {} : { hoveredId } });
10278
11186
  pane.syncScrollbar(sidePaneViewportHeight(win));
10279
11187
  }
10280
11188
  refreshStashState(model) {
@@ -10288,6 +11196,7 @@ class RootView {
10288
11196
  const pane = this.panes.stash;
10289
11197
  const activeView = this.activeListView("stash");
10290
11198
  if (activeView === undefined) {
11199
+ releaseListText(pane.text);
10291
11200
  pane.update("");
10292
11201
  return;
10293
11202
  }
@@ -10295,8 +11204,8 @@ class RootView {
10295
11204
  const focused = this.focusManager.active === "stash";
10296
11205
  const win = this.geometry.windows.stash;
10297
11206
  const width = sidePaneListWidth(win, state);
10298
- const content = renderListRows(state, focused, width, this.hoveredIdFor(activeView));
10299
- pane.update(content);
11207
+ const hoveredId = this.hoveredIdFor(activeView);
11208
+ installListText(pane.text, { state, width, focused, ...hoveredId === undefined ? {} : { hoveredId } });
10300
11209
  pane.syncScrollbar(sidePaneViewportHeight(win));
10301
11210
  }
10302
11211
  renderSidePanes() {
@@ -10857,6 +11766,7 @@ class RootView {
10857
11766
  this.renderCommitsPane();
10858
11767
  this.revealListRow("commits", this.panes.commits, nextView.selectedIndex);
10859
11768
  this.syncPreviewForFocus("commits");
11769
+ this.maybeExpandCommits(nextView.selectedIndex);
10860
11770
  }
10861
11771
  }
10862
11772
  return;
@@ -10945,8 +11855,8 @@ class RootView {
10945
11855
  }
10946
11856
  focusedPageStep() {
10947
11857
  const focus = this.focusManager.active;
10948
- const dimensions = focus === "command-log" ? this.geometry.windows.log : this.geometry.windows[focus] ?? this.geometry.windows.main;
10949
- return Math.max(1, heightOf(dimensions) - 2);
11858
+ const dimensions2 = focus === "command-log" ? this.geometry.windows.log : this.geometry.windows[focus] ?? this.geometry.windows.main;
11859
+ return Math.max(1, heightOf(dimensions2) - 2);
10950
11860
  }
10951
11861
  actionPage(direction) {
10952
11862
  if (this.focusManager.active === "main") {
@@ -10979,11 +11889,63 @@ class RootView {
10979
11889
  }
10980
11890
  return;
10981
11891
  }
10982
- const direction = edge === "bottom" ? "next" : "previous";
10983
- const branchCount = this.branchesPanel.child !== undefined ? this.branchesPanel.child.view.rows.length : this.branchesPanel.views[this.branchesPanel.activeTab]?.rows.length ?? 0;
10984
- const limit = Math.max(this.model.files.length, (this.model.worktrees ?? []).length, (this.model.submodules ?? []).length, (this.model.commits ?? []).length, (this.model.stashes ?? []).length, (this.model.reflog ?? []).length, branchCount) + 1;
10985
- for (let moved = 0;moved < limit; moved += 1)
10986
- this.actionMoveCursor(direction);
11892
+ this.clearTransientMenus();
11893
+ const paneId = this.focusManager.active;
11894
+ if (paneId !== "files" && paneId !== "branches" && paneId !== "commits" && paneId !== "stash")
11895
+ return;
11896
+ if (paneId === "commits" && edge === "bottom") {
11897
+ this.jumpCommitsToBottom();
11898
+ return;
11899
+ }
11900
+ const active = this.activeListView(paneId);
11901
+ if (active === undefined || active.state.rows.length === 0)
11902
+ return;
11903
+ const rows = active.state.rows;
11904
+ const targetId = (edge === "bottom" ? rows[rows.length - 1] : rows[0]).id;
11905
+ const next = this.edgeSelection(active.state, targetId);
11906
+ if (next === active.state)
11907
+ return;
11908
+ this.updateActiveListState(paneId, next);
11909
+ this.renderListPane(paneId);
11910
+ this.revealListRow(paneId, this.panes[paneId], next.selectedIndex);
11911
+ this.syncListSelectionAfterChange(paneId);
11912
+ this.root.requestRender();
11913
+ }
11914
+ edgeSelection(state, targetId) {
11915
+ const direct = selectListRow(state, targetId);
11916
+ if (direct === state)
11917
+ return state;
11918
+ if (state.rangeMode !== "sticky" || state.rangeStartId === undefined)
11919
+ return direct;
11920
+ if (!state.rows.some((row) => row.id === state.rangeStartId))
11921
+ return direct;
11922
+ return { ...direct, rangeMode: state.rangeMode, rangeStartId: state.rangeStartId };
11923
+ }
11924
+ jumpCommitsToBottom() {
11925
+ (async () => {
11926
+ await this.onExpandCommits?.();
11927
+ const active = this.activeListView("commits");
11928
+ if (active === undefined || active.state.rows.length === 0)
11929
+ return;
11930
+ const rows = active.state.rows;
11931
+ const next = this.edgeSelection(active.state, rows[rows.length - 1].id);
11932
+ if (next === active.state)
11933
+ return;
11934
+ this.updateActiveListState("commits", next);
11935
+ this.renderListPane("commits");
11936
+ this.revealListRow("commits", this.panes.commits, next.selectedIndex);
11937
+ this.syncListSelectionAfterChange("commits");
11938
+ this.root.requestRender();
11939
+ })();
11940
+ }
11941
+ maybeExpandCommits(selectedIndex) {
11942
+ if (this.commitsPanel.activeTab !== "commits")
11943
+ return;
11944
+ if (this.commitsPanel.child !== undefined)
11945
+ return;
11946
+ if (selectedIndex <= COMMIT_THRESHOLD)
11947
+ return;
11948
+ this.onExpandCommits?.();
10987
11949
  }
10988
11950
  actionInspect() {
10989
11951
  switch (this.focusManager.active) {
@@ -12333,6 +13295,9 @@ class RootView {
12333
13295
  const existing = this.getFilterForKey(target.key);
12334
13296
  if (existing.length > 0)
12335
13297
  this.filters.delete(target.key);
13298
+ if (target.key === this.filterKey("commits", "commits")) {
13299
+ this.onExpandCommits?.();
13300
+ }
12336
13301
  this.filterInput.open("");
12337
13302
  this.refreshForFilterKey(target.key);
12338
13303
  this.renderForFilterKey(target.key);
@@ -12352,7 +13317,7 @@ class RootView {
12352
13317
  let next = -1;
12353
13318
  for (let i = current + 1;i < view.rows.length; i++) {
12354
13319
  const row = view.rows[i];
12355
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13320
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12356
13321
  if (text.includes(normalizedQuery)) {
12357
13322
  next = i;
12358
13323
  break;
@@ -12361,7 +13326,7 @@ class RootView {
12361
13326
  if (next === -1) {
12362
13327
  for (let i = 0;i <= current; i++) {
12363
13328
  const row = view.rows[i];
12364
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13329
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12365
13330
  if (text.includes(normalizedQuery)) {
12366
13331
  next = i;
12367
13332
  break;
@@ -12387,11 +13352,44 @@ class RootView {
12387
13352
  const query = this.getFilterForKey(this.filterKey("main"));
12388
13353
  if (query.length === 0)
12389
13354
  return;
12390
- const text = getMainRenderedText(this.panes.main) ?? getMainDocument(this.panes.main)?.text ?? "";
13355
+ const document = getMainDocument(this.panes.main);
13356
+ const virtual = virtualMainPaneFor(this.panes.main);
13357
+ const normalizedQuery = query.toLowerCase();
13358
+ if (document !== undefined && virtual?.isActive()) {
13359
+ const layout = virtual.layout();
13360
+ const preambleRows = layout?.preambleRows ?? 0;
13361
+ const totalRows = layout?.totalRows ?? preambleRows + document.lines.length;
13362
+ const currentLine2 = this.panes.main.text.scrollY - preambleRows;
13363
+ let nextLine2 = -1;
13364
+ const firstCandidate = Math.max(0, currentLine2 + 1);
13365
+ for (let i = firstCandidate;i < document.lines.length; i += 1) {
13366
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13367
+ nextLine2 = i;
13368
+ break;
13369
+ }
13370
+ }
13371
+ if (nextLine2 === -1) {
13372
+ for (let i = 0;i <= currentLine2 && i < document.lines.length; i += 1) {
13373
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13374
+ nextLine2 = i;
13375
+ break;
13376
+ }
13377
+ }
13378
+ }
13379
+ if (nextLine2 !== -1) {
13380
+ this.clearNonStickyMainRange();
13381
+ const targetRow = preambleRows + nextLine2;
13382
+ this.panes.main.text.scrollY = Math.max(0, targetRow - 2);
13383
+ this.panes.main.syncScrollbar();
13384
+ this.panes.main.box.bottomTitle = `Search: ${query} (${targetRow + 1}/${totalRows})`;
13385
+ this.root.requestRender();
13386
+ }
13387
+ return;
13388
+ }
13389
+ const text = getMainRenderedText(this.panes.main) ?? document?.text ?? "";
12391
13390
  if (text.length === 0)
12392
13391
  return;
12393
13392
  const normalizedText = text.toLowerCase();
12394
- const normalizedQuery = query.toLowerCase();
12395
13393
  const currentY = this.panes.main.text.scrollY;
12396
13394
  const lines = text.split(`
12397
13395
  `);
@@ -12415,6 +13413,7 @@ class RootView {
12415
13413
  this.clearNonStickyMainRange();
12416
13414
  const targetY = Math.max(0, nextLine - 2);
12417
13415
  this.panes.main.text.scrollY = targetY;
13416
+ this.panes.main.syncScrollbar();
12418
13417
  this.panes.main.box.bottomTitle = `Search: ${query} (${nextLine + 1}/${lines.length})`;
12419
13418
  this.root.requestRender();
12420
13419
  }
@@ -12435,7 +13434,7 @@ class RootView {
12435
13434
  let prev = -1;
12436
13435
  for (let i = current - 1;i >= 0; i--) {
12437
13436
  const row = view.rows[i];
12438
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13437
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12439
13438
  if (text.includes(normalizedQuery)) {
12440
13439
  prev = i;
12441
13440
  break;
@@ -12444,7 +13443,7 @@ class RootView {
12444
13443
  if (prev === -1) {
12445
13444
  for (let i = view.rows.length - 1;i >= current; i--) {
12446
13445
  const row = view.rows[i];
12447
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13446
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12448
13447
  if (text.includes(normalizedQuery)) {
12449
13448
  prev = i;
12450
13449
  break;
@@ -12470,13 +13469,45 @@ class RootView {
12470
13469
  const query = this.getFilterForKey(this.filterKey("main"));
12471
13470
  if (query.length === 0)
12472
13471
  return;
12473
- const text = getMainRenderedText(this.panes.main) ?? getMainDocument(this.panes.main)?.text ?? "";
13472
+ const document = getMainDocument(this.panes.main);
13473
+ const virtual = virtualMainPaneFor(this.panes.main);
13474
+ const normalizedQuery = query.toLowerCase();
13475
+ if (document !== undefined && virtual?.isActive()) {
13476
+ const layout = virtual.layout();
13477
+ const preambleRows = layout?.preambleRows ?? 0;
13478
+ const totalRows = layout?.totalRows ?? preambleRows + document.lines.length;
13479
+ const currentLine = this.panes.main.text.scrollY - preambleRows;
13480
+ let previousLine = -1;
13481
+ for (let i = Math.min(document.lines.length - 1, currentLine - 1);i >= 0; i -= 1) {
13482
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13483
+ previousLine = i;
13484
+ break;
13485
+ }
13486
+ }
13487
+ if (previousLine === -1) {
13488
+ for (let i = document.lines.length - 1;i >= currentLine && i >= 0; i -= 1) {
13489
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13490
+ previousLine = i;
13491
+ break;
13492
+ }
13493
+ }
13494
+ }
13495
+ if (previousLine !== -1) {
13496
+ this.clearNonStickyMainRange();
13497
+ const targetRow = preambleRows + previousLine;
13498
+ this.panes.main.text.scrollY = Math.max(0, targetRow - 2);
13499
+ this.panes.main.syncScrollbar();
13500
+ this.panes.main.box.bottomTitle = `Search: ${query} (${targetRow + 1}/${totalRows})`;
13501
+ this.root.requestRender();
13502
+ }
13503
+ return;
13504
+ }
13505
+ const text = getMainRenderedText(this.panes.main) ?? document?.text ?? "";
12474
13506
  if (text.length === 0)
12475
13507
  return;
12476
13508
  const lines = text.split(`
12477
13509
  `);
12478
13510
  const currentY = this.panes.main.text.scrollY;
12479
- const normalizedQuery = query.toLowerCase();
12480
13511
  let prevLine = -1;
12481
13512
  for (let i = currentY - 1;i >= 0; i--) {
12482
13513
  if (lines[i].toLowerCase().includes(normalizedQuery)) {
@@ -12496,6 +13527,7 @@ class RootView {
12496
13527
  this.clearNonStickyMainRange();
12497
13528
  const targetY = Math.max(0, prevLine - 2);
12498
13529
  this.panes.main.text.scrollY = targetY;
13530
+ this.panes.main.syncScrollbar();
12499
13531
  this.panes.main.box.bottomTitle = `Search: ${query} (${prevLine + 1}/${lines.length})`;
12500
13532
  this.root.requestRender();
12501
13533
  }
@@ -12530,6 +13562,13 @@ class RootView {
12530
13562
  return [...paths];
12531
13563
  }
12532
13564
  mainActionTarget(document) {
13565
+ const pointer = getMainPointerSelection(this.panes.main);
13566
+ if (pointer?.fileIndex !== undefined && pointer.valid && pointer.endUtf16 > pointer.startUtf16) {
13567
+ return {
13568
+ fileIndex: pointer.fileIndex,
13569
+ ...pointer.hunkIndex === undefined ? {} : { hunkIndex: pointer.hunkIndex }
13570
+ };
13571
+ }
12533
13572
  const selected = getMainDiffLineSelection(this.panes.main);
12534
13573
  const firstIndex = selected?.indexes[0];
12535
13574
  const line = firstIndex === undefined ? undefined : document.lines[firstIndex];
@@ -12541,10 +13580,43 @@ class RootView {
12541
13580
  }
12542
13581
  return getMainCursorTarget(this.panes.main);
12543
13582
  }
13583
+ mainPointerCoordinates(event) {
13584
+ const geometry = this.paneTextGeometry("main");
13585
+ if (geometry === undefined)
13586
+ return;
13587
+ const row = event.y - geometry.screenY;
13588
+ const column = event.x - geometry.screenX;
13589
+ if (!Number.isSafeInteger(row) || !Number.isSafeInteger(column) || row < 0 || row >= geometry.height || column < 0 || column >= geometry.width)
13590
+ return;
13591
+ return { row, column };
13592
+ }
13593
+ updateVirtualMainPointer(event) {
13594
+ const virtual = virtualMainPaneFor(this.panes.main);
13595
+ if (!virtual?.isActive())
13596
+ return;
13597
+ const anchor = this.mainPointerAnchor;
13598
+ if (anchor === undefined) {
13599
+ virtual.resetSelection();
13600
+ return;
13601
+ }
13602
+ const point = this.mainPointerCoordinates(event);
13603
+ if (point === undefined) {
13604
+ virtual.resetSelection();
13605
+ return;
13606
+ }
13607
+ virtual.setPointerSelection(anchor.row, anchor.column, point.row, point.column);
13608
+ }
12544
13609
  mainChangeSelection() {
12545
13610
  const document = getMainDocument(this.panes.main);
12546
13611
  if (!document)
12547
13612
  return;
13613
+ const pointerSelection = getMainPointerSelection(this.panes.main);
13614
+ if (pointerSelection?.valid && pointerSelection.endUtf16 > pointerSelection.startUtf16) {
13615
+ return {
13616
+ document,
13617
+ indexes: changeLineIndexes(document, pointerSelection.startUtf16, pointerSelection.endUtf16)
13618
+ };
13619
+ }
12548
13620
  const keyboardSelection = getMainDiffLineSelection(this.panes.main);
12549
13621
  if (keyboardSelection !== undefined) {
12550
13622
  return {
@@ -12939,7 +14011,7 @@ class RootView {
12939
14011
  let commitsState = setListRows(panel.views.commits, rows, displayRows);
12940
14012
  if (commitsSearch.length > 0 && rows.length > 0) {
12941
14013
  const normalized = commitsSearch.toLowerCase();
12942
- const matchIndex = rows.findIndex((row) => (row.columns[3]?.text ?? "").toLowerCase().includes(normalized) || (row.columns[0]?.text ?? "").toLowerCase().includes(normalized));
14014
+ const matchIndex = rows.findIndex((row) => (row.columns[2]?.text ?? "").toLowerCase().includes(normalized) || (row.columns[0]?.text ?? "").toLowerCase().includes(normalized));
12943
14015
  if (matchIndex >= 0) {
12944
14016
  const matchId = rows[matchIndex].id;
12945
14017
  commitsState = selectListRow(commitsState, matchId);
@@ -12970,14 +14042,15 @@ class RootView {
12970
14042
  }
12971
14043
  const activeView = this.activeListView("commits");
12972
14044
  if (activeView === undefined) {
14045
+ releaseListText(pane.text);
12973
14046
  pane.update("");
12974
14047
  return;
12975
14048
  }
12976
14049
  const { state } = activeView;
12977
14050
  const width = sidePaneListWidth(this.geometry.windows.commits, state);
12978
14051
  const focused = this.focusManager.active === "commits";
12979
- const content = renderListRows(state, focused, width, this.hoveredIdFor(activeView));
12980
- pane.update(content);
14052
+ const hoveredId = this.hoveredIdFor(activeView);
14053
+ installListText(pane.text, { state, width, focused, ...hoveredId === undefined ? {} : { hoveredId } });
12981
14054
  pane.syncScrollbar(sidePaneViewportHeight(this.geometry.windows.commits));
12982
14055
  }
12983
14056
  installInitialMainContent(model) {
@@ -13209,16 +14282,19 @@ class RootView {
13209
14282
  this.root.requestRender();
13210
14283
  return;
13211
14284
  }
14285
+ const rawPointerSelection = getMainPointerSelection(pane);
14286
+ const pointerSelection = rawPointerSelection !== undefined && rawPointerSelection.valid && rawPointerSelection.endUtf16 > rawPointerSelection.startUtf16 ? rawPointerSelection : undefined;
13212
14287
  const keyboardSelection = getMainDiffLineSelection(pane);
13213
14288
  const nativeRange = pane.text.getSelection();
13214
- let selection = mode === "hunk" || mode === "file" || keyboardSelection === undefined ? undefined : {
14289
+ let selection = mode === "hunk" || mode === "file" ? undefined : pointerSelection ?? (keyboardSelection === undefined ? undefined : {
13215
14290
  valid: true,
13216
14291
  startUtf16: keyboardSelection.startUtf16,
13217
14292
  endUtf16: keyboardSelection.endUtf16,
13218
14293
  active: true
13219
- };
13220
- if (selection === undefined && keyboardSelection === undefined && nativeRange)
14294
+ });
14295
+ if (selection === undefined && pointerSelection === undefined && keyboardSelection === undefined && nativeRange) {
13221
14296
  selection = selectionFromRenderable(document, nativeRange, pane.text.getSelectedText());
14297
+ }
13222
14298
  if (!selection && (mode === "hunk" || mode === "file")) {
13223
14299
  const target = getMainCursorTarget(pane);
13224
14300
  if (target) {
@@ -13538,6 +14614,9 @@ class RootView {
13538
14614
  bar.onMouseDown = undefined;
13539
14615
  bar.onMouseDrag = undefined;
13540
14616
  bar.onMouseUp = undefined;
14617
+ bar.slider.onMouseDown = undefined;
14618
+ bar.slider.onMouseDrag = undefined;
14619
+ bar.slider.onMouseUp = undefined;
13541
14620
  }
13542
14621
  }
13543
14622
  this.renderer.off("resize", this.handleResize);
@@ -13564,20 +14643,33 @@ class RootView {
13564
14643
  const bar = paneScrollbar(typedPane.text);
13565
14644
  if (!bar)
13566
14645
  continue;
13567
- bar.onMouseDown = (event) => {
14646
+ const beginScrollbarGesture = (event) => {
14647
+ this.updateHoveredListRow(event.x, event.y);
13568
14648
  this.pendingClick = undefined;
13569
14649
  this.lastSplitterPress = undefined;
13570
14650
  this.gestureOwner = { kind: "scrollbar", paneId: typedPane.id };
14651
+ this.scrollPaneByScrollbarPosition(typedPane.id, event.y);
13571
14652
  event.stopPropagation();
13572
14653
  };
13573
- bar.onMouseDrag = (event) => {
13574
- if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id)
14654
+ const continueScrollbarGesture = (event) => {
14655
+ if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id) {
14656
+ this.scrollPaneByScrollbarPosition(typedPane.id, event.y);
13575
14657
  event.stopPropagation();
14658
+ }
13576
14659
  };
13577
- bar.onMouseUp = (event) => {
13578
- if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id)
14660
+ const endScrollbarGesture = (event) => {
14661
+ if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id) {
14662
+ this.scrollPaneByScrollbarPosition(typedPane.id, event.y);
14663
+ this.gestureOwner = undefined;
13579
14664
  event.stopPropagation();
14665
+ }
13580
14666
  };
14667
+ bar.onMouseDown = beginScrollbarGesture;
14668
+ bar.onMouseDrag = continueScrollbarGesture;
14669
+ bar.onMouseUp = endScrollbarGesture;
14670
+ bar.slider.onMouseDown = beginScrollbarGesture;
14671
+ bar.slider.onMouseDrag = continueScrollbarGesture;
14672
+ bar.slider.onMouseUp = endScrollbarGesture;
13581
14673
  }
13582
14674
  this.root.onMouse = (event) => {
13583
14675
  if (this.isBranchReviewActive?.())
@@ -13686,14 +14778,22 @@ class RootView {
13686
14778
  return;
13687
14779
  }
13688
14780
  if (owner.kind === "main-selection") {
14781
+ const virtual = virtualMainPaneFor(this.panes.main);
13689
14782
  if (event.type === "drag") {
13690
14783
  this.pendingClick = undefined;
13691
14784
  this.lastSplitterPress = undefined;
14785
+ if (virtual?.isActive())
14786
+ this.updateVirtualMainPointer(event);
14787
+ event.preventDefault();
13692
14788
  event.stopPropagation();
13693
14789
  return;
13694
14790
  }
13695
- if (event.type === "up") {
14791
+ if (event.type === "up" || event.type === "cancel") {
14792
+ if (virtual?.isActive() && event.type === "up")
14793
+ this.updateVirtualMainPointer(event);
13696
14794
  this.gestureOwner = undefined;
14795
+ this.mainPointerAnchor = undefined;
14796
+ event.preventDefault();
13697
14797
  event.stopPropagation();
13698
14798
  return;
13699
14799
  }
@@ -13824,10 +14924,22 @@ class RootView {
13824
14924
  if (paneId === "main") {
13825
14925
  this.pendingClick = undefined;
13826
14926
  this.lastSplitterPress = undefined;
13827
- this.gestureOwner = { kind: "main-selection" };
13828
14927
  if (this.focusManager.active !== "main")
13829
14928
  this.focusManager.focus("main");
14929
+ const virtual = virtualMainPaneFor(this.panes.main);
14930
+ const canSelect = event.button === 0 && !event.modifiers.ctrl;
14931
+ const point = virtual?.isActive() && canSelect ? this.mainPointerCoordinates(event) : undefined;
14932
+ this.mainPointerAnchor = point;
14933
+ if (virtual?.isActive()) {
14934
+ if (point === undefined)
14935
+ virtual.resetSelection();
14936
+ else
14937
+ virtual.setPointerSelection(point.row, point.column, point.row, point.column);
14938
+ }
14939
+ this.gestureOwner = { kind: "main-selection" };
13830
14940
  this.clearTransientMenus();
14941
+ if (virtual?.isActive() && canSelect)
14942
+ event.preventDefault();
13831
14943
  event.stopPropagation();
13832
14944
  return;
13833
14945
  }
@@ -14096,16 +15208,16 @@ class RootView {
14096
15208
  this.syncPaneBorders();
14097
15209
  const windows = this.geometry.windows;
14098
15210
  const place = (renderable, name) => {
14099
- const dimensions = windows[name];
14100
- if (dimensions === undefined) {
15211
+ const dimensions2 = windows[name];
15212
+ if (dimensions2 === undefined) {
14101
15213
  renderable.visible = false;
14102
15214
  return;
14103
15215
  }
14104
- renderable.left = dimensions.x0;
14105
- renderable.top = dimensions.y0;
14106
- renderable.width = Math.max(1, widthOf(dimensions));
14107
- renderable.height = Math.max(1, heightOf(dimensions));
14108
- renderable.visible = widthOf(dimensions) > 0 && heightOf(dimensions) > 0;
15216
+ renderable.left = dimensions2.x0;
15217
+ renderable.top = dimensions2.y0;
15218
+ renderable.width = Math.max(1, widthOf(dimensions2));
15219
+ renderable.height = Math.max(1, heightOf(dimensions2));
15220
+ renderable.visible = widthOf(dimensions2) > 0 && heightOf(dimensions2) > 0;
14109
15221
  };
14110
15222
  for (const name of SIDE_WINDOWS)
14111
15223
  place(this.panes[name].box, name);
@@ -14306,7 +15418,7 @@ async function loadRefsSnapshot(runner) {
14306
15418
  }
14307
15419
 
14308
15420
  // src/git/editor.ts
14309
- import { join as join4, basename } from "node:path";
15421
+ import { join as join5, basename } from "node:path";
14310
15422
  function standardTerminalPreset(editor) {
14311
15423
  return {
14312
15424
  edit: `${editor} -- {{filename}}`,
@@ -14444,7 +15556,7 @@ async function resolveEditCommand(files, options = {}) {
14444
15556
  function absolutePath(repoRoot, relativePath) {
14445
15557
  if (relativePath.startsWith("/"))
14446
15558
  return relativePath;
14447
- return join4(repoRoot, relativePath);
15559
+ return join5(repoRoot, relativePath);
14448
15560
  }
14449
15561
 
14450
15562
  // src/app/create-app.ts
@@ -14452,7 +15564,7 @@ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
14452
15564
 
14453
15565
  // src/app/index-watcher.ts
14454
15566
  import { watch, statSync } from "node:fs";
14455
- import { basename as basename2, dirname as dirname3 } from "node:path";
15567
+ import { basename as basename2, dirname as dirname4 } from "node:path";
14456
15568
  var DEFAULT_INDEX_EVENT_DEBOUNCE_MS = 50;
14457
15569
  var BUSY_RETRY_MS = 50;
14458
15570
  function fingerprint(path) {
@@ -14493,7 +15605,7 @@ class IndexWatcher {
14493
15605
  this.stopped = false;
14494
15606
  this.baseline = fingerprint(this.options.indexPath);
14495
15607
  try {
14496
- const watcher = watch(dirname3(this.options.indexPath), { persistent: false }, (_eventType, filename) => {
15608
+ const watcher = watch(dirname4(this.options.indexPath), { persistent: false }, (_eventType, filename) => {
14497
15609
  if (this.stopped)
14498
15610
  return;
14499
15611
  const name = eventFileName(filename);
@@ -14705,7 +15817,7 @@ function isWorkerAvailable() {
14705
15817
  }
14706
15818
 
14707
15819
  // src/ui/review-workspace/ReviewWorkspaceApp.tsx
14708
- import { StyledText as StyledText8, parseColor as parseColor2 } from "@opentui/core";
15820
+ import { StyledText as StyledText8, parseColor as parseColor3 } from "@opentui/core";
14709
15821
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
14710
15822
  import { useCallback, useEffect as useEffect3, useLayoutEffect as useLayoutEffect2, useMemo as useMemo4, useRef as useRef2, useState as useState3, useSyncExternalStore } from "react";
14711
15823
 
@@ -15159,7 +16271,7 @@ function reviewHelp(focus, state) {
15159
16271
  }
15160
16272
 
15161
16273
  // src/ui/review-workspace/review-sidebar.ts
15162
- import { basename as basename3, dirname as dirname4 } from "node:path/posix";
16274
+ import { basename as basename3, dirname as dirname5 } from "node:path/posix";
15163
16275
  function normalizeDiffPath(p) {
15164
16276
  return p?.replace(/[\r\n]+$/u, "");
15165
16277
  }
@@ -15245,7 +16357,7 @@ function buildReviewSidebarEntries(state) {
15245
16357
  let activeGroup;
15246
16358
  visible.forEach((file, index) => {
15247
16359
  const path = formatTerminalPath(normalizeDiffPath(file.path) ?? file.path);
15248
- const group = dirname4(path);
16360
+ const group = dirname5(path);
15249
16361
  if (group !== activeGroup) {
15250
16362
  activeGroup = group;
15251
16363
  entries.push({
@@ -15731,7 +16843,7 @@ function buildHunkStackRows(file, state, highlight, options) {
15731
16843
  return rows;
15732
16844
  }
15733
16845
  // src/ui/review-workspace/components/ReviewDiffRow.tsx
15734
- import { StyledText as StyledText7, parseColor } from "@opentui/core";
16846
+ import { StyledText as StyledText7, parseColor as parseColor2 } from "@opentui/core";
15735
16847
 
15736
16848
  // src/ui/review-workspace/hunk-code-columns.ts
15737
16849
  var HUNK_DIFF_RAIL_WIDTH = 1;
@@ -15804,7 +16916,7 @@ function color(value) {
15804
16916
  const cached = colorCache2.get(value);
15805
16917
  if (cached)
15806
16918
  return cached;
15807
- const parsed = parseColor(value);
16919
+ const parsed = parseColor2(value);
15808
16920
  colorCache2.set(value, parsed);
15809
16921
  return parsed;
15810
16922
  }
@@ -17916,7 +19028,7 @@ var colorCache3 = new Map;
17916
19028
  function textChunk(text, style) {
17917
19029
  let fg5 = colorCache3.get(COLORS2[style]);
17918
19030
  if (!fg5) {
17919
- fg5 = parseColor2(COLORS2[style]);
19031
+ fg5 = parseColor3(COLORS2[style]);
17920
19032
  colorCache3.set(COLORS2[style], fg5);
17921
19033
  }
17922
19034
  return { __isChunk: true, text, fg: fg5 };
@@ -18154,14 +19266,14 @@ function ReviewWorkspaceApp({ session }) {
18154
19266
  const finishSummaryRef = useRef2(null);
18155
19267
  const finishSubmitRef = useRef2(false);
18156
19268
  const expandedSourceRef = useRef2(new Map);
18157
- const dimensions = { width: Math.max(1, terminal.width), height: Math.max(1, terminal.height) };
18158
- const maxSidebarWidth = Math.max(REVIEW_SIDEBAR_MIN_WIDTH, dimensions.width - REVIEW_RESIZE_BAR_WIDTH - REVIEW_DIFF_BORDER_WIDTH - REVIEW_DIFF_MIN_CONTENT_WIDTH);
18159
- const sidebarWidth = dimensions.width >= REVIEW_SIDEBAR_VISIBILITY_WIDTH ? Math.min(Math.max(sidebarWidthPreference, REVIEW_SIDEBAR_MIN_WIDTH), maxSidebarWidth) : 0;
18160
- const diffWidth = Math.max(1, dimensions.width - sidebarWidth - (sidebarWidth > 0 ? REVIEW_RESIZE_BAR_WIDTH : 0) - REVIEW_DIFF_BORDER_WIDTH);
19269
+ const dimensions2 = { width: Math.max(1, terminal.width), height: Math.max(1, terminal.height) };
19270
+ const maxSidebarWidth = Math.max(REVIEW_SIDEBAR_MIN_WIDTH, dimensions2.width - REVIEW_RESIZE_BAR_WIDTH - REVIEW_DIFF_BORDER_WIDTH - REVIEW_DIFF_MIN_CONTENT_WIDTH);
19271
+ const sidebarWidth = dimensions2.width >= REVIEW_SIDEBAR_VISIBILITY_WIDTH ? Math.min(Math.max(sidebarWidthPreference, REVIEW_SIDEBAR_MIN_WIDTH), maxSidebarWidth) : 0;
19272
+ const diffWidth = Math.max(1, dimensions2.width - sidebarWidth - (sidebarWidth > 0 ? REVIEW_RESIZE_BAR_WIDTH : 0) - REVIEW_DIFF_BORDER_WIDTH);
18161
19273
  const layout = layoutMode === "auto" ? diffWidth >= 64 ? "split" : "stack" : layoutMode;
18162
19274
  const composerHeight = state?.draft ? canShowReplacementDraft(state) ? 9 : 6 : 0;
18163
- const diffHeight = Math.max(1, dimensions.height - 4 - composerHeight - 2);
18164
- const resizeBarHeight = Math.max(1, dimensions.height - 4 - composerHeight);
19275
+ const diffHeight = Math.max(1, dimensions2.height - 4 - composerHeight - 2);
19276
+ const resizeBarHeight = Math.max(1, dimensions2.height - 4 - composerHeight);
18165
19277
  const sidebarFocused = focus === "sidebar" || focus === "filter";
18166
19278
  const diffFocused = focus === "stream";
18167
19279
  const files = useMemo4(() => state ? toHunkReviewFiles(visibleReviewFiles(state)) : [], [state?.document, state?.feedback, state?.filter, state?.viewed]);
@@ -18953,7 +20065,7 @@ function ReviewWorkspaceApp({ session }) {
18953
20065
  id: "react-review-header",
18954
20066
  style: { width: "100%", height: 3, flexShrink: 0 },
18955
20067
  children: /* @__PURE__ */ jsx4("text", {
18956
- content: headerText(state, dimensions.width, controller.error),
20068
+ content: headerText(state, dimensions2.width, controller.error),
18957
20069
  wrapMode: "none",
18958
20070
  truncate: true
18959
20071
  })
@@ -19180,7 +20292,7 @@ function ReviewWorkspaceApp({ session }) {
19180
20292
  }),
19181
20293
  orphanedFeedback.length > 0 ? /* @__PURE__ */ jsx4("box", {
19182
20294
  id: "review-orphaned-feedback",
19183
- style: { position: "absolute", left: 1, bottom: 1, width: Math.max(20, dimensions.width - 2), height: Math.min(4, orphanedFeedback.length), zIndex: 50, border: true, flexDirection: "column", backgroundColor: "#202020" },
20295
+ style: { position: "absolute", left: 1, bottom: 1, width: Math.max(20, dimensions2.width - 2), height: Math.min(4, orphanedFeedback.length), zIndex: 50, border: true, flexDirection: "column", backgroundColor: "#202020" },
19184
20296
  children: orphanedFeedback.slice(0, 4).map((feedback) => /* @__PURE__ */ jsxs4("box", {
19185
20297
  style: { width: "100%", height: 1, flexDirection: "row" },
19186
20298
  onMouseUp: () => {
@@ -19225,7 +20337,7 @@ function ReviewWorkspaceApp({ session }) {
19225
20337
  }) : null,
19226
20338
  feedbackMessage ? /* @__PURE__ */ jsx4("box", {
19227
20339
  id: "review-feedback-message",
19228
- style: { position: "absolute", left: 1, bottom: orphanedFeedback.length > 0 ? Math.min(5, orphanedFeedback.length + 1) : 1, width: Math.max(20, dimensions.width - 2), height: 1, zIndex: 55, backgroundColor: "#202020" },
20340
+ style: { position: "absolute", left: 1, bottom: orphanedFeedback.length > 0 ? Math.min(5, orphanedFeedback.length + 1) : 1, width: Math.max(20, dimensions2.width - 2), height: 1, zIndex: 55, backgroundColor: "#202020" },
19229
20341
  children: /* @__PURE__ */ jsx4("text", {
19230
20342
  content: feedbackMessage,
19231
20343
  wrapMode: "none",
@@ -19445,7 +20557,7 @@ function ReviewWorkspaceApp({ session }) {
19445
20557
  }) : null,
19446
20558
  helpOpen ? /* @__PURE__ */ jsx4("box", {
19447
20559
  id: "review-help-dialog",
19448
- style: { position: "absolute", left: Math.max(1, Math.floor(dimensions.width / 10)), top: 2, width: Math.max(50, Math.floor(dimensions.width * 4 / 5)), height: Math.min(26, Math.max(14, dimensions.height - 4)), zIndex: 70, border: true, flexDirection: "column", backgroundColor: "#202020" },
20560
+ style: { position: "absolute", left: Math.max(1, Math.floor(dimensions2.width / 10)), top: 2, width: Math.max(50, Math.floor(dimensions2.width * 4 / 5)), height: Math.min(26, Math.max(14, dimensions2.height - 4)), zIndex: 70, border: true, flexDirection: "column", backgroundColor: "#202020" },
19449
20561
  children: /* @__PURE__ */ jsx4("text", {
19450
20562
  content: `Review commands
19451
20563
  ${reviewHelp(focus, state)}
@@ -19456,7 +20568,7 @@ Esc close this help`,
19456
20568
  }) : null,
19457
20569
  finishDialog.isOpen() ? /* @__PURE__ */ jsxs4("box", {
19458
20570
  id: "review-finish-dialog",
19459
- style: { position: "absolute", left: Math.max(1, Math.floor(dimensions.width / 8)), top: 3, width: Math.max(40, Math.floor(dimensions.width * 3 / 4)), height: 10, zIndex: 60, border: true, flexDirection: "column", backgroundColor: "#202020" },
20571
+ style: { position: "absolute", left: Math.max(1, Math.floor(dimensions2.width / 8)), top: 3, width: Math.max(40, Math.floor(dimensions2.width * 3 / 4)), height: 10, zIndex: 60, border: true, flexDirection: "column", backgroundColor: "#202020" },
19460
20572
  children: [
19461
20573
  /* @__PURE__ */ jsx4("text", {
19462
20574
  content: `Finish review — ${finishDialog.getDecision()}`,
@@ -23030,6 +24142,7 @@ function createApp(options) {
23030
24142
  repositoryRoot: options.repositoryRoot,
23031
24143
  runner: options.runner,
23032
24144
  ...pullRequestLoader === undefined ? {} : { loadPullRequests: pullRequestLoader },
24145
+ ...options.loadCommits === undefined ? {} : { loadCommits: options.loadCommits },
23033
24146
  onPullRequestsChanged: (state) => {
23034
24147
  renderPullRequests?.(state);
23035
24148
  }
@@ -23309,6 +24422,12 @@ function createApp(options) {
23309
24422
  },
23310
24423
  loadCommitInspection: (oid) => controller.loadCommitInspection(oid),
23311
24424
  loadBranchCommits: options.loadBranchCommits ?? ((branch) => controller.loadBranchCommits(branch)),
24425
+ onExpandCommits: async () => {
24426
+ const expanded = await controller.expandCommits();
24427
+ if (expanded && (screenController?.shouldRenderRepository() ?? true))
24428
+ view.update(controller.state, { preserveFilterInput: true });
24429
+ return expanded;
24430
+ },
23312
24431
  loadCommitFileInspection: (oid, path) => controller.loadCommitFileInspection(oid, path),
23313
24432
  loadTagInspection: (tag) => controller.loadTagInspection(tag),
23314
24433
  loadRefLogInspection: (target) => controller.loadRefLogInspection(target),
@@ -23675,8 +24794,9 @@ function createApp(options) {
23675
24794
  function shouldQueryTerminalPalette(env = process.env, capabilities) {
23676
24795
  return env.ZELLIJ === undefined && env.ZELLIJ_SESSION_NAME === undefined && env.TERM_PROGRAM?.toLowerCase() !== "zellij" && capabilities?.multiplexer !== "zellij";
23677
24796
  }
23678
- async function startApp() {
23679
- const runner = new GitRunner;
24797
+ async function startApp(options = {}) {
24798
+ const startDirectory = options.startDirectory === undefined ? process.cwd() : resolve4(process.cwd(), options.startDirectory);
24799
+ const runner = new GitRunner(startDirectory);
23680
24800
  let repositoryRoot;
23681
24801
  try {
23682
24802
  repositoryRoot = (await runner.run(["rev-parse", "--show-toplevel"], { readOnly: true })).stdout.trim();
@@ -23721,4 +24841,17 @@ ${detail}
23721
24841
  if (false) {}
23722
24842
 
23723
24843
  // src/cli.ts
23724
- process.exitCode = await startApp();
24844
+ var result = parseCliArgs(process.argv.slice(2));
24845
+ if (result.kind === "help" || result.kind === "version") {
24846
+ process.stdout.write(result.text.endsWith(`
24847
+ `) ? result.text : `${result.text}
24848
+ `);
24849
+ process.exitCode = 0;
24850
+ } else if (result.kind === "error") {
24851
+ process.stderr.write(result.message.endsWith(`
24852
+ `) ? result.message : `${result.message}
24853
+ `);
24854
+ process.exitCode = result.exitCode;
24855
+ } else {
24856
+ process.exitCode = await startApp(result.startDirectory === undefined ? {} : { startDirectory: result.startDirectory });
24857
+ }