Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/features/git/components/GitDiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export function GitDiffPanel({
gitRootScanLoading = false,
gitRootScanError = null,
gitRootScanHasScanned = false,
selectedPath = null,
onGitRootScanDepthChange,
onScanGitRoots,
onSelectGitRoot,
Expand Down Expand Up @@ -461,7 +462,7 @@ export function GitDiffPanel({
return (
<div
key={file.path}
className="diff-row"
className={`diff-row ${selectedPath === file.path ? "active" : ""}`}
role="button"
tabIndex={0}
onClick={() => onSelectFile?.(file.path)}
Expand Down
97 changes: 89 additions & 8 deletions src/features/git/components/GitDiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,21 @@ const DiffCard = memo(function DiffCard({
}, [entry.diff, entry.path]);

return (
<div
data-diff-path={entry.path}
className={`diff-viewer-item ${isSelected ? "active" : ""}`}
>
<div
data-diff-path={entry.path}
className={`diff-viewer-item ${isSelected ? "active" : ""}`}
>
<div className="diff-viewer-header">
<span className="diff-viewer-status">{entry.status}</span>
<span className="diff-viewer-status" data-status={entry.status}>
{entry.status}
</span>
<span className="diff-viewer-path">{entry.path}</span>
</div>
{entry.diff.trim().length > 0 && fileDiff ? (
<div className="diff-viewer-output">
<div className="diff-viewer-output diff-viewer-output-flat">
<FileDiff
fileDiff={fileDiff}
options={diffOptions}
className="diff-viewer-diffs"
style={{ width: "100%", maxWidth: "100%", minWidth: 0 }}
/>
</div>
Expand All @@ -109,9 +110,11 @@ export function GitDiffViewer({
selectedPath,
isLoading,
error,
onActivePathChange,
}: GitDiffViewerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const lastScrolledPathRef = useRef<string | null>(null);
const activePathRef = useRef<string | null>(null);
const poolOptions = useMemo(() => ({ workerFactory }), []);
const highlighterOptions = useMemo(
() => ({ theme: { dark: "pierre-dark", light: "pierre-light" } }),
Expand All @@ -131,6 +134,18 @@ export function GitDiffViewer({
overscan: 6,
});
const virtualItems = rowVirtualizer.getVirtualItems();
const stickyEntry = useMemo(() => {
if (!diffs.length) {
return null;
}
if (selectedPath) {
const index = indexByPath.get(selectedPath);
if (index !== undefined) {
return diffs[index];
}
}
return diffs[0];
}, [diffs, selectedPath, indexByPath]);

useEffect(() => {
if (!selectedPath) {
Expand All @@ -147,12 +162,78 @@ export function GitDiffViewer({
lastScrolledPathRef.current = selectedPath;
}, [selectedPath, indexByPath, rowVirtualizer]);

useEffect(() => {
activePathRef.current = selectedPath;
}, [selectedPath]);

useEffect(() => {
const container = containerRef.current;
if (!container || !onActivePathChange) {
return;
}
let frameId: number | null = null;

const updateActivePath = () => {
frameId = null;
const items = rowVirtualizer.getVirtualItems();
if (!items.length) {
return;
}
const scrollTop = container.scrollTop;
const targetOffset = scrollTop + 8;
let activeItem = items[0];
for (const item of items) {
if (item.start <= targetOffset) {
activeItem = item;
} else {
break;
}
}
const nextPath = diffs[activeItem.index]?.path;
if (!nextPath || nextPath === activePathRef.current) {
return;
}
activePathRef.current = nextPath;
lastScrolledPathRef.current = nextPath;
onActivePathChange(nextPath);
};

const handleScroll = () => {
if (frameId !== null) {
return;
}
frameId = requestAnimationFrame(updateActivePath);
};

handleScroll();
container.addEventListener("scroll", handleScroll, { passive: true });
return () => {
if (frameId !== null) {
cancelAnimationFrame(frameId);
}
container.removeEventListener("scroll", handleScroll);
};
}, [diffs, onActivePathChange, rowVirtualizer]);

return (
<WorkerPoolContextProvider
poolOptions={poolOptions}
highlighterOptions={highlighterOptions}
>
<div className="diff-viewer" ref={containerRef}>
{!error && stickyEntry && (
<div className="diff-viewer-sticky">
<div className="diff-viewer-header diff-viewer-header-sticky">
<span
className="diff-viewer-status"
data-status={stickyEntry.status}
>
{stickyEntry.status}
</span>
<span className="diff-viewer-path">{stickyEntry.path}</span>
</div>
</div>
)}
{error && <div className="diff-viewer-empty">{error}</div>}
{!error && isLoading && diffs.length > 0 && (
<div className="diff-viewer-loading diff-viewer-loading-overlay">
Expand All @@ -178,7 +259,7 @@ export function GitDiffViewer({
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{
transform: `translateY(${virtualRow.start}px)`,
transform: `translate3d(0, ${virtualRow.start}px, 0)`,
}}
>
<DiffCard
Expand Down
1 change: 1 addition & 0 deletions src/features/layout/hooks/useLayoutNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult {
logLoading={options.gitLogLoading}
files={options.gitStatus.files}
onSelectFile={options.onSelectDiff}
selectedPath={options.selectedDiffPath}
logEntries={options.gitLogEntries}
logTotal={options.gitLogTotal}
logAhead={options.gitLogAhead}
Expand Down
82 changes: 71 additions & 11 deletions src/styles/diff-viewer.css
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
.diff-viewer {
display: flex;
flex-direction: column;
gap: 16px;
gap: 0;
overflow-y: auto;
position: relative;
padding: 12px 12px 16px;
padding: 12px 0 16px;
flex: 1;
min-height: 0;
min-width: 0;
background: var(--surface-messages);
background: color-mix(in srgb, var(--surface-messages) 92%, #000);
}

.main .diff-viewer {
margin-top: calc(-1 * var(--main-topbar-height));
padding-top: calc(12px + var(--main-topbar-height));
margin-top: 0;
padding-top: 0;
}

.diff-viewer-list {
Expand All @@ -26,19 +26,23 @@
left: 0;
top: 0;
width: 100%;
padding-bottom: 16px;
padding-bottom: 0;
will-change: transform;
}

.diff-viewer-item {
border-radius: 10px;
border: 1px solid var(--border-subtle);
background: var(--surface-diff-card);
padding: 10px 12px;
border-radius: 0;
border: none;
background: transparent;
padding: 0;
width: 100%;
border-bottom: 1px solid var(--border-subtle);
position: relative;
}

.diff-viewer-item.active {
border-color: var(--border-subtle);
background: color-mix(in srgb, var(--surface-active) 35%, transparent);
box-shadow: none;
}

Expand All @@ -48,7 +52,26 @@
gap: 8px;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 8px;
background: var(--surface-messages);
padding: 12px 12px 10px;
margin: 0;
border-bottom: 1px solid var(--border-subtle);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08);
}

.diff-viewer-sticky {
position: sticky;
top: 0;
z-index: 3;
height: 0;
overflow: visible;
pointer-events: none;
}

.diff-viewer-header-sticky {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
pointer-events: auto;
}

.diff-viewer-status {
Expand All @@ -58,6 +81,33 @@
border-radius: 999px;
border: 1px solid var(--border-stronger);
color: var(--text-stronger);
background: var(--surface-control);
text-transform: uppercase;
}

.diff-viewer-status[data-status="A"] {
color: #47d488;
border-color: rgba(71, 212, 136, 0.4);
background: rgba(71, 212, 136, 0.12);
}

.diff-viewer-status[data-status="M"] {
color: #f5c363;
border-color: rgba(245, 195, 99, 0.4);
background: rgba(245, 195, 99, 0.12);
}

.diff-viewer-status[data-status="D"] {
color: #ff6b6b;
border-color: rgba(255, 107, 107, 0.45);
background: rgba(255, 107, 107, 0.12);
}

.diff-viewer-status[data-status="R"],
.diff-viewer-status[data-status="T"] {
color: var(--text-faint);
border-color: var(--border-subtle);
background: var(--surface-control);
}

.diff-viewer-path {
Expand Down Expand Up @@ -86,6 +136,12 @@
--diffs-tab-size: 2;
}

.diff-viewer-output-flat {
background: transparent;
border-radius: 0;
padding: 0;
box-shadow: none;
}

.diff-viewer-output diffs-container {
display: block;
Expand All @@ -94,6 +150,10 @@
width: 100%;
}

.diff-viewer-output-flat diffs-container {
background: transparent;
}

.diff-line {
display: grid;
grid-template-columns: 64px 1fr;
Expand Down