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
686 changes: 686 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
"devDependencies": {
"@react-router/dev": "^7.13.2",
"@react-router/fs-routes": "^7.13.2",
"@testing-library/react": "^16.3.2",
"@types/nprogress": "^0.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"jsdom": "^30.0.1",
"typescript": "^6.0.2",
"vite": "^8.0.2",
"vitest": "^4.1.0"
Expand Down
16 changes: 14 additions & 2 deletions packages/ui/src/components/diff/diff-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useWrapLines } from '../../hooks/use-wrap-lines';
import { useKeyboard } from '../../hooks/use-keyboard';
import { useReviewThreads } from '../../hooks/use-review-threads';
import { useTours } from '../../hooks/use-tours';
import { useHideWhitespace } from '../../hooks/use-hide-whitespace';
import { pickActiveTour, orderPathsByTour, stopsByPath } from '../../lib/tour-order';
import { TourStepper } from './tour-stepper';
import { useCommentActions } from '../../hooks/use-comment-actions';
Expand Down Expand Up @@ -41,7 +42,7 @@ export function DiffPage() {
}>();

const [viewMode, setViewMode] = useState<ViewMode>(initialViewMode || 'split');
const [hideWhitespace, setHideWhitespace] = useState(false);
const { hideWhitespace, setHideWhitespace } = useHideWhitespace();
const [showHelp, setShowHelp] = useState(false);
const { theme, toggleTheme } = useTheme(initialTheme);
const { wrapLines, toggleWrapLines } = useWrapLines();
Expand Down Expand Up @@ -85,6 +86,16 @@ export function DiffPage() {
const diffPaths = useMemo(() => (diff ? diff.files.map(file => getFilePath(file)) : []), [diff]);
const tourStops = useMemo(() => stopsByPath(activeTour, diffPaths), [activeTour, diffPaths]);

const focusRangesByFile = useMemo(() => {
const ranges = new Map<string, { startLine: number; endLine: number }[]>();
for (const step of activeTour?.steps ?? []) {
const existing = ranges.get(step.filePath) ?? [];
existing.push({ startLine: step.startLine, endLine: step.endLine });
ranges.set(step.filePath, existing);
}
return ranges;
}, [activeTour]);

const orderedDiff = useMemo(() => {
if (!diff || !activeTour || !reviewOrderEnabled || activeTour.steps.length === 0) {
return diff;
Expand Down Expand Up @@ -404,7 +415,7 @@ export function DiffPage() {
onScrollToThread={handleScrollToThread}
repoName={info?.name || null}
branch={info?.branch || null}
description={info?.description || null}
description={hideWhitespace ? `${info?.description ?? ''} · whitespace hidden` : info?.description || null}
githubDetails={githubDetails}
sessionId={sessionId}
onGitHubPulled={() => queryClient.invalidateQueries({ queryKey: ['threads'] })}
Expand Down Expand Up @@ -458,6 +469,7 @@ export function DiffPage() {
onAddThread={handleAddThread}
pendingSelection={pendingSelection}
onPendingSelectionChange={setPendingSelection}
focusRangesByFile={focusRangesByFile}
/>
) : null}
</div>
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/components/diff/diff-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ interface DiffViewProps {
onAddThread: CommentActions['addThread'];
pendingSelection: LineSelection | null;
onPendingSelectionChange: (selection: LineSelection | null) => void;
/** Per file, the line ranges the walkthrough points at. */
focusRangesByFile?: Map<string, { startLine: number; endLine: number }[]>;
}

function estimateFileHeight(file: { hunks: { lines: { length: number } }[]; isBinary: boolean }, collapsed: boolean): number {
Expand All @@ -73,6 +75,7 @@ export function DiffView(props: DiffViewProps) {
handle, baseRef, canRevert, onRevert,
threads, commentsEnabled, commentActions, onAddThread,
pendingSelection, onPendingSelectionChange,
focusRangesByFile,
} = props;
const { highlight } = useHighlighter();
const scrollElementRef = useRef<HTMLElement>(null);
Expand Down Expand Up @@ -277,6 +280,7 @@ export function DiffView(props: DiffViewProps) {
ref={virtualizer.measureElement}
>
<FileBlock
focusRanges={focusRangesByFile?.get(filePath)}
highlighted={highlightedFile === filePath}
onHighlightEnd={() => {
if (highlightedFile === filePath) {
Expand Down
29 changes: 28 additions & 1 deletion packages/ui/src/components/diff/file-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SyntaxToken } from '../../lib/syntax-token';
import type { HighlightedTokens } from '../../hooks/use-highlighter';
import type { CommentSide, LineSelection } from '../comments/types';
import { type ViewMode, getFilePath, buildChangeGroupPatch, extractLinesFromDiff, extractLinesFromExpandedLines } from '../../lib/diff-utils';
import { classifyHunk, hunkIntersectsRanges, MECHANICAL_LABELS } from '../../lib/hunk-attention';
import { revertHunk as apiRevertHunk } from '../../lib/api';
import { ConfirmDialog } from '../ui/confirm-dialog';
import { computeGaps, createContextLines, getExpandRange, type ExpandableGap } from '../../lib/context-expansion';
Expand Down Expand Up @@ -58,6 +59,8 @@ interface FileBlockProps {
onPendingSelectionChange: (selection: LineSelection | null) => void;
highlighted?: boolean;
onHighlightEnd?: () => void;
/** Line ranges the walkthrough asked the reader to look at, on the new side. */
focusRanges?: { startLine: number; endLine: number }[];
}

interface GapExpansion {
Expand All @@ -69,7 +72,7 @@ interface GapExpansion {

export function FileBlock(props: FileBlockProps) {
const {
file, viewMode, collapsed, onToggleCollapse, reviewed, onReviewedChange, highlightLine, baseRef, canRevert, onRevert,
file, viewMode, collapsed, onToggleCollapse, reviewed, onReviewedChange, highlightLine, baseRef, canRevert, onRevert, focusRanges,
threads: allThreads, commentsEnabled, commentActions, onAddThread: rawAddThread, pendingSelection, onPendingSelectionChange,
highlighted, onHighlightEnd,
} = props;
Expand Down Expand Up @@ -262,6 +265,16 @@ export function FileBlock(props: FileBlockProps) {
};
}, [file, highlightLine]);

const hunkAttention = useMemo(
() => file.hunks.map(hunk => classifyHunk(file, hunk)),
[file],
);

const isHunkFocused = useCallback(
(hunk: DiffHunk) => hunkIntersectsRanges(hunk, focusRanges),
[focusRanges],
);

const gaps = useMemo(() => {
if (isNewFile) {
return [];
Expand Down Expand Up @@ -502,6 +515,18 @@ export function FileBlock(props: FileBlockProps) {
</colgroup>
)}
{file.hunks.map((hunk, i) => {
const mechanical = hunkAttention[i];
const focused = isHunkFocused(hunk);
const attentionClass = focused
? 'bg-accent/5'
: mechanical
? 'opacity-45 hover:opacity-100 transition-opacity'
: '';
const attentionTitle = focused
? 'The walkthrough points here'
: mechanical
? `Dimmed: ${MECHANICAL_LABELS[mechanical]}`
: undefined;
const betweenGap = i > 0 ? gapMap.get(`between-${i - 1}`) : undefined;
const betweenExpansion = betweenGap ? expansions.get(betweenGap.id) : undefined;
const topExpansion = i === 0 ? expansions.get('top') : undefined;
Expand All @@ -510,6 +535,8 @@ export function FileBlock(props: FileBlockProps) {
<HunkWithGap
key={i}
hunk={hunk}
attentionClass={attentionClass}
attentionTitle={attentionTitle}
viewMode={viewMode}
syntaxMap={syntaxMap}
expandControls={getExpandControlsForHunk(i)}
Expand Down
12 changes: 8 additions & 4 deletions packages/ui/src/components/diff/hunk-block-split.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { UndoIcon } from '../icons/undo-icon';

interface HunkBlockSplitProps {
hunk: DiffHunk;
/** Applied to every row group of the hunk, so a whole hunk recedes or stands out together. */
attentionClass?: string;
/** Why it was dimmed, so the reader can always find out rather than having to trust it. */
attentionTitle?: string;
syntaxMap?: Map<string, SyntaxToken[]>;
expandControls?: ExpandControls;
topExpansionLines?: DiffLineType[];
Expand Down Expand Up @@ -275,7 +279,7 @@ export function renderSplitRows(

export function HunkBlockSplit(props: HunkBlockSplitProps) {
const {
hunk, syntaxMap, expandControls, topExpansionLines, bottomExpansionLines, expansionSyntaxMap,
hunk, attentionClass = '', attentionTitle, syntaxMap, expandControls, topExpansionLines, bottomExpansionLines, expansionSyntaxMap,
threads, pendingSelection, currentAuthor, isLineSelected,
onLineMouseDown, onLineMouseEnter, onCommentClick,
onAddThread, onReply, onResolve, onUnresolve, onEditComment, onDeleteComment, onDeleteThread,
Expand Down Expand Up @@ -345,7 +349,7 @@ export function HunkBlockSplit(props: HunkBlockSplitProps) {
if (isChangeGroup && onRevertChange) {
const group = changeGroups[groupIdx];
sections.push(
<tbody key={`change-${groupIdx}`} className="group/undo">
<tbody key={`change-${groupIdx}`} className={`group/undo ${attentionClass}`} title={attentionTitle}>
{currentRows}
<tr className="relative z-10">
<td colSpan={4} className="relative h-0">
Expand All @@ -365,7 +369,7 @@ export function HunkBlockSplit(props: HunkBlockSplitProps) {
);
} else {
sections.push(
<tbody key={`context-${sections.length}`}>
<tbody key={`context-${sections.length}`} className={attentionClass} title={attentionTitle}>
{currentRows}
</tbody>
);
Expand Down Expand Up @@ -418,7 +422,7 @@ export function HunkBlockSplit(props: HunkBlockSplitProps) {

return (
<>
<tbody className={tbodyClass}>
<tbody className={`${tbodyClass} ${attentionClass}`} title={attentionTitle}>
<HunkHeader hunk={hunk} expandControls={expandControls} />
{expansionRows}
</tbody>
Expand Down
12 changes: 8 additions & 4 deletions packages/ui/src/components/diff/hunk-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { UndoIcon } from '../icons/undo-icon';

interface HunkBlockProps {
hunk: DiffHunk;
/** Applied to every row group of the hunk, so a whole hunk recedes or stands out together. */
attentionClass?: string;
/** Why it was dimmed, so the reader can always find out rather than having to trust it. */
attentionTitle?: string;
syntaxMap?: Map<string, SyntaxToken[]>;
expandControls?: ExpandControls;
topExpansionLines?: DiffLineType[];
Expand Down Expand Up @@ -107,7 +111,7 @@ export function renderLineWithComments(

export function HunkBlock(props: HunkBlockProps) {
const {
hunk, syntaxMap, expandControls, topExpansionLines, bottomExpansionLines, expansionSyntaxMap,
hunk, attentionClass = '', attentionTitle, syntaxMap, expandControls, topExpansionLines, bottomExpansionLines, expansionSyntaxMap,
threads, pendingSelection, currentAuthor, isLineSelected,
onLineMouseDown, onLineMouseEnter, onCommentClick,
onAddThread, onReply, onResolve, onUnresolve, onDeleteComment, onDeleteThread,
Expand Down Expand Up @@ -163,7 +167,7 @@ export function HunkBlock(props: HunkBlockProps) {
if (isChangeGroup && onRevertChange) {
const group = changeGroups[groupIdx];
sections.push(
<tbody key={`change-${groupIdx}`} className="group/undo">
<tbody key={`change-${groupIdx}`} className={`group/undo ${attentionClass}`} title={attentionTitle}>
{currentRows}
<tr className="relative z-10">
<td colSpan={4} className="relative h-0">
Expand All @@ -183,7 +187,7 @@ export function HunkBlock(props: HunkBlockProps) {
);
} else {
sections.push(
<tbody key={`context-${sections.length}`}>
<tbody key={`context-${sections.length}`} className={attentionClass} title={attentionTitle}>
{currentRows}
</tbody>
);
Expand Down Expand Up @@ -216,7 +220,7 @@ export function HunkBlock(props: HunkBlockProps) {

return (
<>
<tbody className={tbodyClass}>
<tbody className={`${tbodyClass} ${attentionClass}`} title={attentionTitle}>
<HunkHeader hunk={hunk} expandControls={expandControls} />
{expansionRows}
</tbody>
Expand Down
7 changes: 6 additions & 1 deletion packages/ui/src/components/diff/hunk-with-gap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface GapExpansion {

interface HunkWithGapProps {
hunk: DiffHunk;
attentionClass?: string;
attentionTitle?: string;
viewMode: ViewMode;
syntaxMap?: Map<string, SyntaxToken[]>;
expandControls?: ExpandControls;
Expand Down Expand Up @@ -46,7 +48,8 @@ interface HunkWithGapProps {

export function HunkWithGap(props: HunkWithGapProps) {
const {
hunk, viewMode, syntaxMap, expandControls, topExpansionLines, gapExpansion, gapId, highlightLine,
hunk,
attentionClass, attentionTitle, viewMode, syntaxMap, expandControls, topExpansionLines, gapExpansion, gapId, highlightLine,
threads, pendingSelection, currentAuthor, isLineSelected,
onLineMouseDown, onLineMouseEnter, onCommentClick,
onAddThread, onReply, onResolve, onUnresolve, onEditComment, onDeleteComment, onDeleteThread,
Expand Down Expand Up @@ -86,6 +89,8 @@ export function HunkWithGap(props: HunkWithGapProps) {
)}
<HunkComponent
hunk={hunk}
attentionClass={attentionClass}
attentionTitle={attentionTitle}
syntaxMap={syntaxMap}
expandControls={expandControls}
topExpansionLines={topExpansionLines && topExpansionLines.length > 0 ? topExpansionLines : undefined}
Expand Down
26 changes: 26 additions & 0 deletions packages/ui/src/hooks/use-hide-whitespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCallback, useState } from 'react';

const STORAGE_KEY = 'diffity-hide-whitespace';

/**
* On by default: whitespace is the formatter's business, so it should not spend a reviewer's
* attention. The toolbar shows when it is on, because a filtered diff renders fewer lines than
* the forge does and that must never be silent.
*/
function readStored(): boolean {
if (typeof window === 'undefined') {
return true;
}
return localStorage.getItem(STORAGE_KEY) !== 'false';
}

export function useHideWhitespace() {
const [hideWhitespace, setHideWhitespace] = useState<boolean>(readStored);

const setAndRemember = useCallback((hide: boolean) => {
setHideWhitespace(hide);
localStorage.setItem(STORAGE_KEY, String(hide));
}, []);

return { hideWhitespace, setHideWhitespace: setAndRemember };
}
2 changes: 1 addition & 1 deletion packages/ui/src/lib/diff-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function getTotalLineCount(file: DiffFile): number {
return count;
}

function isAutoCollapsible(file: DiffFile): boolean {
export function isAutoCollapsible(file: DiffFile): boolean {
if (file.status === 'deleted' || file.status === 'renamed') {
return true;
}
Expand Down
Loading