@pierre/diffs 1.1.20 → 1.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/components/CodeView.d.ts +324 -0
  2. package/dist/components/CodeView.d.ts.map +1 -0
  3. package/dist/components/CodeView.js +1245 -0
  4. package/dist/components/CodeView.js.map +1 -0
  5. package/dist/components/File.d.ts +13 -12
  6. package/dist/components/File.d.ts.map +1 -1
  7. package/dist/components/File.js +68 -28
  8. package/dist/components/File.js.map +1 -1
  9. package/dist/components/FileDiff.d.ts +9 -10
  10. package/dist/components/FileDiff.d.ts.map +1 -1
  11. package/dist/components/FileDiff.js +57 -30
  12. package/dist/components/FileDiff.js.map +1 -1
  13. package/dist/components/FileStream.js +9 -3
  14. package/dist/components/FileStream.js.map +1 -1
  15. package/dist/components/UnresolvedFile.d.ts.map +1 -1
  16. package/dist/components/VirtualizedFile.d.ts +28 -5
  17. package/dist/components/VirtualizedFile.d.ts.map +1 -1
  18. package/dist/components/VirtualizedFile.js +225 -45
  19. package/dist/components/VirtualizedFile.js.map +1 -1
  20. package/dist/components/VirtualizedFileDiff.d.ts +28 -5
  21. package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
  22. package/dist/components/VirtualizedFileDiff.js +285 -49
  23. package/dist/components/VirtualizedFileDiff.js.map +1 -1
  24. package/dist/components/Virtualizer.d.ts +6 -3
  25. package/dist/components/Virtualizer.d.ts.map +1 -1
  26. package/dist/components/Virtualizer.js +4 -6
  27. package/dist/components/Virtualizer.js.map +1 -1
  28. package/dist/components/VirtulizerDevelopment.d.ts +2 -2
  29. package/dist/components/VirtulizerDevelopment.d.ts.map +1 -1
  30. package/dist/constants.d.ts +6 -2
  31. package/dist/constants.d.ts.map +1 -1
  32. package/dist/constants.js +17 -2
  33. package/dist/constants.js.map +1 -1
  34. package/dist/index.d.ts +6 -5
  35. package/dist/index.js +11 -10
  36. package/dist/managers/InteractionManager.d.ts +11 -7
  37. package/dist/managers/InteractionManager.d.ts.map +1 -1
  38. package/dist/managers/InteractionManager.js +38 -25
  39. package/dist/managers/InteractionManager.js.map +1 -1
  40. package/dist/managers/ResizeManager.d.ts +4 -4
  41. package/dist/managers/ResizeManager.d.ts.map +1 -1
  42. package/dist/managers/ResizeManager.js +89 -54
  43. package/dist/managers/ResizeManager.js.map +1 -1
  44. package/dist/managers/UniversalRenderingManager.d.ts +2 -1
  45. package/dist/managers/UniversalRenderingManager.d.ts.map +1 -1
  46. package/dist/managers/UniversalRenderingManager.js +13 -16
  47. package/dist/managers/UniversalRenderingManager.js.map +1 -1
  48. package/dist/react/CodeView.d.ts +45 -0
  49. package/dist/react/CodeView.d.ts.map +1 -0
  50. package/dist/react/CodeView.js +241 -0
  51. package/dist/react/CodeView.js.map +1 -0
  52. package/dist/react/File.d.ts +0 -1
  53. package/dist/react/File.d.ts.map +1 -1
  54. package/dist/react/File.js +2 -3
  55. package/dist/react/File.js.map +1 -1
  56. package/dist/react/FileDiff.d.ts +0 -1
  57. package/dist/react/FileDiff.d.ts.map +1 -1
  58. package/dist/react/FileDiff.js +3 -4
  59. package/dist/react/FileDiff.js.map +1 -1
  60. package/dist/react/MultiFileDiff.d.ts +0 -1
  61. package/dist/react/MultiFileDiff.d.ts.map +1 -1
  62. package/dist/react/MultiFileDiff.js +3 -4
  63. package/dist/react/MultiFileDiff.js.map +1 -1
  64. package/dist/react/PatchDiff.d.ts +0 -1
  65. package/dist/react/PatchDiff.d.ts.map +1 -1
  66. package/dist/react/PatchDiff.js +3 -4
  67. package/dist/react/PatchDiff.js.map +1 -1
  68. package/dist/react/UnresolvedFile.d.ts +0 -1
  69. package/dist/react/UnresolvedFile.d.ts.map +1 -1
  70. package/dist/react/UnresolvedFile.js +3 -4
  71. package/dist/react/UnresolvedFile.js.map +1 -1
  72. package/dist/react/constants.d.ts.map +1 -1
  73. package/dist/react/index.d.ts +3 -2
  74. package/dist/react/index.js +5 -4
  75. package/dist/react/types.d.ts +0 -8
  76. package/dist/react/types.d.ts.map +1 -1
  77. package/dist/react/utils/renderDiffChildren.d.ts +0 -2
  78. package/dist/react/utils/renderDiffChildren.d.ts.map +1 -1
  79. package/dist/react/utils/renderDiffChildren.js +3 -4
  80. package/dist/react/utils/renderDiffChildren.js.map +1 -1
  81. package/dist/react/utils/renderFileChildren.d.ts +0 -2
  82. package/dist/react/utils/renderFileChildren.d.ts.map +1 -1
  83. package/dist/react/utils/renderFileChildren.js +3 -4
  84. package/dist/react/utils/renderFileChildren.js.map +1 -1
  85. package/dist/react/utils/useFileDiffInstance.js +12 -7
  86. package/dist/react/utils/useFileDiffInstance.js.map +1 -1
  87. package/dist/react/utils/useFileInstance.js +12 -7
  88. package/dist/react/utils/useFileInstance.js.map +1 -1
  89. package/dist/react/utils/useUnresolvedFileInstance.js +6 -2
  90. package/dist/react/utils/useUnresolvedFileInstance.js.map +1 -1
  91. package/dist/renderers/DiffHunksRenderer.d.ts +2 -1
  92. package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
  93. package/dist/renderers/DiffHunksRenderer.js +35 -20
  94. package/dist/renderers/DiffHunksRenderer.js.map +1 -1
  95. package/dist/renderers/FileRenderer.d.ts +2 -1
  96. package/dist/renderers/FileRenderer.d.ts.map +1 -1
  97. package/dist/renderers/FileRenderer.js +34 -20
  98. package/dist/renderers/FileRenderer.js.map +1 -1
  99. package/dist/ssr/index.d.ts +2 -2
  100. package/dist/ssr/preloadDiffs.js +1 -1
  101. package/dist/style.js +1 -1
  102. package/dist/style.js.map +1 -1
  103. package/dist/types.d.ts +98 -3
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/utils/areManagedSnapshotsEqual.d.ts +7 -0
  106. package/dist/utils/areManagedSnapshotsEqual.d.ts.map +1 -0
  107. package/dist/utils/areManagedSnapshotsEqual.js +15 -0
  108. package/dist/utils/areManagedSnapshotsEqual.js.map +1 -0
  109. package/dist/utils/areOptionsEqual.d.ts +2 -1
  110. package/dist/utils/areOptionsEqual.d.ts.map +1 -1
  111. package/dist/utils/areOptionsEqual.js +1 -1
  112. package/dist/utils/areOptionsEqual.js.map +1 -1
  113. package/dist/utils/createFileHeaderElement.d.ts +3 -1
  114. package/dist/utils/createFileHeaderElement.d.ts.map +1 -1
  115. package/dist/utils/createFileHeaderElement.js +3 -2
  116. package/dist/utils/createFileHeaderElement.js.map +1 -1
  117. package/dist/utils/createWindowFromScrollPosition.d.ts +3 -3
  118. package/dist/utils/createWindowFromScrollPosition.d.ts.map +1 -1
  119. package/dist/utils/createWindowFromScrollPosition.js +6 -6
  120. package/dist/utils/createWindowFromScrollPosition.js.map +1 -1
  121. package/dist/utils/iterateOverDiff.d.ts +2 -1
  122. package/dist/utils/iterateOverDiff.d.ts.map +1 -1
  123. package/dist/utils/iterateOverDiff.js +135 -7
  124. package/dist/utils/iterateOverDiff.js.map +1 -1
  125. package/dist/utils/renderFileWithHighlighter.js +1 -1
  126. package/dist/utils/resolveVirtualFileMetrics.d.ts +4 -1
  127. package/dist/utils/resolveVirtualFileMetrics.d.ts.map +1 -1
  128. package/dist/utils/resolveVirtualFileMetrics.js +11 -1
  129. package/dist/utils/resolveVirtualFileMetrics.js.map +1 -1
  130. package/dist/utils/roundToDevicePixel.d.ts +14 -0
  131. package/dist/utils/roundToDevicePixel.d.ts.map +1 -0
  132. package/dist/utils/roundToDevicePixel.js +18 -0
  133. package/dist/utils/roundToDevicePixel.js.map +1 -0
  134. package/dist/worker/worker-portable.js +195 -14
  135. package/dist/worker/worker-portable.js.map +1 -1
  136. package/dist/worker/worker.js +146 -7
  137. package/dist/worker/worker.js.map +1 -1
  138. package/package.json +7 -1
  139. package/dist/components/AdvancedVirtualizedFileDiff.d.ts +0 -40
  140. package/dist/components/AdvancedVirtualizedFileDiff.d.ts.map +0 -1
  141. package/dist/components/AdvancedVirtualizedFileDiff.js +0 -140
  142. package/dist/components/AdvancedVirtualizedFileDiff.js.map +0 -1
  143. package/dist/components/AdvancedVirtualizer.d.ts +0 -38
  144. package/dist/components/AdvancedVirtualizer.d.ts.map +0 -1
  145. package/dist/components/AdvancedVirtualizer.js +0 -201
  146. package/dist/components/AdvancedVirtualizer.js.map +0 -1
@@ -0,0 +1,1245 @@
1
+ import { DEFAULT_CODE_VIEW_FILE_METRICS, DEFAULT_CODE_VIEW_METRICS, DEFAULT_SMOOTH_SCROLL_SETTINGS, DEFAULT_THEMES, DIFFS_TAG_NAME } from "../constants.js";
2
+ import { dequeueRender, queueRender } from "../managers/UniversalRenderingManager.js";
3
+ import { areObjectsEqual } from "../utils/areObjectsEqual.js";
4
+ import { areSelectionsEqual } from "../utils/areSelectionsEqual.js";
5
+ import { createWindowFromScrollPosition } from "../utils/createWindowFromScrollPosition.js";
6
+ import { roundToDevicePixel } from "../utils/roundToDevicePixel.js";
7
+ import { VirtualizedFile } from "./VirtualizedFile.js";
8
+ import { VirtualizedFileDiff } from "./VirtualizedFileDiff.js";
9
+
10
+ //#region src/components/CodeView.ts
11
+ const CODE_VIEW_DIFF_OPTION_KEYS = [
12
+ "theme",
13
+ "disableLineNumbers",
14
+ "overflow",
15
+ "themeType",
16
+ "disableFileHeader",
17
+ "disableVirtualizationBuffers",
18
+ "preferredHighlighter",
19
+ "useCSSClasses",
20
+ "useTokenTransformer",
21
+ "tokenizeMaxLineLength",
22
+ "tokenizeMaxLength",
23
+ "unsafeCSS",
24
+ "diffStyle",
25
+ "diffIndicators",
26
+ "disableBackground",
27
+ "expandUnchanged",
28
+ "collapsedContextThreshold",
29
+ "lineDiffType",
30
+ "maxLineDiffLength",
31
+ "expansionLineCount",
32
+ "lineHoverHighlight",
33
+ "enableTokenInteractionsOnWhitespace",
34
+ "enableGutterUtility",
35
+ "__debugPointerEvents",
36
+ "enableLineSelection",
37
+ "controlledSelection",
38
+ "disableErrorHandling"
39
+ ];
40
+ const CODE_VIEW_FILE_OPTION_KEYS = [
41
+ "theme",
42
+ "disableLineNumbers",
43
+ "overflow",
44
+ "themeType",
45
+ "disableFileHeader",
46
+ "disableVirtualizationBuffers",
47
+ "preferredHighlighter",
48
+ "useCSSClasses",
49
+ "useTokenTransformer",
50
+ "tokenizeMaxLineLength",
51
+ "tokenizeMaxLength",
52
+ "unsafeCSS",
53
+ "lineHoverHighlight",
54
+ "enableTokenInteractionsOnWhitespace",
55
+ "enableGutterUtility",
56
+ "__debugPointerEvents",
57
+ "enableLineSelection",
58
+ "controlledSelection",
59
+ "disableErrorHandling"
60
+ ];
61
+ const CODE_VIEW_SHARED_CALLBACK_KEYS = [
62
+ "renderCustomHeader",
63
+ "renderHeaderPrefix",
64
+ "renderHeaderMetadata",
65
+ "renderAnnotation",
66
+ "renderGutterUtility",
67
+ "onPostRender",
68
+ "onGutterUtilityClick",
69
+ "onLineClick",
70
+ "onLineNumberClick",
71
+ "onLineEnter",
72
+ "onLineLeave",
73
+ "onTokenClick",
74
+ "onTokenEnter",
75
+ "onTokenLeave"
76
+ ];
77
+ const CODE_VIEW_SELECTION_CALLBACK_KEYS = [
78
+ "onLineSelected",
79
+ "onLineSelectionStart",
80
+ "onLineSelectionChange",
81
+ "onLineSelectionEnd"
82
+ ];
83
+ const DEFAULT_POINTER_EVENTS_RESTORE_DELAY_MS = 120;
84
+ var CodeView = class CodeView {
85
+ static __STOP = false;
86
+ static __lastScrollPosition = 0;
87
+ type = "advanced";
88
+ config = {
89
+ overscrollSize: 200,
90
+ intersectionObserverMargin: 0,
91
+ resizeDebugging: false
92
+ };
93
+ items = [];
94
+ idToItem = /* @__PURE__ */ new Map();
95
+ selectedLines = null;
96
+ instanceToItem = /* @__PURE__ */ new Map();
97
+ layoutDirtyIndex;
98
+ slotCoordinator;
99
+ slotSnapshot;
100
+ scrollListeners = /* @__PURE__ */ new Set();
101
+ scrollHeight = 0;
102
+ lastContainerHeight = -1;
103
+ scrollTop = 0;
104
+ scrollDirty = true;
105
+ pointerEventsRestoreTimer;
106
+ pointerEventsDisabled = false;
107
+ height = 0;
108
+ heightDirty = true;
109
+ windowSpecs = {
110
+ top: 0,
111
+ bottom: 0
112
+ };
113
+ renderState = {
114
+ scrollTop: -1,
115
+ firstIndex: -1,
116
+ lastIndex: -1,
117
+ stickyHeight: 0,
118
+ stickyTop: -1,
119
+ stickyBottom: -1
120
+ };
121
+ pendingScrollTarget;
122
+ pendingLayoutAnchor;
123
+ scrollAnimation;
124
+ root;
125
+ resizeObserver;
126
+ container = document.createElement("div");
127
+ stickyContainer = document.createElement("div");
128
+ stickyOffset = document.createElement("div");
129
+ options;
130
+ workerManager;
131
+ isContainerManaged;
132
+ constructor(options = { theme: DEFAULT_THEMES }, workerManager, isContainerManaged = false) {
133
+ this.options = options;
134
+ this.workerManager = workerManager;
135
+ this.isContainerManaged = isContainerManaged;
136
+ this.stickyOffset.style.contain = "layout size";
137
+ this.stickyContainer.style.position = "sticky";
138
+ this.stickyContainer.style.width = "100%";
139
+ this.stickyContainer.style.contain = "layout style inline-size";
140
+ this.stickyContainer.style.isolation = "isolate";
141
+ this.stickyContainer.style.display = "flex";
142
+ this.stickyContainer.style.flexDirection = "column";
143
+ }
144
+ getViewerMetrics() {
145
+ return this.options.viewerMetrics ?? DEFAULT_CODE_VIEW_METRICS;
146
+ }
147
+ getItemMetrics() {
148
+ return this.options.itemMetrics ?? DEFAULT_CODE_VIEW_FILE_METRICS;
149
+ }
150
+ getSmoothScrollSettings() {
151
+ return this.options.smoothScrollSettings ?? DEFAULT_SMOOTH_SCROLL_SETTINGS;
152
+ }
153
+ shouldDisablePointerEvents() {
154
+ return this.options.pointerEventsOnScroll !== true;
155
+ }
156
+ clearPointerEventsTimer() {
157
+ if (this.pointerEventsRestoreTimer != null) {
158
+ clearTimeout(this.pointerEventsRestoreTimer);
159
+ this.pointerEventsRestoreTimer = void 0;
160
+ }
161
+ }
162
+ suspendPointerEvents() {
163
+ if (!this.shouldDisablePointerEvents()) return;
164
+ this.clearPointerEventsTimer();
165
+ if (!this.pointerEventsDisabled) {
166
+ this.stickyContainer.style.pointerEvents = "none";
167
+ this.pointerEventsDisabled = true;
168
+ }
169
+ this.pointerEventsRestoreTimer = setTimeout(this.restorePointerEvents, DEFAULT_POINTER_EVENTS_RESTORE_DELAY_MS);
170
+ }
171
+ restorePointerEvents = () => {
172
+ this.clearPointerEventsTimer();
173
+ if (!this.pointerEventsDisabled) return;
174
+ this.stickyContainer.style.removeProperty("pointer-events");
175
+ this.pointerEventsDisabled = false;
176
+ };
177
+ syncViewerMetrics() {
178
+ const { gap, paddingBottom, paddingTop } = this.getViewerMetrics();
179
+ this.stickyContainer.style.gap = `${gap}px`;
180
+ this.container?.style.setProperty("margin-top", `${paddingTop}px`);
181
+ this.container?.style.setProperty("margin-bottom", `${paddingBottom}px`);
182
+ }
183
+ setup(root) {
184
+ if (this.root != null) throw new Error("CodeView.setup: already setup");
185
+ this.root = root;
186
+ this.container ??= document.createElement("div");
187
+ this.container.style.contain = "layout size style";
188
+ this.syncViewerMetrics();
189
+ this.container.appendChild(this.stickyOffset);
190
+ this.container.appendChild(this.stickyContainer);
191
+ this.root.appendChild(this.container);
192
+ this.scrollDirty = true;
193
+ this.heightDirty = true;
194
+ this.resizeObserver = new ResizeObserver(this.handleResize);
195
+ this.resizeObserver.observe(this.stickyContainer);
196
+ this.root.addEventListener("scroll", this.handleScroll, { passive: true });
197
+ this.root.addEventListener("wheel", this.clearPendingScroll, { passive: true });
198
+ this.root.addEventListener("touchstart", this.clearPendingScroll, { passive: true });
199
+ this.root.addEventListener("pointerdown", this.clearPendingScroll, { passive: true });
200
+ this.root.addEventListener("keydown", this.clearPendingScroll, { passive: true });
201
+ this.resizeObserver.observe(this.root);
202
+ this.render(true);
203
+ window.__INSTANCE = this;
204
+ window.__TOGGLE = () => {
205
+ if (CodeView.__STOP) {
206
+ CodeView.__STOP = false;
207
+ this.scrollTo({
208
+ type: "position",
209
+ position: CodeView.__lastScrollPosition,
210
+ behavior: "instant"
211
+ });
212
+ } else {
213
+ CodeView.__lastScrollPosition = this.getScrollTop();
214
+ CodeView.__STOP = true;
215
+ }
216
+ };
217
+ }
218
+ reset() {
219
+ this.restorePointerEvents();
220
+ this.cleanAllRenderedItems();
221
+ this.selectedLines = null;
222
+ this.items.length = 0;
223
+ this.idToItem.clear();
224
+ this.instanceToItem.clear();
225
+ this.layoutDirtyIndex = void 0;
226
+ this.stickyContainer.textContent = "";
227
+ this.stickyOffset.style.height = "";
228
+ this.container?.style.removeProperty("height");
229
+ this.windowSpecs = {
230
+ top: 0,
231
+ bottom: 0
232
+ };
233
+ this.pendingLayoutAnchor = void 0;
234
+ this.height = 0;
235
+ this.scrollTop = 0;
236
+ this.scrollHeight = 0;
237
+ this.scrollDirty = true;
238
+ this.heightDirty = true;
239
+ this.resetRenderState();
240
+ if (!this.isContainerManaged) this.flushSlotCoordinator();
241
+ }
242
+ cleanUp() {
243
+ this.reset();
244
+ this.restorePointerEvents();
245
+ this.resizeObserver?.disconnect();
246
+ this.resizeObserver = void 0;
247
+ this.root?.removeEventListener("scroll", this.handleScroll);
248
+ this.root?.removeEventListener("wheel", this.clearPendingScroll);
249
+ this.root?.removeEventListener("touchstart", this.clearPendingScroll);
250
+ this.root?.removeEventListener("pointerdown", this.clearPendingScroll);
251
+ this.root?.removeEventListener("keydown", this.clearPendingScroll);
252
+ this.container?.remove();
253
+ this.stickyOffset.remove();
254
+ this.stickyContainer.remove();
255
+ this.stickyContainer.textContent = "";
256
+ this.root = void 0;
257
+ this.container = void 0;
258
+ }
259
+ cleanAllRenderedItems() {
260
+ if (this.renderState.firstIndex === -1) return;
261
+ for (let index = this.renderState.firstIndex; index <= this.renderState.lastIndex; index++) {
262
+ const item = this.items[index];
263
+ if (item == null) throw new Error(`CodeView.cleanAllRenderedItems: Item does not exist at index: ${index}`);
264
+ cleanRenderedItem(item);
265
+ }
266
+ }
267
+ resolveEffectiveScrollBehavior(target, destination) {
268
+ if (target.behavior !== "smooth-auto") return target.behavior ?? "instant";
269
+ return Math.abs(destination - this.getScrollTop()) <= this.getHeight() * 10 ? "smooth" : "instant";
270
+ }
271
+ scrollTo(target) {
272
+ if (this.root == null) return;
273
+ const pendingTarget = this.normalizeScrollTarget(target);
274
+ if (pendingTarget == null) return;
275
+ const destination = this.resolveScrollTargetTop(pendingTarget);
276
+ if (destination == null) return;
277
+ if (this.resolveEffectiveScrollBehavior(pendingTarget, destination) === "smooth") this.scrollAnimation ??= {
278
+ position: this.getScrollTop(),
279
+ velocity: 0,
280
+ lastTimestamp: performance.now()
281
+ };
282
+ else this.scrollAnimation = void 0;
283
+ this.suspendPointerEvents();
284
+ this.pendingLayoutAnchor = void 0;
285
+ this.pendingScrollTarget = pendingTarget;
286
+ this.render();
287
+ }
288
+ setSelectedLines(selection, options) {
289
+ this.applySelectedLines(selection, options);
290
+ }
291
+ getSelectedLines() {
292
+ return this.selectedLines;
293
+ }
294
+ clearSelectedLines(options) {
295
+ this.applySelectedLines(null, options);
296
+ }
297
+ addItem(input) {
298
+ this.addItems([input]);
299
+ this.syncSelection();
300
+ }
301
+ addItems(inputs) {
302
+ this.appendItemsInternal(inputs);
303
+ this.syncSelection();
304
+ }
305
+ setItems(items) {
306
+ if (items.length === 0) this.reset();
307
+ else if (this.items.length === 0) this.appendItemsInternal(items);
308
+ else if (!this.tryAppendItems(items)) this.reconcileItems(items);
309
+ this.syncSelection();
310
+ }
311
+ /**
312
+ * Append new records to the viewer while preserving existing layout state.
313
+ * This is the shared path for imperative adds and the append-only reconcile
314
+ * fast path, so it measures new items immediately and only triggers render
315
+ * once at the end.
316
+ */
317
+ appendItemsInternal(inputs, render = true) {
318
+ if (inputs.length === 0) return;
319
+ const viewerMetrics = this.getViewerMetrics();
320
+ let nextTop = this.items.length === 0 ? 0 : this.scrollHeight + viewerMetrics.gap;
321
+ for (let index = 0; index < inputs.length; index++) {
322
+ const input = inputs[index];
323
+ if (input == null) throw new Error("CodeView.appendItemsInternal: missing input item");
324
+ if (this.idToItem.has(input.id)) throw new Error(`CodeView.addItem: duplicate id "${input.id}"`);
325
+ const item = this.createItem(input, this.items.length, nextTop);
326
+ this.items.push(item);
327
+ this.idToItem.set(item.item.id, item);
328
+ this.instanceToItem.set(item.instance, item);
329
+ item.height = prepareItemInstance(item);
330
+ nextTop += item.height + viewerMetrics.gap;
331
+ }
332
+ this.scrollHeight = nextTop - viewerMetrics.gap;
333
+ this.scrollDirty = true;
334
+ if (render) this.render();
335
+ }
336
+ setOptions(options) {
337
+ if (options == null) return;
338
+ this.capturePendingLayoutAnchor();
339
+ const previousViewerMetrics = this.getViewerMetrics();
340
+ const previousItemMetrics = this.getItemMetrics();
341
+ this.options = options;
342
+ const nextItemMetrics = this.getItemMetrics();
343
+ const itemMetricsChanged = !areObjectsEqual(previousItemMetrics, nextItemMetrics);
344
+ if (!areObjectsEqual(previousViewerMetrics, this.getViewerMetrics())) this.syncViewerMetrics();
345
+ for (let index = 0; index < this.items.length; index++) {
346
+ const item = this.items[index];
347
+ if (item == null) throw new Error("CodeView.setOptions: invalid item index");
348
+ if (itemMetricsChanged) item.instance.setMetrics(nextItemMetrics, true);
349
+ if (item.type === "diff") item.instance.setOptions(this.createOptions(item.item));
350
+ else item.instance.setOptions(this.createOptions(item.item));
351
+ }
352
+ this.markLayoutDirtyFromIndex(0);
353
+ this.scrollDirty = true;
354
+ if (!this.isContainerManaged && this.items.length > 0) this.render();
355
+ }
356
+ capturePendingLayoutAnchor() {
357
+ if (this.root == null || this.items.length === 0 || this.pendingScrollTarget != null) return;
358
+ this.pendingLayoutAnchor = this.getScrollAnchor(this.getScrollTop());
359
+ }
360
+ render(immediate = false) {
361
+ if (CodeView.__STOP) return;
362
+ if (immediate) {
363
+ dequeueRender(this.computeRenderRangeAndEmit);
364
+ this.computeRenderRangeAndEmit();
365
+ } else queueRender(this.computeRenderRangeAndEmit);
366
+ }
367
+ instanceChanged(instance, layoutDirty) {
368
+ const item = this.instanceToItem.get(instance);
369
+ if (item == null) throw new Error("CodeView.instanceChanged: An instance has changed that is not registered");
370
+ if (layoutDirty) this.markItemLayoutDirty(item);
371
+ this.render();
372
+ }
373
+ getWindowSpecs() {
374
+ return this.windowSpecs;
375
+ }
376
+ getContainerElement() {
377
+ return this.root;
378
+ }
379
+ getRenderedItems() {
380
+ const { firstIndex, lastIndex } = this.renderState;
381
+ if (firstIndex === -1 || lastIndex === -1 || lastIndex < firstIndex) return [];
382
+ const renderedItems = [];
383
+ for (let index = firstIndex; index <= lastIndex; index++) {
384
+ const item = this.items[index];
385
+ if (item?.element == null) continue;
386
+ if (item.type === "diff") renderedItems.push({
387
+ id: item.item.id,
388
+ type: "diff",
389
+ item: item.item,
390
+ version: item.version,
391
+ element: item.element,
392
+ instance: item.instance
393
+ });
394
+ else renderedItems.push({
395
+ id: item.item.id,
396
+ type: "file",
397
+ item: item.item,
398
+ version: item.version,
399
+ element: item.element,
400
+ instance: item.instance
401
+ });
402
+ }
403
+ return renderedItems;
404
+ }
405
+ setSlotCoordinator(coordinator) {
406
+ if (coordinator === this.slotCoordinator) return false;
407
+ this.slotCoordinator = coordinator;
408
+ this.slotSnapshot = void 0;
409
+ return true;
410
+ }
411
+ getSlotSnapshot(coordinator) {
412
+ return getSlotSnapshot(this.getRenderedItems(), coordinator);
413
+ }
414
+ subscribeToScroll(listener) {
415
+ this.scrollListeners.add(listener);
416
+ return () => {
417
+ this.scrollListeners.delete(listener);
418
+ };
419
+ }
420
+ getTopForInstance(instance) {
421
+ const item = this.instanceToItem.get(instance);
422
+ if (item == null) throw new Error("CodeView.getTopForInstance: unknown virtualized instance");
423
+ return item.top;
424
+ }
425
+ getTopForItem(id) {
426
+ const item = this.idToItem.get(id);
427
+ if (item == null) return;
428
+ return item.top;
429
+ }
430
+ createItem(input, index, top) {
431
+ const itemMetrics = this.getItemMetrics();
432
+ if (input.type === "diff") return {
433
+ type: "diff",
434
+ item: input,
435
+ version: input.version,
436
+ index,
437
+ instance: new VirtualizedFileDiff(this.createOptions(input), this, itemMetrics, this.workerManager, this.isContainerManaged),
438
+ top,
439
+ height: 0,
440
+ element: void 0
441
+ };
442
+ return {
443
+ type: "file",
444
+ item: input,
445
+ version: input.version,
446
+ index,
447
+ instance: new VirtualizedFile(this.createOptions(input), this, itemMetrics, this.workerManager, this.isContainerManaged),
448
+ top,
449
+ height: 0,
450
+ element: void 0
451
+ };
452
+ }
453
+ getItemById(itemId) {
454
+ const item = this.idToItem.get(itemId);
455
+ if (item == null) console.error(`CodeView.getItemById: unknown item id "${itemId}"`);
456
+ return item;
457
+ }
458
+ getItemByMode(itemId, mode) {
459
+ const item = this.getItemById(itemId);
460
+ if (item == null) return;
461
+ if (item.type !== mode) {
462
+ console.error(`CodeView.getItemByMode: item id "${itemId}" is not a ${mode}`);
463
+ return;
464
+ }
465
+ return item;
466
+ }
467
+ applySelectedLines(selection, options) {
468
+ const { selectedLines: prevSelection } = this;
469
+ if (selection == null && prevSelection == null || selection != null && prevSelection?.id === selection.id && areSelectionsEqual(prevSelection.range, selection.range)) return;
470
+ if (prevSelection != null && prevSelection.id !== selection?.id) this.idToItem.get(prevSelection.id)?.instance.setSelectedLines(null, { notify: false });
471
+ this.selectedLines = selection;
472
+ this.idToItem.get(selection?.id ?? "")?.instance.setSelectedLines(selection?.range ?? null, options);
473
+ }
474
+ syncSelection() {
475
+ if (this.selectedLines == null) return;
476
+ const item = this.idToItem.get(this.selectedLines.id);
477
+ if (item == null) {
478
+ this.selectedLines = null;
479
+ return;
480
+ }
481
+ item.instance.setSelectedLines(this.selectedLines.range, { notify: false });
482
+ }
483
+ wrapCallbackWithContext(mode, itemId, callback) {
484
+ return (...args) => {
485
+ const item = this.getItemByMode(itemId, mode);
486
+ if (item == null) return;
487
+ return callback(...args, item);
488
+ };
489
+ }
490
+ getWrappedOptionCallback(mode, key, itemId) {
491
+ const callback = this.options[key];
492
+ if (callback == null) return;
493
+ return this.wrapCallbackWithContext(mode, itemId, callback);
494
+ }
495
+ getWrappedSelectionOptionCallback(mode, key, itemId) {
496
+ if (this.options.enableLineSelection !== true) return;
497
+ const callback = this.options[key];
498
+ return ((range) => {
499
+ const item = this.getItemByMode(itemId, mode);
500
+ if (item == null) return;
501
+ const selection = range == null ? null : {
502
+ id: itemId,
503
+ range
504
+ };
505
+ if (this.options.controlledSelection !== true) {
506
+ if (range != null || this.selectedLines?.id === itemId) this.applySelectedLines(selection, { notify: false });
507
+ }
508
+ this.options.onSelectedLinesChange?.(selection);
509
+ return callback?.(range, item);
510
+ });
511
+ }
512
+ createOptions(item) {
513
+ const { id: itemId, type: mode } = item;
514
+ const options = mode === "file" ? { stickyHeader: this.options.stickyHeaders } : {
515
+ stickyHeader: this.options.stickyHeaders,
516
+ hunkSeparators: this.options.hunkSeparators
517
+ };
518
+ const target = options;
519
+ const passThroughKeys = mode === "file" ? CODE_VIEW_FILE_OPTION_KEYS : CODE_VIEW_DIFF_OPTION_KEYS;
520
+ for (const key of passThroughKeys) {
521
+ const value = this.options[key];
522
+ if (value !== void 0) target[key] = value;
523
+ }
524
+ target.collapsed = item.collapsed === true;
525
+ for (const key of CODE_VIEW_SHARED_CALLBACK_KEYS) {
526
+ const callback = this.getWrappedOptionCallback(mode, key, itemId);
527
+ if (callback !== void 0) target[key] = callback;
528
+ }
529
+ for (const key of CODE_VIEW_SELECTION_CALLBACK_KEYS) {
530
+ const callback = this.getWrappedSelectionOptionCallback(mode, key, itemId);
531
+ if (callback !== void 0) target[key] = callback;
532
+ }
533
+ return options;
534
+ }
535
+ /**
536
+ * Track the earliest index whose measured layout may now be stale. Later
537
+ * render passes relayout from this point forward so we do not have to rebuild
538
+ * positions for the whole list after every change.
539
+ */
540
+ markLayoutDirtyFromIndex(index) {
541
+ this.layoutDirtyIndex = Math.min(this.layoutDirtyIndex ?? index, index);
542
+ }
543
+ /**
544
+ * Mark the earliest affected item as layout-dirty after an imperative change.
545
+ * Each record carries its current array index so this stays O(1) even when
546
+ * the viewer holds a very large number of items.
547
+ */
548
+ markItemLayoutDirty(item) {
549
+ if (this.items[item.index] !== item) throw new Error(`CodeView.markItemLayoutDirty: unknown item id "${item.item.id}"`);
550
+ this.markLayoutDirtyFromIndex(item.index);
551
+ }
552
+ /**
553
+ * Detect the common controlled-update case where the new list simply extends
554
+ * the existing ordered prefix. When that happens we can reuse every current
555
+ * record in place, sync any versioned payload changes, and append only the new
556
+ * tail instead of rebuilding the whole list.
557
+ */
558
+ tryAppendItems(items) {
559
+ if (items.length <= this.items.length) return false;
560
+ for (let index = 0; index < this.items.length; index++) {
561
+ const existingItem = this.items[index];
562
+ if (existingItem == null) throw new Error("CodeView.tryAppendItems: missing existing item");
563
+ const nextItem = items[index];
564
+ if (nextItem == null || existingItem.item.id !== nextItem.id || existingItem.type !== nextItem.type) return false;
565
+ }
566
+ for (let index = 0; index < this.items.length; index++) {
567
+ const existingItem = this.items[index];
568
+ if (existingItem == null) throw new Error("CodeView.tryAppendItems: missing existing item");
569
+ const nextItem = items[index];
570
+ if (nextItem == null) throw new Error("CodeView.tryAppendItems: append candidate missing prefix item");
571
+ if (this.syncItemRecord(existingItem, nextItem)) this.markLayoutDirtyFromIndex(index);
572
+ }
573
+ this.appendItemsInternal(items.slice(this.items.length), false);
574
+ this.scrollDirty = true;
575
+ this.render();
576
+ return true;
577
+ }
578
+ /**
579
+ * Reconcile a new controlled item list against the existing records by id.
580
+ * This reuses records and instances when type matches, cleans up removed
581
+ * records, rebuilds the lookup maps, and marks layout dirty whenever order,
582
+ * membership, or versioned item data changes.
583
+ */
584
+ reconcileItems(items) {
585
+ const { items: previousItems, idToItem: previousById } = this;
586
+ const removedItems = new Set(previousItems);
587
+ const nextItems = [];
588
+ const nextIdToItem = /* @__PURE__ */ new Map();
589
+ const nextInstanceToItem = /* @__PURE__ */ new Map();
590
+ let firstDirtyIndex;
591
+ for (let index = 0; index < items.length; index++) {
592
+ const input = items[index];
593
+ if (input == null) throw new Error("CodeView.reconcileItems: missing input item");
594
+ if (nextIdToItem.has(input.id)) throw new Error(`CodeView.setItems: duplicate id "${input.id}"`);
595
+ const previousItem = previousById.get(input.id);
596
+ const item = previousItem != null && previousItem.type === input.type ? previousItem : this.createItem(input, index, 0);
597
+ item.index = index;
598
+ if (previousItem != null && previousItem.type === input.type) {
599
+ removedItems.delete(previousItem);
600
+ if (this.syncItemRecord(item, input)) firstDirtyIndex = Math.min(firstDirtyIndex ?? index, index);
601
+ } else firstDirtyIndex = Math.min(firstDirtyIndex ?? index, index);
602
+ if (previousItems[index] !== item) firstDirtyIndex = Math.min(firstDirtyIndex ?? index, index);
603
+ nextItems.push(item);
604
+ nextIdToItem.set(input.id, item);
605
+ nextInstanceToItem.set(item.instance, item);
606
+ }
607
+ for (let index = 0; index < previousItems.length; index++) {
608
+ const removedItem = previousItems[index];
609
+ if (removedItem == null || !removedItems.has(removedItem)) continue;
610
+ cleanRenderedItem(removedItem);
611
+ const dirtyIndex = Math.max(nextItems.length - 1, 0);
612
+ firstDirtyIndex = Math.min(firstDirtyIndex ?? dirtyIndex, dirtyIndex);
613
+ }
614
+ if (firstDirtyIndex == null) return;
615
+ this.items = nextItems;
616
+ this.idToItem = nextIdToItem;
617
+ this.instanceToItem = nextInstanceToItem;
618
+ if (this.renderState.firstIndex >= nextItems.length) this.resetRenderState();
619
+ else if (this.renderState.lastIndex >= nextItems.length) this.renderState.lastIndex = nextItems.length - 1;
620
+ this.markLayoutDirtyFromIndex(firstDirtyIndex);
621
+ this.scrollDirty = true;
622
+ this.render();
623
+ }
624
+ /**
625
+ * Update a reused record from the latest controlled item only when its item
626
+ * version changes. Matching versions mean CodeView keeps the current record
627
+ * snapshot, which lets imperative updates remain in place until the caller
628
+ * intentionally publishes a newer version.
629
+ */
630
+ syncItemRecord(item, nextItem) {
631
+ if (item.type !== nextItem.type) throw new Error(`CodeView.syncItemRecord: type mismatch for id "${nextItem.id}"`);
632
+ if (item.version === nextItem.version) return false;
633
+ item.item = nextItem;
634
+ item.version = nextItem.version;
635
+ if (item.type === "diff") item.instance.setOptions(this.createOptions(item.item));
636
+ else item.instance.setOptions(this.createOptions(item.item));
637
+ return true;
638
+ }
639
+ /**
640
+ * Clamps a scroll position to the min/max allowable scroll range based on
641
+ * the computed total height
642
+ */
643
+ clampScrollTop(value) {
644
+ const { paddingBottom, paddingTop } = this.getViewerMetrics();
645
+ const maxScroll = Math.max(paddingTop + this.getScrollHeight() + paddingBottom - this.getHeight(), 0);
646
+ return Math.max(0, Math.min(value, maxScroll));
647
+ }
648
+ getStickyHeaderOffset() {
649
+ return this.options.stickyHeaders === true && this.options.disableFileHeader !== true ? this.getItemMetrics().diffHeaderHeight : 0;
650
+ }
651
+ getScrollTargetRect(target) {
652
+ const item = this.idToItem.get(target.id);
653
+ if (item == null) {
654
+ console.warn(`CodeView.scrollTo: unknown item id "${target.id}"`);
655
+ return;
656
+ }
657
+ if (target.type === "item") return {
658
+ top: item.top,
659
+ height: item.height
660
+ };
661
+ const linePosition = this.getLineScrollPosition(item, target);
662
+ if (linePosition == null) {
663
+ console.warn(`CodeView.scrollTo: unable to resolve line ${target.lineNumber} for item "${target.id}"`);
664
+ return;
665
+ }
666
+ return {
667
+ top: item.top + linePosition.top,
668
+ height: linePosition.height
669
+ };
670
+ }
671
+ normalizeScrollTarget(target) {
672
+ if (target.type === "position" || target.align !== "nearest") return target;
673
+ const rect = this.getScrollTargetRect(target);
674
+ if (rect == null) return;
675
+ const offset = target.offset ?? 0;
676
+ const targetTop = this.getViewerMetrics().paddingTop + rect.top;
677
+ const targetBottom = targetTop + rect.height;
678
+ const currentTop = this.getScrollTop();
679
+ const visibleTop = currentTop + (target.type === "line" ? this.getStickyHeaderOffset() : 0);
680
+ const visibleBottom = currentTop + this.getHeight();
681
+ if (targetTop - offset <= visibleTop && targetBottom + offset >= visibleBottom) return;
682
+ if (targetTop - offset < visibleTop) return {
683
+ ...target,
684
+ align: "start"
685
+ };
686
+ if (targetBottom + offset > visibleBottom) return {
687
+ ...target,
688
+ align: "end"
689
+ };
690
+ }
691
+ /**
692
+ * Resolve a target's scroll position
693
+
694
+ * Returns `undefined` when we can't resolve a target for whatever reason
695
+ */
696
+ resolveScrollTargetTop(target) {
697
+ if (target.type === "position") {
698
+ const clampedPosition = this.clampScrollTop(target.position);
699
+ return clampedPosition !== target.position ? clampedPosition : this.clampScrollTop(target.position - this.getStickyHeaderOffset());
700
+ }
701
+ const item = this.idToItem.get(target.id);
702
+ if (item == null) {
703
+ console.warn(`CodeView.scrollTo: unknown item id "${target.id}"`);
704
+ return;
705
+ }
706
+ if (target.type === "item") return this.clampScrollTop(this.resolveAlignedScrollPosition(item.top, item.height, target.align, target.offset));
707
+ const linePosition = this.getLineScrollPosition(item, target);
708
+ if (linePosition == null) {
709
+ console.warn(`CodeView.scrollTo: unable to resolve line ${target.lineNumber} for item "${target.id}"`);
710
+ return;
711
+ }
712
+ return this.clampScrollTop(this.resolveAlignedScrollPosition(item.top + linePosition.top, linePosition.height, target.align, target.offset, this.getStickyHeaderOffset()));
713
+ }
714
+ /**
715
+ * Given an existing scroll target (scroll top and height), figure out the
716
+ * correct scroll position to target based on the desired alignment, offset
717
+ * and stickyOffset if necessary
718
+ */
719
+ resolveAlignedScrollPosition(targetTop, targetHeight, align, offset = 0, stickyOffset = 0) {
720
+ targetTop += this.getViewerMetrics().paddingTop;
721
+ const viewportHeight = this.getHeight();
722
+ if (align === "center" && targetHeight + offset < viewportHeight) return targetTop - (viewportHeight - targetHeight) / 2 + offset;
723
+ if (align === "end") return targetTop - (viewportHeight - targetHeight) + offset;
724
+ return targetTop - stickyOffset - offset;
725
+ }
726
+ getLineScrollPosition(item, target) {
727
+ if (item.type === "diff") return item.instance.getLinePosition(target.lineNumber, target.side);
728
+ return item.instance.getLinePosition(target.lineNumber);
729
+ }
730
+ /**
731
+ * Determine target scroll position for current frame.
732
+ *
733
+ * If there's no pendingScrollTarget then we just return the current scroll
734
+ * position
735
+ *
736
+ * If there's a pendingScrollTarget then we depend on whether there's a
737
+ * smooth scroll animation or not. If not just return the destination, or
738
+ * compute next position given the smooth scroll spring physics
739
+ */
740
+ computeFrameScrollTop(scrollTop, frameTimestamp) {
741
+ if (this.pendingScrollTarget == null) return scrollTop;
742
+ const destination = this.resolveScrollTargetTop(this.pendingScrollTarget);
743
+ if (destination == null) return scrollTop;
744
+ const { scrollAnimation } = this;
745
+ if (scrollAnimation == null) return destination;
746
+ return this.computeSpringStep(scrollAnimation, destination, frameTimestamp).position;
747
+ }
748
+ /**
749
+ * Closed-form critical-damped ODE step.
750
+ *
751
+ * Stable at any dt (Euler would blow up once ω·dt ≳ 1), so this survives
752
+ * big RAF gaps (tab-wake, offscreen frames) and resize-driven ticks that
753
+ * fire outside the normal RAF cadence.
754
+ */
755
+ computeSpringStep(animation, destination, frameTimestamp) {
756
+ const dt = Math.max(0, frameTimestamp - animation.lastTimestamp);
757
+ const { omega } = this.getSmoothScrollSettings();
758
+ const decay = Math.exp(-omega * dt);
759
+ const displacement = animation.position - destination;
760
+ const springCoeff = animation.velocity + omega * displacement;
761
+ return {
762
+ position: destination + (displacement + springCoeff * dt) * decay,
763
+ velocity: (springCoeff * (1 - omega * dt) - omega * displacement) * decay
764
+ };
765
+ }
766
+ /**
767
+ * For any given pendingScrollTarget, updates any in flight smooth scroll
768
+ * animations and returns the target scrollTop to move towards
769
+ *
770
+ * Resolves the animation based on frame time and adopts any necessary scroll
771
+ * anchoring corrections if necessary
772
+ */
773
+ advanceScrollAnimation(frameTimestamp, anchorDelta) {
774
+ if (this.pendingScrollTarget == null) return;
775
+ const destination = this.resolveScrollTargetTop(this.pendingScrollTarget);
776
+ if (destination == null) {
777
+ this.pendingScrollTarget = void 0;
778
+ this.scrollAnimation = void 0;
779
+ return;
780
+ }
781
+ const animation = this.scrollAnimation;
782
+ if (animation == null) return destination;
783
+ animation.position += anchorDelta;
784
+ const { position, velocity } = this.computeSpringStep(animation, destination, frameTimestamp);
785
+ animation.lastTimestamp = frameTimestamp;
786
+ animation.position = position;
787
+ animation.velocity = velocity;
788
+ const { positionEpsilon, velocityEpsilon } = this.getSmoothScrollSettings();
789
+ if (Math.abs(destination - position) <= positionEpsilon && Math.abs(velocity) <= velocityEpsilon) {
790
+ animation.position = destination;
791
+ animation.velocity = 0;
792
+ this.scrollAnimation = void 0;
793
+ return destination;
794
+ }
795
+ return animation.position;
796
+ }
797
+ computeRenderRangeAndEmit = (timestamp = performance.now()) => {
798
+ if (CodeView.__STOP || this.container == null) return;
799
+ const height = this.getHeight();
800
+ let currentScrollTop = this.getScrollTop();
801
+ const currentRootScrollTop = currentScrollTop;
802
+ let recomputeScrollTop = this.pendingLayoutAnchor != null;
803
+ let anchor = this.getScrollAnchor(currentScrollTop);
804
+ if (this.layoutDirtyIndex != null) {
805
+ this.recomputeLayout(this.layoutDirtyIndex);
806
+ this.layoutDirtyIndex = void 0;
807
+ recomputeScrollTop = true;
808
+ }
809
+ if (recomputeScrollTop && anchor != null) {
810
+ const newScrollTop = this.resolveAnchoredScrollTop(anchor);
811
+ if (newScrollTop != null) {
812
+ const delta = newScrollTop - currentScrollTop;
813
+ currentScrollTop = newScrollTop;
814
+ if (this.scrollAnimation != null) this.scrollAnimation.position += delta;
815
+ }
816
+ }
817
+ if (recomputeScrollTop) currentScrollTop = this.clampScrollTop(currentScrollTop);
818
+ const frameScrollTop = this.computeFrameScrollTop(currentScrollTop, timestamp);
819
+ const fitPerfectly = !recomputeScrollTop && (this.renderState.scrollTop === -1 || Math.abs(frameScrollTop - this.renderState.scrollTop) > height + this.config.overscrollSize * 2);
820
+ let appliedScrollTop = currentRootScrollTop;
821
+ if (this.pendingScrollTarget != null && frameScrollTop !== appliedScrollTop) {
822
+ this.applyScrollFix(frameScrollTop, appliedScrollTop);
823
+ appliedScrollTop = frameScrollTop;
824
+ }
825
+ if (fitPerfectly) anchor = void 0;
826
+ this.windowSpecs = createWindowFromScrollPosition({
827
+ scrollTop: frameScrollTop,
828
+ height,
829
+ scrollHeight: this.getScrollHeight(),
830
+ fitPerfectly,
831
+ fitPerfectlyOverscroll: this.getFitPerfectlyOverscroll(),
832
+ overscrollSize: this.config.overscrollSize
833
+ });
834
+ const { top, bottom } = this.windowSpecs;
835
+ const { firstIndex, lastIndex } = this.renderState;
836
+ if (firstIndex >= 0) for (let index = firstIndex; index <= lastIndex; index++) {
837
+ const item = this.items[index];
838
+ if (item == null) throw new Error(`CodeView.computeRenderRangeAndEmit: No item at index: ${index}`);
839
+ const renderedTop = item.top;
840
+ if (!(renderedTop > top - item.height && renderedTop <= bottom)) cleanRenderedItem(item);
841
+ }
842
+ let prevElement;
843
+ const updatedItems = /* @__PURE__ */ new Set();
844
+ const startingIndex = this.findFirstVisibleIndex(top);
845
+ const lastRenderedIndex = this.findLastVisibleIndex(bottom);
846
+ for (let itemIndex = startingIndex; itemIndex <= lastRenderedIndex; itemIndex++) {
847
+ const item = this.items[itemIndex];
848
+ if (item == null) throw new Error(`CodeView.computeRenderRangeAndEmit: missing item`);
849
+ const { instance } = item;
850
+ if (item.element == null) {
851
+ item.element = document.createElement(DIFFS_TAG_NAME);
852
+ syncRenderedItemOrder(this.stickyContainer, item.element, prevElement);
853
+ instance.virtualizedSetup();
854
+ if (renderItem(item, item.element)) updatedItems.add(item);
855
+ prevElement = item.element;
856
+ } else {
857
+ syncRenderedItemOrder(this.stickyContainer, item.element, prevElement);
858
+ if (renderItem(item)) updatedItems.add(item);
859
+ prevElement = item.element;
860
+ }
861
+ }
862
+ this.renderState.firstIndex = startingIndex <= lastRenderedIndex ? startingIndex : -1;
863
+ this.renderState.lastIndex = lastRenderedIndex;
864
+ this.flushSlotCoordinator();
865
+ this.reconcileRenderedItems(updatedItems);
866
+ this.updateStickyPositioning();
867
+ const anchoredScrollTop = anchor != null ? this.resolveAnchoredScrollTop(anchor) : void 0;
868
+ if (anchor === this.pendingLayoutAnchor) this.pendingLayoutAnchor = void 0;
869
+ const anchorScrollDelta = anchoredScrollTop != null ? anchoredScrollTop - currentScrollTop : 0;
870
+ let renderedScrollTop = frameScrollTop;
871
+ if (this.pendingScrollTarget == null) {
872
+ renderedScrollTop = anchoredScrollTop ?? frameScrollTop;
873
+ if (renderedScrollTop !== appliedScrollTop) this.applyScrollFix(renderedScrollTop, appliedScrollTop);
874
+ } else if (this.pendingScrollTarget != null) {
875
+ const targetScrollTop = this.advanceScrollAnimation(timestamp, anchorScrollDelta);
876
+ if (targetScrollTop != null) {
877
+ if (targetScrollTop !== appliedScrollTop) this.applyScrollFix(targetScrollTop, appliedScrollTop);
878
+ renderedScrollTop = targetScrollTop;
879
+ if (this.pendingScrollTarget != null && this.isPendingTargetSettled(this.pendingScrollTarget)) {
880
+ this.pendingScrollTarget = void 0;
881
+ this.scrollAnimation = void 0;
882
+ }
883
+ } else renderedScrollTop = currentScrollTop;
884
+ }
885
+ this.renderState.scrollTop = roundToDevicePixel(renderedScrollTop);
886
+ const totalScrollHeight = this.getScrollHeight();
887
+ if (this.lastContainerHeight !== totalScrollHeight) {
888
+ this.container.style.height = `${totalScrollHeight}px`;
889
+ this.lastContainerHeight = totalScrollHeight;
890
+ }
891
+ this.flushManagers(updatedItems);
892
+ if (fitPerfectly || this.scrollAnimation != null) this.render();
893
+ };
894
+ flushManagers(updatedItems) {
895
+ for (const item of updatedItems) item.instance.flushManagers();
896
+ }
897
+ reconcileRenderedItems(updatedItems) {
898
+ const { firstIndex, lastIndex } = this.renderState;
899
+ if (firstIndex === -1) return;
900
+ let currentTop = -1;
901
+ let heightChanged = false;
902
+ for (let index = firstIndex; index < this.items.length; index++) {
903
+ if (!heightChanged && index > lastIndex) break;
904
+ const item = this.items[index];
905
+ if (item == null) throw new Error("CodeView.reconcileRenderedItems: Invalid item");
906
+ if (currentTop === -1) currentTop = item.top;
907
+ else if (item.top !== currentTop) {
908
+ item.top = currentTop;
909
+ item.instance.syncVirtualizedTop();
910
+ heightChanged = true;
911
+ }
912
+ if (updatedItems == null ? index <= lastIndex : updatedItems.has(item)) {
913
+ if (item.instance.reconcileHeights()) {
914
+ heightChanged = true;
915
+ item.height = item.instance.getVirtualizedHeight();
916
+ }
917
+ }
918
+ currentTop += item.instance.getVirtualizedHeight();
919
+ if (index < this.items.length - 1) currentTop += this.getViewerMetrics().gap;
920
+ }
921
+ if (heightChanged && currentTop != null) {
922
+ this.scrollDirty = true;
923
+ this.scrollHeight = currentTop;
924
+ }
925
+ }
926
+ updateStickyPositioning() {
927
+ const { firstIndex, lastIndex } = this.renderState;
928
+ const firstStickySpecs = this.items[firstIndex]?.instance.getAdvancedStickySpecs();
929
+ const lastStickySpecs = this.items[lastIndex]?.instance.getAdvancedStickySpecs();
930
+ if (firstStickySpecs == null || lastStickySpecs == null) return;
931
+ const height = this.getHeight();
932
+ const itemMetrics = this.getItemMetrics();
933
+ const stickyTop = Math.max(firstStickySpecs.topOffset, 0);
934
+ const stickyBottom = lastStickySpecs.topOffset + lastStickySpecs.height;
935
+ const stickyContainerHeight = stickyBottom - stickyTop;
936
+ if (stickyContainerHeight === this.renderState.stickyHeight && stickyTop === this.renderState.stickyTop && stickyBottom === this.renderState.stickyBottom) return;
937
+ this.renderState.stickyHeight = stickyContainerHeight;
938
+ this.renderState.stickyTop = stickyTop;
939
+ this.renderState.stickyBottom = stickyBottom;
940
+ this.stickyOffset.style.height = `${stickyTop}px`;
941
+ const randomOffset = (Math.random() * itemMetrics.lineHeight >> 0) * -1;
942
+ const stickyJitter = -Math.max(stickyContainerHeight + randomOffset, 0) + height;
943
+ this.stickyContainer.style.top = `${stickyJitter}px`;
944
+ this.stickyContainer.style.bottom = `${stickyJitter + itemMetrics.diffHeaderHeight}px`;
945
+ }
946
+ handleScroll = () => {
947
+ if (CodeView.__STOP) return;
948
+ this.suspendPointerEvents();
949
+ this.scrollDirty = true;
950
+ this.notifyScroll();
951
+ this.render();
952
+ };
953
+ clearPendingScroll = () => {
954
+ this.pendingScrollTarget = void 0;
955
+ this.pendingLayoutAnchor = void 0;
956
+ this.scrollAnimation = void 0;
957
+ };
958
+ handleResize = (entries) => {
959
+ for (const entry of entries) if (entry.target === this.stickyContainer) {
960
+ if (entry.borderBoxSize[0].blockSize !== this.renderState.stickyHeight) {
961
+ const currentScrollTop = this.getScrollTop();
962
+ const anchor = this.getScrollAnchor(currentScrollTop);
963
+ this.reconcileRenderedItems();
964
+ this.updateStickyPositioning();
965
+ const anchoredScrollTop = anchor != null ? this.resolveAnchoredScrollTop(anchor) : void 0;
966
+ if (anchoredScrollTop != null) {
967
+ const resizeAnchorDelta = anchoredScrollTop - currentScrollTop;
968
+ this.applyScrollFix(anchoredScrollTop, currentScrollTop);
969
+ if (this.scrollAnimation != null) this.scrollAnimation.position += resizeAnchorDelta;
970
+ }
971
+ if (this.pendingScrollTarget != null && this.isPendingTargetSettled(this.pendingScrollTarget)) {
972
+ this.pendingScrollTarget = void 0;
973
+ this.scrollAnimation = void 0;
974
+ }
975
+ }
976
+ } else {
977
+ this.scrollDirty = true;
978
+ this.heightDirty = true;
979
+ this.render();
980
+ }
981
+ };
982
+ /**
983
+ * Figure out scrollTop accounting for sticky header if enabled and
984
+ * necessary
985
+ */
986
+ getScrollAnchorViewportTop(absoluteItemTop, scrollTop) {
987
+ return absoluteItemTop < scrollTop ? scrollTop + this.getStickyHeaderOffset() : scrollTop;
988
+ }
989
+ /**
990
+ * Attempt to find a scroll anchor based on build in metrics of the existing
991
+ * rendered files/diff.
992
+ *
993
+ * A scroll anchor represents the first fully visible element (in other
994
+ * words, the first file or first line who's top is fully in the viewport).
995
+ */
996
+ getScrollAnchor(scrollTop) {
997
+ if (this.pendingLayoutAnchor != null) return this.pendingLayoutAnchor;
998
+ const { firstIndex, lastIndex, stickyTop, stickyBottom } = this.renderState;
999
+ if (firstIndex === -1 || lastIndex === -1) return;
1000
+ const viewportHeight = this.getHeight();
1001
+ if (stickyTop === -1 || stickyBottom === -1) return;
1002
+ for (let index = firstIndex; index <= lastIndex; index++) {
1003
+ const item = this.items[index];
1004
+ if (item == null) continue;
1005
+ const absoluteItemTop = this.getViewerMetrics().paddingTop + item.top;
1006
+ if (absoluteItemTop + item.height <= scrollTop) continue;
1007
+ if (absoluteItemTop >= scrollTop + viewportHeight) break;
1008
+ if (absoluteItemTop >= scrollTop) return {
1009
+ type: "item",
1010
+ id: item.item.id,
1011
+ viewportOffset: absoluteItemTop - scrollTop
1012
+ };
1013
+ const localViewportTop = this.getScrollAnchorViewportTop(absoluteItemTop, scrollTop) - absoluteItemTop;
1014
+ const lineAnchor = item.instance.getNumericScrollAnchor(localViewportTop);
1015
+ if (lineAnchor != null) {
1016
+ const absoluteLineTop = absoluteItemTop + lineAnchor.top;
1017
+ return {
1018
+ type: "line",
1019
+ id: item.item.id,
1020
+ lineNumber: lineAnchor.lineNumber,
1021
+ side: lineAnchor.side,
1022
+ viewportOffset: absoluteLineTop - scrollTop
1023
+ };
1024
+ }
1025
+ }
1026
+ }
1027
+ /**
1028
+ * Given a scroll anchor, attempt to resolve a newly updated (and clamped)
1029
+ * scroll position to keep the anchored element in place.
1030
+ *
1031
+ * If we can't resolve a position for whatever reason, we'll return
1032
+ * undefined.
1033
+ */
1034
+ resolveAnchoredScrollTop(anchor) {
1035
+ const item = this.idToItem.get(anchor.id);
1036
+ if (item == null) return;
1037
+ const { paddingTop } = this.getViewerMetrics();
1038
+ if (anchor.type === "item") {
1039
+ const absoluteItemTop = paddingTop + item.top;
1040
+ return this.clampScrollTop(absoluteItemTop - anchor.viewportOffset);
1041
+ }
1042
+ const linePosition = item.type === "diff" ? item.instance.getLinePosition(anchor.lineNumber, anchor.side) : item.instance.getLinePosition(anchor.lineNumber);
1043
+ if (linePosition == null) return;
1044
+ const absoluteLineTop = paddingTop + item.top + linePosition.top;
1045
+ return this.clampScrollTop(absoluteLineTop - anchor.viewportOffset);
1046
+ }
1047
+ /**
1048
+ * Apply a device-pixel-rounded scroll position if it differs from the last
1049
+ * rendered/applied scrollTop we've already recorded in renderState.
1050
+ */
1051
+ applyScrollFix(target, currentScrollTop) {
1052
+ if (this.root == null) return;
1053
+ const rounded = roundToDevicePixel(this.clampScrollTop(target));
1054
+ const roundedCurrentScrollTop = roundToDevicePixel(currentScrollTop ?? this.scrollTop);
1055
+ if (rounded === this.renderState.scrollTop && rounded === roundedCurrentScrollTop) return;
1056
+ this.suspendPointerEvents();
1057
+ if (rounded !== roundedCurrentScrollTop) this.root.scrollTo({
1058
+ top: rounded,
1059
+ behavior: "instant"
1060
+ });
1061
+ this.renderState.scrollTop = rounded;
1062
+ this.scrollTop = rounded;
1063
+ this.scrollDirty = false;
1064
+ }
1065
+ /**
1066
+ * Decide whether a pending programmatic scroll has reached its
1067
+ * destination and should be cleared.
1068
+ */
1069
+ isPendingTargetSettled(target) {
1070
+ const top = this.resolveScrollTargetTop(target);
1071
+ if (top == null) return true;
1072
+ return roundToDevicePixel(this.getScrollTop()) === roundToDevicePixel(top);
1073
+ }
1074
+ getScrollTop() {
1075
+ if (!this.scrollDirty) return this.scrollTop;
1076
+ this.scrollDirty = false;
1077
+ this.scrollTop = this.clampScrollTop(this.root?.scrollTop ?? 0);
1078
+ return this.scrollTop;
1079
+ }
1080
+ getHeight() {
1081
+ if (!this.heightDirty) return this.height;
1082
+ this.heightDirty = false;
1083
+ this.height = this.root?.getBoundingClientRect().height ?? 0;
1084
+ return this.height;
1085
+ }
1086
+ getScrollHeight() {
1087
+ return this.scrollHeight;
1088
+ }
1089
+ flushSlotCoordinator() {
1090
+ if (this.slotCoordinator == null) return;
1091
+ const { onSnapshotChange } = this.slotCoordinator;
1092
+ const slotSnapshot = getSlotSnapshot(this.getRenderedItems(), this.slotCoordinator);
1093
+ if (areSlotSnapshotsEqual(this.slotSnapshot, slotSnapshot)) return;
1094
+ this.slotSnapshot = slotSnapshot;
1095
+ onSnapshotChange(slotSnapshot);
1096
+ }
1097
+ notifyScroll() {
1098
+ if (this.scrollListeners.size === 0) return;
1099
+ const scrollTop = this.getScrollTop();
1100
+ for (const listener of this.scrollListeners) listener(scrollTop, this);
1101
+ }
1102
+ /**
1103
+ * Find the first item whose bottom edge crosses into the viewport window.
1104
+ * This lets scroll-time rendering jump directly near the visible range instead
1105
+ * of linearly scanning from the start of very large item lists.
1106
+ */
1107
+ findFirstVisibleIndex(top) {
1108
+ let low = 0;
1109
+ let high = this.items.length - 1;
1110
+ let result = this.items.length;
1111
+ while (low <= high) {
1112
+ const mid = low + high >> 1;
1113
+ const item = this.items[mid];
1114
+ if (item == null) throw new Error("CodeView.findFirstVisibleIndex: invalid item index");
1115
+ if (item.top + item.height > top) {
1116
+ result = mid;
1117
+ high = mid - 1;
1118
+ } else low = mid + 1;
1119
+ }
1120
+ return result;
1121
+ }
1122
+ /**
1123
+ * Find the last item whose top edge is still within the viewport window.
1124
+ * Paired with findFirstVisibleIndex, this bounds the render loop to only the
1125
+ * slice of items that can actually intersect the current scroll range.
1126
+ */
1127
+ findLastVisibleIndex(bottom) {
1128
+ let low = 0;
1129
+ let high = this.items.length - 1;
1130
+ let result = -1;
1131
+ while (low <= high) {
1132
+ const mid = low + high >> 1;
1133
+ const item = this.items[mid];
1134
+ if (item == null) throw new Error("CodeView.findLastVisibleIndex: invalid item index");
1135
+ if (item.top <= bottom) {
1136
+ result = mid;
1137
+ low = mid + 1;
1138
+ } else high = mid - 1;
1139
+ }
1140
+ return result;
1141
+ }
1142
+ /**
1143
+ * Recompute measured tops and heights starting from the earliest dirty item.
1144
+ * Earlier items keep their existing layout, while everything from startIndex
1145
+ * onward is remeasured so downstream positions and total scroll height stay
1146
+ * consistent after inserts, removals, or versioned item updates.
1147
+ */
1148
+ recomputeLayout(startIndex = 0) {
1149
+ if (this.items.length === 0) {
1150
+ this.scrollHeight = 0;
1151
+ return;
1152
+ }
1153
+ const viewerMetrics = this.getViewerMetrics();
1154
+ let runningTop = 0;
1155
+ if (startIndex > 0) {
1156
+ const previousItem = this.items[startIndex - 1];
1157
+ if (previousItem == null) throw new Error("CodeView.recomputeLayout: invalid dirty index");
1158
+ runningTop = previousItem.top + previousItem.height + viewerMetrics.gap;
1159
+ }
1160
+ for (let index = startIndex; index < this.items.length; index++) {
1161
+ const item = this.items[index];
1162
+ if (item == null) throw new Error("CodeView.recomputeLayout: invalid item index");
1163
+ item.top = runningTop;
1164
+ if (item.type === "diff") item.height = item.instance.prepareVirtualizedItem(item.item.fileDiff);
1165
+ else item.height = item.instance.prepareVirtualizedItem(item.item.file);
1166
+ runningTop += item.height;
1167
+ if (index < this.items.length - 1) runningTop += viewerMetrics.gap;
1168
+ }
1169
+ if (runningTop !== this.scrollHeight) this.scrollDirty = true;
1170
+ this.scrollHeight = runningTop;
1171
+ }
1172
+ resetRenderState() {
1173
+ this.renderState.scrollTop = -1;
1174
+ this.renderState.firstIndex = -1;
1175
+ this.renderState.lastIndex = -1;
1176
+ this.renderState.stickyHeight = 0;
1177
+ this.renderState.stickyTop = -1;
1178
+ this.renderState.stickyBottom = -1;
1179
+ }
1180
+ getFitPerfectlyOverscroll() {
1181
+ return this.getViewerMetrics().gap + this.getItemMetrics().diffHeaderHeight;
1182
+ }
1183
+ };
1184
+ function cleanRenderedItem(item) {
1185
+ item.instance.cleanUp(true);
1186
+ item.element?.remove();
1187
+ item.element = void 0;
1188
+ }
1189
+ function prepareItemInstance(item) {
1190
+ item.instance.cleanUp(true);
1191
+ if (item.type === "diff") return item.instance.prepareVirtualizedItem(item.item.fileDiff);
1192
+ else return item.instance.prepareVirtualizedItem(item.item.file);
1193
+ }
1194
+ function renderItem(item, fileContainer) {
1195
+ if (item.type === "diff") return item.instance.render({
1196
+ deferManagers: true,
1197
+ fileContainer,
1198
+ fileDiff: item.item.fileDiff,
1199
+ lineAnnotations: item.item.annotations
1200
+ });
1201
+ else return item.instance.render({
1202
+ deferManagers: true,
1203
+ fileContainer,
1204
+ file: item.item.file,
1205
+ lineAnnotations: item.item.annotations
1206
+ });
1207
+ }
1208
+ /**
1209
+ * Keep the rendered DOM order aligned with the current record order even when
1210
+ * we reuse existing elements. Reused items may already be mounted elsewhere in
1211
+ * the sticky container, so this moves them into the correct sibling position
1212
+ * before rendering updates.
1213
+ */
1214
+ function syncRenderedItemOrder(container, element, prevElement) {
1215
+ if (prevElement == null) {
1216
+ if (container.firstChild !== element) container.prepend(element);
1217
+ return;
1218
+ }
1219
+ if (prevElement.nextSibling !== element) prevElement.after(element);
1220
+ }
1221
+ function hasAnnotations(item) {
1222
+ return (item.annotations?.length ?? 0) > 0;
1223
+ }
1224
+ function getSlotSnapshot(renderedItems, { hasHeaderRenderers, hasAnnotationRenderer, hasGutterRenderer }) {
1225
+ if (renderedItems.length === 0) return;
1226
+ if (hasHeaderRenderers || hasGutterRenderer) return renderedItems;
1227
+ if (!hasAnnotationRenderer) return;
1228
+ const slotSnapshot = [];
1229
+ for (const renderedItem of renderedItems) if (hasAnnotations(renderedItem.item)) slotSnapshot.push(renderedItem);
1230
+ return slotSnapshot.length > 0 ? slotSnapshot : void 0;
1231
+ }
1232
+ function areSlotSnapshotsEqual(previous, next) {
1233
+ if (previous == null || next == null) return previous === next;
1234
+ if (previous.length !== next.length) return false;
1235
+ for (let index = 0; index < previous.length; index++) {
1236
+ const previousItem = previous[index];
1237
+ const nextItem = next[index];
1238
+ if (previousItem == null || nextItem == null || previousItem.id !== nextItem.id || previousItem.type !== nextItem.type || previousItem.element !== nextItem.element || previousItem.version !== nextItem.version) return false;
1239
+ }
1240
+ return true;
1241
+ }
1242
+
1243
+ //#endregion
1244
+ export { CodeView };
1245
+ //# sourceMappingURL=CodeView.js.map