@pierre/diffs 1.1.20 → 1.2.0-beta.1

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