@pierre/diffs 1.2.1 → 1.2.3

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 (67) hide show
  1. package/dist/components/CodeView.d.ts +22 -7
  2. package/dist/components/CodeView.d.ts.map +1 -1
  3. package/dist/components/CodeView.js +202 -105
  4. package/dist/components/CodeView.js.map +1 -1
  5. package/dist/components/File.d.ts +2 -0
  6. package/dist/components/File.d.ts.map +1 -1
  7. package/dist/components/File.js +13 -9
  8. package/dist/components/File.js.map +1 -1
  9. package/dist/components/FileDiff.d.ts +2 -0
  10. package/dist/components/FileDiff.d.ts.map +1 -1
  11. package/dist/components/FileDiff.js +12 -6
  12. package/dist/components/FileDiff.js.map +1 -1
  13. package/dist/components/UnresolvedFile.d.ts.map +1 -1
  14. package/dist/components/VirtualizedFile.d.ts +4 -2
  15. package/dist/components/VirtualizedFile.d.ts.map +1 -1
  16. package/dist/components/VirtualizedFile.js +23 -6
  17. package/dist/components/VirtualizedFile.js.map +1 -1
  18. package/dist/components/VirtualizedFileDiff.d.ts +9 -8
  19. package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
  20. package/dist/components/VirtualizedFileDiff.js +329 -142
  21. package/dist/components/VirtualizedFileDiff.js.map +1 -1
  22. package/dist/constants.d.ts.map +1 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +3 -3
  25. package/dist/react/index.d.ts +2 -2
  26. package/dist/renderers/DiffHunksRenderer.d.ts +1 -0
  27. package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
  28. package/dist/renderers/DiffHunksRenderer.js +19 -9
  29. package/dist/renderers/DiffHunksRenderer.js.map +1 -1
  30. package/dist/renderers/FileRenderer.d.ts +1 -0
  31. package/dist/renderers/FileRenderer.d.ts.map +1 -1
  32. package/dist/renderers/FileRenderer.js +12 -6
  33. package/dist/renderers/FileRenderer.js.map +1 -1
  34. package/dist/ssr/index.d.ts +2 -2
  35. package/dist/types.d.ts +7 -1
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/utils/computeEstimatedDiffHeights.d.ts +28 -0
  38. package/dist/utils/computeEstimatedDiffHeights.d.ts.map +1 -0
  39. package/dist/utils/computeEstimatedDiffHeights.js +111 -0
  40. package/dist/utils/computeEstimatedDiffHeights.js.map +1 -0
  41. package/dist/utils/getDiffHunksRendererOptions.d.ts +8 -0
  42. package/dist/utils/getDiffHunksRendererOptions.d.ts.map +1 -0
  43. package/dist/utils/getDiffHunksRendererOptions.js +31 -0
  44. package/dist/utils/getDiffHunksRendererOptions.js.map +1 -0
  45. package/dist/utils/getFileRendererOptions.d.ts +8 -0
  46. package/dist/utils/getFileRendererOptions.d.ts.map +1 -0
  47. package/dist/utils/getFileRendererOptions.js +24 -0
  48. package/dist/utils/getFileRendererOptions.js.map +1 -0
  49. package/dist/utils/iterateOverDiff.js +29 -30
  50. package/dist/utils/iterateOverDiff.js.map +1 -1
  51. package/dist/utils/parsePatchFiles.js +8 -1
  52. package/dist/utils/parsePatchFiles.js.map +1 -1
  53. package/dist/utils/virtualDiffLayout.d.ts +65 -0
  54. package/dist/utils/virtualDiffLayout.d.ts.map +1 -0
  55. package/dist/utils/virtualDiffLayout.js +94 -0
  56. package/dist/utils/virtualDiffLayout.js.map +1 -0
  57. package/dist/worker/WorkerPoolManager.d.ts +4 -1
  58. package/dist/worker/WorkerPoolManager.d.ts.map +1 -1
  59. package/dist/worker/WorkerPoolManager.js +49 -24
  60. package/dist/worker/WorkerPoolManager.js.map +1 -1
  61. package/dist/worker/types.d.ts +2 -0
  62. package/dist/worker/types.d.ts.map +1 -1
  63. package/dist/worker/worker-portable.js +163 -40
  64. package/dist/worker/worker-portable.js.map +1 -1
  65. package/dist/worker/worker.js +60 -30
  66. package/dist/worker/worker.js.map +1 -1
  67. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
1
  import { DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } from "../constants.js";
2
2
  import { areObjectsEqual } from "../utils/areObjectsEqual.js";
3
3
  import { areOptionsEqual } from "../utils/areOptionsEqual.js";
4
- import { computeVirtualFileMetrics, getDefaultHunkSeparatorHeight, getVirtualFileHeaderRegion, getVirtualFilePaddingBottom } from "../utils/computeVirtualFileMetrics.js";
4
+ import { computeVirtualFileMetrics, getVirtualFileHeaderRegion, getVirtualFilePaddingBottom } from "../utils/computeVirtualFileMetrics.js";
5
+ import { areDiffTargetsEqual } from "../utils/areDiffTargetsEqual.js";
6
+ import { getExpandedRegion, getLeadingHunkSeparatorLayout, getTrailingHunkSeparatorLayout } from "../utils/virtualDiffLayout.js";
7
+ import { computeEstimatedDiffHeights } from "../utils/computeEstimatedDiffHeights.js";
5
8
  import { iterateOverDiff } from "../utils/iterateOverDiff.js";
6
9
  import { parseDiffFromFile } from "../utils/parseDiffFromFile.js";
7
10
  import { FileDiff } from "./FileDiff.js";
@@ -15,7 +18,10 @@ var VirtualizedFileDiff = class extends FileDiff {
15
18
  height = 0;
16
19
  metrics;
17
20
  cache = {
18
- heights: /* @__PURE__ */ new Map(),
21
+ heightDeltas: /* @__PURE__ */ new Map(),
22
+ measuredHeightDeltaTotal: 0,
23
+ estimatedSplitHeight: void 0,
24
+ estimatedUnifiedHeight: void 0,
19
25
  checkpoints: [],
20
26
  totalLines: 0
21
27
  };
@@ -24,6 +30,7 @@ var VirtualizedFileDiff = class extends FileDiff {
24
30
  virtualizer;
25
31
  layoutDirty = true;
26
32
  forceRenderOverride;
33
+ currentCollapsed;
27
34
  constructor(options, virtualizer, metrics, workerManager, isContainerManaged = false) {
28
35
  super(options, workerManager, isContainerManaged);
29
36
  this.virtualizer = virtualizer;
@@ -33,31 +40,45 @@ var VirtualizedFileDiff = class extends FileDiff {
33
40
  const nextMetrics = computeVirtualFileMetrics(metrics);
34
41
  if (!force && areObjectsEqual(this.metrics, nextMetrics)) return;
35
42
  this.metrics = nextMetrics;
36
- this.resetLayoutCache();
43
+ this.resetLayoutCache({ includeEstimatedHeights: true });
37
44
  }
38
45
  getLineHeight(lineIndex, hasMetadataLine = false) {
39
- const cached = this.cache.heights.get(lineIndex);
40
- if (cached != null) return cached;
46
+ return this.getEstimatedLineHeight(hasMetadataLine) + (this.cache.heightDeltas.get(lineIndex) ?? 0);
47
+ }
48
+ getEstimatedLineHeight(hasMetadataLine = false) {
41
49
  const multiplier = hasMetadataLine ? 2 : 1;
42
50
  return this.metrics.lineHeight * multiplier;
43
51
  }
44
52
  setOptions(options) {
53
+ if (this.isAdvancedMode()) throw new Error("VirtualizedFileDiff.setOptions cannot be used inside CodeView. Update CodeView options instead.");
45
54
  if (options == null) return;
46
55
  const { options: previousOptions } = this;
47
56
  const optionsChanged = !areOptionsEqual(previousOptions, options);
48
57
  const layoutChanged = optionsChanged && hasDiffLayoutOptionChanged(previousOptions, options);
49
58
  super.setOptions(options);
50
- if (layoutChanged) this.resetLayoutCache(true);
59
+ if (layoutChanged) this.resetLayoutCache({
60
+ forceSimpleRecompute: true,
61
+ includeEstimatedHeights: hasDiffEstimateOptionChanged(previousOptions, options)
62
+ });
51
63
  if (optionsChanged) this.forceRenderOverride = true;
52
64
  if (optionsChanged && this.isSimpleMode()) this.virtualizer.instanceChanged(this, layoutChanged);
53
65
  }
54
- resetLayoutCache(recompute = false) {
66
+ setThemeType(themeType) {
67
+ if (this.isAdvancedMode()) throw new Error("VirtualizedFileDiff.setThemeType cannot be used inside CodeView. Update CodeView options instead.");
68
+ super.setThemeType(themeType);
69
+ }
70
+ resetLayoutCache({ forceSimpleRecompute = false, includeEstimatedHeights = false } = {}) {
55
71
  this.layoutDirty = true;
56
- this.cache.heights.clear();
57
- this.cache.checkpoints = [];
58
- this.cache.totalLines = 0;
59
- this.renderRange = void 0;
60
- if (recompute && this.isSimpleMode()) this.computeApproximateSize();
72
+ if (this.cache.heightDeltas.size > 0) this.cache.heightDeltas.clear();
73
+ if (this.cache.measuredHeightDeltaTotal !== 0) this.cache.measuredHeightDeltaTotal = 0;
74
+ if (this.cache.checkpoints.length > 0) this.cache.checkpoints.length = 0;
75
+ if (this.cache.totalLines !== 0) this.cache.totalLines = 0;
76
+ if (includeEstimatedHeights) {
77
+ this.cache.estimatedSplitHeight = void 0;
78
+ this.cache.estimatedUnifiedHeight = void 0;
79
+ }
80
+ if (this.renderRange != null) this.renderRange = void 0;
81
+ if (forceSimpleRecompute && this.isSimpleMode()) this.computeApproximateSize();
61
82
  }
62
83
  reconcileHeights() {
63
84
  let hasHeightChange = false;
@@ -86,11 +107,14 @@ var VirtualizedFileDiff = class extends FileDiff {
86
107
  if ("noNewline" in line.nextElementSibling.dataset) hasMetadata = true;
87
108
  measuredHeight += line.nextElementSibling.getBoundingClientRect().height;
88
109
  }
89
- const expectedHeight = this.getLineHeight(lineIndex, hasMetadata);
90
- if (measuredHeight === expectedHeight) continue;
110
+ const estimatedHeight = this.getEstimatedLineHeight(hasMetadata);
111
+ const previousDelta = this.cache.heightDeltas.get(lineIndex) ?? 0;
112
+ const nextDelta = measuredHeight - estimatedHeight;
113
+ if (nextDelta === previousDelta) continue;
91
114
  hasHeightChange = true;
92
- if (measuredHeight === this.metrics.lineHeight * (hasMetadata ? 2 : 1)) this.cache.heights.delete(lineIndex);
93
- else this.cache.heights.set(lineIndex, measuredHeight);
115
+ this.cache.measuredHeightDeltaTotal += nextDelta - previousDelta;
116
+ if (nextDelta === 0) this.cache.heightDeltas.delete(lineIndex);
117
+ else this.cache.heightDeltas.set(lineIndex, nextDelta);
94
118
  }
95
119
  }
96
120
  if (hasHeightChange || this.isResizeDebuggingEnabled()) this.computeApproximateSize(true);
@@ -101,10 +125,23 @@ var VirtualizedFileDiff = class extends FileDiff {
101
125
  if (dirty) this.top = this.getVirtualizedTop();
102
126
  return this.render();
103
127
  };
104
- prepareVirtualizedItem(fileDiff) {
105
- if (this.fileDiff !== fileDiff) this.resetLayoutCache();
128
+ prepareCodeViewItem(fileDiff, top, reset) {
129
+ const targetChanged = !areDiffTargetsEqual(this.fileDiff, fileDiff);
130
+ let shouldResetLayoutCache = reset?.resetDiffLayoutCache === true || targetChanged;
131
+ let includeEstimatedHeights = targetChanged || reset?.resetDiffLayoutCache === true && reset.includeEstimatedDiffHeights;
132
+ if (reset?.metrics != null) {
133
+ this.metrics = computeVirtualFileMetrics(reset.metrics);
134
+ shouldResetLayoutCache = true;
135
+ includeEstimatedHeights = true;
136
+ }
137
+ const { collapsed = false } = this.options;
138
+ if (this.currentCollapsed !== collapsed) {
139
+ this.currentCollapsed = collapsed;
140
+ shouldResetLayoutCache = true;
141
+ }
142
+ if (shouldResetLayoutCache) this.resetLayoutCache({ includeEstimatedHeights });
106
143
  this.fileDiff = fileDiff;
107
- this.top = this.getVirtualizedTop();
144
+ this.top = top;
108
145
  this.computeApproximateSize();
109
146
  return this.height;
110
147
  }
@@ -115,9 +152,8 @@ var VirtualizedFileDiff = class extends FileDiff {
115
152
  const { disableFileHeader = false, expandUnchanged = false, collapsed = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options;
116
153
  const diffStyle = this.getDiffStyle();
117
154
  const hunkSeparators = this.getHunkSeparatorType();
118
- const hunkSeparatorHeight = this.getHunkSeparatorHeight(hunkSeparators);
119
- const separatorGap = this.getSeparatorGap(hunkSeparators);
120
155
  const targetLineIndex = diffStyle === "split" ? targetLineIndexes[1] : targetLineIndexes[0];
156
+ this.approximateLayoutCheckpoints();
121
157
  const checkpoint = this.getLayoutCheckpointBeforeLineIndex(targetLineIndex);
122
158
  let top = checkpoint?.top ?? getVirtualFileHeaderRegion(this.metrics, disableFileHeader);
123
159
  if (collapsed) return {
@@ -134,16 +170,24 @@ var VirtualizedFileDiff = class extends FileDiff {
134
170
  callback: ({ hunkIndex, hunk, collapsedBefore, collapsedAfter, deletionLine, additionLine }) => {
135
171
  const lineIndex = diffStyle === "split" ? additionLine?.splitLineIndex ?? deletionLine?.splitLineIndex : additionLine?.unifiedLineIndex ?? deletionLine?.unifiedLineIndex;
136
172
  if (lineIndex == null) throw new Error("VirtualizedFileDiff.getLinePosition: missing line index data");
137
- if (collapsedBefore > 0 && this.hasLeadingHunkSeparator(hunkIndex, hunk?.hunkSpecs, hunkSeparators)) {
138
- if (hunkIndex > 0) top += separatorGap;
139
- if (targetLineIndex >= lineIndex - collapsedBefore && targetLineIndex < lineIndex) {
140
- position = {
141
- top,
142
- height: hunkSeparatorHeight
143
- };
144
- return true;
173
+ if (collapsedBefore > 0) {
174
+ const separator = getLeadingHunkSeparatorLayout({
175
+ type: hunkSeparators,
176
+ metrics: this.metrics,
177
+ hunkIndex,
178
+ hunkSpecs: hunk?.hunkSpecs
179
+ });
180
+ if (separator != null) {
181
+ top += separator.gapBefore;
182
+ if (targetLineIndex >= lineIndex - collapsedBefore && targetLineIndex < lineIndex) {
183
+ position = {
184
+ top,
185
+ height: separator.height
186
+ };
187
+ return true;
188
+ }
189
+ top += separator.height + separator.gapAfter;
145
190
  }
146
- top += hunkSeparatorHeight + separatorGap;
147
191
  }
148
192
  const lineHeight = this.getLineHeight(lineIndex, (additionLine?.noEOFCR ?? false) || (deletionLine?.noEOFCR ?? false));
149
193
  if (lineIndex === targetLineIndex) {
@@ -154,15 +198,21 @@ var VirtualizedFileDiff = class extends FileDiff {
154
198
  return true;
155
199
  }
156
200
  top += lineHeight;
157
- if (collapsedAfter > 0 && this.hasTrailingHunkSeparator(hunkSeparators)) {
158
- if (targetLineIndex > lineIndex && targetLineIndex <= lineIndex + collapsedAfter) {
159
- position = {
160
- top: top + separatorGap,
161
- height: hunkSeparatorHeight
162
- };
163
- return true;
201
+ if (collapsedAfter > 0) {
202
+ const separator = getTrailingHunkSeparatorLayout({
203
+ type: hunkSeparators,
204
+ metrics: this.metrics
205
+ });
206
+ if (separator != null) {
207
+ if (targetLineIndex > lineIndex && targetLineIndex <= lineIndex + collapsedAfter) {
208
+ position = {
209
+ top: top + separator.gapBefore,
210
+ height: separator.height
211
+ };
212
+ return true;
213
+ }
214
+ top += separator.totalHeight;
164
215
  }
165
- top += separatorGap + hunkSeparatorHeight;
166
216
  }
167
217
  return false;
168
218
  }
@@ -175,8 +225,7 @@ var VirtualizedFileDiff = class extends FileDiff {
175
225
  if (collapsed) return;
176
226
  const diffStyle = this.getDiffStyle();
177
227
  const hunkSeparators = this.getHunkSeparatorType();
178
- const hunkSeparatorHeight = this.getHunkSeparatorHeight(hunkSeparators);
179
- const separatorGap = this.getSeparatorGap(hunkSeparators);
228
+ this.approximateLayoutCheckpoints();
180
229
  const checkpoint = this.getLayoutCheckpointBeforeTop(localViewportTop);
181
230
  let top = checkpoint?.top ?? getVirtualFileHeaderRegion(this.metrics, disableFileHeader);
182
231
  let anchor;
@@ -189,9 +238,14 @@ var VirtualizedFileDiff = class extends FileDiff {
189
238
  callback: ({ hunkIndex, hunk, collapsedBefore, collapsedAfter, deletionLine, additionLine }) => {
190
239
  const lineIndex = diffStyle === "split" ? additionLine?.splitLineIndex ?? deletionLine?.splitLineIndex : additionLine?.unifiedLineIndex ?? deletionLine?.unifiedLineIndex;
191
240
  if (lineIndex == null) throw new Error("VirtualizedFileDiff.getNumericScrollAnchor: missing line index data");
192
- if (collapsedBefore > 0 && this.hasLeadingHunkSeparator(hunkIndex, hunk?.hunkSpecs, hunkSeparators)) {
193
- if (hunkIndex > 0) top += separatorGap;
194
- top += hunkSeparatorHeight + separatorGap;
241
+ if (collapsedBefore > 0) {
242
+ const separator = getLeadingHunkSeparatorLayout({
243
+ type: hunkSeparators,
244
+ metrics: this.metrics,
245
+ hunkIndex,
246
+ hunkSpecs: hunk?.hunkSpecs
247
+ });
248
+ if (separator != null) top += separator.totalHeight;
195
249
  }
196
250
  if (top >= localViewportTop) {
197
251
  if (deletionLine != null) anchor = {
@@ -208,7 +262,13 @@ var VirtualizedFileDiff = class extends FileDiff {
208
262
  }
209
263
  const lineHeight = this.getLineHeight(lineIndex, (additionLine?.noEOFCR ?? false) || (deletionLine?.noEOFCR ?? false));
210
264
  top += lineHeight;
211
- if (collapsedAfter > 0 && this.hasTrailingHunkSeparator(hunkSeparators)) top += separatorGap + hunkSeparatorHeight;
265
+ if (collapsedAfter > 0) {
266
+ const separator = getTrailingHunkSeparatorLayout({
267
+ type: hunkSeparators,
268
+ metrics: this.metrics
269
+ });
270
+ if (separator != null) top += separator.totalHeight;
271
+ }
212
272
  return false;
213
273
  }
214
274
  });
@@ -233,15 +293,14 @@ var VirtualizedFileDiff = class extends FileDiff {
233
293
  }
234
294
  cleanUp(recycle = false) {
235
295
  if (this.fileContainer != null && this.isSimpleMode()) this.getSimpleVirtualizer()?.disconnect(this.fileContainer);
236
- if (!recycle) this.resetLayoutCache();
296
+ if (!recycle) this.resetLayoutCache({ includeEstimatedHeights: true });
237
297
  this.isSetup = false;
238
298
  super.cleanUp(recycle);
239
299
  }
240
300
  expandHunk = (hunkIndex, direction, expansionLineCountOverride) => {
241
301
  this.hunksRenderer.expandHunk(hunkIndex, direction, expansionLineCountOverride);
242
- this.layoutDirty = true;
302
+ this.resetLayoutCache({ includeEstimatedHeights: true });
243
303
  this.computeApproximateSize();
244
- this.renderRange = void 0;
245
304
  this.virtualizer.instanceChanged(this, true);
246
305
  };
247
306
  setVisibility(visible) {
@@ -271,52 +330,53 @@ var VirtualizedFileDiff = class extends FileDiff {
271
330
  this.layoutDirty = false;
272
331
  return;
273
332
  }
274
- const { disableFileHeader = false, expandUnchanged = false, collapsed = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options;
275
- const diffStyle = this.getDiffStyle();
276
- const hunkSeparators = this.getHunkSeparatorType();
277
- const hunkSeparatorHeight = this.getHunkSeparatorHeight(hunkSeparators);
278
- const separatorGap = this.getSeparatorGap(hunkSeparators);
333
+ const { disableFileHeader = false, collapsed = false } = this.options;
279
334
  const headerRegion = getVirtualFileHeaderRegion(this.metrics, disableFileHeader);
280
- const paddingBottom = getVirtualFilePaddingBottom(this.metrics);
281
335
  this.height += headerRegion;
282
336
  if (collapsed) {
283
337
  this.layoutDirty = false;
284
338
  return;
285
339
  }
286
- let renderedLineIndex = 0;
287
- iterateOverDiff({
288
- diff: this.fileDiff,
289
- diffStyle,
290
- expandedHunks: expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap(),
291
- collapsedContextThreshold,
292
- callback: ({ hunkIndex, hunk, collapsedBefore, collapsedAfter, deletionLine, additionLine }) => {
293
- const splitLineIndex = additionLine != null ? additionLine.splitLineIndex : deletionLine.splitLineIndex;
294
- const unifiedLineIndex = additionLine != null ? additionLine.unifiedLineIndex : deletionLine.unifiedLineIndex;
295
- const hasMetadata = (additionLine?.noEOFCR ?? false) || (deletionLine?.noEOFCR ?? false);
296
- const lineIndex = diffStyle === "split" ? splitLineIndex : unifiedLineIndex;
297
- this.addLayoutCheckpoint(renderedLineIndex, lineIndex, this.height);
298
- if (collapsedBefore > 0 && this.hasLeadingHunkSeparator(hunkIndex, hunk?.hunkSpecs, hunkSeparators)) {
299
- if (hunkIndex > 0) this.height += separatorGap;
300
- this.height += hunkSeparatorHeight + separatorGap;
301
- }
302
- this.height += this.getLineHeight(lineIndex, hasMetadata);
303
- if (collapsedAfter > 0 && this.hasTrailingHunkSeparator(hunkSeparators)) this.height += separatorGap + hunkSeparatorHeight;
304
- renderedLineIndex++;
305
- }
306
- });
307
- this.cache.totalLines = renderedLineIndex;
308
- if (this.fileDiff.hunks.length > 0) this.height += paddingBottom;
309
- if (this.fileContainer != null && shouldValidateSize && !isFirstCompute) {
310
- const rect = this.fileContainer.getBoundingClientRect();
311
- if (rect.height !== this.height) console.log("VirtualizedFileDiff.computeApproximateSize: computed height doesnt match", {
312
- name: this.fileDiff.name,
313
- elementHeight: rect.height,
314
- computedHeight: this.height
315
- });
316
- else console.log("VirtualizedFileDiff.computeApproximateSize: computed height IS CORRECT");
317
- }
340
+ this.height = this.getActiveEstimatedHeight() + this.cache.measuredHeightDeltaTotal;
341
+ if (shouldValidateSize && !isFirstCompute) this.validateComputedHeight();
318
342
  this.layoutDirty = false;
319
343
  }
344
+ getActiveEstimatedHeight() {
345
+ this.ensureEstimatedDiffHeights();
346
+ const estimatedHeight = this.getDiffStyle() === "split" ? this.cache.estimatedSplitHeight : this.cache.estimatedUnifiedHeight;
347
+ if (estimatedHeight == null) throw new Error("VirtualizedFileDiff.getActiveEstimatedHeight: missing estimated height");
348
+ return estimatedHeight;
349
+ }
350
+ ensureEstimatedDiffHeights() {
351
+ if (this.fileDiff == null) {
352
+ this.cache.estimatedSplitHeight = void 0;
353
+ this.cache.estimatedUnifiedHeight = void 0;
354
+ return;
355
+ }
356
+ if (this.cache.estimatedSplitHeight != null && this.cache.estimatedUnifiedHeight != null) return;
357
+ const { disableFileHeader = false, expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options;
358
+ const { splitHeight, unifiedHeight } = computeEstimatedDiffHeights({
359
+ fileDiff: this.fileDiff,
360
+ metrics: this.metrics,
361
+ disableFileHeader,
362
+ hunkSeparators: this.getHunkSeparatorType(),
363
+ expandUnchanged,
364
+ expandedHunks: this.hunksRenderer.getExpandedHunksMap(),
365
+ collapsedContextThreshold
366
+ });
367
+ this.cache.estimatedSplitHeight = splitHeight;
368
+ this.cache.estimatedUnifiedHeight = unifiedHeight;
369
+ }
370
+ validateComputedHeight() {
371
+ if (this.fileContainer == null || this.fileDiff == null) return;
372
+ const rect = this.fileContainer.getBoundingClientRect();
373
+ if (rect.height !== this.height) console.log("VirtualizedFileDiff.computeApproximateSize: computed height doesnt match", {
374
+ name: this.fileDiff.name,
375
+ elementHeight: rect.height,
376
+ computedHeight: this.height
377
+ });
378
+ else console.log("VirtualizedFileDiff.computeApproximateSize: computed height IS CORRECT");
379
+ }
320
380
  render({ fileContainer, oldFile, newFile, fileDiff, forceRender = false,...props } = {}) {
321
381
  const { forceRenderOverride, isSetup } = this;
322
382
  this.forceRenderOverride = void 0;
@@ -380,31 +440,92 @@ var VirtualizedFileDiff = class extends FileDiff {
380
440
  getHunkSeparatorType() {
381
441
  return getOptionHunkSeparatorType(this.options.hunkSeparators);
382
442
  }
383
- getHunkSeparatorHeight(type = this.getHunkSeparatorType()) {
384
- return this.metrics.hunkSeparatorHeight ?? getDefaultHunkSeparatorHeight(type);
385
- }
386
- getSeparatorGap(type = this.getHunkSeparatorType()) {
387
- return type === "simple" || type === "metadata" || type === "line-info-basic" ? 0 : this.metrics.spacing;
388
- }
389
- hasLeadingHunkSeparator(hunkIndex, hunkSpecs, type = this.getHunkSeparatorType()) {
390
- switch (type) {
391
- case "simple": return hunkIndex > 0;
392
- case "metadata": return hunkSpecs != null;
393
- case "line-info":
394
- case "line-info-basic":
395
- case "custom": return true;
443
+ approximateLayoutCheckpoints() {
444
+ if (this.cache.checkpoints.length > 0 || this.fileDiff == null || this.fileDiff.hunks.length === 0 || this.options.collapsed === true) return;
445
+ const { disableFileHeader = false, expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options;
446
+ const diffStyle = this.getDiffStyle();
447
+ const hunkSeparators = this.getHunkSeparatorType();
448
+ const expandedHunks = expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap();
449
+ const heightDeltaPrefix = createHeightDeltaPrefix(this.cache.heightDeltas);
450
+ let top = getVirtualFileHeaderRegion(this.metrics, disableFileHeader);
451
+ let renderedLineIndex = 0;
452
+ const processRows = ({ rowCount, startLineIndex, preSeparatorHeight = 0, postSeparatorHeight = 0, metadataOffsets = [] }) => {
453
+ if (rowCount <= 0) return;
454
+ const blockStart = renderedLineIndex;
455
+ const blockEnd = renderedLineIndex + rowCount;
456
+ let nextCheckpoint = getNextCheckpointIndex(blockStart);
457
+ while (nextCheckpoint < blockEnd) {
458
+ const offset = nextCheckpoint - blockStart;
459
+ const checkpointTop = top + (offset > 0 ? preSeparatorHeight : 0) + offset * this.metrics.lineHeight + countMetadataOffsetsBefore(metadataOffsets, offset) * this.metrics.lineHeight + sumHeightDeltas(heightDeltaPrefix, startLineIndex, startLineIndex + offset);
460
+ this.cache.checkpoints.push({
461
+ renderedLineIndex: nextCheckpoint,
462
+ lineIndex: startLineIndex + offset,
463
+ top: checkpointTop
464
+ });
465
+ nextCheckpoint += LAYOUT_CHECKPOINT_INTERVAL;
466
+ }
467
+ top += preSeparatorHeight + rowCount * this.metrics.lineHeight + metadataOffsets.length * this.metrics.lineHeight + sumHeightDeltas(heightDeltaPrefix, startLineIndex, startLineIndex + rowCount) + postSeparatorHeight;
468
+ renderedLineIndex = blockEnd;
469
+ };
470
+ for (let hunkIndex = 0; hunkIndex < this.fileDiff.hunks.length; hunkIndex++) {
471
+ const hunk = this.fileDiff.hunks[hunkIndex];
472
+ if (hunk == null) throw new Error("VirtualizedFileDiff.approximateLayoutCheckpoints: invalid hunk index");
473
+ const leadingRegion = getExpandedRegion({
474
+ isPartial: this.fileDiff.isPartial,
475
+ rangeSize: hunk.collapsedBefore,
476
+ expandedHunks,
477
+ hunkIndex,
478
+ collapsedContextThreshold
479
+ });
480
+ const leadingSeparatorHeight = leadingRegion.collapsedLines > 0 ? getLeadingHunkSeparatorLayout({
481
+ type: hunkSeparators,
482
+ metrics: this.metrics,
483
+ hunkIndex,
484
+ hunkSpecs: hunk.hunkSpecs
485
+ })?.totalHeight ?? 0 : 0;
486
+ processRows({
487
+ rowCount: leadingRegion.fromStart,
488
+ startLineIndex: (diffStyle === "split" ? hunk.splitLineStart : hunk.unifiedLineStart) - leadingRegion.rangeSize
489
+ });
490
+ let pendingLeadingSeparatorHeight = leadingSeparatorHeight;
491
+ processRows({
492
+ rowCount: leadingRegion.fromEnd,
493
+ startLineIndex: (diffStyle === "split" ? hunk.splitLineStart : hunk.unifiedLineStart) - leadingRegion.fromEnd,
494
+ preSeparatorHeight: pendingLeadingSeparatorHeight
495
+ });
496
+ if (leadingRegion.fromEnd > 0) pendingLeadingSeparatorHeight = 0;
497
+ const trailingRegion = getTrailingExpandedRegion({
498
+ fileDiff: this.fileDiff,
499
+ hunk,
500
+ hunkIndex,
501
+ expandedHunks,
502
+ collapsedContextThreshold
503
+ });
504
+ const trailingSeparatorHeight = trailingRegion != null && trailingRegion.collapsedLines > 0 ? getTrailingHunkSeparatorLayout({
505
+ type: hunkSeparators,
506
+ metrics: this.metrics
507
+ })?.totalHeight ?? 0 : 0;
508
+ const trailingExpandedCount = trailingRegion != null ? trailingRegion.fromStart + trailingRegion.fromEnd : 0;
509
+ const hunkBodyRowCount = diffStyle === "split" ? hunk.splitLineCount : hunk.unifiedLineCount;
510
+ const hunkBodyStartLineIndex = diffStyle === "split" ? hunk.splitLineStart : hunk.unifiedLineStart;
511
+ processRows({
512
+ rowCount: hunkBodyRowCount,
513
+ startLineIndex: hunkBodyStartLineIndex,
514
+ preSeparatorHeight: pendingLeadingSeparatorHeight,
515
+ postSeparatorHeight: trailingExpandedCount === 0 ? trailingSeparatorHeight : 0,
516
+ metadataOffsets: getHunkMetadataOffsets({
517
+ diffStyle,
518
+ hunk,
519
+ rowCount: hunkBodyRowCount
520
+ })
521
+ });
522
+ if (trailingRegion != null && trailingExpandedCount > 0) processRows({
523
+ rowCount: trailingExpandedCount,
524
+ startLineIndex: hunkBodyStartLineIndex + hunkBodyRowCount,
525
+ postSeparatorHeight: trailingSeparatorHeight
526
+ });
396
527
  }
397
- }
398
- hasTrailingHunkSeparator(type = this.getHunkSeparatorType()) {
399
- return type !== "simple" && type !== "metadata";
400
- }
401
- addLayoutCheckpoint(renderedLineIndex, lineIndex, top) {
402
- if (renderedLineIndex % LAYOUT_CHECKPOINT_INTERVAL !== 0) return;
403
- this.cache.checkpoints.push({
404
- renderedLineIndex,
405
- lineIndex,
406
- top
407
- });
528
+ this.cache.totalLines = renderedLineIndex;
408
529
  }
409
530
  getLayoutCheckpointBeforeLineIndex(lineIndex) {
410
531
  if (lineIndex <= 0 || this.cache.checkpoints.length === 0) return;
@@ -442,43 +563,25 @@ var VirtualizedFileDiff = class extends FileDiff {
442
563
  if (checkpoint.renderedLineIndex % hunkLineCount === 0) return checkpoint;
443
564
  }
444
565
  }
445
- getExpandedRegion(isPartial, hunkIndex, rangeSize) {
446
- if (rangeSize <= 0 || isPartial) return {
447
- fromStart: 0,
448
- fromEnd: 0,
449
- collapsedLines: Math.max(rangeSize, 0),
450
- renderAll: false
451
- };
452
- const { expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options;
453
- if (expandUnchanged || rangeSize <= collapsedContextThreshold) return {
454
- fromStart: rangeSize,
455
- fromEnd: 0,
456
- collapsedLines: 0,
457
- renderAll: true
458
- };
459
- const region = this.hunksRenderer.getExpandedHunk(hunkIndex);
460
- const fromStart = Math.min(Math.max(region.fromStart, 0), rangeSize);
461
- const fromEnd = Math.min(Math.max(region.fromEnd, 0), rangeSize);
462
- const expandedCount = fromStart + fromEnd;
463
- const renderAll = expandedCount >= rangeSize;
464
- return {
465
- fromStart,
466
- fromEnd,
467
- collapsedLines: Math.max(rangeSize - expandedCount, 0),
468
- renderAll
469
- };
470
- }
471
566
  getExpandedLineCount(fileDiff, diffStyle) {
472
567
  let count = 0;
473
568
  if (fileDiff.isPartial) {
474
569
  for (const hunk of fileDiff.hunks) count += diffStyle === "split" ? hunk.splitLineCount : hunk.unifiedLineCount;
475
570
  return count;
476
571
  }
572
+ const { expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options;
573
+ const expandedHunks = expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap();
477
574
  for (const [hunkIndex, hunk] of fileDiff.hunks.entries()) {
478
575
  const hunkCount = diffStyle === "split" ? hunk.splitLineCount : hunk.unifiedLineCount;
479
576
  count += hunkCount;
480
577
  const collapsedBefore = Math.max(hunk.collapsedBefore, 0);
481
- const { fromStart, fromEnd, renderAll } = this.getExpandedRegion(fileDiff.isPartial, hunkIndex, collapsedBefore);
578
+ const { fromStart, fromEnd, renderAll } = getExpandedRegion({
579
+ isPartial: fileDiff.isPartial,
580
+ rangeSize: collapsedBefore,
581
+ expandedHunks,
582
+ hunkIndex,
583
+ collapsedContextThreshold
584
+ });
482
585
  if (collapsedBefore > 0) count += renderAll ? collapsedBefore : fromStart + fromEnd;
483
586
  }
484
587
  const lastHunk = fileDiff.hunks.at(-1);
@@ -488,7 +591,13 @@ var VirtualizedFileDiff = class extends FileDiff {
488
591
  if (lastHunk != null && additionRemaining !== deletionRemaining) throw new Error(`VirtualizedFileDiff: trailing context mismatch (additions=${additionRemaining}, deletions=${deletionRemaining}) for ${fileDiff.name}`);
489
592
  const trailingRangeSize = Math.min(additionRemaining, deletionRemaining);
490
593
  if (lastHunk != null && trailingRangeSize > 0) {
491
- const { fromStart, renderAll } = this.getExpandedRegion(fileDiff.isPartial, fileDiff.hunks.length, trailingRangeSize);
594
+ const { fromStart, renderAll } = getExpandedRegion({
595
+ isPartial: fileDiff.isPartial,
596
+ rangeSize: trailingRangeSize,
597
+ expandedHunks,
598
+ hunkIndex: fileDiff.hunks.length,
599
+ collapsedContextThreshold
600
+ });
492
601
  count += renderAll ? trailingRangeSize : fromStart;
493
602
  }
494
603
  }
@@ -499,9 +608,8 @@ var VirtualizedFileDiff = class extends FileDiff {
499
608
  const { hunkLineCount, lineHeight } = this.metrics;
500
609
  const diffStyle = this.getDiffStyle();
501
610
  const hunkSeparators = this.getHunkSeparatorType();
502
- const hunkSeparatorHeight = this.getHunkSeparatorHeight(hunkSeparators);
503
611
  const fileHeight = this.height;
504
- const lineCount = this.cache.totalLines > 0 ? this.cache.totalLines : this.getExpandedLineCount(fileDiff, diffStyle);
612
+ let lineCount = this.cache.totalLines > 0 ? this.cache.totalLines : this.getExpandedLineCount(fileDiff, diffStyle);
505
613
  const headerRegion = getVirtualFileHeaderRegion(this.metrics, disableFileHeader);
506
614
  const paddingBottom = fileDiff.hunks.length > 0 ? getVirtualFilePaddingBottom(this.metrics) : 0;
507
615
  if (fileTop < top - fileHeight || fileTop > bottom) return {
@@ -516,13 +624,14 @@ var VirtualizedFileDiff = class extends FileDiff {
516
624
  bufferBefore: 0,
517
625
  bufferAfter: 0
518
626
  };
627
+ this.approximateLayoutCheckpoints();
628
+ lineCount = this.cache.totalLines > 0 ? this.cache.totalLines : lineCount;
519
629
  const estimatedTargetLines = Math.ceil(Math.max(bottom - top, 0) / lineHeight);
520
630
  const totalLines = Math.ceil(estimatedTargetLines / hunkLineCount) * hunkLineCount + hunkLineCount;
521
631
  const totalHunks = totalLines / hunkLineCount;
522
632
  const overflowHunks = totalHunks;
523
633
  const hunkOffsets = [];
524
634
  const viewportCenter = (top + bottom) / 2;
525
- const separatorGap = this.getSeparatorGap(hunkSeparators);
526
635
  const checkpoint = this.getLayoutCheckpointBeforeTop(Math.max(0, top - fileTop - totalLines * lineHeight * 2), hunkLineCount);
527
636
  let absoluteLineTop = fileTop + (checkpoint?.top ?? headerRegion);
528
637
  let currentLine = checkpoint?.renderedLineIndex ?? 0;
@@ -539,7 +648,12 @@ var VirtualizedFileDiff = class extends FileDiff {
539
648
  const splitLineIndex = additionLine != null ? additionLine.splitLineIndex : deletionLine.splitLineIndex;
540
649
  const unifiedLineIndex = additionLine != null ? additionLine.unifiedLineIndex : deletionLine.unifiedLineIndex;
541
650
  const hasMetadata = (additionLine?.noEOFCR ?? false) || (deletionLine?.noEOFCR ?? false);
542
- const gapAdjustment = collapsedBefore > 0 && this.hasLeadingHunkSeparator(hunkIndex, hunk?.hunkSpecs, hunkSeparators) ? hunkSeparatorHeight + separatorGap + (hunkIndex > 0 ? separatorGap : 0) : 0;
651
+ const gapAdjustment = (collapsedBefore > 0 ? getLeadingHunkSeparatorLayout({
652
+ type: hunkSeparators,
653
+ metrics: this.metrics,
654
+ hunkIndex,
655
+ hunkSpecs: hunk?.hunkSpecs
656
+ }) : void 0)?.totalHeight ?? 0;
543
657
  absoluteLineTop += gapAdjustment;
544
658
  const isAtHunkBoundary = currentLine % hunkLineCount === 0;
545
659
  const currentHunk = Math.floor(currentLine / hunkLineCount);
@@ -556,7 +670,10 @@ var VirtualizedFileDiff = class extends FileDiff {
556
670
  if (overflowCounter == null && absoluteLineTop >= bottom && isAtHunkBoundary) overflowCounter = overflowHunks;
557
671
  currentLine++;
558
672
  absoluteLineTop += lineHeight$1;
559
- if (collapsedAfter > 0 && this.hasTrailingHunkSeparator(hunkSeparators)) absoluteLineTop += hunkSeparatorHeight + separatorGap;
673
+ if (collapsedAfter > 0) absoluteLineTop += getTrailingHunkSeparatorLayout({
674
+ type: hunkSeparators,
675
+ metrics: this.metrics
676
+ })?.totalHeight ?? 0;
560
677
  return false;
561
678
  }
562
679
  });
@@ -582,9 +699,79 @@ var VirtualizedFileDiff = class extends FileDiff {
582
699
  };
583
700
  }
584
701
  };
702
+ function createHeightDeltaPrefix(heightDeltas) {
703
+ const entries = Array.from(heightDeltas).sort((a, b) => a[0] - b[0]);
704
+ const lineIndexes = [];
705
+ const prefixTotals = [0];
706
+ let total = 0;
707
+ for (const [lineIndex, delta] of entries) {
708
+ lineIndexes.push(lineIndex);
709
+ total += delta;
710
+ prefixTotals.push(total);
711
+ }
712
+ return {
713
+ lineIndexes,
714
+ prefixTotals
715
+ };
716
+ }
717
+ function sumHeightDeltas({ lineIndexes, prefixTotals }, startLineIndex, endLineIndex) {
718
+ if (startLineIndex >= endLineIndex || lineIndexes.length === 0) return 0;
719
+ const start = lowerBound(lineIndexes, startLineIndex);
720
+ return (prefixTotals[lowerBound(lineIndexes, endLineIndex)] ?? 0) - (prefixTotals[start] ?? 0);
721
+ }
722
+ function lowerBound(values, target) {
723
+ let low = 0;
724
+ let high = values.length;
725
+ while (low < high) {
726
+ const mid = low + high >> 1;
727
+ const value = values[mid];
728
+ if (value == null) throw new Error("VirtualizedFileDiff: invalid prefix index");
729
+ if (value < target) low = mid + 1;
730
+ else high = mid;
731
+ }
732
+ return low;
733
+ }
734
+ function getNextCheckpointIndex(renderedLineIndex) {
735
+ return Math.ceil(renderedLineIndex / LAYOUT_CHECKPOINT_INTERVAL) * LAYOUT_CHECKPOINT_INTERVAL;
736
+ }
737
+ function countMetadataOffsetsBefore(metadataOffsets, offset) {
738
+ let count = 0;
739
+ for (const metadataOffset of metadataOffsets) if (metadataOffset < offset) count++;
740
+ return count;
741
+ }
742
+ function getHunkMetadataOffsets({ diffStyle, hunk, rowCount }) {
743
+ if (rowCount <= 0 || !hunk.noEOFCRAdditions && !hunk.noEOFCRDeletions) return [];
744
+ const lastContent = hunk.hunkContent.at(-1);
745
+ if (lastContent == null) return [];
746
+ if (lastContent.type === "context") return [rowCount - 1];
747
+ const splitCount = Math.max(lastContent.deletions, lastContent.additions);
748
+ const unifiedCount = lastContent.deletions + lastContent.additions;
749
+ if (diffStyle === "split") return splitCount > 0 && (hunk.noEOFCRAdditions || hunk.noEOFCRDeletions) ? [rowCount - 1] : [];
750
+ const offsets = [];
751
+ const contentStartOffset = rowCount - unifiedCount;
752
+ if (lastContent.deletions > 0 && hunk.noEOFCRDeletions) offsets.push(contentStartOffset + lastContent.deletions - 1);
753
+ if (lastContent.additions > 0 && hunk.noEOFCRAdditions) offsets.push(rowCount - 1);
754
+ return offsets;
755
+ }
756
+ function getTrailingExpandedRegion({ fileDiff, hunk, hunkIndex, expandedHunks, collapsedContextThreshold }) {
757
+ if (hunkIndex !== fileDiff.hunks.length - 1 || !hasFinalHunk(fileDiff)) return;
758
+ const additionRemaining = fileDiff.additionLines.length - (hunk.additionLineIndex + hunk.additionCount);
759
+ const deletionRemaining = fileDiff.deletionLines.length - (hunk.deletionLineIndex + hunk.deletionCount);
760
+ if (additionRemaining !== deletionRemaining) throw new Error(`VirtualizedFileDiff: trailing context mismatch (additions=${additionRemaining}, deletions=${deletionRemaining}) for ${fileDiff.name}`);
761
+ return getExpandedRegion({
762
+ isPartial: fileDiff.isPartial,
763
+ rangeSize: Math.min(additionRemaining, deletionRemaining),
764
+ expandedHunks,
765
+ hunkIndex: fileDiff.hunks.length,
766
+ collapsedContextThreshold
767
+ });
768
+ }
585
769
  function hasDiffLayoutOptionChanged(previousOptions, nextOptions) {
586
770
  return (previousOptions.diffStyle ?? "split") !== (nextOptions.diffStyle ?? "split") || (previousOptions.overflow ?? "scroll") !== (nextOptions.overflow ?? "scroll") || (previousOptions.collapsed ?? false) !== (nextOptions.collapsed ?? false) || (previousOptions.disableLineNumbers ?? false) !== (nextOptions.disableLineNumbers ?? false) || (previousOptions.disableFileHeader ?? false) !== (nextOptions.disableFileHeader ?? false) || (previousOptions.diffIndicators ?? "bars") !== (nextOptions.diffIndicators ?? "bars") || (previousOptions.hunkSeparators ?? "line-info") !== (nextOptions.hunkSeparators ?? "line-info") || (previousOptions.expandUnchanged ?? false) !== (nextOptions.expandUnchanged ?? false) || (previousOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD) !== (nextOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD) || previousOptions.unsafeCSS !== nextOptions.unsafeCSS;
587
771
  }
772
+ function hasDiffEstimateOptionChanged(previousOptions, nextOptions) {
773
+ return (previousOptions.disableFileHeader ?? false) !== (nextOptions.disableFileHeader ?? false) || (previousOptions.hunkSeparators ?? "line-info") !== (nextOptions.hunkSeparators ?? "line-info") || (previousOptions.expandUnchanged ?? false) !== (nextOptions.expandUnchanged ?? false) || (previousOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD) !== (nextOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD);
774
+ }
588
775
  function getOptionHunkSeparatorType(hunkSeparators) {
589
776
  return typeof hunkSeparators === "function" ? "custom" : hunkSeparators ?? "line-info";
590
777
  }