From 3c36ee6cd5f181e794522987d70e6f42a8fea46f Mon Sep 17 00:00:00 2001 From: Solomon Date: Fri, 24 Oct 2025 19:24:31 +0100 Subject: [PATCH] feat: ignore diff whitespace (#1067) Add an option to ignore diff whitespace for diffs. --- frontend/src/components/DiffCard.tsx | 15 ++- frontend/src/components/diff-view-switch.tsx | 111 ++++++++++++------- frontend/src/i18n/locales/en/tasks.json | 1 + frontend/src/i18n/locales/es/tasks.json | 1 + frontend/src/i18n/locales/ja/tasks.json | 1 + frontend/src/i18n/locales/ko/tasks.json | 1 + frontend/src/stores/useDiffViewStore.ts | 6 + 7 files changed, 96 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/DiffCard.tsx b/frontend/src/components/DiffCard.tsx index 1116f507..a85e5f85 100644 --- a/frontend/src/components/DiffCard.tsx +++ b/frontend/src/components/DiffCard.tsx @@ -25,7 +25,10 @@ import type { TaskAttempt } from 'shared/types'; import { useReview, type ReviewDraft } from '@/contexts/ReviewProvider'; import { CommentWidgetLine } from '@/components/diff/CommentWidgetLine'; import { ReviewCommentRenderer } from '@/components/diff/ReviewCommentRenderer'; -import { useDiffViewMode } from '@/stores/useDiffViewStore'; +import { + useDiffViewMode, + useIgnoreWhitespaceDiff, +} from '@/stores/useDiffViewStore'; import { useProject } from '@/contexts/project-context'; type Props = { @@ -76,6 +79,7 @@ export default function DiffCard({ const theme = getActualTheme(config?.theme); const { comments, drafts, setDraft } = useReview(); const globalMode = useDiffViewMode(); + const ignoreWhitespace = useIgnoreWhitespaceDiff(); const { projectId } = useProject(); const oldName = diff.oldPath || undefined; @@ -92,6 +96,11 @@ export default function DiffCard({ const newContentSafe = diff.newContent || ''; const isContentEqual = oldContentSafe === newContentSafe; + const diffOptions = useMemo( + () => (ignoreWhitespace ? { ignoreWhitespace: true as const } : undefined), + [ignoreWhitespace] + ); + const diffFile = useMemo(() => { if (isContentEqual || isOmitted) return null; try { @@ -103,7 +112,8 @@ export default function DiffCard({ newFileName, newContentSafe, oldLang, - newLang + newLang, + diffOptions ); file.initRaw(); return file; @@ -120,6 +130,7 @@ export default function DiffCard({ newLang, oldContentSafe, newContentSafe, + diffOptions, ]); const add = isOmitted diff --git a/frontend/src/components/diff-view-switch.tsx b/frontend/src/components/diff-view-switch.tsx index e1dd3f3e..10d86cb2 100644 --- a/frontend/src/components/diff-view-switch.tsx +++ b/frontend/src/components/diff-view-switch.tsx @@ -1,7 +1,11 @@ -import { Columns, FileText } from 'lucide-react'; +import { Columns, FileText, Pilcrow } from 'lucide-react'; import { useTranslation } from 'react-i18next'; 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 { Tooltip, @@ -18,46 +22,77 @@ export default function DiffViewSwitch({ className }: Props) { const { t } = useTranslation('tasks'); const mode = useDiffViewMode(); const setMode = useDiffViewStore((s) => s.setMode); + const ignoreWhitespace = useIgnoreWhitespaceDiff(); + const setIgnoreWhitespace = useDiffViewStore((s) => s.setIgnoreWhitespace); + + const whitespaceValue = ignoreWhitespace ? ['ignoreWhitespace'] : []; return ( - v && setMode(v as 'unified' | 'split')} - className={cn('inline-flex gap-4', className)} - aria-label="Diff view mode" - > - - - - - - - - {t('diff.viewModes.inline')} - - +
+ v && setMode(v as 'unified' | 'split')} + className="inline-flex gap-4" + aria-label="Diff view mode" + > + + + + + + + + {t('diff.viewModes.inline')} + + - - - - - - - - {t('diff.viewModes.split')} - - - + + + + + + + + {t('diff.viewModes.split')} + + + + + + setIgnoreWhitespace(values.includes('ignoreWhitespace')) + } + className="inline-flex gap-4" + aria-label={t('diff.ignoreWhitespace')} + > + + + + + + + + {t('diff.ignoreWhitespace')} + + + +
); } diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index a9ba2762..34d42105 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -91,6 +91,7 @@ "inline": "Inline view", "split": "Split view" }, + "ignoreWhitespace": "Ignore whitespace changes", "errorLoadingDiff": "Failed to load diff: {{error}}", "expandAll": "Expand all diffs", "collapseAll": "Collapse all diffs", diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index 5855f922..e6be9469 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -75,6 +75,7 @@ "collapseAll": "Collapse all diffs", "errorLoadingDiff": "Failed to load diff: {{error}}", "expandAll": "Expand all diffs", + "ignoreWhitespace": "Ignorar cambios de espacios en blanco", "filesChanged_one": "{{count}} file changed", "filesChanged_other": "{{count}} files changed", "noChanges": "No changes have been made yet", diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index ea9237ab..7b00641d 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -75,6 +75,7 @@ "collapseAll": "Collapse all diffs", "errorLoadingDiff": "Failed to load diff: {{error}}", "expandAll": "Expand all diffs", + "ignoreWhitespace": "空白の変更を無視", "filesChanged_one": "{{count}} file changed", "filesChanged_other": "{{count}} files changed", "noChanges": "No changes have been made yet", diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index c4bf07ee..aeef5c58 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -75,6 +75,7 @@ "collapseAll": "Collapse all diffs", "errorLoadingDiff": "Failed to load diff: {{error}}", "expandAll": "Expand all diffs", + "ignoreWhitespace": "공백 변경 무시", "filesChanged_one": "{{count}} file changed", "filesChanged_other": "{{count}} files changed", "noChanges": "No changes have been made yet", diff --git a/frontend/src/stores/useDiffViewStore.ts b/frontend/src/stores/useDiffViewStore.ts index f4572a21..ade630e6 100644 --- a/frontend/src/stores/useDiffViewStore.ts +++ b/frontend/src/stores/useDiffViewStore.ts @@ -6,6 +6,8 @@ type State = { mode: DiffViewMode; setMode: (mode: DiffViewMode) => void; toggle: () => void; + ignoreWhitespace: boolean; + setIgnoreWhitespace: (value: boolean) => void; }; export const useDiffViewStore = create((set) => ({ @@ -13,6 +15,10 @@ export const useDiffViewStore = create((set) => ({ setMode: (mode) => set({ mode }), toggle: () => set((s) => ({ mode: s.mode === 'unified' ? 'split' : 'unified' })), + ignoreWhitespace: true, + setIgnoreWhitespace: (value) => set({ ignoreWhitespace: value }), })); export const useDiffViewMode = () => useDiffViewStore((s) => s.mode); +export const useIgnoreWhitespaceDiff = () => + useDiffViewStore((s) => s.ignoreWhitespace);