@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.
- package/dist/components/CodeView.d.ts +325 -0
- package/dist/components/CodeView.d.ts.map +1 -0
- package/dist/components/CodeView.js +1252 -0
- package/dist/components/CodeView.js.map +1 -0
- package/dist/components/File.d.ts +13 -12
- package/dist/components/File.d.ts.map +1 -1
- package/dist/components/File.js +68 -28
- package/dist/components/File.js.map +1 -1
- package/dist/components/FileDiff.d.ts +9 -10
- package/dist/components/FileDiff.d.ts.map +1 -1
- package/dist/components/FileDiff.js +57 -30
- package/dist/components/FileDiff.js.map +1 -1
- package/dist/components/FileStream.js +9 -3
- package/dist/components/FileStream.js.map +1 -1
- package/dist/components/VirtualizedFile.d.ts +28 -5
- package/dist/components/VirtualizedFile.d.ts.map +1 -1
- package/dist/components/VirtualizedFile.js +225 -45
- package/dist/components/VirtualizedFile.js.map +1 -1
- package/dist/components/VirtualizedFileDiff.d.ts +28 -5
- package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
- package/dist/components/VirtualizedFileDiff.js +285 -49
- package/dist/components/VirtualizedFileDiff.js.map +1 -1
- package/dist/components/Virtualizer.d.ts +6 -3
- package/dist/components/Virtualizer.d.ts.map +1 -1
- package/dist/components/Virtualizer.js +4 -6
- package/dist/components/Virtualizer.js.map +1 -1
- package/dist/components/VirtulizerDevelopment.d.ts +2 -2
- package/dist/components/VirtulizerDevelopment.d.ts.map +1 -1
- package/dist/constants.d.ts +6 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +17 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +11 -10
- package/dist/managers/InteractionManager.d.ts +11 -7
- package/dist/managers/InteractionManager.d.ts.map +1 -1
- package/dist/managers/InteractionManager.js +38 -25
- package/dist/managers/InteractionManager.js.map +1 -1
- package/dist/managers/ResizeManager.d.ts +4 -4
- package/dist/managers/ResizeManager.d.ts.map +1 -1
- package/dist/managers/ResizeManager.js +89 -54
- package/dist/managers/ResizeManager.js.map +1 -1
- package/dist/managers/UniversalRenderingManager.d.ts +2 -1
- package/dist/managers/UniversalRenderingManager.d.ts.map +1 -1
- package/dist/managers/UniversalRenderingManager.js +13 -16
- package/dist/managers/UniversalRenderingManager.js.map +1 -1
- package/dist/react/CodeView.d.ts +45 -0
- package/dist/react/CodeView.d.ts.map +1 -0
- package/dist/react/CodeView.js +241 -0
- package/dist/react/CodeView.js.map +1 -0
- package/dist/react/File.d.ts +0 -1
- package/dist/react/File.d.ts.map +1 -1
- package/dist/react/File.js +2 -3
- package/dist/react/File.js.map +1 -1
- package/dist/react/FileDiff.d.ts +0 -1
- package/dist/react/FileDiff.d.ts.map +1 -1
- package/dist/react/FileDiff.js +3 -4
- package/dist/react/FileDiff.js.map +1 -1
- package/dist/react/MultiFileDiff.d.ts +0 -1
- package/dist/react/MultiFileDiff.d.ts.map +1 -1
- package/dist/react/MultiFileDiff.js +3 -4
- package/dist/react/MultiFileDiff.js.map +1 -1
- package/dist/react/PatchDiff.d.ts +0 -1
- package/dist/react/PatchDiff.d.ts.map +1 -1
- package/dist/react/PatchDiff.js +3 -4
- package/dist/react/PatchDiff.js.map +1 -1
- package/dist/react/UnresolvedFile.d.ts +0 -1
- package/dist/react/UnresolvedFile.d.ts.map +1 -1
- package/dist/react/UnresolvedFile.js +3 -4
- package/dist/react/UnresolvedFile.js.map +1 -1
- package/dist/react/constants.d.ts.map +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +5 -4
- package/dist/react/jsx.d.ts.map +1 -1
- package/dist/react/types.d.ts +0 -8
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/utils/renderDiffChildren.d.ts +0 -2
- package/dist/react/utils/renderDiffChildren.d.ts.map +1 -1
- package/dist/react/utils/renderDiffChildren.js +3 -4
- package/dist/react/utils/renderDiffChildren.js.map +1 -1
- package/dist/react/utils/renderFileChildren.d.ts +0 -2
- package/dist/react/utils/renderFileChildren.d.ts.map +1 -1
- package/dist/react/utils/renderFileChildren.js +3 -4
- package/dist/react/utils/renderFileChildren.js.map +1 -1
- package/dist/react/utils/useFileDiffInstance.js +12 -7
- package/dist/react/utils/useFileDiffInstance.js.map +1 -1
- package/dist/react/utils/useFileInstance.js +12 -7
- package/dist/react/utils/useFileInstance.js.map +1 -1
- package/dist/react/utils/useUnresolvedFileInstance.js +6 -2
- package/dist/react/utils/useUnresolvedFileInstance.js.map +1 -1
- package/dist/renderers/DiffHunksRenderer.d.ts +2 -1
- package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
- package/dist/renderers/DiffHunksRenderer.js +35 -20
- package/dist/renderers/DiffHunksRenderer.js.map +1 -1
- package/dist/renderers/FileRenderer.d.ts +2 -1
- package/dist/renderers/FileRenderer.d.ts.map +1 -1
- package/dist/renderers/FileRenderer.js +34 -20
- package/dist/renderers/FileRenderer.js.map +1 -1
- package/dist/ssr/index.d.ts +2 -2
- package/dist/ssr/preloadDiffs.js +1 -1
- package/dist/style.js +1 -1
- package/dist/style.js.map +1 -1
- package/dist/types.d.ts +98 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/areManagedSnapshotsEqual.d.ts +7 -0
- package/dist/utils/areManagedSnapshotsEqual.d.ts.map +1 -0
- package/dist/utils/areManagedSnapshotsEqual.js +15 -0
- package/dist/utils/areManagedSnapshotsEqual.js.map +1 -0
- package/dist/utils/areOptionsEqual.d.ts +2 -1
- package/dist/utils/areOptionsEqual.d.ts.map +1 -1
- package/dist/utils/areOptionsEqual.js +1 -1
- package/dist/utils/areOptionsEqual.js.map +1 -1
- package/dist/utils/createFileHeaderElement.d.ts +3 -1
- package/dist/utils/createFileHeaderElement.d.ts.map +1 -1
- package/dist/utils/createFileHeaderElement.js +3 -2
- package/dist/utils/createFileHeaderElement.js.map +1 -1
- package/dist/utils/createWindowFromScrollPosition.d.ts +3 -3
- package/dist/utils/createWindowFromScrollPosition.d.ts.map +1 -1
- package/dist/utils/createWindowFromScrollPosition.js +6 -6
- package/dist/utils/createWindowFromScrollPosition.js.map +1 -1
- package/dist/utils/iterateOverDiff.d.ts +2 -1
- package/dist/utils/iterateOverDiff.d.ts.map +1 -1
- package/dist/utils/iterateOverDiff.js +135 -7
- package/dist/utils/iterateOverDiff.js.map +1 -1
- package/dist/utils/renderFileWithHighlighter.js +1 -1
- package/dist/utils/resolveVirtualFileMetrics.d.ts +4 -1
- package/dist/utils/resolveVirtualFileMetrics.d.ts.map +1 -1
- package/dist/utils/resolveVirtualFileMetrics.js +11 -1
- package/dist/utils/resolveVirtualFileMetrics.js.map +1 -1
- package/dist/utils/roundToDevicePixel.d.ts +14 -0
- package/dist/utils/roundToDevicePixel.d.ts.map +1 -0
- package/dist/utils/roundToDevicePixel.js +18 -0
- package/dist/utils/roundToDevicePixel.js.map +1 -0
- package/dist/worker/worker-portable.js +195 -14
- package/dist/worker/worker-portable.js.map +1 -1
- package/dist/worker/worker.js +146 -7
- package/dist/worker/worker.js.map +1 -1
- package/package.json +7 -1
- package/dist/components/AdvancedVirtualizedFileDiff.d.ts +0 -40
- package/dist/components/AdvancedVirtualizedFileDiff.d.ts.map +0 -1
- package/dist/components/AdvancedVirtualizedFileDiff.js +0 -140
- package/dist/components/AdvancedVirtualizedFileDiff.js.map +0 -1
- package/dist/components/AdvancedVirtualizer.d.ts +0 -38
- package/dist/components/AdvancedVirtualizer.d.ts.map +0 -1
- package/dist/components/AdvancedVirtualizer.js +0 -201
- 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
|