@xuhaojun/githunk 0.1.0 → 0.1.1

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 (2) hide show
  1. package/dist/githunk.js +1287 -311
  2. package/package.json +3 -3
package/dist/githunk.js CHANGED
@@ -4408,6 +4408,13 @@ function syncVerticalScrollbar(bar, text, viewportHeight) {
4408
4408
  bar.viewportSize = scrollbarViewportOverrides.get(bar) ?? Math.max(0, Math.floor(text.height));
4409
4409
  bar.scrollPosition = text.scrollY;
4410
4410
  }
4411
+ function clearScrollbarViewportOverride(text) {
4412
+ const bar = scrollbars.get(text);
4413
+ if (bar === undefined)
4414
+ return;
4415
+ scrollbarViewportOverrides.delete(bar);
4416
+ syncVerticalScrollbar(bar, text);
4417
+ }
4411
4418
  function createPane(renderer, id, title, content, selectable = false, options = {}) {
4412
4419
  const tabsConfig = options.tabs;
4413
4420
  const BoxClass = tabsConfig === undefined ? BoxRenderable2 : PaneTabsBoxRenderable;
@@ -4892,36 +4899,62 @@ function computeColumnLayout(rows, width) {
4892
4899
  const widths = indexes.map((j) => j === flexIndex ? flexWidth : rawWidths[j]);
4893
4900
  return { indexes, widths };
4894
4901
  }
4895
- function renderColumns(row, layout) {
4896
- const chunks = [];
4902
+ function layoutListRowSegments(row, layout) {
4903
+ const segments = [];
4897
4904
  for (let i = 0;i < layout.indexes.length; i++) {
4898
4905
  const column = row.columns[layout.indexes[i]];
4899
4906
  const cellWidth = layout.widths[i];
4900
4907
  const isLast = i === layout.indexes.length - 1;
4901
4908
  if (i > 0)
4902
- chunks.push(plainChunk2(" "));
4909
+ segments.push({ text: " " });
4903
4910
  const text = column?.text ?? "";
4904
4911
  const truncated = truncateToWidth(text, cellWidth);
4905
4912
  if (truncated.length > 0) {
4906
- if (column?.segments !== undefined && truncated === text) {
4913
+ if (column?.segments !== undefined) {
4914
+ let remaining = [...truncated].length;
4907
4915
  for (const segment of column.segments) {
4916
+ if (remaining <= 0)
4917
+ break;
4908
4918
  if (segment.text.length === 0)
4909
4919
  continue;
4910
- chunks.push(styleToChunk(segment.text, column.style, segment.color));
4920
+ const chars = [...segment.text];
4921
+ if (chars.length <= remaining) {
4922
+ segments.push({
4923
+ text: segment.text,
4924
+ ...column.style === undefined ? {} : { style: column.style },
4925
+ ...segment.color === undefined ? {} : { color: segment.color }
4926
+ });
4927
+ remaining -= chars.length;
4928
+ } else {
4929
+ segments.push({
4930
+ text: chars.slice(0, remaining).join(""),
4931
+ ...column.style === undefined ? {} : { style: column.style },
4932
+ ...segment.color === undefined ? {} : { color: segment.color }
4933
+ });
4934
+ remaining = 0;
4935
+ break;
4936
+ }
4911
4937
  }
4912
4938
  } else {
4913
- chunks.push(styleToChunk(truncated, column?.style, column?.color));
4939
+ segments.push({
4940
+ text: truncated,
4941
+ ...column?.style === undefined ? {} : { style: column.style },
4942
+ ...column?.color === undefined ? {} : { color: column.color }
4943
+ });
4914
4944
  }
4915
4945
  }
4916
4946
  if (!isLast) {
4917
4947
  const pad = cellWidth - visualLength(truncated);
4918
4948
  if (pad > 0)
4919
- chunks.push(plainChunk2(" ".repeat(pad)));
4949
+ segments.push({ text: " ".repeat(pad) });
4920
4950
  }
4921
4951
  }
4922
- while (chunks.length > 0 && chunks[chunks.length - 1].text.trim().length === 0)
4923
- chunks.pop();
4924
- return chunks;
4952
+ while (segments.length > 0 && segments[segments.length - 1].text.trim().length === 0)
4953
+ segments.pop();
4954
+ return segments;
4955
+ }
4956
+ function renderColumns(row, layout) {
4957
+ return layoutListRowSegments(row, layout).map((segment) => styleToChunk(segment.text, segment.style, segment.color));
4925
4958
  }
4926
4959
  function highlightChunk(chunk, selectedBg) {
4927
4960
  const current = chunk.fg;
@@ -5017,6 +5050,393 @@ function renderListRows(state, focused, width, hoveredId) {
5017
5050
  return new StyledText2(allChunks);
5018
5051
  }
5019
5052
 
5053
+ // src/ui/panes/list-text.ts
5054
+ import { parseColor } from "@opentui/core";
5055
+
5056
+ // src/domain/diff/cell-width.ts
5057
+ var WIDE_RANGES = [
5058
+ [4352, 4447],
5059
+ [11904, 42191],
5060
+ [44032, 55203],
5061
+ [63744, 64255],
5062
+ [65072, 65135],
5063
+ [65280, 65376],
5064
+ [65504, 65510],
5065
+ [127744, 129535],
5066
+ [129648, 129791],
5067
+ [131072, 262141]
5068
+ ];
5069
+ function isWide(grapheme) {
5070
+ const code = grapheme.codePointAt(0);
5071
+ if (code === undefined)
5072
+ return false;
5073
+ for (const [low, high] of WIDE_RANGES) {
5074
+ if (code >= low && code <= high)
5075
+ return true;
5076
+ }
5077
+ return false;
5078
+ }
5079
+ function cellWidth(value) {
5080
+ let ascii = true;
5081
+ for (let index = 0;index < value.length; index++) {
5082
+ if (value.charCodeAt(index) > 127) {
5083
+ ascii = false;
5084
+ break;
5085
+ }
5086
+ }
5087
+ if (ascii)
5088
+ return value.length;
5089
+ let width = 0;
5090
+ for (const codePoint of value)
5091
+ width += isWide(codePoint) ? 2 : 1;
5092
+ return width;
5093
+ }
5094
+
5095
+ // src/ui/panes/pane-text.ts
5096
+ function internalsOf(text) {
5097
+ const candidate = text;
5098
+ const buffer = candidate.textBuffer;
5099
+ const style = candidate._textBufferSyntaxStyle;
5100
+ if (buffer === undefined || style === undefined)
5101
+ return;
5102
+ if (typeof buffer.setText !== "function" || typeof buffer.addHighlight !== "function" || typeof buffer.clearAllHighlights !== "function")
5103
+ return;
5104
+ if (typeof buffer.clearLineHighlights !== "function")
5105
+ return;
5106
+ if (typeof style.registerStyle !== "function" || typeof candidate.updateTextInfo !== "function")
5107
+ return;
5108
+ return candidate;
5109
+ }
5110
+ function paneTextBuffer(text) {
5111
+ const internals = internalsOf(text);
5112
+ if (internals === undefined)
5113
+ return;
5114
+ internals._hasManualStyledText = true;
5115
+ return {
5116
+ setText(value) {
5117
+ internals.textBuffer.setText(value);
5118
+ internals.updateTextInfo();
5119
+ },
5120
+ addHighlight(row, highlight) {
5121
+ internals.textBuffer.addHighlight(row, highlight);
5122
+ },
5123
+ clearRow(row) {
5124
+ internals.textBuffer.clearLineHighlights(row);
5125
+ },
5126
+ clearAllHighlights() {
5127
+ internals.textBuffer.clearAllHighlights();
5128
+ },
5129
+ registerStyle(name, definition) {
5130
+ return internals._textBufferSyntaxStyle.registerStyle(name, definition);
5131
+ },
5132
+ refresh() {
5133
+ internals.updateTextInfo();
5134
+ }
5135
+ };
5136
+ }
5137
+ function onPaneLifecyclePass(text, callback) {
5138
+ const host = text;
5139
+ const previous = host.onLifecyclePass;
5140
+ host.onLifecyclePass = () => {
5141
+ previous?.call(text);
5142
+ callback();
5143
+ };
5144
+ }
5145
+
5146
+ // src/ui/panes/viewport-highlights.ts
5147
+ var MARGIN_LINES = 32;
5148
+ var LINE_END_COLS = 1e6;
5149
+ function createViewportHighlights(text, spec) {
5150
+ const { buffer, paintLine, scrollY: logicalScrollY } = spec;
5151
+ let content;
5152
+ let installed = "";
5153
+ let active = false;
5154
+ let rowSources;
5155
+ let rowSourcesWidth = -1;
5156
+ let appliedScrollY = -1;
5157
+ let appliedHeight = -1;
5158
+ let painted;
5159
+ const paint = (force) => {
5160
+ if (!active)
5161
+ return;
5162
+ const height = Math.max(1, Math.floor(text.height));
5163
+ const scrollY = Math.max(0, Math.floor(logicalScrollY?.(content) ?? text.scrollY));
5164
+ const width = Math.max(1, Math.floor(text.width));
5165
+ if (!force && painted !== undefined && appliedScrollY === scrollY && appliedHeight === height && rowSourcesWidth === width)
5166
+ return;
5167
+ if (rowSources === undefined || rowSourcesWidth !== width) {
5168
+ rowSources = text.lineInfo.lineSources;
5169
+ rowSourcesWidth = width;
5170
+ }
5171
+ const sources = rowSources;
5172
+ const lastRow = Math.max(0, Math.min(scrollY + height - 1, sources.length - 1));
5173
+ const firstLine = sources[Math.min(scrollY, lastRow)] ?? scrollY;
5174
+ const lastLine = sources[lastRow] ?? lastRow;
5175
+ const from = Math.max(0, firstLine - MARGIN_LINES);
5176
+ const to = lastLine + MARGIN_LINES;
5177
+ const previous = painted;
5178
+ if (force || previous === undefined || previous.to < from || previous.from > to) {
5179
+ buffer.clearAllHighlights();
5180
+ for (let line = from;line <= to; line++)
5181
+ paintLine(line, content);
5182
+ } else {
5183
+ for (let line = previous.from;line < from; line++)
5184
+ buffer.clearRow(line);
5185
+ for (let line = to + 1;line <= previous.to; line++)
5186
+ buffer.clearRow(line);
5187
+ for (let line = from;line < previous.from; line++)
5188
+ paintLine(line, content);
5189
+ for (let line = previous.to + 1;line <= to; line++)
5190
+ paintLine(line, content);
5191
+ }
5192
+ painted = { from, to };
5193
+ appliedScrollY = scrollY;
5194
+ appliedHeight = height;
5195
+ };
5196
+ onPaneLifecyclePass(text, () => paint(false));
5197
+ return {
5198
+ install(full, next) {
5199
+ content = next;
5200
+ active = true;
5201
+ const changed = installed !== full;
5202
+ if (changed) {
5203
+ installed = full;
5204
+ buffer.setText(full);
5205
+ rowSources = undefined;
5206
+ painted = undefined;
5207
+ }
5208
+ paint(changed);
5209
+ },
5210
+ repaint() {
5211
+ paint(true);
5212
+ },
5213
+ release() {
5214
+ if (!active)
5215
+ return;
5216
+ buffer.clearAllHighlights();
5217
+ active = false;
5218
+ installed = "";
5219
+ rowSources = undefined;
5220
+ rowSourcesWidth = -1;
5221
+ appliedScrollY = -1;
5222
+ appliedHeight = -1;
5223
+ painted = undefined;
5224
+ }
5225
+ };
5226
+ }
5227
+
5228
+ // src/ui/panes/list-text.ts
5229
+ var painters = new WeakMap;
5230
+ function resolveSegment(text, style, color) {
5231
+ let fg3;
5232
+ let dim2;
5233
+ if (color !== undefined) {
5234
+ fg3 = parseColor(color);
5235
+ } else {
5236
+ switch (style) {
5237
+ case "dim":
5238
+ dim2 = true;
5239
+ break;
5240
+ case "cyan":
5241
+ fg3 = ANSI_CYAN;
5242
+ break;
5243
+ case "green":
5244
+ fg3 = ANSI_GREEN;
5245
+ break;
5246
+ case "yellow":
5247
+ fg3 = ANSI_YELLOW;
5248
+ break;
5249
+ case "magenta":
5250
+ fg3 = ANSI_MAGENTA;
5251
+ break;
5252
+ case "default":
5253
+ case undefined:
5254
+ break;
5255
+ }
5256
+ }
5257
+ return {
5258
+ text,
5259
+ ...fg3 === undefined ? {} : { fg: fg3 },
5260
+ ...dim2 === undefined ? {} : { dim: dim2 }
5261
+ };
5262
+ }
5263
+ function styleKey(fg3, bold3, dim2, bg2) {
5264
+ const fgKey = fg3 === undefined ? "" : fg3.toInts().join(",");
5265
+ const bgKey = bg2 === undefined ? "" : bg2.toInts().join(",");
5266
+ return `${fgKey}|${bold3 ? 1 : 0}|${dim2 ? 1 : 0}|${bgKey}`;
5267
+ }
5268
+ function styleIdFor(record, fg3, bold3, dim2, bg2) {
5269
+ const key = styleKey(fg3, bold3, dim2, bg2);
5270
+ const cached = record.styleIds.get(key);
5271
+ if (cached !== undefined)
5272
+ return cached;
5273
+ const id = record.buffer.registerStyle(`githunk.list.${record.styleIds.size}`, {
5274
+ ...fg3 === undefined ? {} : { fg: fg3 },
5275
+ ...bg2 === undefined ? {} : { bg: bg2 },
5276
+ ...bold3 ? { bold: bold3 } : {},
5277
+ ...dim2 ? { dim: dim2 } : {}
5278
+ });
5279
+ record.styleIds.set(key, id);
5280
+ return id;
5281
+ }
5282
+ function rowVisual(state, focused, hoveredId, rowIndexById, range, displayRow) {
5283
+ if (displayRow.kind !== "item")
5284
+ return 0;
5285
+ if (focused && displayRow.id === state.selectedId)
5286
+ return 1;
5287
+ if (range !== undefined) {
5288
+ const index = rowIndexById.get(displayRow.id);
5289
+ if (index !== undefined && index >= range.startIndex && index <= range.endIndex)
5290
+ return 2;
5291
+ }
5292
+ if (displayRow.id === hoveredId)
5293
+ return 3;
5294
+ return 0;
5295
+ }
5296
+ function layoutFor(state, safeWidth) {
5297
+ const displayRows = state.displayRows;
5298
+ const rowMap = new Map(state.rows.map((row) => [row.id, row]));
5299
+ const visibleRows = displayRows.flatMap((dr) => {
5300
+ if (dr.kind !== "item")
5301
+ return [];
5302
+ const row = rowMap.get(dr.id);
5303
+ return row === undefined ? [] : [row];
5304
+ });
5305
+ const layout = computeColumnLayout(visibleRows, safeWidth);
5306
+ const rowTexts = [];
5307
+ const rowSegments = [];
5308
+ for (const dr of displayRows) {
5309
+ if (dr.kind !== "item") {
5310
+ const truncated = [...dr.text].slice(0, safeWidth).join("");
5311
+ rowTexts.push(truncated);
5312
+ rowSegments.push(truncated.length === 0 ? [] : [{ text: truncated }]);
5313
+ continue;
5314
+ }
5315
+ const row = rowMap.get(dr.id);
5316
+ const laidOut = row === undefined ? [] : layoutListRowSegments(row, layout).map((segment) => resolveSegment(segment.text, segment.style, segment.color));
5317
+ const joined = laidOut.map((segment) => segment.text).join("");
5318
+ const pad = Math.max(0, safeWidth - cellWidth(joined));
5319
+ rowTexts.push(pad === 0 ? joined : `${joined}${" ".repeat(pad)}`);
5320
+ rowSegments.push(laidOut);
5321
+ }
5322
+ return { rows: state.rows, width: safeWidth, rowTexts, rowSegments, joined: rowTexts.join(`
5323
+ `) };
5324
+ }
5325
+ function visualsFor(state, focused, hoveredId) {
5326
+ const rowIndexById = new Map(state.rows.map((row, index) => [row.id, index]));
5327
+ const rangeActive = focused && isListRangeActive(state);
5328
+ const range = rangeActive ? getListSelectionRange(state) : undefined;
5329
+ return state.displayRows.map((dr) => rowVisual(state, focused, hoveredId, rowIndexById, range, dr));
5330
+ }
5331
+ function paintRow(record, line) {
5332
+ const snap = record.ref.snap;
5333
+ if (snap === undefined)
5334
+ return;
5335
+ const segments = snap.rowSegments[line];
5336
+ const visual = snap.visuals[line];
5337
+ if (segments === undefined || visual === undefined)
5338
+ return;
5339
+ const { buffer } = record;
5340
+ buffer.clearRow(line);
5341
+ const bg2 = visual === 1 || visual === 2 ? SELECTED_LINE_BG : visual === 3 ? HOVER_LINE_BG : undefined;
5342
+ let column = 0;
5343
+ for (const segment of segments) {
5344
+ const cells = cellWidth(segment.text);
5345
+ if (cells > 0) {
5346
+ if (visual === 1) {
5347
+ buffer.addHighlight(line, {
5348
+ start: column,
5349
+ end: column + cells,
5350
+ styleId: styleIdFor(record, segment.fg === undefined ? undefined : brightenAnsiForeground(segment.fg), true, segment.dim === true, bg2)
5351
+ });
5352
+ } else if (bg2 !== undefined) {
5353
+ buffer.addHighlight(line, {
5354
+ start: column,
5355
+ end: column + cells,
5356
+ styleId: styleIdFor(record, segment.fg, false, segment.dim === true, bg2)
5357
+ });
5358
+ } else if (segment.fg !== undefined || segment.dim === true) {
5359
+ buffer.addHighlight(line, {
5360
+ start: column,
5361
+ end: column + cells,
5362
+ styleId: styleIdFor(record, segment.fg, false, segment.dim === true, undefined)
5363
+ });
5364
+ }
5365
+ }
5366
+ column += cells;
5367
+ }
5368
+ if (bg2 !== undefined) {
5369
+ buffer.addHighlight(line, { start: column, end: LINE_END_COLS, styleId: styleIdFor(record, undefined, false, false, bg2) });
5370
+ }
5371
+ }
5372
+ function ensurePainter(text, buffer) {
5373
+ const existing = painters.get(text);
5374
+ if (existing !== undefined)
5375
+ return existing;
5376
+ const ref = { snap: undefined };
5377
+ const record = {
5378
+ buffer,
5379
+ viewport: undefined,
5380
+ ref,
5381
+ styleIds: new Map,
5382
+ cache: undefined,
5383
+ joined: "",
5384
+ visuals: []
5385
+ };
5386
+ record.viewport = createViewportHighlights(text, {
5387
+ buffer,
5388
+ paintLine: (line) => paintRow(record, line)
5389
+ });
5390
+ painters.set(text, record);
5391
+ return record;
5392
+ }
5393
+ function installListText(text, content) {
5394
+ const buffer = paneTextBuffer(text);
5395
+ if (buffer === undefined) {
5396
+ text.content = renderListRows(content.state, content.focused, content.width, content.hoveredId);
5397
+ return;
5398
+ }
5399
+ const record = ensurePainter(text, buffer);
5400
+ const safeWidth = Math.max(0, Math.floor(content.width));
5401
+ const previousCache = record.cache;
5402
+ const cache = previousCache !== undefined && previousCache.rows === content.state.rows && previousCache.width === safeWidth ? previousCache : layoutFor(content.state, safeWidth);
5403
+ record.cache = cache;
5404
+ const visuals = visualsFor(content.state, content.focused, content.hoveredId);
5405
+ record.ref.snap = { rowSegments: cache.rowSegments, visuals };
5406
+ if (cache.joined !== record.joined) {
5407
+ record.joined = cache.joined;
5408
+ record.visuals = visuals;
5409
+ record.viewport.install(cache.joined, record.ref);
5410
+ return;
5411
+ }
5412
+ if (cache !== previousCache) {
5413
+ record.visuals = visuals;
5414
+ record.viewport.repaint();
5415
+ record.buffer.refresh();
5416
+ return;
5417
+ }
5418
+ const previous = record.visuals;
5419
+ record.visuals = visuals;
5420
+ let repainted = false;
5421
+ const lines = Math.max(previous.length, visuals.length);
5422
+ for (let line = 0;line < lines; line++) {
5423
+ if (previous[line] !== visuals[line]) {
5424
+ paintRow(record, line);
5425
+ repainted = true;
5426
+ }
5427
+ }
5428
+ if (repainted)
5429
+ record.buffer.refresh();
5430
+ }
5431
+ function releaseListText(text) {
5432
+ const record = painters.get(text);
5433
+ if (record === undefined)
5434
+ return;
5435
+ record.viewport.release();
5436
+ record.buffer.refresh();
5437
+ painters.delete(text);
5438
+ }
5439
+
5020
5440
  // src/ui/panes/branches-pane.ts
5021
5441
  function localBranchRows(model, filter = "", options = {}) {
5022
5442
  const listing = model.branches;
@@ -5067,8 +5487,7 @@ function createBranchesPane(renderer, model) {
5067
5487
  const rows = localBranchRows(model);
5068
5488
  const displayRows = rows.length === 0 ? [{ kind: "message", text: "No branches" }] : undefined;
5069
5489
  const state = createListState(rows, displayRows);
5070
- const content = renderListRows(state, false, 80);
5071
- pane.update(content);
5490
+ installListText(pane.text, { state, width: 80, focused: false });
5072
5491
  pane.syncScrollbar();
5073
5492
  return pane;
5074
5493
  }
@@ -5389,47 +5808,6 @@ function commitGraphRows(commits, getColor) {
5389
5808
 
5390
5809
  // src/ui/author-style.ts
5391
5810
  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
5811
  var initialsCache = new Map;
5434
5812
  var colorCache = new Map;
5435
5813
  function randInt(bytes, max) {
@@ -5559,20 +5937,22 @@ function buildCommitRows(commits, now, filter = "") {
5559
5937
  const shortHash2 = commit.oid.length >= 8 ? commit.oid.slice(0, 8) : commit.shortOid;
5560
5938
  const initials = authorInitials(commit.authorName).padEnd(AUTHOR_COLUMN_WIDTH, " ");
5561
5939
  const relative = formatRelativeTime(commit.authoredAt, now);
5940
+ const graphText = graph?.text ?? "";
5941
+ const graphSegments = graph?.segments ?? [];
5942
+ const subjectSegments = commit.subject.length === 0 ? [] : [{ text: commit.subject }];
5562
5943
  return {
5563
5944
  id: commit.oid,
5564
5945
  columns: [
5565
5946
  { 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" }
5947
+ { text: initials, priority: 2, color: authorColor(commit.authorName) },
5948
+ { text: `${graphText}${commit.subject}`, priority: 2, flex: true, segments: [...graphSegments, ...subjectSegments] },
5949
+ { text: relative, priority: 0, style: "dim" }
5570
5950
  ]
5571
5951
  };
5572
5952
  });
5573
5953
  if (filter.length === 0)
5574
5954
  return rows;
5575
- return [...filterItems(filter, rows, (row) => `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? row.id}`)];
5955
+ return [...filterItems(filter, rows, (row) => `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? row.id}`)];
5576
5956
  }
5577
5957
  function createCommitsPane(renderer, model) {
5578
5958
  const pane = createPane(renderer, "commits", "", "No commit selected", false, {
@@ -5586,6 +5966,7 @@ function updateCommitsPane(pane, model) {
5586
5966
  if (commits.length === 0) {
5587
5967
  const empty = createListState([]);
5588
5968
  paneStates.set(pane, empty);
5969
+ releaseListText(pane.text);
5589
5970
  pane.update(model.loading ? "Loading…" : "No commits");
5590
5971
  return;
5591
5972
  }
@@ -5599,8 +5980,7 @@ function updateCommitsPane(pane, model) {
5599
5980
  state = withPrev;
5600
5981
  }
5601
5982
  paneStates.set(pane, state);
5602
- const content = renderListRows(state, false, 80);
5603
- pane.update(content);
5983
+ installListText(pane.text, { state, width: 80, focused: false });
5604
5984
  pane.box.bottomTitle = undefined;
5605
5985
  }
5606
5986
 
@@ -5736,183 +6116,56 @@ function parseAnsi(input) {
5736
6116
  let runStart = 0;
5737
6117
  let runStyle = { ...DEFAULT_STYLE };
5738
6118
  const closeRun = () => {
5739
- if (column > runStart && styled(runStyle)) {
5740
- spans.push({
5741
- row,
5742
- start: runStart,
5743
- end: column,
5744
- ...runStyle.fg === undefined ? {} : { fg: runStyle.fg },
5745
- ...runStyle.bold ? { bold: true } : {},
5746
- ...runStyle.dim ? { dim: true } : {}
5747
- });
5748
- }
5749
- runStart = column;
5750
- };
5751
- for (let index = 0;index < input.length; ) {
5752
- const char = input[index];
5753
- if (char === "\x1B") {
5754
- const length = escapeLength(input, index);
5755
- if (input[index + 1] === "[" && input[index + length - 1] === "m") {
5756
- const next = applySgr(style, parseParams(input.slice(index + 2, index + length - 1)));
5757
- if (!sameStyle(next, runStyle)) {
5758
- closeRun();
5759
- runStyle = next;
5760
- }
5761
- style = next;
5762
- }
5763
- index += length;
5764
- continue;
5765
- }
5766
- if (char === `
5767
- `) {
5768
- closeRun();
5769
- out.push(char);
5770
- row++;
5771
- 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;
6119
+ if (column > runStart && styled(runStyle)) {
6120
+ spans.push({
6121
+ row,
6122
+ start: runStart,
6123
+ end: column,
6124
+ ...runStyle.fg === undefined ? {} : { fg: runStyle.fg },
6125
+ ...runStyle.bold ? { bold: true } : {},
6126
+ ...runStyle.dim ? { dim: true } : {}
6127
+ });
5912
6128
  }
6129
+ runStart = column;
5913
6130
  };
6131
+ for (let index = 0;index < input.length; ) {
6132
+ const char = input[index];
6133
+ if (char === "\x1B") {
6134
+ const length = escapeLength(input, index);
6135
+ if (input[index + 1] === "[" && input[index + length - 1] === "m") {
6136
+ const next = applySgr(style, parseParams(input.slice(index + 2, index + length - 1)));
6137
+ if (!sameStyle(next, runStyle)) {
6138
+ closeRun();
6139
+ runStyle = next;
6140
+ }
6141
+ style = next;
6142
+ }
6143
+ index += length;
6144
+ continue;
6145
+ }
6146
+ if (char === `
6147
+ `) {
6148
+ closeRun();
6149
+ out.push(char);
6150
+ row++;
6151
+ column = 0;
6152
+ runStart = 0;
6153
+ index += 1;
6154
+ continue;
6155
+ }
6156
+ const codePoint = input.codePointAt(index);
6157
+ const size = codePoint > 65535 ? 2 : 1;
6158
+ out.push(input.slice(index, index + size));
6159
+ column += 1;
6160
+ index += size;
6161
+ }
6162
+ closeRun();
6163
+ return { text: out.join(""), spans };
5914
6164
  }
5915
6165
 
6166
+ // src/ui/panes/command-log-pane.ts
6167
+ import { BoxRenderable as BoxRenderable3, TextRenderable as TextRenderable2 } from "@opentui/core";
6168
+
5916
6169
  // src/ui/panes/command-log-text.ts
5917
6170
  var STYLE_DEFINITIONS = {
5918
6171
  action: { fg: ANSI_YELLOW },
@@ -5947,7 +6200,7 @@ function registerStyles(buffer) {
5947
6200
  }
5948
6201
  return ids;
5949
6202
  }
5950
- var painters = new WeakMap;
6203
+ var painters2 = new WeakMap;
5951
6204
  function installCommandLogText(text, lines) {
5952
6205
  const full = lines.map((line) => line.spans.map((span) => span.text).join("")).join(`
5953
6206
  `);
@@ -5956,7 +6209,7 @@ function installCommandLogText(text, lines) {
5956
6209
  text.content = full;
5957
6210
  return;
5958
6211
  }
5959
- let painter = painters.get(text);
6212
+ let painter = painters2.get(text);
5960
6213
  if (painter === undefined) {
5961
6214
  const styleIds = registerStyles(buffer);
5962
6215
  painter = createViewportHighlights(text, {
@@ -5970,7 +6223,7 @@ function installCommandLogText(text, lines) {
5970
6223
  }
5971
6224
  }
5972
6225
  });
5973
- painters.set(text, painter);
6226
+ painters2.set(text, painter);
5974
6227
  }
5975
6228
  painter.install(full, lines);
5976
6229
  }
@@ -6452,7 +6705,7 @@ function createFilesPane(renderer, model) {
6452
6705
  const initialRows = filesTreeRows(createFilesTreeState(model), model);
6453
6706
  const displayRows = initialRows.length === 0 ? [{ kind: "message", text: NO_CHANGED_FILES }] : undefined;
6454
6707
  const state = createListState(initialRows, displayRows);
6455
- pane.update(renderListRows(state, false, 80));
6708
+ installListText(pane.text, { state, width: 80, focused: false });
6456
6709
  return pane;
6457
6710
  }
6458
6711
  function anyStagedChanges(model) {
@@ -6737,14 +6990,14 @@ function changedIndexesInDiffLineRange(document, state) {
6737
6990
 
6738
6991
  // src/ui/panes/ansi-text.ts
6739
6992
  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) {
6993
+ var painters3 = new WeakMap;
6994
+ function styleKey2(span) {
6742
6995
  const color = span.fg;
6743
6996
  const colorKey = color === undefined ? "-" : `${color.intent}:${color.slot}:${color.toInts().join(",")}`;
6744
6997
  return `${colorKey}|${span.bold === true ? "b" : "-"}${span.dim === true ? "d" : "-"}`;
6745
6998
  }
6746
- function styleIdFor(buffer, styleIds, span) {
6747
- const key = styleKey(span);
6999
+ function styleIdFor2(buffer, styleIds, span) {
7000
+ const key = styleKey2(span);
6748
7001
  const existing = styleIds.get(key);
6749
7002
  if (existing !== undefined)
6750
7003
  return existing;
@@ -6826,7 +7079,7 @@ function installAnsiText(text, content) {
6826
7079
  }
6827
7080
  const { text: full, firstBodyRow } = joined(content);
6828
7081
  const spansByRow = groupByRow(content.spans, firstBodyRow);
6829
- let painter = painters2.get(text);
7082
+ let painter = painters3.get(text);
6830
7083
  if (painter === undefined) {
6831
7084
  const styleIds = new Map;
6832
7085
  painter = {
@@ -6838,17 +7091,17 @@ function installAnsiText(text, content) {
6838
7091
  if (spans === undefined)
6839
7092
  return;
6840
7093
  for (const span of spans) {
6841
- buffer.addHighlight(row, { start: span.start, end: span.end, styleId: styleIdFor(buffer, styleIds, span) });
7094
+ buffer.addHighlight(row, { start: span.start, end: span.end, styleId: styleIdFor2(buffer, styleIds, span) });
6842
7095
  }
6843
7096
  }
6844
7097
  })
6845
7098
  };
6846
- painters2.set(text, painter);
7099
+ painters3.set(text, painter);
6847
7100
  }
6848
7101
  painter.highlights.install(full, spansByRow);
6849
7102
  }
6850
7103
  function releaseAnsiText(text) {
6851
- painters2.get(text)?.highlights.release();
7104
+ painters3.get(text)?.highlights.release();
6852
7105
  }
6853
7106
 
6854
7107
  // src/ui/panes/diff-text.ts
@@ -6937,7 +7190,7 @@ function statSpansForRow(value, spans) {
6937
7190
  function preambleSpansForRow(value, spans) {
6938
7191
  return spans === undefined ? [plainChunk3(value)] : statSpansForRow(value, spans);
6939
7192
  }
6940
- var painters3 = new WeakMap;
7193
+ var painters4 = new WeakMap;
6941
7194
  function registerStyles2(buffer) {
6942
7195
  const ids = {};
6943
7196
  for (const [name, definition] of Object.entries(STYLE_DEFINITIONS2)) {
@@ -6975,7 +7228,7 @@ function styledChunk(style, value) {
6975
7228
  }
6976
7229
  function paintAsChunks2(text, content) {
6977
7230
  const { text: full, firstDiffRow } = joined2(content);
6978
- const preambleSpans = statSpansForPreamble(content.preamble);
7231
+ const preambleSpans = content.preambleSpans ?? statSpansForPreamble(content.preamble);
6979
7232
  const rows = full.split(`
6980
7233
  `);
6981
7234
  const chunks = [];
@@ -7008,13 +7261,15 @@ function installDiffText(text, content) {
7008
7261
  const paint = {
7009
7262
  displayLines: content.displayLines,
7010
7263
  firstDiffRow,
7011
- preambleSpans: statSpansForPreamble(content.preamble)
7264
+ preambleSpans: content.preambleSpans ?? statSpansForPreamble(content.preamble),
7265
+ ...content.highlightScrollY === undefined ? {} : { highlightScrollY: content.highlightScrollY }
7012
7266
  };
7013
- let painter = painters3.get(text);
7267
+ let painter = painters4.get(text);
7014
7268
  if (painter === undefined) {
7015
7269
  const styleIds = registerStyles2(buffer);
7016
7270
  painter = createViewportHighlights(text, {
7017
7271
  buffer,
7272
+ scrollY: (current) => current.highlightScrollY?.() ?? text.scrollY,
7018
7273
  paintLine: (row, current) => {
7019
7274
  const preambleSpans = current.preambleSpans.get(row);
7020
7275
  if (preambleSpans !== undefined) {
@@ -7031,12 +7286,508 @@ function installDiffText(text, content) {
7031
7286
  buffer.addHighlight(row, { start: display.gutterCols, end: LINE_END_COLS, styleId: styleIds[display.style] });
7032
7287
  }
7033
7288
  });
7034
- painters3.set(text, painter);
7289
+ painters4.set(text, painter);
7035
7290
  }
7036
7291
  painter.install(full, paint);
7037
7292
  }
7038
7293
  function releaseDiffText(text) {
7039
- painters3.get(text)?.release();
7294
+ painters4.get(text)?.release();
7295
+ }
7296
+
7297
+ // src/domain/diff/virtual.ts
7298
+ var VIRTUAL_DIFF_LINE_THRESHOLD = 1e4;
7299
+ function isSourceLine(line) {
7300
+ return line.kind === "context" || line.kind === "addition" || line.kind === "deletion";
7301
+ }
7302
+ function styleFor2(line) {
7303
+ if (line.kind === "addition")
7304
+ return "addition";
7305
+ if (line.kind === "deletion")
7306
+ return "deletion";
7307
+ if (line.kind === "hunk-header")
7308
+ return "hunk-header";
7309
+ if (line.kind === "metadata" || line.kind === "no-newline")
7310
+ return "metadata";
7311
+ return "plain";
7312
+ }
7313
+ function lineNumberWidth2(document) {
7314
+ let largest = 1;
7315
+ for (const line of document.lines) {
7316
+ largest = Math.max(largest, line.oldLine ?? 0, line.newLine ?? 0);
7317
+ }
7318
+ return String(largest).length;
7319
+ }
7320
+ function linePrefix(line, width) {
7321
+ if (!isSourceLine(line))
7322
+ return "";
7323
+ const old = line.oldLine === undefined ? "" : String(line.oldLine);
7324
+ const next = line.newLine === undefined ? "" : String(line.newLine);
7325
+ return `${old.padStart(width, " ")} ${next.padStart(width, " ")} `;
7326
+ }
7327
+ function withoutLineEnding2(raw) {
7328
+ if (raw.endsWith(`\r
7329
+ `))
7330
+ return raw.slice(0, -2);
7331
+ if (raw.endsWith(`
7332
+ `) || raw.endsWith("\r"))
7333
+ return raw.slice(0, -1);
7334
+ return raw;
7335
+ }
7336
+ function normalizePreamble(preamble) {
7337
+ if (preamble.length === 0)
7338
+ return { text: "", rows: [], starts: [], ends: [] };
7339
+ const text = preamble.endsWith(`
7340
+ `) ? preamble : `${preamble}
7341
+ `;
7342
+ const rows = text.slice(0, -1).split(`
7343
+ `).map(withoutLineEnding2);
7344
+ const starts = [];
7345
+ const ends = [];
7346
+ let start = 0;
7347
+ for (let row = 0;row < rows.length; row += 1) {
7348
+ const newline = text.indexOf(`
7349
+ `, start);
7350
+ starts.push(start);
7351
+ ends.push(newline < 0 ? text.length : newline + 1);
7352
+ start = newline < 0 ? text.length : newline + 1;
7353
+ }
7354
+ return { text, rows, starts, ends };
7355
+ }
7356
+ function boundedIndex(value, length) {
7357
+ if (!Number.isFinite(value))
7358
+ return value < 0 ? 0 : length;
7359
+ return Math.min(length, Math.max(0, Math.floor(value)));
7360
+ }
7361
+ function createVirtualDiffLayout(document, preamble) {
7362
+ const normalized = normalizePreamble(preamble);
7363
+ const width = lineNumberWidth2(document);
7364
+ const prefixes = document.lines.map((line) => linePrefix(line, width));
7365
+ const displayStarts = new Array(document.lines.length);
7366
+ let displayLength = normalized.text.length;
7367
+ let contentWidth = 0;
7368
+ for (const row of normalized.rows)
7369
+ contentWidth = Math.max(contentWidth, cellWidth(row));
7370
+ for (let index = 0;index < document.lines.length; index += 1) {
7371
+ const line = document.lines[index];
7372
+ const prefix = prefixes[index];
7373
+ const text = `${prefix}${withoutLineEnding2(line.raw)}`;
7374
+ displayStarts[index] = displayLength;
7375
+ displayLength += prefix.length + line.raw.length;
7376
+ contentWidth = Math.max(contentWidth, cellWidth(text));
7377
+ }
7378
+ const totalRows = normalized.rows.length + document.lines.length;
7379
+ const bodyRow = (lineIndex) => {
7380
+ const line = document.lines[lineIndex];
7381
+ const prefix = prefixes[lineIndex];
7382
+ return {
7383
+ text: `${prefix}${withoutLineEnding2(line.raw)}`,
7384
+ gutterCols: prefix.length,
7385
+ style: styleFor2(line),
7386
+ lineIndex,
7387
+ rawStartUtf16: line.startUtf16,
7388
+ rawEndUtf16: line.endUtf16,
7389
+ displayStartUtf16: displayStarts[lineIndex],
7390
+ displayEndUtf16: displayStarts[lineIndex] + prefix.length + line.raw.length
7391
+ };
7392
+ };
7393
+ const rowAt = (row) => {
7394
+ if (!Number.isSafeInteger(row) || row < 0 || row >= totalRows)
7395
+ return;
7396
+ if (row < normalized.rows.length) {
7397
+ const text = normalized.rows[row];
7398
+ return {
7399
+ text,
7400
+ gutterCols: 0,
7401
+ style: "plain",
7402
+ displayStartUtf16: normalized.starts[row],
7403
+ displayEndUtf16: normalized.ends[row]
7404
+ };
7405
+ }
7406
+ return bodyRow(row - normalized.rows.length);
7407
+ };
7408
+ const window = (scrollTop, viewportHeight, overscan) => {
7409
+ const viewport = Math.max(0, Math.floor(Number.isFinite(viewportHeight) ? viewportHeight : 0));
7410
+ if (totalRows === 0 || viewport === 0)
7411
+ return [0, -1];
7412
+ const margin = Math.max(0, Math.floor(Number.isFinite(overscan) ? overscan : 0));
7413
+ const maxScroll = Math.max(0, totalRows - viewport);
7414
+ const top = Math.min(maxScroll, Math.max(0, Math.floor(Number.isFinite(scrollTop) ? scrollTop : 0)));
7415
+ return [Math.max(0, top - margin), Math.min(totalRows - 1, top + viewport - 1 + margin)];
7416
+ };
7417
+ const displayOffsetsForLines = (startIndex, endIndex) => {
7418
+ let start = boundedIndex(startIndex, document.lines.length);
7419
+ let end = boundedIndex(endIndex, document.lines.length);
7420
+ if (end < start)
7421
+ [start, end] = [end, start];
7422
+ const rawStartUtf16 = start < document.lines.length ? document.lines[start].startUtf16 : document.text.length;
7423
+ const rawEndUtf16 = end < document.lines.length ? document.lines[end].startUtf16 : document.text.length;
7424
+ const displayStartUtf16 = start < document.lines.length ? displayStarts[start] : displayLength;
7425
+ const displayEndUtf16 = end < document.lines.length ? displayStarts[end] : displayLength;
7426
+ return { rawStartUtf16, rawEndUtf16, displayStartUtf16, displayEndUtf16 };
7427
+ };
7428
+ const rawOffsetAt = (row, column) => {
7429
+ const value = rowAt(row);
7430
+ if (value?.lineIndex === undefined || value.rawStartUtf16 === undefined || value.rawEndUtf16 === undefined)
7431
+ return;
7432
+ const line = document.lines[value.lineIndex];
7433
+ const target = Math.max(0, Math.floor(Number.isFinite(column) ? column : 0));
7434
+ if (target <= value.gutterCols)
7435
+ return line.startUtf16;
7436
+ const body = withoutLineEnding2(line.raw);
7437
+ const bodyColumn = target - value.gutterCols;
7438
+ let cells = 0;
7439
+ let utf16 = 0;
7440
+ for (const codePoint of body) {
7441
+ const widthInCells = cellWidth(codePoint);
7442
+ if (bodyColumn <= cells)
7443
+ return line.startUtf16 + utf16;
7444
+ utf16 += codePoint.length;
7445
+ cells += widthInCells;
7446
+ if (bodyColumn <= cells)
7447
+ return line.startUtf16 + utf16;
7448
+ }
7449
+ return line.endUtf16;
7450
+ };
7451
+ return { preambleRows: normalized.rows.length, totalRows, contentWidth, rowAt, window, displayOffsetsForLines, rawOffsetAt };
7452
+ }
7453
+
7454
+ // src/ui/panes/virtual-main-pane.ts
7455
+ var ACCESSORS = ["scrollY", "scrollHeight", "maxScrollY", "scrollX", "scrollWidth", "maxScrollX"];
7456
+ var virtualPanes = new WeakMap;
7457
+ var VIRTUAL_MAIN_OVERSCAN_MIN = 10;
7458
+ function prototypeDescriptor(text, name) {
7459
+ let prototype = Object.getPrototypeOf(text);
7460
+ while (prototype !== null) {
7461
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
7462
+ if (descriptor !== undefined)
7463
+ return descriptor;
7464
+ prototype = Object.getPrototypeOf(prototype);
7465
+ }
7466
+ return;
7467
+ }
7468
+ function isFiniteNonNegative(value) {
7469
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
7470
+ }
7471
+ function documentSelection(document, startUtf16, endUtf16) {
7472
+ const line = document.lines.find((entry) => startUtf16 >= entry.startUtf16 && startUtf16 <= entry.endUtf16) ?? document.lines[0];
7473
+ return {
7474
+ valid: true,
7475
+ startUtf16,
7476
+ endUtf16,
7477
+ ...line === undefined ? {} : {
7478
+ fileIndex: line.fileIndex,
7479
+ ...line.hunkIndex === undefined ? {} : { hunkIndex: line.hunkIndex }
7480
+ },
7481
+ active: true
7482
+ };
7483
+ }
7484
+ function dimensions(text) {
7485
+ return {
7486
+ height: Math.max(1, Math.floor(isFiniteNonNegative(text.height))),
7487
+ width: Math.max(0, Math.floor(isFiniteNonNegative(text.width)))
7488
+ };
7489
+ }
7490
+ function padDisplayRow(value, width) {
7491
+ return `${value}${" ".repeat(Math.max(0, width - cellWidth(value)))}`;
7492
+ }
7493
+ function withoutLineEnding3(raw) {
7494
+ if (raw.endsWith(`\r
7495
+ `))
7496
+ return raw.slice(0, -2);
7497
+ if (raw.endsWith(`
7498
+ `) || raw.endsWith("\r"))
7499
+ return raw.slice(0, -1);
7500
+ return raw;
7501
+ }
7502
+ function rawDisplayCells(raw, relativeUtf16, includeLineEnding) {
7503
+ const body = withoutLineEnding3(raw);
7504
+ const prefixLength = Math.min(body.length, Math.max(0, Math.floor(relativeUtf16)));
7505
+ const cells = cellWidth(body.slice(0, prefixLength));
7506
+ return cells + (includeLineEnding && prefixLength >= body.length && body.length < raw.length ? 1 : 0);
7507
+ }
7508
+ function installAccessors(pane, state, rerender) {
7509
+ const text = pane.text;
7510
+ for (const name of ACCESSORS) {
7511
+ const descriptor = state.originalDescriptors.get(name);
7512
+ if (descriptor === undefined)
7513
+ continue;
7514
+ const nextDescriptor = {
7515
+ configurable: true,
7516
+ enumerable: descriptor.enumerable ?? false,
7517
+ get: () => {
7518
+ if (!state.active)
7519
+ return descriptor.get?.call(text);
7520
+ const layout = state.layout;
7521
+ if (name === "scrollY")
7522
+ return state.scrollY;
7523
+ if (name === "scrollX")
7524
+ return state.scrollX;
7525
+ if (name === "scrollHeight")
7526
+ return layout?.totalRows ?? descriptor.get?.call(text) ?? 0;
7527
+ if (name === "scrollWidth")
7528
+ return layout?.contentWidth ?? descriptor.get?.call(text) ?? 0;
7529
+ const viewport = name === "maxScrollY" ? state.viewportHeight : state.viewportWidth;
7530
+ const size = name === "maxScrollY" ? layout?.totalRows ?? 0 : layout?.contentWidth ?? 0;
7531
+ return Math.max(0, size - viewport);
7532
+ },
7533
+ ...name === "scrollY" || name === "scrollX" ? {
7534
+ set: (value) => {
7535
+ if (!state.active) {
7536
+ descriptor.set?.call(text, value);
7537
+ return;
7538
+ }
7539
+ const numeric = typeof value === "number" ? value : Number(value);
7540
+ if (name === "scrollY") {
7541
+ const max = Math.max(0, (state.layout?.totalRows ?? 0) - state.viewportHeight);
7542
+ const next = Math.min(max, Math.floor(isFiniteNonNegative(numeric)));
7543
+ if (state.scrollY === next)
7544
+ return;
7545
+ state.scrollY = next;
7546
+ rerender();
7547
+ } else {
7548
+ const max = Math.max(0, (state.layout?.contentWidth ?? 0) - state.viewportWidth);
7549
+ const next = Math.min(max, Math.floor(isFiniteNonNegative(numeric)));
7550
+ state.scrollX = next;
7551
+ descriptor.set?.call(text, next);
7552
+ pane.text.requestRender();
7553
+ }
7554
+ }
7555
+ } : {}
7556
+ };
7557
+ Object.defineProperty(text, name, nextDescriptor);
7558
+ }
7559
+ }
7560
+ function createAdapter(pane) {
7561
+ const text = pane.text;
7562
+ const originalOwnDescriptors = new Map(ACCESSORS.map((name) => [name, Object.getOwnPropertyDescriptor(text, name)]));
7563
+ const originalDescriptors = new Map(ACCESSORS.map((name) => [name, Object.getOwnPropertyDescriptor(text, name) ?? prototypeDescriptor(text, name)]));
7564
+ const state = {
7565
+ active: false,
7566
+ document: undefined,
7567
+ layout: undefined,
7568
+ preamble: "",
7569
+ scrollY: 0,
7570
+ scrollX: 0,
7571
+ viewportHeight: Math.max(1, Math.floor(text.height)),
7572
+ viewportWidth: Math.max(0, Math.floor(text.width)),
7573
+ rawSelection: undefined,
7574
+ renderedWindow: undefined,
7575
+ preambleSpans: new Map,
7576
+ originalDescriptors,
7577
+ originalOwnDescriptors
7578
+ };
7579
+ const restoreAccessors = () => {
7580
+ const target = text;
7581
+ for (const name of ACCESSORS) {
7582
+ delete target[name];
7583
+ const own = state.originalOwnDescriptors.get(name);
7584
+ if (own !== undefined)
7585
+ Object.defineProperty(target, name, own);
7586
+ }
7587
+ };
7588
+ const visibleSelection = () => {
7589
+ const selection = state.rawSelection;
7590
+ const layout = state.layout;
7591
+ const window = state.renderedWindow;
7592
+ if (selection === undefined || layout === undefined || window === undefined)
7593
+ return;
7594
+ let start;
7595
+ let end;
7596
+ let localOffset = 0;
7597
+ for (let row = window[0];row <= window[1]; row += 1) {
7598
+ const current = layout.rowAt(row);
7599
+ if (current === undefined)
7600
+ continue;
7601
+ const rowStart = localOffset;
7602
+ localOffset += layout.contentWidth + (row < window[1] ? 1 : 0);
7603
+ if (current.lineIndex === undefined || current.rawStartUtf16 === undefined || current.rawEndUtf16 === undefined)
7604
+ continue;
7605
+ const line = state.document?.lines[current.lineIndex];
7606
+ if (line === undefined)
7607
+ continue;
7608
+ const overlapStart = Math.max(selection.startUtf16, current.rawStartUtf16);
7609
+ const overlapEnd = Math.min(selection.endUtf16, current.rawEndUtf16);
7610
+ if (overlapStart >= overlapEnd)
7611
+ continue;
7612
+ const displayStart = rowStart + (overlapStart === current.rawStartUtf16 ? 0 : current.gutterCols + rawDisplayCells(line.raw, overlapStart - current.rawStartUtf16, false));
7613
+ const displayEnd = rowStart + current.gutterCols + rawDisplayCells(line.raw, overlapEnd - current.rawStartUtf16, overlapEnd > current.rawStartUtf16 + withoutLineEnding3(line.raw).length);
7614
+ start = start === undefined ? displayStart : Math.min(start, displayStart);
7615
+ end = end === undefined ? displayEnd : Math.max(end, displayEnd);
7616
+ }
7617
+ return start === undefined || end === undefined ? undefined : { start, end };
7618
+ };
7619
+ const paintSelection = () => {
7620
+ const selected = visibleSelection();
7621
+ const surface = text;
7622
+ if (selected === undefined)
7623
+ surface.resetSelection?.();
7624
+ else
7625
+ surface.setSelection?.(selected.start, selected.end);
7626
+ };
7627
+ const renderWindow = () => {
7628
+ if (!state.active || state.layout === undefined)
7629
+ return;
7630
+ const current = dimensions(text);
7631
+ state.viewportHeight = current.height;
7632
+ state.viewportWidth = current.width;
7633
+ const max = Math.max(0, state.layout.totalRows - state.viewportHeight);
7634
+ if (state.scrollY > max)
7635
+ state.scrollY = max;
7636
+ const maxX = Math.max(0, state.layout.contentWidth - state.viewportWidth);
7637
+ if (state.scrollX > maxX)
7638
+ state.scrollX = maxX;
7639
+ const overscan = Math.max(VIRTUAL_MAIN_OVERSCAN_MIN, state.viewportHeight);
7640
+ const window = state.layout.window(state.scrollY, state.viewportHeight, overscan);
7641
+ const localScrollY = state.scrollY - window[0];
7642
+ state.renderedWindow = window;
7643
+ const rows = [];
7644
+ const displays = [];
7645
+ const preambleRows = [];
7646
+ const preambleSpans = new Map;
7647
+ const first = window[0];
7648
+ const last = window[1];
7649
+ if (last >= first && first < state.layout.preambleRows) {
7650
+ const preambleLast = Math.min(last, state.layout.preambleRows - 1);
7651
+ for (let row = first;row <= preambleLast; row += 1) {
7652
+ const value = state.layout.rowAt(row);
7653
+ if (value !== undefined)
7654
+ preambleRows.push(padDisplayRow(value.text, state.layout.contentWidth));
7655
+ const spans = state.preambleSpans.get(row);
7656
+ if (spans !== undefined)
7657
+ preambleSpans.set(row - first, spans);
7658
+ }
7659
+ }
7660
+ const bodyFirst = Math.max(first, state.layout.preambleRows);
7661
+ for (let row = bodyFirst;row <= last; row += 1) {
7662
+ const value = state.layout.rowAt(row);
7663
+ if (value === undefined || value.lineIndex === undefined)
7664
+ continue;
7665
+ rows.push(padDisplayRow(value.text, state.layout.contentWidth));
7666
+ displays.push({ gutterCols: value.gutterCols, style: value.style });
7667
+ }
7668
+ const preamble = preambleRows.length === 0 ? "" : `${preambleRows.join(`
7669
+ `)}
7670
+ `;
7671
+ installDiffText(text, { preamble, body: rows.join(`
7672
+ `), displayLines: displays, highlightScrollY: () => localScrollY, preambleSpans });
7673
+ const originalScrollY = state.originalDescriptors.get("scrollY")?.set;
7674
+ originalScrollY?.call(text, localScrollY);
7675
+ const originalScrollX = state.originalDescriptors.get("scrollX")?.set;
7676
+ originalScrollX?.call(text, state.scrollX);
7677
+ paintSelection();
7678
+ pane.syncScrollbar(state.viewportHeight);
7679
+ text.requestRender?.();
7680
+ };
7681
+ const rerender = () => renderWindow();
7682
+ const adapter = {
7683
+ install(document, preamble) {
7684
+ if (!state.active) {
7685
+ state.scrollY = isFiniteNonNegative(Number(text.scrollY));
7686
+ state.scrollX = isFiniteNonNegative(Number(text.scrollX));
7687
+ }
7688
+ state.active = true;
7689
+ state.document = document;
7690
+ state.layout = createVirtualDiffLayout(document, preamble);
7691
+ state.preamble = preamble;
7692
+ state.preambleSpans = statSpansForPreamble(preamble);
7693
+ const current = dimensions(text);
7694
+ state.viewportHeight = current.height;
7695
+ state.viewportWidth = current.width;
7696
+ installAccessors(pane, state, rerender);
7697
+ text.wrapMode = "none";
7698
+ renderWindow();
7699
+ },
7700
+ deactivate() {
7701
+ if (!state.active)
7702
+ return;
7703
+ const originalY = state.originalDescriptors.get("scrollY")?.set;
7704
+ const originalX = state.originalDescriptors.get("scrollX")?.set;
7705
+ originalY?.call(text, state.scrollY);
7706
+ originalX?.call(text, state.scrollX);
7707
+ state.active = false;
7708
+ state.document = undefined;
7709
+ state.layout = undefined;
7710
+ state.rawSelection = undefined;
7711
+ state.renderedWindow = undefined;
7712
+ releaseDiffText(text);
7713
+ state.preambleSpans = new Map;
7714
+ restoreAccessors();
7715
+ clearScrollbarViewportOverride(text);
7716
+ text.wrapMode = "char";
7717
+ },
7718
+ isActive: () => state.active,
7719
+ layout: () => state.layout,
7720
+ lineOffsets: (startIndex, endIndex) => state.layout?.displayOffsetsForLines(startIndex, endIndex),
7721
+ visualRowRange: (startIndex, endIndex) => {
7722
+ if (state.layout === undefined)
7723
+ return;
7724
+ const start = state.layout.preambleRows + Math.max(0, startIndex);
7725
+ const end = state.layout.preambleRows + Math.max(startIndex, endIndex);
7726
+ return { startRow: start, endRow: end };
7727
+ },
7728
+ setLineSelection(startUtf16, endUtf16) {
7729
+ if (!state.active || state.document === undefined)
7730
+ return;
7731
+ state.rawSelection = documentSelection(state.document, startUtf16, endUtf16);
7732
+ paintSelection();
7733
+ },
7734
+ setPointerSelection(startRow, startColumn, endRow, endColumn) {
7735
+ if (!state.active || state.layout === undefined || state.document === undefined)
7736
+ return;
7737
+ const start = state.layout.rawOffsetAt(Math.max(0, Math.floor(state.scrollY + startRow)), Math.max(0, Math.floor(state.scrollX + startColumn)));
7738
+ const end = state.layout.rawOffsetAt(Math.max(0, Math.floor(state.scrollY + endRow)), Math.max(0, Math.floor(state.scrollX + endColumn)));
7739
+ if (start === undefined || end === undefined) {
7740
+ state.rawSelection = undefined;
7741
+ paintSelection();
7742
+ return;
7743
+ }
7744
+ const selection = documentSelection(state.document, Math.min(start, end), Math.max(start, end));
7745
+ state.rawSelection = selection;
7746
+ paintSelection();
7747
+ return selection;
7748
+ },
7749
+ selection: () => state.rawSelection,
7750
+ resetSelection() {
7751
+ state.rawSelection = undefined;
7752
+ text.resetSelection?.();
7753
+ },
7754
+ clampScroll() {
7755
+ if (!state.active || state.layout === undefined) {
7756
+ const originalY = state.originalDescriptors.get("scrollY")?.set;
7757
+ const originalX = state.originalDescriptors.get("scrollX")?.set;
7758
+ originalY?.call(text, text.scrollY);
7759
+ originalX?.call(text, text.scrollX);
7760
+ return;
7761
+ }
7762
+ const maxY = Math.max(0, state.layout.totalRows - state.viewportHeight);
7763
+ const maxX = Math.max(0, state.layout.contentWidth - state.viewportWidth);
7764
+ state.scrollY = Math.min(maxY, Math.max(0, state.scrollY));
7765
+ state.scrollX = Math.min(maxX, Math.max(0, state.scrollX));
7766
+ renderWindow();
7767
+ }
7768
+ };
7769
+ onPaneLifecyclePass(text, () => {
7770
+ if (!state.active || state.layout === undefined)
7771
+ return;
7772
+ const current = dimensions(text);
7773
+ if (current.height !== state.viewportHeight || current.width !== state.viewportWidth)
7774
+ renderWindow();
7775
+ });
7776
+ return adapter;
7777
+ }
7778
+ function createVirtualMainPane(pane) {
7779
+ const existing = virtualPanes.get(pane);
7780
+ if (existing !== undefined)
7781
+ return existing;
7782
+ const adapter = createAdapter(pane);
7783
+ virtualPanes.set(pane, adapter);
7784
+ return adapter;
7785
+ }
7786
+ function virtualMainPaneFor(pane) {
7787
+ return virtualPanes.get(pane);
7788
+ }
7789
+ function isVirtualDiffDocument(document) {
7790
+ return document.lines.length > VIRTUAL_DIFF_LINE_THRESHOLD;
7040
7791
  }
7041
7792
 
7042
7793
  // src/ui/panes/main-pane.ts
@@ -7071,6 +7822,7 @@ function createMainPane(renderer, _model) {
7071
7822
  const pane = createPane(renderer, "main", "0 Main", "", true);
7072
7823
  pane.text.selectionBg = SELECTED_LINE_BG;
7073
7824
  ensureMainTextSelectionSurface(pane);
7825
+ createVirtualMainPane(pane);
7074
7826
  pane.box.title = "0 Main";
7075
7827
  paneTitles.set(pane, "0 Main");
7076
7828
  return pane;
@@ -7119,6 +7871,9 @@ function mainDiffVisualRowRange(pane, startIndex, endIndex) {
7119
7871
  const document = documents.get(pane);
7120
7872
  if (document === undefined)
7121
7873
  return;
7874
+ const virtual = virtualMainPaneFor(pane);
7875
+ if (virtual?.isActive())
7876
+ return virtual.visualRowRange(startIndex, endIndex);
7122
7877
  const content = installedContents.get(pane);
7123
7878
  const preambleRows = (normalizedPreamble(content?.preamble ?? "").match(/\n/g) ?? []).length;
7124
7879
  const firstSource = preambleRows + Math.max(0, startIndex);
@@ -7143,7 +7898,16 @@ function getMainDiffLineSelection(pane) {
7143
7898
  if (document === undefined || state === undefined || state.rangeMode === "none")
7144
7899
  return;
7145
7900
  const range = diffLineSelectionRange(state);
7146
- const offsets = mainDiffLineOffsets(document, range.startIndex, range.endIndex, installedContents.get(pane)?.preamble ?? "");
7901
+ const virtual = virtualMainPaneFor(pane);
7902
+ const offsets = virtual?.isActive() ? (() => {
7903
+ const value = virtual.lineOffsets(range.startIndex, range.endIndex + 1);
7904
+ return value === undefined ? undefined : {
7905
+ startUtf16: value.rawStartUtf16,
7906
+ endUtf16: value.rawEndUtf16,
7907
+ displayStartUtf16: value.displayStartUtf16,
7908
+ displayEndUtf16: value.displayEndUtf16
7909
+ };
7910
+ })() : mainDiffLineOffsets(document, range.startIndex, range.endIndex, installedContents.get(pane)?.preamble ?? "");
7147
7911
  if (offsets === undefined)
7148
7912
  return;
7149
7913
  return {
@@ -7154,16 +7918,42 @@ function getMainDiffLineSelection(pane) {
7154
7918
  };
7155
7919
  }
7156
7920
  function applyMainDiffLineVisualSelection(pane, resetWhenInactive = true) {
7157
- const text = pane.text;
7921
+ const virtual = virtualMainPaneFor(pane);
7158
7922
  const selection = getMainDiffLineSelection(pane);
7159
- if (selection !== undefined) {
7923
+ if (virtual?.isActive()) {
7924
+ if (selection !== undefined)
7925
+ virtual.setLineSelection(selection.startUtf16, selection.endUtf16);
7926
+ else if (resetWhenInactive)
7927
+ virtual.resetSelection();
7928
+ return;
7929
+ }
7930
+ const text = pane.text;
7931
+ if (selection !== undefined)
7160
7932
  text.setSelection?.(selection.displayStartUtf16, selection.displayEndUtf16);
7161
- } else if (resetWhenInactive) {
7933
+ else if (resetWhenInactive)
7162
7934
  text.resetSelection?.();
7163
- }
7935
+ }
7936
+ function getMainPointerSelection(pane) {
7937
+ return virtualMainPaneFor(pane)?.selection();
7164
7938
  }
7165
7939
  function setMainDiffLineRangeState(pane, state) {
7166
7940
  lineRanges.set(pane, state);
7941
+ const virtual = virtualMainPaneFor(pane);
7942
+ if (state.rangeMode === "none") {
7943
+ if (virtual?.isActive())
7944
+ virtual.resetSelection();
7945
+ else
7946
+ applyMainDiffLineVisualSelection(pane);
7947
+ return;
7948
+ }
7949
+ const document = documents.get(pane);
7950
+ const range = diffLineSelectionRange(state);
7951
+ const start = document?.lines[range.startIndex];
7952
+ const end = document?.lines[range.endIndex];
7953
+ if (virtual?.isActive() && start !== undefined && end !== undefined) {
7954
+ virtual.setLineSelection(start.startUtf16, end.endUtf16);
7955
+ return;
7956
+ }
7167
7957
  applyMainDiffLineVisualSelection(pane);
7168
7958
  }
7169
7959
  function getMainDocument(pane) {
@@ -7243,11 +8033,17 @@ function buildPlainContent(content) {
7243
8033
  return "No content";
7244
8034
  }
7245
8035
  function clampMainScroll(pane) {
8036
+ const virtual = virtualMainPaneFor(pane);
8037
+ if (virtual?.isActive()) {
8038
+ virtual.clampScroll();
8039
+ return;
8040
+ }
7246
8041
  pane.text.scrollY = Math.max(0, Math.min(pane.text.maxScrollY, pane.text.scrollY));
7247
8042
  pane.text.scrollX = Math.max(0, Math.min(pane.text.maxScrollX, pane.text.scrollX));
7248
8043
  pane.syncScrollbar();
7249
8044
  }
7250
8045
  function updatePlain(pane, value) {
8046
+ virtualMainPaneFor(pane)?.deactivate();
7251
8047
  releaseDiffText(pane.text);
7252
8048
  releaseAnsiText(pane.text);
7253
8049
  pane.update(value);
@@ -7255,20 +8051,25 @@ function updatePlain(pane, value) {
7255
8051
  function installMainContent(pane, content, tooSmall) {
7256
8052
  const previousContent = installedContents.get(pane);
7257
8053
  const previousText = renderedTexts.get(pane);
7258
- const nextText = renderedTextFor(content);
8054
+ const virtualDocument = content.document !== undefined && isVirtualDiffDocument(content.document);
8055
+ const nextText = virtualDocument ? undefined : renderedTextFor(content);
7259
8056
  const previousIdentity = previousContent === undefined ? undefined : `${previousContent.source}:${previousContent.stableId}`;
7260
8057
  const nextIdentity = `${content.source}:${content.stableId}`;
7261
8058
  const sameIdentity = previousIdentity !== undefined && previousIdentity === nextIdentity;
7262
- const identicalText = previousText !== undefined && previousText === nextText;
8059
+ const identicalText = virtualDocument ? previousContent?.document?.text === content.document?.text && previousContent?.preamble === content.preamble : previousText !== undefined && previousText === nextText;
7263
8060
  const previousWasDocument = documents.has(pane);
7264
8061
  const replacingDocument = previousWasDocument && content.document === undefined;
7265
8062
  const enteringDocument = !previousWasDocument && content.document !== undefined;
7266
8063
  const leavingAnsi = previousContent?.ansi !== undefined && content.ansi === undefined;
7267
8064
  installedContents.set(pane, content);
7268
- renderedTexts.set(pane, nextText);
8065
+ if (nextText === undefined)
8066
+ renderedTexts.delete(pane);
8067
+ else
8068
+ renderedTexts.set(pane, nextText);
7269
8069
  paneTitles.set(pane, `0 Main — ${content.label}`);
7270
8070
  const previousRange = lineRanges.get(pane);
7271
8071
  const clearSelection = () => {
8072
+ virtualMainPaneFor(pane)?.resetSelection();
7272
8073
  const view = pane.text;
7273
8074
  if (view !== null && typeof view === "object" && "resetSelection" in view) {
7274
8075
  const reset = view.resetSelection;
@@ -7339,6 +8140,16 @@ function installMainContent(pane, content, tooSmall) {
7339
8140
  } else {
7340
8141
  cursorTargets.delete(pane);
7341
8142
  }
8143
+ const virtual = virtualMainPaneFor(pane);
8144
+ if (virtualDocument && virtual !== undefined) {
8145
+ releaseAnsiText(pane.text);
8146
+ virtual.install(doc, content.preamble ?? "");
8147
+ renderedTexts.delete(pane);
8148
+ if (previousRange?.rangeMode !== "none")
8149
+ applyMainDiffLineVisualSelection(pane);
8150
+ return;
8151
+ }
8152
+ virtual?.deactivate();
7342
8153
  pane.text.wrapMode = "char";
7343
8154
  releaseAnsiText(pane.text);
7344
8155
  if (enteringDocument || !sameIdentity || !identicalText) {
@@ -7353,6 +8164,7 @@ function installMainContent(pane, content, tooSmall) {
7353
8164
  return;
7354
8165
  }
7355
8166
  if (content.ansi !== undefined) {
8167
+ virtualMainPaneFor(pane)?.deactivate();
7356
8168
  documents.delete(pane);
7357
8169
  if (replacingDocument || !sameIdentity || !identicalText)
7358
8170
  clearSelection();
@@ -7364,6 +8176,7 @@ function installMainContent(pane, content, tooSmall) {
7364
8176
  pane.syncScrollbar();
7365
8177
  return;
7366
8178
  }
8179
+ virtualMainPaneFor(pane)?.deactivate();
7367
8180
  documents.delete(pane);
7368
8181
  if (replacingDocument || leavingAnsi || !sameIdentity || !identicalText)
7369
8182
  clearSelection();
@@ -7415,8 +8228,7 @@ function createStashPane(renderer, model) {
7415
8228
  const rows = stashRows(model);
7416
8229
  const displayRows = stashDisplayRows(model, rows);
7417
8230
  const state = createListState(rows, displayRows);
7418
- const content = renderListRows(state, false, 80);
7419
- pane.update(content);
8231
+ installListText(pane.text, { state, width: 80, focused: false });
7420
8232
  return pane;
7421
8233
  }
7422
8234
  function selectedStashEntryFromState(state, model) {
@@ -9336,6 +10148,7 @@ class RootView {
9336
10148
  lastSplitterPress;
9337
10149
  activeSplitterDrag;
9338
10150
  gestureOwner;
10151
+ mainPointerAnchor;
9339
10152
  pendingClick;
9340
10153
  hoveredListRow;
9341
10154
  model;
@@ -9624,6 +10437,10 @@ class RootView {
9624
10437
  this.syncPreviewForFocus(focus);
9625
10438
  };
9626
10439
  this.handleResize = () => {
10440
+ if (this.gestureOwner?.kind === "main-selection") {
10441
+ virtualMainPaneFor(this.panes.main)?.resetSelection();
10442
+ this.cancelGesture();
10443
+ }
9627
10444
  this.recomputeLayout();
9628
10445
  };
9629
10446
  this.handleKey = (key) => {
@@ -9672,6 +10489,10 @@ class RootView {
9672
10489
  this.applyLayout();
9673
10490
  }
9674
10491
  update(model, options = {}) {
10492
+ if (this.gestureOwner?.kind === "main-selection") {
10493
+ virtualMainPaneFor(this.panes.main)?.resetSelection();
10494
+ this.cancelGesture();
10495
+ }
9675
10496
  this.branchActionGeneration += 1;
9676
10497
  this.invalidateBranchCommitsRequest();
9677
10498
  if (!options.preserveRemoteCheckout) {
@@ -9902,7 +10723,11 @@ class RootView {
9902
10723
  };
9903
10724
  }
9904
10725
  cancelGesture() {
10726
+ if (this.gestureOwner?.kind === "main-selection") {
10727
+ virtualMainPaneFor(this.panes.main)?.resetSelection();
10728
+ }
9905
10729
  this.gestureOwner = undefined;
10730
+ this.mainPointerAnchor = undefined;
9906
10731
  this.activeSplitterDrag = undefined;
9907
10732
  }
9908
10733
  get activeBranchesTab() {
@@ -10201,14 +11026,15 @@ class RootView {
10201
11026
  const focused = this.focusManager.active === "branches";
10202
11027
  const activeView = this.activeListView("branches");
10203
11028
  if (activeView === undefined) {
11029
+ releaseListText(pane.text);
10204
11030
  pane.update("");
10205
11031
  return;
10206
11032
  }
10207
11033
  const { state } = activeView;
10208
11034
  const win = this.geometry.windows.branches;
10209
11035
  const width = sidePaneListWidth(win, state);
10210
- const content = renderListRows(state, focused, width, this.hoveredIdFor(activeView));
10211
- pane.update(content);
11036
+ const hoveredId = this.hoveredIdFor(activeView);
11037
+ installListText(pane.text, { state, width, focused, ...hoveredId === undefined ? {} : { hoveredId } });
10212
11038
  pane.syncScrollbar(sidePaneViewportHeight(win));
10213
11039
  }
10214
11040
  get filesTitleStyled() {
@@ -10268,13 +11094,15 @@ class RootView {
10268
11094
  pane.setTabs?.({ tabs: tabsInput.tabs, activeIndex: tabsInput.activeIndex, focused: tabsInput.focused });
10269
11095
  const activeView = this.activeListView("files");
10270
11096
  if (activeView === undefined) {
11097
+ releaseListText(pane.text);
10271
11098
  pane.update("");
10272
11099
  return;
10273
11100
  }
10274
11101
  const { state } = activeView;
10275
11102
  const win = this.geometry.windows.files;
10276
11103
  const width = sidePaneListWidth(win, state);
10277
- pane.update(renderListRows(state, tabsInput.focused, width, this.hoveredIdFor(activeView)));
11104
+ const hoveredId = this.hoveredIdFor(activeView);
11105
+ installListText(pane.text, { state, width, focused: tabsInput.focused, ...hoveredId === undefined ? {} : { hoveredId } });
10278
11106
  pane.syncScrollbar(sidePaneViewportHeight(win));
10279
11107
  }
10280
11108
  refreshStashState(model) {
@@ -10288,6 +11116,7 @@ class RootView {
10288
11116
  const pane = this.panes.stash;
10289
11117
  const activeView = this.activeListView("stash");
10290
11118
  if (activeView === undefined) {
11119
+ releaseListText(pane.text);
10291
11120
  pane.update("");
10292
11121
  return;
10293
11122
  }
@@ -10295,8 +11124,8 @@ class RootView {
10295
11124
  const focused = this.focusManager.active === "stash";
10296
11125
  const win = this.geometry.windows.stash;
10297
11126
  const width = sidePaneListWidth(win, state);
10298
- const content = renderListRows(state, focused, width, this.hoveredIdFor(activeView));
10299
- pane.update(content);
11127
+ const hoveredId = this.hoveredIdFor(activeView);
11128
+ installListText(pane.text, { state, width, focused, ...hoveredId === undefined ? {} : { hoveredId } });
10300
11129
  pane.syncScrollbar(sidePaneViewportHeight(win));
10301
11130
  }
10302
11131
  renderSidePanes() {
@@ -10945,8 +11774,8 @@ class RootView {
10945
11774
  }
10946
11775
  focusedPageStep() {
10947
11776
  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);
11777
+ const dimensions2 = focus === "command-log" ? this.geometry.windows.log : this.geometry.windows[focus] ?? this.geometry.windows.main;
11778
+ return Math.max(1, heightOf(dimensions2) - 2);
10950
11779
  }
10951
11780
  actionPage(direction) {
10952
11781
  if (this.focusManager.active === "main") {
@@ -12352,7 +13181,7 @@ class RootView {
12352
13181
  let next = -1;
12353
13182
  for (let i = current + 1;i < view.rows.length; i++) {
12354
13183
  const row = view.rows[i];
12355
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13184
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12356
13185
  if (text.includes(normalizedQuery)) {
12357
13186
  next = i;
12358
13187
  break;
@@ -12361,7 +13190,7 @@ class RootView {
12361
13190
  if (next === -1) {
12362
13191
  for (let i = 0;i <= current; i++) {
12363
13192
  const row = view.rows[i];
12364
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13193
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12365
13194
  if (text.includes(normalizedQuery)) {
12366
13195
  next = i;
12367
13196
  break;
@@ -12387,11 +13216,44 @@ class RootView {
12387
13216
  const query = this.getFilterForKey(this.filterKey("main"));
12388
13217
  if (query.length === 0)
12389
13218
  return;
12390
- const text = getMainRenderedText(this.panes.main) ?? getMainDocument(this.panes.main)?.text ?? "";
13219
+ const document = getMainDocument(this.panes.main);
13220
+ const virtual = virtualMainPaneFor(this.panes.main);
13221
+ const normalizedQuery = query.toLowerCase();
13222
+ if (document !== undefined && virtual?.isActive()) {
13223
+ const layout = virtual.layout();
13224
+ const preambleRows = layout?.preambleRows ?? 0;
13225
+ const totalRows = layout?.totalRows ?? preambleRows + document.lines.length;
13226
+ const currentLine2 = this.panes.main.text.scrollY - preambleRows;
13227
+ let nextLine2 = -1;
13228
+ const firstCandidate = Math.max(0, currentLine2 + 1);
13229
+ for (let i = firstCandidate;i < document.lines.length; i += 1) {
13230
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13231
+ nextLine2 = i;
13232
+ break;
13233
+ }
13234
+ }
13235
+ if (nextLine2 === -1) {
13236
+ for (let i = 0;i <= currentLine2 && i < document.lines.length; i += 1) {
13237
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13238
+ nextLine2 = i;
13239
+ break;
13240
+ }
13241
+ }
13242
+ }
13243
+ if (nextLine2 !== -1) {
13244
+ this.clearNonStickyMainRange();
13245
+ const targetRow = preambleRows + nextLine2;
13246
+ this.panes.main.text.scrollY = Math.max(0, targetRow - 2);
13247
+ this.panes.main.syncScrollbar();
13248
+ this.panes.main.box.bottomTitle = `Search: ${query} (${targetRow + 1}/${totalRows})`;
13249
+ this.root.requestRender();
13250
+ }
13251
+ return;
13252
+ }
13253
+ const text = getMainRenderedText(this.panes.main) ?? document?.text ?? "";
12391
13254
  if (text.length === 0)
12392
13255
  return;
12393
13256
  const normalizedText = text.toLowerCase();
12394
- const normalizedQuery = query.toLowerCase();
12395
13257
  const currentY = this.panes.main.text.scrollY;
12396
13258
  const lines = text.split(`
12397
13259
  `);
@@ -12415,6 +13277,7 @@ class RootView {
12415
13277
  this.clearNonStickyMainRange();
12416
13278
  const targetY = Math.max(0, nextLine - 2);
12417
13279
  this.panes.main.text.scrollY = targetY;
13280
+ this.panes.main.syncScrollbar();
12418
13281
  this.panes.main.box.bottomTitle = `Search: ${query} (${nextLine + 1}/${lines.length})`;
12419
13282
  this.root.requestRender();
12420
13283
  }
@@ -12435,7 +13298,7 @@ class RootView {
12435
13298
  let prev = -1;
12436
13299
  for (let i = current - 1;i >= 0; i--) {
12437
13300
  const row = view.rows[i];
12438
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13301
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12439
13302
  if (text.includes(normalizedQuery)) {
12440
13303
  prev = i;
12441
13304
  break;
@@ -12444,7 +13307,7 @@ class RootView {
12444
13307
  if (prev === -1) {
12445
13308
  for (let i = view.rows.length - 1;i >= current; i--) {
12446
13309
  const row = view.rows[i];
12447
- const text = `${row.columns[0]?.text ?? ""} ${row.columns[3]?.text ?? ""}`.toLowerCase();
13310
+ const text = `${row.columns[0]?.text ?? ""} ${row.columns[2]?.text ?? ""}`.toLowerCase();
12448
13311
  if (text.includes(normalizedQuery)) {
12449
13312
  prev = i;
12450
13313
  break;
@@ -12470,13 +13333,45 @@ class RootView {
12470
13333
  const query = this.getFilterForKey(this.filterKey("main"));
12471
13334
  if (query.length === 0)
12472
13335
  return;
12473
- const text = getMainRenderedText(this.panes.main) ?? getMainDocument(this.panes.main)?.text ?? "";
13336
+ const document = getMainDocument(this.panes.main);
13337
+ const virtual = virtualMainPaneFor(this.panes.main);
13338
+ const normalizedQuery = query.toLowerCase();
13339
+ if (document !== undefined && virtual?.isActive()) {
13340
+ const layout = virtual.layout();
13341
+ const preambleRows = layout?.preambleRows ?? 0;
13342
+ const totalRows = layout?.totalRows ?? preambleRows + document.lines.length;
13343
+ const currentLine = this.panes.main.text.scrollY - preambleRows;
13344
+ let previousLine = -1;
13345
+ for (let i = Math.min(document.lines.length - 1, currentLine - 1);i >= 0; i -= 1) {
13346
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13347
+ previousLine = i;
13348
+ break;
13349
+ }
13350
+ }
13351
+ if (previousLine === -1) {
13352
+ for (let i = document.lines.length - 1;i >= currentLine && i >= 0; i -= 1) {
13353
+ if (document.lines[i].raw.toLowerCase().includes(normalizedQuery)) {
13354
+ previousLine = i;
13355
+ break;
13356
+ }
13357
+ }
13358
+ }
13359
+ if (previousLine !== -1) {
13360
+ this.clearNonStickyMainRange();
13361
+ const targetRow = preambleRows + previousLine;
13362
+ this.panes.main.text.scrollY = Math.max(0, targetRow - 2);
13363
+ this.panes.main.syncScrollbar();
13364
+ this.panes.main.box.bottomTitle = `Search: ${query} (${targetRow + 1}/${totalRows})`;
13365
+ this.root.requestRender();
13366
+ }
13367
+ return;
13368
+ }
13369
+ const text = getMainRenderedText(this.panes.main) ?? document?.text ?? "";
12474
13370
  if (text.length === 0)
12475
13371
  return;
12476
13372
  const lines = text.split(`
12477
13373
  `);
12478
13374
  const currentY = this.panes.main.text.scrollY;
12479
- const normalizedQuery = query.toLowerCase();
12480
13375
  let prevLine = -1;
12481
13376
  for (let i = currentY - 1;i >= 0; i--) {
12482
13377
  if (lines[i].toLowerCase().includes(normalizedQuery)) {
@@ -12496,6 +13391,7 @@ class RootView {
12496
13391
  this.clearNonStickyMainRange();
12497
13392
  const targetY = Math.max(0, prevLine - 2);
12498
13393
  this.panes.main.text.scrollY = targetY;
13394
+ this.panes.main.syncScrollbar();
12499
13395
  this.panes.main.box.bottomTitle = `Search: ${query} (${prevLine + 1}/${lines.length})`;
12500
13396
  this.root.requestRender();
12501
13397
  }
@@ -12530,6 +13426,13 @@ class RootView {
12530
13426
  return [...paths];
12531
13427
  }
12532
13428
  mainActionTarget(document) {
13429
+ const pointer = getMainPointerSelection(this.panes.main);
13430
+ if (pointer?.fileIndex !== undefined && pointer.valid && pointer.endUtf16 > pointer.startUtf16) {
13431
+ return {
13432
+ fileIndex: pointer.fileIndex,
13433
+ ...pointer.hunkIndex === undefined ? {} : { hunkIndex: pointer.hunkIndex }
13434
+ };
13435
+ }
12533
13436
  const selected = getMainDiffLineSelection(this.panes.main);
12534
13437
  const firstIndex = selected?.indexes[0];
12535
13438
  const line = firstIndex === undefined ? undefined : document.lines[firstIndex];
@@ -12541,10 +13444,43 @@ class RootView {
12541
13444
  }
12542
13445
  return getMainCursorTarget(this.panes.main);
12543
13446
  }
13447
+ mainPointerCoordinates(event) {
13448
+ const geometry = this.paneTextGeometry("main");
13449
+ if (geometry === undefined)
13450
+ return;
13451
+ const row = event.y - geometry.screenY;
13452
+ const column = event.x - geometry.screenX;
13453
+ if (!Number.isSafeInteger(row) || !Number.isSafeInteger(column) || row < 0 || row >= geometry.height || column < 0 || column >= geometry.width)
13454
+ return;
13455
+ return { row, column };
13456
+ }
13457
+ updateVirtualMainPointer(event) {
13458
+ const virtual = virtualMainPaneFor(this.panes.main);
13459
+ if (!virtual?.isActive())
13460
+ return;
13461
+ const anchor = this.mainPointerAnchor;
13462
+ if (anchor === undefined) {
13463
+ virtual.resetSelection();
13464
+ return;
13465
+ }
13466
+ const point = this.mainPointerCoordinates(event);
13467
+ if (point === undefined) {
13468
+ virtual.resetSelection();
13469
+ return;
13470
+ }
13471
+ virtual.setPointerSelection(anchor.row, anchor.column, point.row, point.column);
13472
+ }
12544
13473
  mainChangeSelection() {
12545
13474
  const document = getMainDocument(this.panes.main);
12546
13475
  if (!document)
12547
13476
  return;
13477
+ const pointerSelection = getMainPointerSelection(this.panes.main);
13478
+ if (pointerSelection?.valid && pointerSelection.endUtf16 > pointerSelection.startUtf16) {
13479
+ return {
13480
+ document,
13481
+ indexes: changeLineIndexes(document, pointerSelection.startUtf16, pointerSelection.endUtf16)
13482
+ };
13483
+ }
12548
13484
  const keyboardSelection = getMainDiffLineSelection(this.panes.main);
12549
13485
  if (keyboardSelection !== undefined) {
12550
13486
  return {
@@ -12939,7 +13875,7 @@ class RootView {
12939
13875
  let commitsState = setListRows(panel.views.commits, rows, displayRows);
12940
13876
  if (commitsSearch.length > 0 && rows.length > 0) {
12941
13877
  const normalized = commitsSearch.toLowerCase();
12942
- const matchIndex = rows.findIndex((row) => (row.columns[3]?.text ?? "").toLowerCase().includes(normalized) || (row.columns[0]?.text ?? "").toLowerCase().includes(normalized));
13878
+ const matchIndex = rows.findIndex((row) => (row.columns[2]?.text ?? "").toLowerCase().includes(normalized) || (row.columns[0]?.text ?? "").toLowerCase().includes(normalized));
12943
13879
  if (matchIndex >= 0) {
12944
13880
  const matchId = rows[matchIndex].id;
12945
13881
  commitsState = selectListRow(commitsState, matchId);
@@ -12970,14 +13906,15 @@ class RootView {
12970
13906
  }
12971
13907
  const activeView = this.activeListView("commits");
12972
13908
  if (activeView === undefined) {
13909
+ releaseListText(pane.text);
12973
13910
  pane.update("");
12974
13911
  return;
12975
13912
  }
12976
13913
  const { state } = activeView;
12977
13914
  const width = sidePaneListWidth(this.geometry.windows.commits, state);
12978
13915
  const focused = this.focusManager.active === "commits";
12979
- const content = renderListRows(state, focused, width, this.hoveredIdFor(activeView));
12980
- pane.update(content);
13916
+ const hoveredId = this.hoveredIdFor(activeView);
13917
+ installListText(pane.text, { state, width, focused, ...hoveredId === undefined ? {} : { hoveredId } });
12981
13918
  pane.syncScrollbar(sidePaneViewportHeight(this.geometry.windows.commits));
12982
13919
  }
12983
13920
  installInitialMainContent(model) {
@@ -13209,16 +14146,19 @@ class RootView {
13209
14146
  this.root.requestRender();
13210
14147
  return;
13211
14148
  }
14149
+ const rawPointerSelection = getMainPointerSelection(pane);
14150
+ const pointerSelection = rawPointerSelection !== undefined && rawPointerSelection.valid && rawPointerSelection.endUtf16 > rawPointerSelection.startUtf16 ? rawPointerSelection : undefined;
13212
14151
  const keyboardSelection = getMainDiffLineSelection(pane);
13213
14152
  const nativeRange = pane.text.getSelection();
13214
- let selection = mode === "hunk" || mode === "file" || keyboardSelection === undefined ? undefined : {
14153
+ let selection = mode === "hunk" || mode === "file" ? undefined : pointerSelection ?? (keyboardSelection === undefined ? undefined : {
13215
14154
  valid: true,
13216
14155
  startUtf16: keyboardSelection.startUtf16,
13217
14156
  endUtf16: keyboardSelection.endUtf16,
13218
14157
  active: true
13219
- };
13220
- if (selection === undefined && keyboardSelection === undefined && nativeRange)
14158
+ });
14159
+ if (selection === undefined && pointerSelection === undefined && keyboardSelection === undefined && nativeRange) {
13221
14160
  selection = selectionFromRenderable(document, nativeRange, pane.text.getSelectedText());
14161
+ }
13222
14162
  if (!selection && (mode === "hunk" || mode === "file")) {
13223
14163
  const target = getMainCursorTarget(pane);
13224
14164
  if (target) {
@@ -13538,6 +14478,9 @@ class RootView {
13538
14478
  bar.onMouseDown = undefined;
13539
14479
  bar.onMouseDrag = undefined;
13540
14480
  bar.onMouseUp = undefined;
14481
+ bar.slider.onMouseDown = undefined;
14482
+ bar.slider.onMouseDrag = undefined;
14483
+ bar.slider.onMouseUp = undefined;
13541
14484
  }
13542
14485
  }
13543
14486
  this.renderer.off("resize", this.handleResize);
@@ -13564,20 +14507,33 @@ class RootView {
13564
14507
  const bar = paneScrollbar(typedPane.text);
13565
14508
  if (!bar)
13566
14509
  continue;
13567
- bar.onMouseDown = (event) => {
14510
+ const beginScrollbarGesture = (event) => {
14511
+ this.updateHoveredListRow(event.x, event.y);
13568
14512
  this.pendingClick = undefined;
13569
14513
  this.lastSplitterPress = undefined;
13570
14514
  this.gestureOwner = { kind: "scrollbar", paneId: typedPane.id };
14515
+ this.scrollPaneByScrollbarPosition(typedPane.id, event.y);
13571
14516
  event.stopPropagation();
13572
14517
  };
13573
- bar.onMouseDrag = (event) => {
13574
- if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id)
14518
+ const continueScrollbarGesture = (event) => {
14519
+ if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id) {
14520
+ this.scrollPaneByScrollbarPosition(typedPane.id, event.y);
13575
14521
  event.stopPropagation();
14522
+ }
13576
14523
  };
13577
- bar.onMouseUp = (event) => {
13578
- if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id)
14524
+ const endScrollbarGesture = (event) => {
14525
+ if (this.gestureOwner?.kind === "scrollbar" && this.gestureOwner.paneId === typedPane.id) {
14526
+ this.scrollPaneByScrollbarPosition(typedPane.id, event.y);
14527
+ this.gestureOwner = undefined;
13579
14528
  event.stopPropagation();
14529
+ }
13580
14530
  };
14531
+ bar.onMouseDown = beginScrollbarGesture;
14532
+ bar.onMouseDrag = continueScrollbarGesture;
14533
+ bar.onMouseUp = endScrollbarGesture;
14534
+ bar.slider.onMouseDown = beginScrollbarGesture;
14535
+ bar.slider.onMouseDrag = continueScrollbarGesture;
14536
+ bar.slider.onMouseUp = endScrollbarGesture;
13581
14537
  }
13582
14538
  this.root.onMouse = (event) => {
13583
14539
  if (this.isBranchReviewActive?.())
@@ -13686,14 +14642,22 @@ class RootView {
13686
14642
  return;
13687
14643
  }
13688
14644
  if (owner.kind === "main-selection") {
14645
+ const virtual = virtualMainPaneFor(this.panes.main);
13689
14646
  if (event.type === "drag") {
13690
14647
  this.pendingClick = undefined;
13691
14648
  this.lastSplitterPress = undefined;
14649
+ if (virtual?.isActive())
14650
+ this.updateVirtualMainPointer(event);
14651
+ event.preventDefault();
13692
14652
  event.stopPropagation();
13693
14653
  return;
13694
14654
  }
13695
- if (event.type === "up") {
14655
+ if (event.type === "up" || event.type === "cancel") {
14656
+ if (virtual?.isActive() && event.type === "up")
14657
+ this.updateVirtualMainPointer(event);
13696
14658
  this.gestureOwner = undefined;
14659
+ this.mainPointerAnchor = undefined;
14660
+ event.preventDefault();
13697
14661
  event.stopPropagation();
13698
14662
  return;
13699
14663
  }
@@ -13824,10 +14788,22 @@ class RootView {
13824
14788
  if (paneId === "main") {
13825
14789
  this.pendingClick = undefined;
13826
14790
  this.lastSplitterPress = undefined;
13827
- this.gestureOwner = { kind: "main-selection" };
13828
14791
  if (this.focusManager.active !== "main")
13829
14792
  this.focusManager.focus("main");
14793
+ const virtual = virtualMainPaneFor(this.panes.main);
14794
+ const canSelect = event.button === 0 && !event.modifiers.ctrl;
14795
+ const point = virtual?.isActive() && canSelect ? this.mainPointerCoordinates(event) : undefined;
14796
+ this.mainPointerAnchor = point;
14797
+ if (virtual?.isActive()) {
14798
+ if (point === undefined)
14799
+ virtual.resetSelection();
14800
+ else
14801
+ virtual.setPointerSelection(point.row, point.column, point.row, point.column);
14802
+ }
14803
+ this.gestureOwner = { kind: "main-selection" };
13830
14804
  this.clearTransientMenus();
14805
+ if (virtual?.isActive() && canSelect)
14806
+ event.preventDefault();
13831
14807
  event.stopPropagation();
13832
14808
  return;
13833
14809
  }
@@ -14096,16 +15072,16 @@ class RootView {
14096
15072
  this.syncPaneBorders();
14097
15073
  const windows = this.geometry.windows;
14098
15074
  const place = (renderable, name) => {
14099
- const dimensions = windows[name];
14100
- if (dimensions === undefined) {
15075
+ const dimensions2 = windows[name];
15076
+ if (dimensions2 === undefined) {
14101
15077
  renderable.visible = false;
14102
15078
  return;
14103
15079
  }
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;
15080
+ renderable.left = dimensions2.x0;
15081
+ renderable.top = dimensions2.y0;
15082
+ renderable.width = Math.max(1, widthOf(dimensions2));
15083
+ renderable.height = Math.max(1, heightOf(dimensions2));
15084
+ renderable.visible = widthOf(dimensions2) > 0 && heightOf(dimensions2) > 0;
14109
15085
  };
14110
15086
  for (const name of SIDE_WINDOWS)
14111
15087
  place(this.panes[name].box, name);
@@ -14705,7 +15681,7 @@ function isWorkerAvailable() {
14705
15681
  }
14706
15682
 
14707
15683
  // src/ui/review-workspace/ReviewWorkspaceApp.tsx
14708
- import { StyledText as StyledText8, parseColor as parseColor2 } from "@opentui/core";
15684
+ import { StyledText as StyledText8, parseColor as parseColor3 } from "@opentui/core";
14709
15685
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
14710
15686
  import { useCallback, useEffect as useEffect3, useLayoutEffect as useLayoutEffect2, useMemo as useMemo4, useRef as useRef2, useState as useState3, useSyncExternalStore } from "react";
14711
15687
 
@@ -15731,7 +16707,7 @@ function buildHunkStackRows(file, state, highlight, options) {
15731
16707
  return rows;
15732
16708
  }
15733
16709
  // src/ui/review-workspace/components/ReviewDiffRow.tsx
15734
- import { StyledText as StyledText7, parseColor } from "@opentui/core";
16710
+ import { StyledText as StyledText7, parseColor as parseColor2 } from "@opentui/core";
15735
16711
 
15736
16712
  // src/ui/review-workspace/hunk-code-columns.ts
15737
16713
  var HUNK_DIFF_RAIL_WIDTH = 1;
@@ -15804,7 +16780,7 @@ function color(value) {
15804
16780
  const cached = colorCache2.get(value);
15805
16781
  if (cached)
15806
16782
  return cached;
15807
- const parsed = parseColor(value);
16783
+ const parsed = parseColor2(value);
15808
16784
  colorCache2.set(value, parsed);
15809
16785
  return parsed;
15810
16786
  }
@@ -17916,7 +18892,7 @@ var colorCache3 = new Map;
17916
18892
  function textChunk(text, style) {
17917
18893
  let fg5 = colorCache3.get(COLORS2[style]);
17918
18894
  if (!fg5) {
17919
- fg5 = parseColor2(COLORS2[style]);
18895
+ fg5 = parseColor3(COLORS2[style]);
17920
18896
  colorCache3.set(COLORS2[style], fg5);
17921
18897
  }
17922
18898
  return { __isChunk: true, text, fg: fg5 };
@@ -18154,14 +19130,14 @@ function ReviewWorkspaceApp({ session }) {
18154
19130
  const finishSummaryRef = useRef2(null);
18155
19131
  const finishSubmitRef = useRef2(false);
18156
19132
  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);
19133
+ const dimensions2 = { width: Math.max(1, terminal.width), height: Math.max(1, terminal.height) };
19134
+ const maxSidebarWidth = Math.max(REVIEW_SIDEBAR_MIN_WIDTH, dimensions2.width - REVIEW_RESIZE_BAR_WIDTH - REVIEW_DIFF_BORDER_WIDTH - REVIEW_DIFF_MIN_CONTENT_WIDTH);
19135
+ const sidebarWidth = dimensions2.width >= REVIEW_SIDEBAR_VISIBILITY_WIDTH ? Math.min(Math.max(sidebarWidthPreference, REVIEW_SIDEBAR_MIN_WIDTH), maxSidebarWidth) : 0;
19136
+ const diffWidth = Math.max(1, dimensions2.width - sidebarWidth - (sidebarWidth > 0 ? REVIEW_RESIZE_BAR_WIDTH : 0) - REVIEW_DIFF_BORDER_WIDTH);
18161
19137
  const layout = layoutMode === "auto" ? diffWidth >= 64 ? "split" : "stack" : layoutMode;
18162
19138
  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);
19139
+ const diffHeight = Math.max(1, dimensions2.height - 4 - composerHeight - 2);
19140
+ const resizeBarHeight = Math.max(1, dimensions2.height - 4 - composerHeight);
18165
19141
  const sidebarFocused = focus === "sidebar" || focus === "filter";
18166
19142
  const diffFocused = focus === "stream";
18167
19143
  const files = useMemo4(() => state ? toHunkReviewFiles(visibleReviewFiles(state)) : [], [state?.document, state?.feedback, state?.filter, state?.viewed]);
@@ -18953,7 +19929,7 @@ function ReviewWorkspaceApp({ session }) {
18953
19929
  id: "react-review-header",
18954
19930
  style: { width: "100%", height: 3, flexShrink: 0 },
18955
19931
  children: /* @__PURE__ */ jsx4("text", {
18956
- content: headerText(state, dimensions.width, controller.error),
19932
+ content: headerText(state, dimensions2.width, controller.error),
18957
19933
  wrapMode: "none",
18958
19934
  truncate: true
18959
19935
  })
@@ -19180,7 +20156,7 @@ function ReviewWorkspaceApp({ session }) {
19180
20156
  }),
19181
20157
  orphanedFeedback.length > 0 ? /* @__PURE__ */ jsx4("box", {
19182
20158
  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" },
20159
+ 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
20160
  children: orphanedFeedback.slice(0, 4).map((feedback) => /* @__PURE__ */ jsxs4("box", {
19185
20161
  style: { width: "100%", height: 1, flexDirection: "row" },
19186
20162
  onMouseUp: () => {
@@ -19225,7 +20201,7 @@ function ReviewWorkspaceApp({ session }) {
19225
20201
  }) : null,
19226
20202
  feedbackMessage ? /* @__PURE__ */ jsx4("box", {
19227
20203
  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" },
20204
+ 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
20205
  children: /* @__PURE__ */ jsx4("text", {
19230
20206
  content: feedbackMessage,
19231
20207
  wrapMode: "none",
@@ -19445,7 +20421,7 @@ function ReviewWorkspaceApp({ session }) {
19445
20421
  }) : null,
19446
20422
  helpOpen ? /* @__PURE__ */ jsx4("box", {
19447
20423
  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" },
20424
+ 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
20425
  children: /* @__PURE__ */ jsx4("text", {
19450
20426
  content: `Review commands
19451
20427
  ${reviewHelp(focus, state)}
@@ -19456,7 +20432,7 @@ Esc close this help`,
19456
20432
  }) : null,
19457
20433
  finishDialog.isOpen() ? /* @__PURE__ */ jsxs4("box", {
19458
20434
  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" },
20435
+ 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
20436
  children: [
19461
20437
  /* @__PURE__ */ jsx4("text", {
19462
20438
  content: `Finish review — ${finishDialog.getDecision()}`,