Add diff word wrap toggle (#6214)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-06-24 19:26:50 -07:00
committed by GitHub
co-authored by Orca
parent 634a1b27e4
commit 0be512fc23
18 changed files with 186 additions and 18 deletions
@@ -144,6 +144,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
terminalWindowsPowerShellImplementation: 'powershell.exe',
enableGitHubAttribution: true,
...overrides,
diffWordWrap: overrides.diffWordWrap ?? false,
localWindowsRuntimeDefault: overrides.localWindowsRuntimeDefault ?? { kind: 'windows-host' },
leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default',
appFontFamily,
+1
View File
@@ -148,6 +148,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
terminalWindowsPowerShellImplementation: 'powershell.exe',
enableGitHubAttribution: true,
...overrides,
diffWordWrap: overrides.diffWordWrap ?? false,
localWindowsRuntimeDefault: overrides.localWindowsRuntimeDefault ?? { kind: 'windows-host' },
leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default',
appFontFamily,
@@ -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')}
</button>
<button
className={`inline-flex h-6 items-center gap-1 rounded border border-border px-2 text-xs transition-colors hover:text-foreground ${
settings?.diffWordWrap === true
? 'bg-accent text-foreground'
: 'text-muted-foreground'
}`}
onClick={toggleDiffWordWrap}
aria-pressed={settings?.diffWordWrap === true}
>
<WrapText className="size-3.5" />
{settings?.diffWordWrap === true
? translate('auto.components.editor.CombinedDiffViewer.a4420ca1f7', 'Wrap On')
: translate('auto.components.editor.CombinedDiffViewer.dde325ddfe', 'Wrap Off')}
</button>
</div>
</div>
@@ -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<void>
@@ -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,
@@ -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}
@@ -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,
@@ -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({
)}
<EditorPanelMarkdownActionsMenu
isMarkdown={isMarkdown}
isDiffSurface={isDiffSurface}
diffWordWrap={diffWordWrap}
shouldShowMarkdownExportAction={shouldShowMarkdownExportAction}
canExportMarkdownToPdf={canExportMarkdownToPdf}
canShowMarkdownFrontmatterToggle={canShowMarkdownFrontmatterToggle}
markdownFrontmatterVisible={markdownFrontmatterVisible}
onToggleDiffWordWrap={() => void updateSettings({ diffWordWrap: !diffWordWrap })}
onToggleMarkdownFrontmatter={onToggleMarkdownFrontmatter}
onExportMarkdownToPdf={onExportMarkdownToPdf}
/>
@@ -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({
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={4}>
{isDiffSurface ? (
<>
<DropdownMenuCheckboxItem checked={diffWordWrap} onCheckedChange={onToggleDiffWordWrap}>
{translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.1eef809708',
'Word Wrap'
)}
</DropdownMenuCheckboxItem>
{hasMarkdownActions ? <DropdownMenuSeparator /> : null}
</>
) : null}
{canShowMarkdownFrontmatterToggle ? (
<>
<DropdownMenuItem
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
import { buildDiffEditorWordWrapOptions } from './diff-editor-word-wrap-options'
describe('buildDiffEditorWordWrapOptions', () => {
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' })
})
})
@@ -0,0 +1,9 @@
import type { editor } from 'monaco-editor'
export function buildDiffEditorWordWrapOptions(
diffWordWrap: boolean | undefined
): Pick<editor.IStandaloneDiffEditorConstructionOptions, 'wordWrap'> {
return {
wordWrap: diffWordWrap === true ? 'on' : 'off'
}
}
@@ -247,6 +247,58 @@ export function GeneralEditorSettingsSection({
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.GeneralEditorSettingsSection.8f1afdfbd8',
'Diff Word Wrap'
)}
description={translate(
'auto.components.settings.GeneralEditorSettingsSection.4aa4d9fb73',
'Wrap long lines in diff editors instead of requiring horizontal scrolling.'
)}
keywords={['diff', 'word wrap', 'wrap', 'markdown', 'long lines']}
className="flex items-center justify-between gap-4 py-2"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
'auto.components.settings.GeneralEditorSettingsSection.8f1afdfbd8',
'Diff Word Wrap'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.GeneralEditorSettingsSection.4aa4d9fb73',
'Wrap long lines in diff editors instead of requiring horizontal scrolling.'
)}
</p>
</div>
<SettingsSegmentedControl
ariaLabel={translate(
'auto.components.settings.GeneralEditorSettingsSection.8f1afdfbd8',
'Diff Word Wrap'
)}
value={settings.diffWordWrap ? 'on' : 'off'}
onChange={(option) => 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'
)
}
]}
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.GeneralEditorSettingsSection.1de48ad940',
+10 -3
View File
@@ -4941,7 +4941,11 @@
"70bb30feb1": "Save editor and editable diff changes automatically after a short pause.",
"0df2e4fd12": "Auto Save Files",
"d21136d9ef": "Configure how Orca persists file edits.",
"45c6e85c4d": "Editor"
"45c6e85c4d": "Editor",
"8f1afdfbd8": "Diff Word Wrap",
"4aa4d9fb73": "Wrap long lines in diff editors instead of requiring horizontal scrolling.",
"bf16ef0af2": "Off",
"3f6892f307": "On"
},
"AdvancedNetworkSettingsSection": {
"3e431564b5": "localhost, 127.0.0.1, *.internal",
@@ -10465,7 +10469,9 @@
"8368d256ec": "combined-branch",
"8f68ad9ca9": "Show {{value0}} AI {{value1}}",
"724a13568d": " in {{value0}}",
"6094135eec": " vs {{value0}}"
"6094135eec": " vs {{value0}}",
"a4420ca1f7": "Wrap On",
"dde325ddfe": "Wrap Off"
},
"ConflictComponents": {
"f338288514": "Loading conflict contents...",
@@ -10553,7 +10559,8 @@
"3e0ce48c24": "Export as PDF",
"561251019a": "More actions",
"8c8b7f5ff5": "Show front matter",
"10c39d58c1": "Hide front matter"
"10c39d58c1": "Hide front matter",
"1eef809708": "Word Wrap"
},
"EditorPanelShell": {
"e2c4dec350": "Loading editor..."
+10 -3
View File
@@ -4941,7 +4941,11 @@
"70bb30feb1": "Guarde el editor y los cambios de diferencias editables automáticamente después de una breve pausa.",
"0df2e4fd12": "Guardar archivos automáticamente",
"d21136d9ef": "Configure cómo Orca persiste en las ediciones de archivos.",
"45c6e85c4d": "Editor"
"45c6e85c4d": "Editor",
"8f1afdfbd8": "Diff Word Wrap",
"4aa4d9fb73": "Wrap long lines in diff editors instead of requiring horizontal scrolling.",
"bf16ef0af2": "Off",
"3f6892f307": "On"
},
"AdvancedNetworkSettingsSection": {
"3e431564b5": "host local, 127.0.0.1, *.interno",
@@ -10465,7 +10469,9 @@
"8368d256ec": "rama combinada",
"8f68ad9ca9": "Mostrar {{value0}} AI {{value1}}",
"724a13568d": "en {{value0}}",
"6094135eec": "frente a {{value0}}"
"6094135eec": "frente a {{value0}}",
"a4420ca1f7": "Wrap On",
"dde325ddfe": "Wrap Off"
},
"ConflictComponents": {
"f338288514": "Cargando contenidos conflictivos...",
@@ -10553,7 +10559,8 @@
"3e0ce48c24": "Exportar como PDF",
"561251019a": "Más acciones",
"8c8b7f5ff5": "Mostrar portada",
"10c39d58c1": "Ocultar materia preliminar"
"10c39d58c1": "Ocultar materia preliminar",
"1eef809708": "Word Wrap"
},
"EditorPanelShell": {
"e2c4dec350": "Cargando editor..."
+10 -3
View File
@@ -4926,7 +4926,11 @@
"70bb30feb1": "エディターと編集可能な差分を保存すると、少し停止した後、自動的に変更されます。",
"0df2e4fd12": "ファイルの自動保存",
"d21136d9ef": "Orca がファイル編集を保持する方法を構成します。",
"45c6e85c4d": "エディタ"
"45c6e85c4d": "エディタ",
"8f1afdfbd8": "Diff Word Wrap",
"4aa4d9fb73": "Wrap long lines in diff editors instead of requiring horizontal scrolling.",
"bf16ef0af2": "Off",
"3f6892f307": "On"
},
"AdvancedNetworkSettingsSection": {
"3e431564b5": "ローカルホスト、127.0.0.1、*.internal",
@@ -10465,7 +10469,9 @@
"8368d256ec": "結合分岐",
"8f68ad9ca9": "{{value0}} AI {{value1}} を表示",
"724a13568d": "{{value0}} に",
"6094135eec": "vs {{value0}}"
"6094135eec": "vs {{value0}}",
"a4420ca1f7": "Wrap On",
"dde325ddfe": "Wrap Off"
},
"ConflictComponents": {
"f338288514": "競合するコンテンツを読み込んでいます...",
@@ -10553,7 +10559,8 @@
"3e0ce48c24": "PDFとしてエクスポート",
"561251019a": "その他の操作",
"8c8b7f5ff5": "前付を表示",
"10c39d58c1": "前付を隠す"
"10c39d58c1": "前付を隠す",
"1eef809708": "Word Wrap"
},
"EditorPanelShell": {
"e2c4dec350": "エディターを読み込み中..."
+10 -3
View File
@@ -4926,7 +4926,11 @@
"70bb30feb1": "잠시 후에 편집기 및 편집 가능한 diff 변경 사항을 자동으로 저장합니다.",
"0df2e4fd12": "자동 저장 파일",
"d21136d9ef": "Orca가 파일 편집을 유지하는 방법을 구성합니다.",
"45c6e85c4d": "편집기"
"45c6e85c4d": "편집기",
"8f1afdfbd8": "Diff Word Wrap",
"4aa4d9fb73": "Wrap long lines in diff editors instead of requiring horizontal scrolling.",
"bf16ef0af2": "Off",
"3f6892f307": "On"
},
"AdvancedNetworkSettingsSection": {
"3e431564b5": "로컬호스트, 127.0.0.1, *.internal",
@@ -10465,7 +10469,9 @@
"8368d256ec": "결합 브랜치",
"8f68ad9ca9": "{{value0}} AI {{value1}} 표시",
"724a13568d": "{{value0}}에서",
"6094135eec": "대 {{value0}}"
"6094135eec": "대 {{value0}}",
"a4420ca1f7": "Wrap On",
"dde325ddfe": "Wrap Off"
},
"ConflictComponents": {
"f338288514": "충돌 콘텐츠 로드 중...",
@@ -10553,7 +10559,8 @@
"3e0ce48c24": "PDF로 내보내기",
"561251019a": "추가 작업",
"8c8b7f5ff5": "머리말 표시",
"10c39d58c1": "머리말 숨기기"
"10c39d58c1": "머리말 숨기기",
"1eef809708": "Word Wrap"
},
"EditorPanelShell": {
"e2c4dec350": "편집기 로드 중..."
+10 -3
View File
@@ -4926,7 +4926,11 @@
"70bb30feb1": "短暂暂停后自动保存编辑器和可编辑差异更改。",
"0df2e4fd12": "自动保存文件",
"d21136d9ef": "配置 Orca 如何保留文件编辑。",
"45c6e85c4d": "编辑"
"45c6e85c4d": "编辑",
"8f1afdfbd8": "Diff Word Wrap",
"4aa4d9fb73": "Wrap long lines in diff editors instead of requiring horizontal scrolling.",
"bf16ef0af2": "Off",
"3f6892f307": "On"
},
"AdvancedNetworkSettingsSection": {
"3e431564b5": "本地主机,127.0.0.1*.internal",
@@ -10465,7 +10469,9 @@
"8368d256ec": "组合分支",
"8f68ad9ca9": "显示 {{value0}} AI {{value1}}",
"724a13568d": "在 {{value0}}",
"6094135eec": "与 {{value0}}"
"6094135eec": "与 {{value0}}",
"a4420ca1f7": "Wrap On",
"dde325ddfe": "Wrap Off"
},
"ConflictComponents": {
"f338288514": "正在加载冲突内容...",
@@ -10553,7 +10559,8 @@
"3e0ce48c24": "导出为 PDF",
"561251019a": "更多操作",
"8c8b7f5ff5": "显示前面的内容",
"10c39d58c1": "隐藏前面的内容"
"10c39d58c1": "隐藏前面的内容",
"1eef809708": "Word Wrap"
},
"EditorPanelShell": {
"e2c4dec350": "正在加载编辑器..."
+1
View File
@@ -286,6 +286,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
floatingTerminalTriggerLocation: 'floating-button',
notifications: getDefaultNotificationSettings(),
diffDefaultView: 'inline',
diffWordWrap: false,
combinedDiffFileTreeVisibleByDefault: false,
promptCacheTimerEnabled: false,
promptCacheTtlMs: 300_000,
+1
View File
@@ -2559,6 +2559,7 @@ export type GlobalSettings = {
* ~/.orca/keybindings.json; main migrates this once when present. */
keybindings?: KeybindingOverrides
diffDefaultView: 'inline' | 'side-by-side'
diffWordWrap: boolean
combinedDiffFileTreeVisibleByDefault: boolean
notifications: NotificationSettings
/** When true, a countdown timer is shown after a Claude agent becomes idle,