feat: ignore diff whitespace (#1067)

Add an option to ignore diff whitespace for diffs.
This commit is contained in:
Solomon
2025-10-24 19:24:31 +01:00
committed by GitHub
parent c9a5609550
commit 3c36ee6cd5
7 changed files with 96 additions and 40 deletions

View File

@@ -25,7 +25,10 @@ import type { TaskAttempt } from 'shared/types';
import { useReview, type ReviewDraft } from '@/contexts/ReviewProvider'; import { useReview, type ReviewDraft } from '@/contexts/ReviewProvider';
import { CommentWidgetLine } from '@/components/diff/CommentWidgetLine'; import { CommentWidgetLine } from '@/components/diff/CommentWidgetLine';
import { ReviewCommentRenderer } from '@/components/diff/ReviewCommentRenderer'; import { ReviewCommentRenderer } from '@/components/diff/ReviewCommentRenderer';
import { useDiffViewMode } from '@/stores/useDiffViewStore'; import {
useDiffViewMode,
useIgnoreWhitespaceDiff,
} from '@/stores/useDiffViewStore';
import { useProject } from '@/contexts/project-context'; import { useProject } from '@/contexts/project-context';
type Props = { type Props = {
@@ -76,6 +79,7 @@ export default function DiffCard({
const theme = getActualTheme(config?.theme); const theme = getActualTheme(config?.theme);
const { comments, drafts, setDraft } = useReview(); const { comments, drafts, setDraft } = useReview();
const globalMode = useDiffViewMode(); const globalMode = useDiffViewMode();
const ignoreWhitespace = useIgnoreWhitespaceDiff();
const { projectId } = useProject(); const { projectId } = useProject();
const oldName = diff.oldPath || undefined; const oldName = diff.oldPath || undefined;
@@ -92,6 +96,11 @@ export default function DiffCard({
const newContentSafe = diff.newContent || ''; const newContentSafe = diff.newContent || '';
const isContentEqual = oldContentSafe === newContentSafe; const isContentEqual = oldContentSafe === newContentSafe;
const diffOptions = useMemo(
() => (ignoreWhitespace ? { ignoreWhitespace: true as const } : undefined),
[ignoreWhitespace]
);
const diffFile = useMemo(() => { const diffFile = useMemo(() => {
if (isContentEqual || isOmitted) return null; if (isContentEqual || isOmitted) return null;
try { try {
@@ -103,7 +112,8 @@ export default function DiffCard({
newFileName, newFileName,
newContentSafe, newContentSafe,
oldLang, oldLang,
newLang newLang,
diffOptions
); );
file.initRaw(); file.initRaw();
return file; return file;
@@ -120,6 +130,7 @@ export default function DiffCard({
newLang, newLang,
oldContentSafe, oldContentSafe,
newContentSafe, newContentSafe,
diffOptions,
]); ]);
const add = isOmitted const add = isOmitted

View File

@@ -1,7 +1,11 @@
import { Columns, FileText } from 'lucide-react'; import { Columns, FileText, Pilcrow } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useDiffViewMode, useDiffViewStore } from '@/stores/useDiffViewStore'; import {
useDiffViewMode,
useDiffViewStore,
useIgnoreWhitespaceDiff,
} from '@/stores/useDiffViewStore';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { import {
Tooltip, Tooltip,
@@ -18,46 +22,77 @@ export default function DiffViewSwitch({ className }: Props) {
const { t } = useTranslation('tasks'); const { t } = useTranslation('tasks');
const mode = useDiffViewMode(); const mode = useDiffViewMode();
const setMode = useDiffViewStore((s) => s.setMode); const setMode = useDiffViewStore((s) => s.setMode);
const ignoreWhitespace = useIgnoreWhitespaceDiff();
const setIgnoreWhitespace = useDiffViewStore((s) => s.setIgnoreWhitespace);
const whitespaceValue = ignoreWhitespace ? ['ignoreWhitespace'] : [];
return ( return (
<TooltipProvider> <TooltipProvider>
<ToggleGroup <div className={cn('inline-flex gap-4', className)}>
type="single" <ToggleGroup
value={mode ?? ''} type="single"
onValueChange={(v) => v && setMode(v as 'unified' | 'split')} value={mode ?? ''}
className={cn('inline-flex gap-4', className)} onValueChange={(v) => v && setMode(v as 'unified' | 'split')}
aria-label="Diff view mode" className="inline-flex gap-4"
> aria-label="Diff view mode"
<Tooltip> >
<TooltipTrigger asChild> <Tooltip>
<ToggleGroupItem <TooltipTrigger asChild>
value="unified" <ToggleGroupItem
aria-label="Inline view" value="unified"
active={mode === 'unified'} aria-label="Inline view"
> active={mode === 'unified'}
<FileText className="h-4 w-4" /> >
</ToggleGroupItem> <FileText className="h-4 w-4" />
</TooltipTrigger> </ToggleGroupItem>
<TooltipContent side="bottom"> </TooltipTrigger>
{t('diff.viewModes.inline')} <TooltipContent side="bottom">
</TooltipContent> {t('diff.viewModes.inline')}
</Tooltip> </TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<ToggleGroupItem <ToggleGroupItem
value="split" value="split"
aria-label="Split view" aria-label="Split view"
active={mode === 'split'} active={mode === 'split'}
> >
<Columns className="h-4 w-4" /> <Columns className="h-4 w-4" />
</ToggleGroupItem> </ToggleGroupItem>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
{t('diff.viewModes.split')} {t('diff.viewModes.split')}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</ToggleGroup> </ToggleGroup>
<ToggleGroup
type="multiple"
value={whitespaceValue}
onValueChange={(values) =>
setIgnoreWhitespace(values.includes('ignoreWhitespace'))
}
className="inline-flex gap-4"
aria-label={t('diff.ignoreWhitespace')}
>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="ignoreWhitespace"
aria-label={t('diff.ignoreWhitespace')}
active={ignoreWhitespace}
>
<Pilcrow className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent side="bottom">
{t('diff.ignoreWhitespace')}
</TooltipContent>
</Tooltip>
</ToggleGroup>
</div>
</TooltipProvider> </TooltipProvider>
); );
} }

View File

@@ -91,6 +91,7 @@
"inline": "Inline view", "inline": "Inline view",
"split": "Split view" "split": "Split view"
}, },
"ignoreWhitespace": "Ignore whitespace changes",
"errorLoadingDiff": "Failed to load diff: {{error}}", "errorLoadingDiff": "Failed to load diff: {{error}}",
"expandAll": "Expand all diffs", "expandAll": "Expand all diffs",
"collapseAll": "Collapse all diffs", "collapseAll": "Collapse all diffs",

View File

@@ -75,6 +75,7 @@
"collapseAll": "Collapse all diffs", "collapseAll": "Collapse all diffs",
"errorLoadingDiff": "Failed to load diff: {{error}}", "errorLoadingDiff": "Failed to load diff: {{error}}",
"expandAll": "Expand all diffs", "expandAll": "Expand all diffs",
"ignoreWhitespace": "Ignorar cambios de espacios en blanco",
"filesChanged_one": "{{count}} file changed", "filesChanged_one": "{{count}} file changed",
"filesChanged_other": "{{count}} files changed", "filesChanged_other": "{{count}} files changed",
"noChanges": "No changes have been made yet", "noChanges": "No changes have been made yet",

View File

@@ -75,6 +75,7 @@
"collapseAll": "Collapse all diffs", "collapseAll": "Collapse all diffs",
"errorLoadingDiff": "Failed to load diff: {{error}}", "errorLoadingDiff": "Failed to load diff: {{error}}",
"expandAll": "Expand all diffs", "expandAll": "Expand all diffs",
"ignoreWhitespace": "空白の変更を無視",
"filesChanged_one": "{{count}} file changed", "filesChanged_one": "{{count}} file changed",
"filesChanged_other": "{{count}} files changed", "filesChanged_other": "{{count}} files changed",
"noChanges": "No changes have been made yet", "noChanges": "No changes have been made yet",

View File

@@ -75,6 +75,7 @@
"collapseAll": "Collapse all diffs", "collapseAll": "Collapse all diffs",
"errorLoadingDiff": "Failed to load diff: {{error}}", "errorLoadingDiff": "Failed to load diff: {{error}}",
"expandAll": "Expand all diffs", "expandAll": "Expand all diffs",
"ignoreWhitespace": "공백 변경 무시",
"filesChanged_one": "{{count}} file changed", "filesChanged_one": "{{count}} file changed",
"filesChanged_other": "{{count}} files changed", "filesChanged_other": "{{count}} files changed",
"noChanges": "No changes have been made yet", "noChanges": "No changes have been made yet",

View File

@@ -6,6 +6,8 @@ type State = {
mode: DiffViewMode; mode: DiffViewMode;
setMode: (mode: DiffViewMode) => void; setMode: (mode: DiffViewMode) => void;
toggle: () => void; toggle: () => void;
ignoreWhitespace: boolean;
setIgnoreWhitespace: (value: boolean) => void;
}; };
export const useDiffViewStore = create<State>((set) => ({ export const useDiffViewStore = create<State>((set) => ({
@@ -13,6 +15,10 @@ export const useDiffViewStore = create<State>((set) => ({
setMode: (mode) => set({ mode }), setMode: (mode) => set({ mode }),
toggle: () => toggle: () =>
set((s) => ({ mode: s.mode === 'unified' ? 'split' : 'unified' })), set((s) => ({ mode: s.mode === 'unified' ? 'split' : 'unified' })),
ignoreWhitespace: true,
setIgnoreWhitespace: (value) => set({ ignoreWhitespace: value }),
})); }));
export const useDiffViewMode = () => useDiffViewStore((s) => s.mode); export const useDiffViewMode = () => useDiffViewStore((s) => s.mode);
export const useIgnoreWhitespaceDiff = () =>
useDiffViewStore((s) => s.ignoreWhitespace);