From 0be512fc23d93aca4a013fd5e2bd5443fb43027d Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:26:50 -0700 Subject: [PATCH] Add diff word wrap toggle (#6214) Co-authored-by: Orca --- .../runtime-home-service.test.ts | 1 + src/main/codex-accounts/service.test.ts | 1 + .../components/editor/CombinedDiffViewer.tsx | 21 +++++++- .../src/components/editor/DiffSectionBody.tsx | 4 ++ .../src/components/editor/DiffSectionItem.tsx | 7 ++- .../src/components/editor/DiffViewer.tsx | 2 + .../components/editor/EditorPanelHeader.tsx | 5 ++ .../editor/EditorPanelMarkdownActionsMenu.tsx | 22 +++++++- .../diff-editor-word-wrap-options.test.ts | 13 +++++ .../editor/diff-editor-word-wrap-options.ts | 9 ++++ .../settings/GeneralEditorSettingsSection.tsx | 52 +++++++++++++++++++ src/renderer/src/i18n/locales/en.json | 13 +++-- src/renderer/src/i18n/locales/es.json | 13 +++-- src/renderer/src/i18n/locales/ja.json | 13 +++-- src/renderer/src/i18n/locales/ko.json | 13 +++-- src/renderer/src/i18n/locales/zh.json | 13 +++-- src/shared/constants.ts | 1 + src/shared/types.ts | 1 + 18 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 src/renderer/src/components/editor/diff-editor-word-wrap-options.test.ts create mode 100644 src/renderer/src/components/editor/diff-editor-word-wrap-options.ts diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index fd09a86fc08..cfee7b8ad10 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -144,6 +144,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings terminalWindowsPowerShellImplementation: 'powershell.exe', enableGitHubAttribution: true, ...overrides, + diffWordWrap: overrides.diffWordWrap ?? false, localWindowsRuntimeDefault: overrides.localWindowsRuntimeDefault ?? { kind: 'windows-host' }, leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default', appFontFamily, diff --git a/src/main/codex-accounts/service.test.ts b/src/main/codex-accounts/service.test.ts index 3d899b25eae..1f52a6470a0 100644 --- a/src/main/codex-accounts/service.test.ts +++ b/src/main/codex-accounts/service.test.ts @@ -148,6 +148,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings terminalWindowsPowerShellImplementation: 'powershell.exe', enableGitHubAttribution: true, ...overrides, + diffWordWrap: overrides.diffWordWrap ?? false, localWindowsRuntimeDefault: overrides.localWindowsRuntimeDefault ?? { kind: 'windows-host' }, leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default', appFontFamily, diff --git a/src/renderer/src/components/editor/CombinedDiffViewer.tsx b/src/renderer/src/components/editor/CombinedDiffViewer.tsx index f140c5b392b..380bda6b3bb 100644 --- a/src/renderer/src/components/editor/CombinedDiffViewer.tsx +++ b/src/renderer/src/components/editor/CombinedDiffViewer.tsx @@ -46,7 +46,7 @@ import type { GitDiffResult, GitStatusEntry } from '../../../../shared/types' -import { Check, Copy, MessageSquare, PanelLeftOpen, Sparkles, Trash2 } from 'lucide-react' +import { Check, Copy, MessageSquare, PanelLeftOpen, Sparkles, Trash2, WrapText } from 'lucide-react' import { toast } from 'sonner' import { DiffSectionItem } from './DiffSectionItem' import { DiffNotesSendMenu } from './DiffNotesSendMenu' @@ -251,6 +251,7 @@ export default function CombinedDiffViewer({ const openCommitDiff = useAppStore((s) => s.openCommitDiff) const openConflictReview = useAppStore((s) => s.openConflictReview) const openBranchAllDiffs = useAppStore((s) => s.openBranchAllDiffs) + const updateSettings = useAppStore((s) => s.updateSettings) const clearDiffComments = useAppStore((s) => s.clearDiffComments) const diffCommentsForWorktree = useAppStore((s) => s.getDiffComments(file.worktreeId)) const activeGroupId = useAppStore((s) => s.activeGroupIdByWorktree[file.worktreeId]) @@ -1129,6 +1130,10 @@ export default function CombinedDiffViewer({ }) }, []) + const toggleDiffWordWrap = useCallback(() => { + void updateSettings({ diffWordWrap: settings?.diffWordWrap !== true }) + }, [settings?.diffWordWrap, updateSettings]) + const openSection = useCallback( (index: number) => { const section = sectionsRef.current[index] @@ -1838,6 +1843,20 @@ export default function CombinedDiffViewer({ ? translate('auto.components.editor.CombinedDiffViewer.f786fd54e1', 'Inline') : translate('auto.components.editor.CombinedDiffViewer.ec5053c7f5', 'Side by Side')} + diff --git a/src/renderer/src/components/editor/DiffSectionBody.tsx b/src/renderer/src/components/editor/DiffSectionBody.tsx index 7792e902e17..87994196dc6 100644 --- a/src/renderer/src/components/editor/DiffSectionBody.tsx +++ b/src/renderer/src/components/editor/DiffSectionBody.tsx @@ -9,6 +9,7 @@ import { combinedDiffSectionScrollbarOptions } from './diff-editor-scrollbar-opt import type { DiffSection } from './diff-section-types' import { translate } from '@/i18n/i18n' import { LargeDiffFallback } from './LargeDiffFallback' +import { buildDiffEditorWordWrapOptions } from './diff-editor-word-wrap-options' const ImageDiffViewer = lazy(() => import('./ImageDiffViewer')) @@ -34,6 +35,7 @@ type DiffSectionBodyProps = { modelPathBase: string isEditable: boolean diffEditorFontSize: number + diffWordWrap?: boolean terminalFontFamily?: string onCancelComment: () => void onSubmitComment: (body: string) => Promise @@ -58,6 +60,7 @@ export function DiffSectionBody({ modelPathBase, isEditable, diffEditorFontSize, + diffWordWrap, terminalFontFamily, onCancelComment, onSubmitComment, @@ -190,6 +193,7 @@ export function DiffSectionBody({ fontSize: diffEditorFontSize, fontFamily: terminalFontFamily || 'monospace', lineNumbers: 'on', + ...buildDiffEditorWordWrapOptions(diffWordWrap), automaticLayout: true, renderOverviewRuler: false, scrollbar: combinedDiffSectionScrollbarOptions, diff --git a/src/renderer/src/components/editor/DiffSectionItem.tsx b/src/renderer/src/components/editor/DiffSectionItem.tsx index 8cbe52dd7cb..71fe21cf983 100644 --- a/src/renderer/src/components/editor/DiffSectionItem.tsx +++ b/src/renderer/src/components/editor/DiffSectionItem.tsx @@ -65,7 +65,11 @@ export function DiffSectionItem({ isBranchMode: boolean sideBySide: boolean isDark: boolean - settings: { terminalFontSize?: number; terminalFontFamily?: string } | null + settings: { + terminalFontSize?: number + terminalFontFamily?: string + diffWordWrap?: boolean + } | null sectionHeight: number | undefined worktreeId?: string loadSection: (index: number) => void @@ -421,6 +425,7 @@ export function DiffSectionItem({ modelPathBase={modelPathBase} isEditable={isEditable} diffEditorFontSize={diffEditorFontSize} + diffWordWrap={settings?.diffWordWrap} terminalFontFamily={settings?.terminalFontFamily} onCancelComment={() => setPopover(null)} onSubmitComment={handleSubmitComment} diff --git a/src/renderer/src/components/editor/DiffViewer.tsx b/src/renderer/src/components/editor/DiffViewer.tsx index 256810494f3..7f58688754f 100644 --- a/src/renderer/src/components/editor/DiffViewer.tsx +++ b/src/renderer/src/components/editor/DiffViewer.tsx @@ -23,6 +23,7 @@ import { getLargeDiffRenderLimit } from './large-diff-render-limit' import { useDiffViewerLargeDiffLifecycle } from './useDiffViewerLargeDiffLifecycle' import { getDiffViewerLargeDiffSaveAction } from './diff-viewer-large-diff-save-action' import type { DiffViewerProps } from './diff-viewer-props' +import { buildDiffEditorWordWrapOptions } from './diff-editor-word-wrap-options' export default function DiffViewer({ modelKey, @@ -450,6 +451,7 @@ export default function DiffViewer({ fontSize: diffEditorFontSize, fontFamily: settings?.terminalFontFamily || 'monospace', lineNumbers: 'on', + ...buildDiffEditorWordWrapOptions(settings?.diffWordWrap), automaticLayout: true, renderOverviewRuler: true, scrollbar: diffEditorScrollbarOptions, diff --git a/src/renderer/src/components/editor/EditorPanelHeader.tsx b/src/renderer/src/components/editor/EditorPanelHeader.tsx index 0218c0dd058..f47c9190ccd 100644 --- a/src/renderer/src/components/editor/EditorPanelHeader.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeader.tsx @@ -83,6 +83,8 @@ export function EditorPanelHeader({ }: EditorPanelHeaderProps): React.JSX.Element { const diffComments = useAppStore((s) => s.getDiffComments(activeFile.worktreeId)) const activeGroupId = useAppStore((s) => s.activeGroupIdByWorktree[activeFile.worktreeId]) + const diffWordWrap = useAppStore((s) => s.settings?.diffWordWrap === true) + const updateSettings = useAppStore((s) => s.updateSettings) const fileDiffComments = useMemo( () => diffComments.filter((comment) => comment.filePath === activeFile.relativePath), [activeFile.relativePath, diffComments] @@ -246,10 +248,13 @@ export function EditorPanelHeader({ )} void updateSettings({ diffWordWrap: !diffWordWrap })} onToggleMarkdownFrontmatter={onToggleMarkdownFrontmatter} onExportMarkdownToPdf={onExportMarkdownToPdf} /> diff --git a/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx b/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx index 0f71ae150e9..e7232096b8e 100644 --- a/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx +++ b/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx @@ -2,6 +2,7 @@ import type React from 'react' import { MoreHorizontal } from 'lucide-react' import { DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, @@ -11,24 +12,32 @@ import { translate } from '@/i18n/i18n' type EditorPanelMarkdownActionsMenuProps = { isMarkdown: boolean + isDiffSurface: boolean + diffWordWrap: boolean shouldShowMarkdownExportAction: boolean canExportMarkdownToPdf: boolean canShowMarkdownFrontmatterToggle: boolean markdownFrontmatterVisible: boolean + onToggleDiffWordWrap: () => void onToggleMarkdownFrontmatter: () => void onExportMarkdownToPdf: () => void } export function EditorPanelMarkdownActionsMenu({ isMarkdown, + isDiffSurface, + diffWordWrap, shouldShowMarkdownExportAction, canExportMarkdownToPdf, canShowMarkdownFrontmatterToggle, markdownFrontmatterVisible, + onToggleDiffWordWrap, onToggleMarkdownFrontmatter, onExportMarkdownToPdf }: EditorPanelMarkdownActionsMenuProps): React.JSX.Element | null { - if (!isMarkdown || (!shouldShowMarkdownExportAction && !canShowMarkdownFrontmatterToggle)) { + const hasMarkdownActions = + isMarkdown && (shouldShowMarkdownExportAction || canShowMarkdownFrontmatterToggle) + if (!isDiffSurface && !hasMarkdownActions) { return null } @@ -51,6 +60,17 @@ export function EditorPanelMarkdownActionsMenu({ + {isDiffSurface ? ( + <> + + {translate( + 'auto.components.editor.EditorPanelMarkdownActionsMenu.1eef809708', + 'Word Wrap' + )} + + {hasMarkdownActions ? : null} + + ) : null} {canShowMarkdownFrontmatterToggle ? ( <> { + it('keeps long diff lines unwrapped by default', () => { + expect(buildDiffEditorWordWrapOptions(undefined)).toEqual({ wordWrap: 'off' }) + expect(buildDiffEditorWordWrapOptions(false)).toEqual({ wordWrap: 'off' }) + }) + + it('enables Monaco diff word wrapping when the diff preference is on', () => { + expect(buildDiffEditorWordWrapOptions(true)).toEqual({ wordWrap: 'on' }) + }) +}) diff --git a/src/renderer/src/components/editor/diff-editor-word-wrap-options.ts b/src/renderer/src/components/editor/diff-editor-word-wrap-options.ts new file mode 100644 index 00000000000..c7f1e2cbea2 --- /dev/null +++ b/src/renderer/src/components/editor/diff-editor-word-wrap-options.ts @@ -0,0 +1,9 @@ +import type { editor } from 'monaco-editor' + +export function buildDiffEditorWordWrapOptions( + diffWordWrap: boolean | undefined +): Pick { + return { + wordWrap: diffWordWrap === true ? 'on' : 'off' + } +} diff --git a/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx b/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx index 6fa3af42929..2d8ca10c2f7 100644 --- a/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx +++ b/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx @@ -247,6 +247,58 @@ export function GeneralEditorSettingsSection({ /> + +
+ +

+ {translate( + 'auto.components.settings.GeneralEditorSettingsSection.4aa4d9fb73', + 'Wrap long lines in diff editors instead of requiring horizontal scrolling.' + )} +

+
+ updateSettings({ diffWordWrap: option === 'on' })} + options={[ + { + value: 'off', + label: translate( + 'auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2', + 'Off' + ) + }, + { + value: 'on', + label: translate( + 'auto.components.settings.GeneralEditorSettingsSection.3f6892f307', + 'On' + ) + } + ]} + /> +
+