UNPKG

@pierre/diffs

Version:

832 lines (830 loc) 38.2 kB
import { DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } from "../constants.js"; import { areObjectsEqual } from "../utils/areObjectsEqual.js"; import { areOptionsEqual } from "../utils/areOptionsEqual.js"; import { computeVirtualFileMetrics, getVirtualFileHeaderRegion, getVirtualFilePaddingBottom } from "../utils/computeVirtualFileMetrics.js"; import { FILE_ANNOTATION_DOM_KEY, FILE_ANNOTATION_LINE_NUMBER, includesFileAnnotations, shouldRenderFileAnnotations } from "../utils/includesFileAnnotations.js"; import { areDiffTargetsEqual } from "../utils/areDiffTargetsEqual.js"; import { getExpandedRegion, getLeadingHunkSeparatorLayout, getTrailingExpandedRegion, getTrailingHunkSeparatorLayout } from "../utils/virtualDiffLayout.js"; import { computeEstimatedDiffHeights } from "../utils/computeEstimatedDiffHeights.js"; import { iterateOverDiff } from "../utils/iterateOverDiff.js"; import { parseDiffFromFile } from "../utils/parseDiffFromFile.js"; import { FileDiff } from "./FileDiff.js"; //#region src/components/VirtualizedFileDiff.ts const LAYOUT_CHECKPOINT_INTERVAL = 5e3; let instanceId = -1; var VirtualizedFileDiff = class extends FileDiff { __id = `little-virtualized-file-diff:${++instanceId}`; top; height = 0; metrics; cache = { heightDeltas: /* @__PURE__ */ new Map(), measuredHeightDeltaTotal: 0, estimatedSplitHeight: void 0, estimatedUnifiedHeight: void 0, checkpoints: [], totalLines: 0, fileAnnotationHeight: 0 }; isVisible = false; isSetup = false; virtualizer; layoutDirty = true; forceRenderOverride; currentCollapsed; constructor(options, virtualizer, metrics, workerManager, isContainerManaged = false) { super(options, workerManager, isContainerManaged); this.virtualizer = virtualizer; this.metrics = computeVirtualFileMetrics(metrics); } setMetrics(metrics, force = false) { const nextMetrics = computeVirtualFileMetrics(metrics); if (!force && areObjectsEqual(this.metrics, nextMetrics)) return; this.metrics = nextMetrics; this.resetLayoutCache({ includeEstimatedHeights: true }); } setLineAnnotations(lineAnnotations) { if (this.syncLineAnnotations(lineAnnotations)) this.resetLayoutCache({ includeEstimatedHeights: false }); } syncLineAnnotations(lineAnnotations) { if (lineAnnotations == null || lineAnnotations === this.lineAnnotations || lineAnnotations.length === 0 && this.lineAnnotations.length === 0) return false; super.setLineAnnotations(lineAnnotations); return true; } setFileAnnotationHeight(nextHeight) { const previousHeight = this.cache.fileAnnotationHeight; if (nextHeight === previousHeight) return false; this.cache.fileAnnotationHeight = nextHeight; this.cache.measuredHeightDeltaTotal += nextHeight - previousHeight; return true; } hasFileAnnotations(fileDiff = this.fileDiff) { if (fileDiff == null || !includesFileAnnotations(this.lineAnnotations)) return false; return this.lineAnnotations.some((annotation) => { if (annotation.lineNumber !== FILE_ANNOTATION_LINE_NUMBER) return false; if (fileDiff.type === "new") return annotation.side === "additions"; if (fileDiff.type === "deleted") return annotation.side === "deletions"; return true; }); } getLineHeight(lineIndex, hasMetadataLine = false) { return this.getEstimatedLineHeight(hasMetadataLine) + (this.cache.heightDeltas.get(lineIndex) ?? 0); } getEstimatedLineHeight(hasMetadataLine = false) { const multiplier = hasMetadataLine ? 2 : 1; return this.metrics.lineHeight * multiplier; } setOptions(options) { if (this.isAdvancedMode()) throw new Error("VirtualizedFileDiff.setOptions cannot be used inside CodeView. Update CodeView options instead."); if (options == null) return; const { options: previousOptions } = this; const optionsChanged = !areOptionsEqual(previousOptions, options); const layoutChanged = optionsChanged && hasDiffLayoutOptionChanged(previousOptions, options); super.setOptions(options); if (layoutChanged) this.resetLayoutCache({ forceSimpleRecompute: true, includeEstimatedHeights: hasDiffEstimateOptionChanged(previousOptions, options) }); if (optionsChanged) this.forceRenderOverride = true; if (optionsChanged && this.isSimpleMode()) this.virtualizer.instanceChanged(this, layoutChanged); } setThemeType(themeType) { if (this.isAdvancedMode()) throw new Error("VirtualizedFileDiff.setThemeType cannot be used inside CodeView. Update CodeView options instead."); super.setThemeType(themeType); } resetLayoutCache({ forceSimpleRecompute = false, includeEstimatedHeights = false } = {}) { this.layoutDirty = true; this.cache.fileAnnotationHeight = 0; if (this.cache.heightDeltas.size > 0) this.cache.heightDeltas.clear(); if (this.cache.measuredHeightDeltaTotal !== 0) this.cache.measuredHeightDeltaTotal = 0; if (this.cache.checkpoints.length > 0) this.cache.checkpoints.length = 0; if (this.cache.totalLines !== 0) this.cache.totalLines = 0; if (includeEstimatedHeights) { this.cache.estimatedSplitHeight = void 0; this.cache.estimatedUnifiedHeight = void 0; } if (this.renderRange != null) this.renderRange = void 0; if (forceSimpleRecompute && this.isSimpleMode()) this.computeApproximateSize(); } reconcileHeights() { let hasHeightChange = false; const { overflow = "scroll" } = this.options; if (this.fileContainer == null || this.fileDiff == null) { if (this.height !== 0) hasHeightChange = true; this.height = 0; return hasHeightChange; } this.top = this.getVirtualizedTop(); if (overflow === "scroll" && this.lineAnnotations.length === 0 && !this.isResizeDebuggingEnabled()) return hasHeightChange; const diffStyle = this.getDiffStyle(); const codeGroups = diffStyle === "split" ? [this.codeDeletions, this.codeAdditions] : [this.codeUnified]; const hasFileAnnotations = this.hasFileAnnotations(this.fileDiff); if (this.renderRange != null && hasFileAnnotations && shouldRenderFileAnnotations(this.renderRange)) { const nextFileAnnotationHeight = measureFileAnnotationHeight(codeGroups) ?? 0; if (this.setFileAnnotationHeight(nextFileAnnotationHeight)) hasHeightChange = true; } else if (!hasFileAnnotations && this.setFileAnnotationHeight(0)) hasHeightChange = true; for (const codeGroup of codeGroups) { if (codeGroup == null) continue; const content = codeGroup.children[1]; if (!(content instanceof HTMLElement)) continue; for (const line of content.children) { if (!(line instanceof HTMLElement)) continue; const lineIndexAttr = line.dataset.lineIndex; if (lineIndexAttr == null) continue; const lineIndex = parseLineIndex(lineIndexAttr, diffStyle); let measuredHeight = line.getBoundingClientRect().height; let hasMetadata = false; if (line.nextElementSibling instanceof HTMLElement && ("lineAnnotation" in line.nextElementSibling.dataset || "noNewline" in line.nextElementSibling.dataset)) { if ("noNewline" in line.nextElementSibling.dataset) hasMetadata = true; measuredHeight += line.nextElementSibling.getBoundingClientRect().height; } const estimatedHeight = this.getEstimatedLineHeight(hasMetadata); const previousDelta = this.cache.heightDeltas.get(lineIndex) ?? 0; const nextDelta = measuredHeight - estimatedHeight; if (nextDelta === previousDelta) continue; hasHeightChange = true; this.cache.measuredHeightDeltaTotal += nextDelta - previousDelta; if (nextDelta === 0) this.cache.heightDeltas.delete(lineIndex); else this.cache.heightDeltas.set(lineIndex, nextDelta); } } if (hasHeightChange || this.isResizeDebuggingEnabled()) this.computeApproximateSize(true); return hasHeightChange; } onRender = (dirty) => { if (this.fileContainer == null) return false; if (dirty) this.top = this.getVirtualizedTop(); return this.render(); }; prepareCodeViewItem(fileDiff, top, reset, lineAnnotations) { const targetChanged = !areDiffTargetsEqual(this.fileDiff, fileDiff); const annotationsChanged = this.syncLineAnnotations(lineAnnotations); let shouldResetLayoutCache = reset?.resetDiffLayoutCache === true || targetChanged || annotationsChanged; let includeEstimatedHeights = targetChanged || reset?.resetDiffLayoutCache === true && reset.includeEstimatedDiffHeights; if (reset?.metrics != null) { this.metrics = computeVirtualFileMetrics(reset.metrics); shouldResetLayoutCache = true; includeEstimatedHeights = true; } const { collapsed = false } = this.options; if (this.currentCollapsed !== collapsed) { this.currentCollapsed = collapsed; shouldResetLayoutCache = true; } if (shouldResetLayoutCache) this.resetLayoutCache({ includeEstimatedHeights }); this.fileDiff = fileDiff; this.top = top; this.computeApproximateSize(); return this.height; } getLinePosition(lineNumber, side = "additions") { if (this.fileDiff == null || lineNumber < 1) return; const targetLineIndexes = this.getLineIndex(lineNumber, side); if (targetLineIndexes == null) return; const { disableFileHeader = false, expandUnchanged = false, collapsed = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options; const diffStyle = this.getDiffStyle(); const hunkSeparators = this.getHunkSeparatorType(); const targetLineIndex = diffStyle === "split" ? targetLineIndexes[1] : targetLineIndexes[0]; this.approximateLayoutCheckpoints(); const headerRegion = getVirtualFileHeaderRegion(this.metrics, disableFileHeader); const checkpoint = this.getLayoutCheckpointBeforeLineIndex(targetLineIndex); let top = checkpoint?.top ?? headerRegion + this.cache.fileAnnotationHeight; if (collapsed) return { top: headerRegion, height: 0 }; let position; iterateOverDiff({ diff: this.fileDiff, diffStyle, startingLine: checkpoint?.renderedLineIndex ?? 0, expandedHunks: expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap(), collapsedContextThreshold, callback: ({ hunkIndex, hunk, collapsedBefore, collapsedAfter, deletionLine, additionLine }) => { const lineIndex = diffStyle === "split" ? additionLine?.splitLineIndex ?? deletionLine?.splitLineIndex : additionLine?.unifiedLineIndex ?? deletionLine?.unifiedLineIndex; if (lineIndex == null) throw new Error("VirtualizedFileDiff.getLinePosition: missing line index data"); if (collapsedBefore > 0) { const separator = getLeadingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics, hunkIndex, hunkSpecs: hunk?.hunkSpecs }); if (separator != null) { top += separator.gapBefore; if (targetLineIndex >= lineIndex - collapsedBefore && targetLineIndex < lineIndex) { position = { top, height: separator.height }; return true; } top += separator.height + separator.gapAfter; } } const lineHeight = this.getLineHeight(lineIndex, (additionLine?.noEOFCR ?? false) || (deletionLine?.noEOFCR ?? false)); if (lineIndex === targetLineIndex) { position = { top, height: lineHeight }; return true; } top += lineHeight; if (collapsedAfter > 0) { const separator = getTrailingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics }); if (separator != null) { if (targetLineIndex > lineIndex && targetLineIndex <= lineIndex + collapsedAfter) { position = { top: top + separator.gapBefore, height: separator.height }; return true; } top += separator.totalHeight; } } return false; } }); return position; } getNumericScrollAnchor(localViewportTop) { if (this.fileDiff == null) return; const { disableFileHeader = false, expandUnchanged = false, collapsed = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options; if (collapsed) return; const diffStyle = this.getDiffStyle(); const hunkSeparators = this.getHunkSeparatorType(); this.approximateLayoutCheckpoints(); const checkpoint = this.getLayoutCheckpointBeforeTop(localViewportTop); let top = checkpoint?.top ?? getVirtualFileHeaderRegion(this.metrics, disableFileHeader) + this.cache.fileAnnotationHeight; let anchor; iterateOverDiff({ diff: this.fileDiff, diffStyle, startingLine: checkpoint?.renderedLineIndex ?? 0, expandedHunks: expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap(), collapsedContextThreshold, callback: ({ hunkIndex, hunk, collapsedBefore, collapsedAfter, deletionLine, additionLine }) => { const lineIndex = diffStyle === "split" ? additionLine?.splitLineIndex ?? deletionLine?.splitLineIndex : additionLine?.unifiedLineIndex ?? deletionLine?.unifiedLineIndex; if (lineIndex == null) throw new Error("VirtualizedFileDiff.getNumericScrollAnchor: missing line index data"); if (collapsedBefore > 0) { const separator = getLeadingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics, hunkIndex, hunkSpecs: hunk?.hunkSpecs }); if (separator != null) top += separator.totalHeight; } if (top >= localViewportTop) { if (deletionLine != null) anchor = { lineNumber: deletionLine.lineNumber, side: "deletions", top }; else if (additionLine != null) anchor = { lineNumber: additionLine.lineNumber, side: "additions", top }; if (anchor != null) return true; } const lineHeight = this.getLineHeight(lineIndex, (additionLine?.noEOFCR ?? false) || (deletionLine?.noEOFCR ?? false)); top += lineHeight; if (collapsedAfter > 0) { const separator = getTrailingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics }); if (separator != null) top += separator.totalHeight; } return false; } }); return anchor; } getVirtualizedHeight() { return this.height; } getAdvancedStickySpecs(windowSpecs) { if (this.top == null || this.fileDiff == null) return; if (this.options.collapsed === true) return { topOffset: this.top, height: this.height }; const renderRange = windowSpecs != null ? this.computeRenderRangeFromWindow(this.fileDiff, this.top, windowSpecs) : this.renderRange; if (renderRange == null) return; const { bufferBefore, bufferAfter, totalLines } = renderRange; let headerOnlyOffset = 0; if (totalLines === 0) { const activeWindow = windowSpecs ?? this.virtualizer.getWindowSpecs(); if (this.top < activeWindow.top) headerOnlyOffset = bufferAfter; } return { topOffset: this.top + bufferBefore + headerOnlyOffset, height: this.height - (bufferBefore + bufferAfter) }; } cleanUp(recycle = false) { if (this.fileContainer != null && this.isSimpleMode()) this.getSimpleVirtualizer()?.disconnect(this.fileContainer); if (!recycle) this.resetLayoutCache({ includeEstimatedHeights: true }); this.isSetup = false; super.cleanUp(recycle); } expandHunk = (hunkIndex, direction, expansionLineCountOverride) => { this.hunksRenderer.expandHunk(hunkIndex, direction, expansionLineCountOverride); this.forceRenderOverride = true; this.resetLayoutCache({ includeEstimatedHeights: true }); if (this.isSimpleMode()) this.computeApproximateSize(); this.virtualizer.instanceChanged(this, true); }; setVisibility(visible) { if (this.isAdvancedMode() || this.fileContainer == null) return; this.renderRange = void 0; if (visible && !this.isVisible) { this.top = this.getVirtualizedTop(); this.isVisible = true; } else if (!visible && this.isVisible) { this.isVisible = false; this.rerender(); } } rerender() { if (!this.enabled || this.fileDiff == null && this.additionFile == null && this.deletionFile == null) return; this.forceRenderOverride = true; this.virtualizer.instanceChanged(this, false); } computeApproximateSize(force = false) { const shouldValidateSize = this.isResizeDebuggingEnabled(); if (!force && !this.layoutDirty && !shouldValidateSize) return; const isFirstCompute = this.height === 0; this.height = 0; this.cache.checkpoints = []; this.cache.totalLines = 0; if (this.fileDiff == null) { this.layoutDirty = false; return; } const { disableFileHeader = false, collapsed = false } = this.options; const headerRegion = getVirtualFileHeaderRegion(this.metrics, disableFileHeader); this.height += headerRegion; if (collapsed) { this.layoutDirty = false; return; } this.height = this.getActiveEstimatedHeight() + this.cache.measuredHeightDeltaTotal; if (shouldValidateSize && !isFirstCompute) this.validateComputedHeight(); this.layoutDirty = false; } getActiveEstimatedHeight() { this.ensureEstimatedDiffHeights(); const estimatedHeight = this.getDiffStyle() === "split" ? this.cache.estimatedSplitHeight : this.cache.estimatedUnifiedHeight; if (estimatedHeight == null) throw new Error("VirtualizedFileDiff.getActiveEstimatedHeight: missing estimated height"); return estimatedHeight; } ensureEstimatedDiffHeights() { if (this.fileDiff == null) { this.cache.estimatedSplitHeight = void 0; this.cache.estimatedUnifiedHeight = void 0; return; } if (this.cache.estimatedSplitHeight != null && this.cache.estimatedUnifiedHeight != null) return; const { disableFileHeader = false, expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options; const { splitHeight, unifiedHeight } = computeEstimatedDiffHeights({ fileDiff: this.fileDiff, metrics: this.metrics, disableFileHeader, hunkSeparators: this.getHunkSeparatorType(), expandUnchanged, expandedHunks: this.hunksRenderer.getExpandedHunksMap(), collapsedContextThreshold }); this.cache.estimatedSplitHeight = splitHeight; this.cache.estimatedUnifiedHeight = unifiedHeight; } validateComputedHeight() { if (this.fileContainer == null || this.fileDiff == null) return; const rect = this.fileContainer.getBoundingClientRect(); if (rect.height !== this.height) console.log("VirtualizedFileDiff.computeApproximateSize: computed height doesnt match", { name: this.fileDiff.name, elementHeight: rect.height, computedHeight: this.height }); else console.log("VirtualizedFileDiff.computeApproximateSize: computed height IS CORRECT"); } render({ fileContainer, oldFile, newFile, fileDiff, forceRender = false, lineAnnotations,...props } = {}) { const { forceRenderOverride, isSetup } = this; this.forceRenderOverride = void 0; const annotationsChanged = this.syncLineAnnotations(lineAnnotations); if (annotationsChanged) this.resetLayoutCache({ includeEstimatedHeights: false }); this.fileDiff ??= fileDiff ?? (oldFile != null && newFile != null ? parseDiffFromFile(oldFile, newFile, this.options.parseDiffOptions) : void 0); fileContainer = this.getOrCreateFileContainer(fileContainer); if (this.fileDiff == null) { console.error("VirtualizedFileDiff.render: attempting to virtually render when we dont have the correct data"); return false; } if (!isSetup) { this.computeApproximateSize(); const virtualizer = this.getSimpleVirtualizer(); this.top ??= this.getVirtualizedTop(); if (this.isAdvancedMode()) this.isVisible = true; else { if (virtualizer == null) throw new Error("VirtualizedFileDiff.render: simple virtualizer is not available"); virtualizer.connect(fileContainer, this); this.isVisible = virtualizer.isInstanceVisible(this.top ?? 0, this.height); } this.isSetup = true; } else this.top ??= this.getVirtualizedTop(); if (!this.isVisible && this.isSimpleMode()) return this.renderPlaceholder(this.height); const windowSpecs = this.virtualizer.getWindowSpecs(); const fileTop = this.top ?? 0; const renderRange = this.computeRenderRangeFromWindow(this.fileDiff, fileTop, windowSpecs); return super.render({ fileDiff: this.fileDiff, fileContainer, renderRange, oldFile, newFile, lineAnnotations, forceRender: (forceRenderOverride ?? forceRender) || annotationsChanged, ...props }); } syncVirtualizedTop() { this.top = this.getVirtualizedTop(); } shouldDisableVirtualizationBuffers() { return this.isAdvancedMode() || super.shouldDisableVirtualizationBuffers(); } isSimpleMode() { return this.virtualizer.type === "simple"; } isAdvancedMode() { return this.virtualizer.type === "advanced"; } getVirtualizedTop() { if (this.virtualizer.type === "advanced") return this.virtualizer.getLocalTopForInstance(this); return this.fileContainer != null ? this.virtualizer.getOffsetInScrollContainer(this.fileContainer) : 0; } getSimpleVirtualizer() { return this.virtualizer.type === "simple" ? this.virtualizer : void 0; } isResizeDebuggingEnabled() { return this.getSimpleVirtualizer()?.config.resizeDebugging ?? false; } getDiffStyle() { return this.options.diffStyle ?? "split"; } getHunkSeparatorType() { return getOptionHunkSeparatorType(this.options.hunkSeparators); } approximateLayoutCheckpoints() { if (this.cache.checkpoints.length > 0 || this.fileDiff == null || this.fileDiff.hunks.length === 0 || this.options.collapsed === true) return; const { disableFileHeader = false, expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options; const finalHunkIndex = this.fileDiff.hunks.length - 1; const diffStyle = this.getDiffStyle(); const hunkSeparators = this.getHunkSeparatorType(); const expandedHunks = expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap(); const heightDeltaPrefix = createHeightDeltaPrefix(this.cache.heightDeltas); let top = getVirtualFileHeaderRegion(this.metrics, disableFileHeader) + this.cache.fileAnnotationHeight; let renderedLineIndex = 0; const processRows = ({ rowCount, startLineIndex, preSeparatorHeight = 0, postSeparatorHeight = 0, metadataOffsets = [] }) => { if (rowCount <= 0) return; const blockStart = renderedLineIndex; const blockEnd = renderedLineIndex + rowCount; let nextCheckpoint = getNextCheckpointIndex(blockStart); while (nextCheckpoint < blockEnd) { const offset = nextCheckpoint - blockStart; const checkpointTop = top + (offset > 0 ? preSeparatorHeight : 0) + offset * this.metrics.lineHeight + countMetadataOffsetsBefore(metadataOffsets, offset) * this.metrics.lineHeight + sumHeightDeltas(heightDeltaPrefix, startLineIndex, startLineIndex + offset); this.cache.checkpoints.push({ renderedLineIndex: nextCheckpoint, lineIndex: startLineIndex + offset, top: checkpointTop }); nextCheckpoint += LAYOUT_CHECKPOINT_INTERVAL; } top += preSeparatorHeight + rowCount * this.metrics.lineHeight + metadataOffsets.length * this.metrics.lineHeight + sumHeightDeltas(heightDeltaPrefix, startLineIndex, startLineIndex + rowCount) + postSeparatorHeight; renderedLineIndex = blockEnd; }; for (let hunkIndex = 0; hunkIndex < this.fileDiff.hunks.length; hunkIndex++) { const hunk = this.fileDiff.hunks[hunkIndex]; if (hunk == null) throw new Error("VirtualizedFileDiff.approximateLayoutCheckpoints: invalid hunk index"); const leadingRegion = getExpandedRegion({ isPartial: this.fileDiff.isPartial, rangeSize: hunk.collapsedBefore, expandedHunks, hunkIndex, collapsedContextThreshold }); const leadingSeparatorHeight = leadingRegion.collapsedLines > 0 ? getLeadingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics, hunkIndex, hunkSpecs: hunk.hunkSpecs })?.totalHeight ?? 0 : 0; processRows({ rowCount: leadingRegion.fromStart, startLineIndex: (diffStyle === "split" ? hunk.splitLineStart : hunk.unifiedLineStart) - leadingRegion.rangeSize }); let pendingLeadingSeparatorHeight = leadingSeparatorHeight; processRows({ rowCount: leadingRegion.fromEnd, startLineIndex: (diffStyle === "split" ? hunk.splitLineStart : hunk.unifiedLineStart) - leadingRegion.fromEnd, preSeparatorHeight: pendingLeadingSeparatorHeight }); if (leadingRegion.fromEnd > 0) pendingLeadingSeparatorHeight = 0; const trailingRegion = hunkIndex === finalHunkIndex ? getTrailingExpandedRegion({ fileDiff: this.fileDiff, hunkIndex, expandedHunks, collapsedContextThreshold, errorPrefix: "VirtualizedFileDiff" }) : void 0; const trailingSeparatorHeight = trailingRegion != null && trailingRegion.collapsedLines > 0 ? getTrailingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics })?.totalHeight ?? 0 : 0; const trailingExpandedCount = trailingRegion != null ? trailingRegion.fromStart + trailingRegion.fromEnd : 0; const hunkBodyRowCount = diffStyle === "split" ? hunk.splitLineCount : hunk.unifiedLineCount; const hunkBodyStartLineIndex = diffStyle === "split" ? hunk.splitLineStart : hunk.unifiedLineStart; processRows({ rowCount: hunkBodyRowCount, startLineIndex: hunkBodyStartLineIndex, preSeparatorHeight: pendingLeadingSeparatorHeight, postSeparatorHeight: trailingExpandedCount === 0 ? trailingSeparatorHeight : 0, metadataOffsets: getHunkMetadataOffsets({ diffStyle, hunk, rowCount: hunkBodyRowCount }) }); if (trailingRegion != null && trailingExpandedCount > 0) processRows({ rowCount: trailingExpandedCount, startLineIndex: hunkBodyStartLineIndex + hunkBodyRowCount, postSeparatorHeight: trailingSeparatorHeight }); } this.cache.totalLines = renderedLineIndex; } getLayoutCheckpointBeforeLineIndex(lineIndex) { if (lineIndex <= 0 || this.cache.checkpoints.length === 0) return; let low = 0; let high = this.cache.checkpoints.length - 1; let result; while (low <= high) { const mid = low + high >> 1; const checkpoint = this.cache.checkpoints[mid]; if (checkpoint == null) throw new Error("VirtualizedFileDiff: invalid checkpoint index"); if (checkpoint.lineIndex <= lineIndex) { result = checkpoint; low = mid + 1; } else high = mid - 1; } return result; } getLayoutCheckpointBeforeTop(top, hunkLineCount) { let low = 0; let high = this.cache.checkpoints.length - 1; let resultIndex = -1; while (low <= high) { const mid = low + high >> 1; const checkpoint = this.cache.checkpoints[mid]; if (checkpoint == null) throw new Error("VirtualizedFileDiff: invalid checkpoint index"); if (checkpoint.top <= top) { resultIndex = mid; low = mid + 1; } else high = mid - 1; } if (hunkLineCount == null) return resultIndex >= 0 ? this.cache.checkpoints[resultIndex] : void 0; for (let index = resultIndex; index >= 0; index--) { const checkpoint = this.cache.checkpoints[index]; if (checkpoint == null) throw new Error("VirtualizedFileDiff: invalid checkpoint index"); if (checkpoint.renderedLineIndex % hunkLineCount === 0) return checkpoint; } } getExpandedLineCount(fileDiff, diffStyle) { let count = 0; if (fileDiff.isPartial) { for (const hunk of fileDiff.hunks) count += diffStyle === "split" ? hunk.splitLineCount : hunk.unifiedLineCount; return count; } const { expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options; const expandedHunks = expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap(); for (const [hunkIndex, hunk] of fileDiff.hunks.entries()) { const hunkCount = diffStyle === "split" ? hunk.splitLineCount : hunk.unifiedLineCount; count += hunkCount; const collapsedBefore = Math.max(hunk.collapsedBefore, 0); const { fromStart, fromEnd, renderAll } = getExpandedRegion({ isPartial: fileDiff.isPartial, rangeSize: collapsedBefore, expandedHunks, hunkIndex, collapsedContextThreshold }); if (collapsedBefore > 0) count += renderAll ? collapsedBefore : fromStart + fromEnd; } const trailingRegion = getTrailingExpandedRegion({ fileDiff, hunkIndex: fileDiff.hunks.length - 1, expandedHunks, collapsedContextThreshold, errorPrefix: "VirtualizedFileDiff" }); if (trailingRegion != null) count += trailingRegion.fromStart + trailingRegion.fromEnd; return count; } computeRenderRangeFromWindow(fileDiff, fileTop, { top, bottom }) { const { disableFileHeader = false, expandUnchanged = false, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } = this.options; const { hunkLineCount, lineHeight } = this.metrics; const diffStyle = this.getDiffStyle(); const hunkSeparators = this.getHunkSeparatorType(); const fileHeight = this.height; let lineCount = this.cache.totalLines > 0 ? this.cache.totalLines : this.getExpandedLineCount(fileDiff, diffStyle); const headerRegion = getVirtualFileHeaderRegion(this.metrics, disableFileHeader); const paddingBottom = fileDiff.hunks.length > 0 ? getVirtualFilePaddingBottom(this.metrics) : 0; const { fileAnnotationHeight } = this.cache; const codeRegionTop = headerRegion + fileAnnotationHeight; const codeHeight = Math.max(0, fileHeight - headerRegion - fileAnnotationHeight - paddingBottom); const hasFileAnnotations = this.hasFileAnnotations(fileDiff); const fileAnnotationTop = fileTop + headerRegion; const measuredFileAnnotationVisible = fileAnnotationHeight > 0 && hasFileAnnotations && fileAnnotationTop < bottom && fileAnnotationTop + fileAnnotationHeight > top; if (fileTop < top - fileHeight || fileTop > bottom) return { startingLine: 0, totalLines: 0, bufferBefore: 0, bufferAfter: fileHeight - headerRegion - paddingBottom }; if (lineCount <= hunkLineCount || fileDiff.hunks.length === 0) return { startingLine: 0, totalLines: hunkLineCount, bufferBefore: 0, bufferAfter: 0 }; this.approximateLayoutCheckpoints(); lineCount = this.cache.totalLines > 0 ? this.cache.totalLines : lineCount; const estimatedTargetLines = Math.ceil(Math.max(bottom - top, 0) / lineHeight); const totalLines = Math.ceil(estimatedTargetLines / hunkLineCount) * hunkLineCount + hunkLineCount; const totalHunks = totalLines / hunkLineCount; const overflowHunks = totalHunks; const hunkOffsets = []; const viewportCenter = (top + bottom) / 2; const checkpoint = this.getLayoutCheckpointBeforeTop(Math.max(0, top - fileTop - totalLines * lineHeight * 2), hunkLineCount); let absoluteLineTop = fileTop + (checkpoint?.top ?? codeRegionTop); let currentLine = checkpoint?.renderedLineIndex ?? 0; let firstVisibleHunk; let centerHunk; let overflowCounter; iterateOverDiff({ diff: fileDiff, diffStyle, startingLine: checkpoint?.renderedLineIndex ?? 0, expandedHunks: expandUnchanged ? true : this.hunksRenderer.getExpandedHunksMap(), collapsedContextThreshold, callback: ({ hunkIndex, hunk, collapsedBefore, collapsedAfter, deletionLine, additionLine }) => { const splitLineIndex = additionLine != null ? additionLine.splitLineIndex : deletionLine.splitLineIndex; const unifiedLineIndex = additionLine != null ? additionLine.unifiedLineIndex : deletionLine.unifiedLineIndex; const hasMetadata = (additionLine?.noEOFCR ?? false) || (deletionLine?.noEOFCR ?? false); const gapAdjustment = (collapsedBefore > 0 ? getLeadingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics, hunkIndex, hunkSpecs: hunk?.hunkSpecs }) : void 0)?.totalHeight ?? 0; absoluteLineTop += gapAdjustment; const isAtHunkBoundary = currentLine % hunkLineCount === 0; const currentHunk = Math.floor(currentLine / hunkLineCount); if (isAtHunkBoundary) { hunkOffsets[currentHunk] = absoluteLineTop - (fileTop + codeRegionTop + gapAdjustment); if (overflowCounter != null) { if (overflowCounter <= 0) return true; overflowCounter--; } } const lineHeight$1 = this.getLineHeight(diffStyle === "split" ? splitLineIndex : unifiedLineIndex, hasMetadata); if (absoluteLineTop > top - lineHeight$1 && absoluteLineTop < bottom) firstVisibleHunk ??= currentHunk; if (centerHunk == null && absoluteLineTop + lineHeight$1 > viewportCenter) centerHunk = currentHunk; if (overflowCounter == null && absoluteLineTop >= bottom && isAtHunkBoundary) overflowCounter = overflowHunks; currentLine++; absoluteLineTop += lineHeight$1; if (collapsedAfter > 0) absoluteLineTop += getTrailingHunkSeparatorLayout({ type: hunkSeparators, metrics: this.metrics })?.totalHeight ?? 0; return false; } }); if (firstVisibleHunk == null) if (measuredFileAnnotationVisible) { firstVisibleHunk = 0; centerHunk = 0; } else return { startingLine: 0, totalLines: 0, bufferBefore: 0, bufferAfter: fileHeight - headerRegion - paddingBottom }; centerHunk ??= firstVisibleHunk; const idealStartHunk = Math.round(centerHunk - totalHunks / 2); const maxStartHunk = Math.max(0, Math.ceil(lineCount / hunkLineCount) - totalHunks); const startHunk = Math.max(0, Math.min(idealStartHunk, maxStartHunk)); const startingLine = startHunk * hunkLineCount; const clampedTotalLines = idealStartHunk < 0 ? totalLines + idealStartHunk * hunkLineCount : totalLines; const codeBufferBefore = hunkOffsets[startHunk] ?? 0; const bufferBefore = startingLine === 0 ? 0 : fileAnnotationHeight + codeBufferBefore; const finalHunkIndex = startHunk + clampedTotalLines / hunkLineCount; const bufferAfter = finalHunkIndex < hunkOffsets.length ? codeHeight - hunkOffsets[finalHunkIndex] : codeHeight - (absoluteLineTop - fileTop - codeRegionTop); return { startingLine, totalLines: clampedTotalLines, bufferBefore, bufferAfter: Math.max(0, bufferAfter) }; } }; function measureFileAnnotationHeight(codeGroups) { let height; for (const codeGroup of codeGroups) { if (codeGroup == null) continue; const content = codeGroup.children[1]; if (!(content instanceof HTMLElement)) continue; for (const child of content.children) { if (!(child instanceof HTMLElement)) continue; if (child.dataset.lineAnnotation !== FILE_ANNOTATION_DOM_KEY) continue; height = Math.max(height ?? 0, child.getBoundingClientRect().height); } } return height; } function createHeightDeltaPrefix(heightDeltas) { const entries = Array.from(heightDeltas).sort((a, b) => a[0] - b[0]); const lineIndexes = []; const prefixTotals = [0]; let total = 0; for (const [lineIndex, delta] of entries) { lineIndexes.push(lineIndex); total += delta; prefixTotals.push(total); } return { lineIndexes, prefixTotals }; } function sumHeightDeltas({ lineIndexes, prefixTotals }, startLineIndex, endLineIndex) { if (startLineIndex >= endLineIndex || lineIndexes.length === 0) return 0; const start = lowerBound(lineIndexes, startLineIndex); return (prefixTotals[lowerBound(lineIndexes, endLineIndex)] ?? 0) - (prefixTotals[start] ?? 0); } function lowerBound(values, target) { let low = 0; let high = values.length; while (low < high) { const mid = low + high >> 1; const value = values[mid]; if (value == null) throw new Error("VirtualizedFileDiff: invalid prefix index"); if (value < target) low = mid + 1; else high = mid; } return low; } function getNextCheckpointIndex(renderedLineIndex) { return Math.ceil(renderedLineIndex / LAYOUT_CHECKPOINT_INTERVAL) * LAYOUT_CHECKPOINT_INTERVAL; } function countMetadataOffsetsBefore(metadataOffsets, offset) { let count = 0; for (const metadataOffset of metadataOffsets) if (metadataOffset < offset) count++; return count; } function getHunkMetadataOffsets({ diffStyle, hunk, rowCount }) { if (rowCount <= 0 || !hunk.noEOFCRAdditions && !hunk.noEOFCRDeletions) return []; const lastContent = hunk.hunkContent.at(-1); if (lastContent == null) return []; if (lastContent.type === "context") return [rowCount - 1]; const splitCount = Math.max(lastContent.deletions, lastContent.additions); const unifiedCount = lastContent.deletions + lastContent.additions; if (diffStyle === "split") return splitCount > 0 && (hunk.noEOFCRAdditions || hunk.noEOFCRDeletions) ? [rowCount - 1] : []; const offsets = []; const contentStartOffset = rowCount - unifiedCount; if (lastContent.deletions > 0 && hunk.noEOFCRDeletions) offsets.push(contentStartOffset + lastContent.deletions - 1); if (lastContent.additions > 0 && hunk.noEOFCRAdditions) offsets.push(rowCount - 1); return offsets; } function hasDiffLayoutOptionChanged(previousOptions, nextOptions) { return (previousOptions.diffStyle ?? "split") !== (nextOptions.diffStyle ?? "split") || (previousOptions.overflow ?? "scroll") !== (nextOptions.overflow ?? "scroll") || (previousOptions.collapsed ?? false) !== (nextOptions.collapsed ?? false) || (previousOptions.disableLineNumbers ?? false) !== (nextOptions.disableLineNumbers ?? false) || (previousOptions.disableFileHeader ?? false) !== (nextOptions.disableFileHeader ?? false) || (previousOptions.diffIndicators ?? "bars") !== (nextOptions.diffIndicators ?? "bars") || (previousOptions.hunkSeparators ?? "line-info") !== (nextOptions.hunkSeparators ?? "line-info") || (previousOptions.expandUnchanged ?? false) !== (nextOptions.expandUnchanged ?? false) || (previousOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD) !== (nextOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD) || previousOptions.unsafeCSS !== nextOptions.unsafeCSS; } function hasDiffEstimateOptionChanged(previousOptions, nextOptions) { return (previousOptions.disableFileHeader ?? false) !== (nextOptions.disableFileHeader ?? false) || (previousOptions.hunkSeparators ?? "line-info") !== (nextOptions.hunkSeparators ?? "line-info") || (previousOptions.expandUnchanged ?? false) !== (nextOptions.expandUnchanged ?? false) || (previousOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD) !== (nextOptions.collapsedContextThreshold ?? DEFAULT_COLLAPSED_CONTEXT_THRESHOLD); } function getOptionHunkSeparatorType(hunkSeparators) { return typeof hunkSeparators === "function" ? "custom" : hunkSeparators ?? "line-info"; } function parseLineIndex(lineIndexAttr, diffStyle) { const [unifiedIndex, splitIndex] = lineIndexAttr.split(",").map(Number); return diffStyle === "split" ? splitIndex : unifiedIndex; } //#endregion export { VirtualizedFileDiff }; //# sourceMappingURL=VirtualizedFileDiff.js.map