diff --git a/apps/demo/index.html b/apps/demo/index.html index 3701b11e6..929d3ec0c 100644 --- a/apps/demo/index.html +++ b/apps/demo/index.html @@ -19,6 +19,7 @@ Diff Two Files Load Large-ish Diff Worker Render Diff + Clean Unified Diffs diff --git a/apps/demo/src/main.ts b/apps/demo/src/main.ts index 07085041e..f23d205a6 100644 --- a/apps/demo/src/main.ts +++ b/apps/demo/src/main.ts @@ -632,6 +632,15 @@ function getThemeType() { : 'system'; } +const cleanButton = document.getElementById('clean'); +cleanButton?.addEventListener('click', () => { + const container = document.getElementById('wrapper'); + if (container == null) { + return; + } + cleanupInstances(container); +}); + // For quick testing diffs // FAKE_DIFF_LINE_ANNOTATIONS.length = 0; // (() => { diff --git a/packages/diffs/src/components/VirtualizedFile.ts b/packages/diffs/src/components/VirtualizedFile.ts index d799a0f88..e83720659 100644 --- a/packages/diffs/src/components/VirtualizedFile.ts +++ b/packages/diffs/src/components/VirtualizedFile.ts @@ -157,6 +157,7 @@ export class VirtualizedFile< // Compute the approximate size of the file using cached line heights. // Uses lineHeight for lines without cached measurements. private computeApproximateSize(): void { + const isFirstCompute = this.height === 0; this.height = 0; if (this.file == null) { return; @@ -189,7 +190,11 @@ export class VirtualizedFile< this.height += fileGap; } - if (this.fileContainer != null && this.virtualizer.config.resizeDebugging) { + if ( + this.fileContainer != null && + this.virtualizer.config.resizeDebugging && + !isFirstCompute + ) { const rect = this.fileContainer.getBoundingClientRect(); if (rect.height !== this.height) { console.log( @@ -241,14 +246,16 @@ export class VirtualizedFile< return false; } - this.top ??= this.virtualizer.getOffsetInScrollContainer(fileContainer); if (isFirstRender) { this.computeApproximateSize(); this.virtualizer.connect(fileContainer, this); + this.top ??= this.virtualizer.getOffsetInScrollContainer(fileContainer); this.isVisible = this.virtualizer.isInstanceVisible( this.top, this.height ); + } else { + this.top ??= this.virtualizer.getOffsetInScrollContainer(fileContainer); } if (!this.isVisible) { diff --git a/packages/diffs/src/components/VirtualizedFileDiff.ts b/packages/diffs/src/components/VirtualizedFileDiff.ts index 80fb2160a..c8d4dc0a7 100644 --- a/packages/diffs/src/components/VirtualizedFileDiff.ts +++ b/packages/diffs/src/components/VirtualizedFileDiff.ts @@ -204,6 +204,7 @@ export class VirtualizedFileDiff< if (this.fileContainer == null) { return; } + this.renderRange = undefined; if (visible && !this.isVisible) { this.top = this.virtualizer.getOffsetInScrollContainer( this.fileContainer @@ -222,6 +223,7 @@ export class VirtualizedFileDiff< // dynamically change for a number of reasons so we can never be fully sure // if the height is 100% accurate private computeApproximateSize(): void { + const isFirstCompute = this.height === 0; this.height = 0; if (this.fileDiff == null) { return; @@ -296,7 +298,11 @@ export class VirtualizedFileDiff< this.height += fileGap; } - if (this.fileContainer != null && this.virtualizer.config.resizeDebugging) { + if ( + this.fileContainer != null && + this.virtualizer.config.resizeDebugging && + !isFirstCompute + ) { const rect = this.fileContainer.getBoundingClientRect(); if (rect.height !== this.height) { console.log( @@ -345,15 +351,16 @@ export class VirtualizedFileDiff< return false; } - this.top ??= this.virtualizer.getOffsetInScrollContainer(fileContainer); if (isFirstRender) { this.computeApproximateSize(); - // Figure out how to properly manage this... this.virtualizer.connect(fileContainer, this); + this.top ??= this.virtualizer.getOffsetInScrollContainer(fileContainer); this.isVisible = this.virtualizer.isInstanceVisible( this.top, this.height ); + } else { + this.top ??= this.virtualizer.getOffsetInScrollContainer(fileContainer); } if (!this.isVisible) { diff --git a/packages/diffs/src/components/Virtualizer.ts b/packages/diffs/src/components/Virtualizer.ts index 2af9181c4..27fb7d222 100644 --- a/packages/diffs/src/components/Virtualizer.ts +++ b/packages/diffs/src/components/Virtualizer.ts @@ -37,18 +37,22 @@ const DEFAULT_VIRTUALIZER_CONFIG: VirtualizerConfig = { let lastSize = 0; +let instance = -1; + export class Virtualizer { static __STOP: boolean = false; static __lastScrollPosition = 0; - public type = 'basic'; + public readonly __id: string = `virtualizer-${++instance}`; public readonly config: VirtualizerConfig; + public type = 'basic'; private intersectionObserver: IntersectionObserver | undefined; private scrollTop: number = 0; private height: number = 0; private scrollHeight: number = 0; private windowSpecs: VirtualWindowSpecs = { top: 0, bottom: 0 }; private root: HTMLElement | Document | undefined; + private contentContainer: HTMLElement | undefined; private resizeObserver: ResizeObserver | undefined; private observers: Map = new Map(); @@ -146,7 +150,7 @@ export class Virtualizer { this.scrollHeightDirty = true; shouldQueueUpdate = true; if (this.config.resizeDebugging) { - console.log('Virtualizer: content size change', { + console.log('Virtualizer: content size change', this.__id, { sizeChange: blockSize - lastSize, newSize: blockSize, }); @@ -159,11 +163,11 @@ export class Virtualizer { this.heightDirty = true; shouldQueueUpdate = true; } - } else if (entry.target === this.root.firstElementChild) { + } else if (entry.target === this.contentContainer) { this.scrollHeightDirty = true; shouldQueueUpdate = true; if (this.config.resizeDebugging) { - console.log('Virtualizer: scroller size change', { + console.log('Virtualizer: scroller size change', this.__id, { sizeChange: blockSize - lastSize, newSize: blockSize, }); @@ -200,15 +204,31 @@ export class Virtualizer { }); this.resizeObserver?.observe(this.root); contentContainer ??= this.root.firstElementChild ?? undefined; - if (contentContainer != null) { + if (contentContainer instanceof HTMLElement) { + this.contentContainer = contentContainer; this.resizeObserver?.observe(contentContainer); } } cleanUp(): void { + this.resizeObserver?.disconnect(); + this.resizeObserver = undefined; this.intersectionObserver?.disconnect(); this.intersectionObserver = undefined; + this.root?.removeEventListener('scroll', this.handleElementScroll); + window.removeEventListener('scroll', this.handleWindowScroll); + window.removeEventListener('resize', this.handleWindowResize); this.root = undefined; + this.contentContainer = undefined; + this.observers.clear(); + this.visibleInstances.clear(); + this.instancesChanged.clear(); + this.connectQueue.clear(); + this.visibleInstancesDirty = false; + this.windowSpecs = { top: 0, bottom: 0 }; + this.scrollTop = 0; + this.height = 0; + this.scrollHeight = 0; } getOffsetInScrollContainer(element: HTMLElement): number { @@ -245,6 +265,9 @@ export class Virtualizer { } this.intersectionObserver?.unobserve(container); this.observers.delete(container); + if (this.visibleInstances.delete(container)) { + this.visibleInstancesDirty = true; + } this.markDOMDirty(); queueRender(this.computeRenderRangeAndEmit); } diff --git a/packages/diffs/src/utils/createWindowFromScrollPosition.ts b/packages/diffs/src/utils/createWindowFromScrollPosition.ts index 16ced1305..98142eb14 100644 --- a/packages/diffs/src/utils/createWindowFromScrollPosition.ts +++ b/packages/diffs/src/utils/createWindowFromScrollPosition.ts @@ -18,13 +18,19 @@ export function createWindowFromScrollPosition({ overscrollSize, }: WindowFromScrollPositionProps): VirtualWindowSpecs { const windowHeight = height + overscrollSize * 2; - if (windowHeight > scrollHeight || fitPerfectly) { + const effectiveHeight = fitPerfectly ? height : windowHeight; + scrollHeight = Math.max(scrollHeight, effectiveHeight); + + if (windowHeight >= scrollHeight || fitPerfectly) { + const top = Math.max(scrollTop - containerOffset, 0); + const bottom = + Math.min(scrollTop + effectiveHeight, scrollHeight) - containerOffset; return { - top: Math.max(scrollTop - containerOffset, 0), - bottom: - scrollTop + (fitPerfectly ? height : windowHeight) - containerOffset, + top, + bottom: Math.max(bottom, top), }; } + const scrollCenter = scrollTop + height / 2; let top = scrollCenter - windowHeight / 2; let bottom = top + windowHeight;