Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/diffshub/app/_components/CodeViewSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import type { CodeViewHandle } from '@pierre/diffs/react';
import {
IconComment,
IconFileTree,
Expand Down Expand Up @@ -28,6 +29,7 @@ import type {
CodeViewFileTreeSource,
CodeViewSavedCommentEntry,
CodeViewSavedCommentItem,
CommentMetadata,
} from './types';
import type { ThemeCycleControls } from './useThemeCycle';
import { WorkerPoolStatus } from './WorkerPoolStatus';
Expand All @@ -52,6 +54,7 @@ interface CodeViewSidebarProps {
source: CodeViewFileTreeSource;
streaming: boolean;
themeCycle: ThemeCycleControls;
viewerRef: RefObject<CodeViewHandle<CommentMetadata> | null>;
}

export const CodeViewSidebar = memo(function CodeViewSidebar({
Expand All @@ -66,6 +69,7 @@ export const CodeViewSidebar = memo(function CodeViewSidebar({
source,
streaming,
themeCycle,
viewerRef,
}: CodeViewSidebarProps) {
const [activeTab, setActiveTab] = useState<SidebarTab>('files');
let totalCommentCount = 0;
Expand Down Expand Up @@ -238,7 +242,7 @@ export const CodeViewSidebar = memo(function CodeViewSidebar({
<WorkerPoolStatus
expanded={activeStatusPanel === 'systemMonitor'}
onToggle={() => toggleStatusPanel('systemMonitor')}
scrollRef={scrollRef}
viewerRef={viewerRef}
themeCycle={themeCycle}
/>
</SidebarWrapper>
Expand Down
1 change: 1 addition & 0 deletions apps/diffshub/app/_components/ReviewUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
source={treeSource}
streaming={loadState === 'streaming'}
themeCycle={themeCycle}
viewerRef={viewerRef}
onSelectItem={handleSelectTreeItem}
/>
<CodeViewWrapper
Expand Down
35 changes: 22 additions & 13 deletions apps/diffshub/app/_components/WorkerPoolStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
DEFAULT_CODE_VIEW_FILE_METRICS,
queueRender,
} from '@pierre/diffs';
import { useWorkerPool } from '@pierre/diffs/react';
import { type CodeViewHandle, useWorkerPool } from '@pierre/diffs/react';
import type { WorkerStats } from '@pierre/diffs/worker';
import {
IconCircleFill,
Expand All @@ -27,17 +27,18 @@ import {
useState,
} from 'react';

import type { CommentMetadata } from './types';
import type { ThemeCycleControls } from './useThemeCycle';
import { cn } from '@/lib/utils';

const NUMBER_FORMATTER = new Intl.NumberFormat('en-US');

class AutoScrollTester {
class AutoScrollTester<LAnnotation> {
private running: 0 | 1 | 2 = 0;
private direction = 1;

constructor(
private scrollRef: RefObject<HTMLDivElement | null>,
private viewerRef: RefObject<CodeViewHandle<LAnnotation> | null>,
private onStateChange?: (running: boolean) => unknown
) {}

Expand All @@ -49,10 +50,17 @@ class AutoScrollTester {
}

render = () => {
if (this.running === 0 || this.scrollRef.current == null) {
const { current: viewerHandle } = this.viewerRef;
if (this.running === 0 || viewerHandle == null) {
return;
}
const { scrollHeight, scrollTop, clientHeight } = this.scrollRef.current;
const viewer = viewerHandle.getInstance();
if (viewer == null) {
return;
}
const scrollHeight = viewer.getScrollHeight();
const scrollTop = viewer.getScrollTop();
const clientHeight = viewer.getHeight();

// The first scroll tick should always attempt to scroll
if (this.running === 1) {
Expand All @@ -68,8 +76,9 @@ class AutoScrollTester {
this.stop();
return;
}
this.scrollRef.current.scrollTo({
top:
viewerHandle.scrollTo({
type: 'position',
position:
scrollTop +
clientHeight * 2 * this.direction +
Math.random() * DEFAULT_CODE_VIEW_FILE_METRICS.lineHeight,
Expand All @@ -94,15 +103,15 @@ class AutoScrollTester {
interface WorkerPoolStatusProps {
expanded: boolean;
onToggle(): void;
scrollRef: RefObject<HTMLDivElement | null>;
themeCycle: ThemeCycleControls;
viewerRef: RefObject<CodeViewHandle<CommentMetadata> | null>;
}

export const WorkerPoolStatus = memo(function WorkerPoolStatus({
expanded,
onToggle,
scrollRef,
themeCycle,
viewerRef,
}: WorkerPoolStatusProps) {
const pool = useWorkerPool();
const [stats, setStats] = useState<WorkerStats | undefined>(undefined);
Expand All @@ -127,8 +136,8 @@ export const WorkerPoolStatus = memo(function WorkerPoolStatus({
expanded={expanded}
onToggle={onToggle}
stats={stats}
scrollRef={scrollRef}
themeCycle={themeCycle}
viewerRef={viewerRef}
/>
)
);
Expand Down Expand Up @@ -164,8 +173,8 @@ interface StatsDisplayProps {
expanded: boolean;
onToggle(): void;
stats: WorkerStats;
scrollRef: RefObject<HTMLDivElement | null>;
themeCycle: ThemeCycleControls;
viewerRef: RefObject<CodeViewHandle<CommentMetadata> | null>;
}

// Map worker pool status to a single icon component + color so the legend row
Expand Down Expand Up @@ -207,12 +216,12 @@ function StatsDisplay({
expanded,
onToggle,
stats,
scrollRef,
themeCycle,
viewerRef,
}: StatsDisplayProps) {
const [isBrrt, setIsBrrt] = useState(false);
const [scrollTester] = useState(
() => new AutoScrollTester(scrollRef, setIsBrrt)
() => new AutoScrollTester(viewerRef, setIsBrrt)
);

// Mirror the inline (F3) hint with an actual keybinding so the label
Expand Down
103 changes: 52 additions & 51 deletions packages/diffs/src/components/CodeView.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import {
CORE_CSS_ATTRIBUTE,
DEFAULT_CODE_VIEW_FILE_METRICS,
DEFAULT_CODE_VIEW_LAYOUT,
DEFAULT_COLLAPSED_CONTEXT_THRESHOLD,
DEFAULT_SMOOTH_SCROLL_SETTINGS,
DEFAULT_THEMES,
DIFFS_DEVELOPMENT_BUILD,
DIFFS_TAG_NAME,
THEME_CSS_ATTRIBUTE,
UNSAFE_CSS_ATTRIBUTE,
} from '../constants';
import type { SelectionWriteOptions } from '../managers/InteractionManager';
import {
Expand Down Expand Up @@ -39,7 +36,6 @@ import { areOptionsEqual } from '../utils/areOptionsEqual';
import { areSelectionsEqual } from '../utils/areSelectionsEqual';
import { areThemesEqual } from '../utils/areThemesEqual';
import { createWindowFromScrollPosition } from '../utils/createWindowFromScrollPosition';
import { isStyleNode } from '../utils/isStyleNode';
import { prefersReducedMotion } from '../utils/prefersReducedMotion';
import { roundToDevicePixel } from '../utils/roundToDevicePixel';
import type { WorkerPoolManager } from '../worker';
Expand Down Expand Up @@ -978,10 +974,12 @@ export class CodeView<LAnnotation = undefined> {
this.promotePendingPooledElements();
let element = this.elementPool.pop();
while (element != null && !this.isElementPoolGenerationCurrent(element)) {
this.discardPooledElement(element);
element = this.elementPool.pop();
}
element ??= document.createElement(DIFFS_TAG_NAME);
this.markElementPoolGenerationCurrent(element);
element.style.removeProperty('display');
return element;
}

Expand All @@ -990,15 +988,16 @@ export class CodeView<LAnnotation = undefined> {
if (element != null && this.renderedItemOwnsFocus(element)) {
this.shouldFixContainerFocus = true;
}
if (element != null) {
element.style.display = 'none';
}

item.instance.cleanUp(true);
item.element = undefined;
if (element == null) {
return;
}

element.remove();
this.cleanElement(element);
this.queueElementForPool(element);
}

Expand All @@ -1018,30 +1017,9 @@ export class CodeView<LAnnotation = undefined> {
}
}

// Strip item-specific DOM while keeping the expensive shared shell assets
// that are valid for every item in this CodeView until shared options
// change.
private cleanElement(element: HTMLElement): void {
const { shadowRoot } = element;
if (shadowRoot != null) {
for (const child of Array.from(shadowRoot.children)) {
if (!isPooledShadowChild(child)) {
child.remove();
}
}
}

if (!this.isContainerManaged) {
element.replaceChildren();
}
}

private queueElementForPool(element: HTMLElement): void {
const poolLimit = this.getElementPoolLimit();
if (
!this.isElementPoolGenerationCurrent(element) ||
this.getElementPoolSize() >= poolLimit
) {
if (!this.isElementPoolGenerationCurrent(element)) {
this.discardPooledElement(element);
return;
}

Expand All @@ -1059,23 +1037,51 @@ export class CodeView<LAnnotation = undefined> {

const { pendingElementPool: pendingElements } = this;
this.pendingElementPool = [];
const poolLimit = this.getElementPoolLimit();
for (const element of pendingElements) {
if (
this.isElementPoolGenerationCurrent(element) &&
this.isElementClean(element) &&
this.elementPool.length < poolLimit
) {
if (!this.isElementPoolGenerationCurrent(element)) {
this.discardPooledElement(element);
} else if (this.isElementClean(element)) {
this.elementPool.push(element);
} else if (
this.isElementPoolGenerationCurrent(element) &&
this.getElementPoolSize() < poolLimit
) {
} else {
this.pendingElementPool.push(element);
}
}
}

private trimElementPoolToLimit(): void {
let overflow = this.getElementPoolSize() - this.getElementPoolLimit();
if (overflow <= 0) {
return;
}

const pendingDiscardCount = Math.min(
overflow,
this.pendingElementPool.length
);
const pendingDiscarded = this.pendingElementPool.splice(
0,
pendingDiscardCount
);
overflow -= pendingDiscarded.length;
for (const element of pendingDiscarded) {
this.discardPooledElement(element);
}

if (overflow <= 0) {
return;
}

const pooledDiscarded = this.elementPool.splice(0, overflow);
for (const element of pooledDiscarded) {
this.discardPooledElement(element);
}
}

private discardPooledElement(element: HTMLElement): void {
this.elementPoolTracker.delete(element);
element.remove();
}

private isElementClean(element: HTMLElement): boolean {
return element.childNodes.length === 0;
}
Expand All @@ -1085,6 +1091,12 @@ export class CodeView<LAnnotation = undefined> {
}

private clearElementPool(): void {
for (const element of this.elementPool) {
this.discardPooledElement(element);
}
for (const element of this.pendingElementPool) {
this.discardPooledElement(element);
}
this.elementPool.length = 0;
this.pendingElementPool.length = 0;
}
Expand Down Expand Up @@ -2676,6 +2688,7 @@ export class CodeView<LAnnotation = undefined> {
this.reconcileRenderedItems(updatedItems);
this.syncContainerHeight();
this.updateStickyPositioning();
this.trimElementPoolToLimit();

// Now that the dom has been flushed and we've computed our updated
// item/line metrics, we should attempt to resolve any scroll anchors and
Expand Down Expand Up @@ -3409,18 +3422,6 @@ function hasCodeViewDiffEstimateOptionChanged<LAnnotation>(
);
}

function isPooledShadowChild(child: Element): boolean {
if (child instanceof SVGElement) {
return true;
}
return (
isStyleNode(child) &&
(child.hasAttribute(CORE_CSS_ATTRIBUTE) ||
child.hasAttribute(THEME_CSS_ATTRIBUTE) ||
child.hasAttribute(UNSAFE_CSS_ATTRIBUTE))
);
}

function formatSelectedLineRange(range: SelectedLineRange): string {
const start = formatSelectedLinePoint(range.start, range.side);
const end = formatSelectedLinePoint(range.end, range.endSide ?? range.side);
Expand Down
Loading