feat(diff): add persisted side-by-side and wrap toggles to the diff viewer
This commit is contained in:
@@ -357,7 +357,9 @@
|
|||||||
"merge": "Mergen…",
|
"merge": "Mergen…",
|
||||||
"filesHeader": "Dateien",
|
"filesHeader": "Dateien",
|
||||||
"binary": "Binärdatei — kein Text-Diff",
|
"binary": "Binärdatei — kein Text-Diff",
|
||||||
"empty": "Kein Inhalt"
|
"empty": "Kein Inhalt",
|
||||||
|
"splitView": "Nebeneinander",
|
||||||
|
"wrapLines": "Zeilenumbruch"
|
||||||
},
|
},
|
||||||
"worktreesOverview": {
|
"worktreesOverview": {
|
||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
|
|||||||
@@ -357,7 +357,9 @@
|
|||||||
"merge": "Merge…",
|
"merge": "Merge…",
|
||||||
"filesHeader": "Files",
|
"filesHeader": "Files",
|
||||||
"binary": "Binary file — no text diff",
|
"binary": "Binary file — no text diff",
|
||||||
"empty": "No content"
|
"empty": "No content",
|
||||||
|
"splitView": "Side by side",
|
||||||
|
"wrapLines": "Wrap lines"
|
||||||
},
|
},
|
||||||
"worktreesOverview": {
|
"worktreesOverview": {
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data.Git;
|
using ClaudeDo.Data.Git;
|
||||||
|
using ClaudeDo.Ui;
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
@@ -21,6 +24,7 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
private readonly GitService _git;
|
private readonly GitService _git;
|
||||||
private readonly IWorkerClient _worker;
|
private readonly IWorkerClient _worker;
|
||||||
|
private readonly AppSettings _settings;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(IsPlanning))]
|
[NotifyPropertyChangedFor(nameof(IsPlanning))]
|
||||||
@@ -56,6 +60,31 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _displayedDiff = "";
|
[ObservableProperty] private string _displayedDiff = "";
|
||||||
[ObservableProperty] private string? _statusMessage;
|
[ObservableProperty] private string? _statusMessage;
|
||||||
|
|
||||||
|
// ── View toggles (persisted to ui.config.json) ──────────────────────────
|
||||||
|
[ObservableProperty] private bool _isSplitView;
|
||||||
|
[ObservableProperty] private bool _wrapLines;
|
||||||
|
|
||||||
|
partial void OnIsSplitViewChanged(bool value)
|
||||||
|
{
|
||||||
|
_settings.DiffViewMode = value ? "split" : "unified";
|
||||||
|
PersistViewPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnWrapLinesChanged(bool value)
|
||||||
|
{
|
||||||
|
_settings.DiffWrapLines = value;
|
||||||
|
PersistViewPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A failed preference write must never take the diff viewer down with it; the toggle
|
||||||
|
/// still works for this session, it just won't survive a restart.
|
||||||
|
private void PersistViewPreferences()
|
||||||
|
{
|
||||||
|
try { _settings.Save(); }
|
||||||
|
catch (IOException) { }
|
||||||
|
catch (UnauthorizedAccessException) { }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Planning combined toggle ────────────────────────────────────────────
|
// ── Planning combined toggle ────────────────────────────────────────────
|
||||||
[ObservableProperty] private bool _isCombinedMode;
|
[ObservableProperty] private bool _isCombinedMode;
|
||||||
[ObservableProperty] private string? _combinedWarning;
|
[ObservableProperty] private string? _combinedWarning;
|
||||||
@@ -63,10 +92,13 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|||||||
|
|
||||||
public Action? CloseAction { get; set; }
|
public Action? CloseAction { get; set; }
|
||||||
|
|
||||||
public DiffViewerViewModel(GitService git, IWorkerClient worker)
|
public DiffViewerViewModel(GitService git, IWorkerClient worker, AppSettings settings)
|
||||||
{
|
{
|
||||||
_git = git;
|
_git = git;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
|
_settings = settings;
|
||||||
|
_isSplitView = string.Equals(settings.DiffViewMode, "split", StringComparison.OrdinalIgnoreCase);
|
||||||
|
_wrapLines = settings.DiffWrapLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -30,6 +30,12 @@
|
|||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
|
||||||
|
<!-- View toolbar: layout + wrap, both persisted -->
|
||||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="16,8,16,0">
|
||||||
|
<ToggleButton Content="{loc:Tr modals.diff.splitView}" IsChecked="{Binding IsSplitView}"/>
|
||||||
|
<ToggleButton Content="{loc:Tr modals.diff.wrapLines}" IsChecked="{Binding WrapLines}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Planning toolbar: combined-mode toggle + warning/loading -->
|
<!-- Planning toolbar: combined-mode toggle + warning/loading -->
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="16,8,16,0"
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="16,8,16,0"
|
||||||
IsVisible="{Binding IsPlanning}">
|
IsVisible="{Binding IsPlanning}">
|
||||||
@@ -149,10 +155,10 @@
|
|||||||
Foreground="{DynamicResource TextMuteBrush}"
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
IsVisible="{Binding SelectedFile.IsEmptyContent}"
|
IsVisible="{Binding SelectedFile.IsEmptyContent}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
|
<ctl:DiffTextView IsVisible="{Binding SelectedFile.HasLines}"
|
||||||
IsVisible="{Binding SelectedFile.HasLines}">
|
File="{Binding SelectedFile}"
|
||||||
<ctl:DiffLinesView Lines="{Binding SelectedFile.Lines}"/>
|
IsSplit="{Binding IsSplitView}"
|
||||||
</ScrollViewer>
|
WrapLines="{Binding WrapLines}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ public class DiffViewerViewModelTests
|
|||||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A throwaway config path — the view toggles call Save(), and a test must never
|
||||||
|
// overwrite the developer's real ~/.todo-app/ui.config.json.
|
||||||
|
private static AppSettings TestSettings() => new()
|
||||||
|
{
|
||||||
|
ConfigPath = Path.Combine(Path.GetTempPath(), $"claudedo-uicfg-{Guid.NewGuid():N}.json"),
|
||||||
|
};
|
||||||
|
|
||||||
private sealed class FakePlanningWorker : StubWorkerClient
|
private sealed class FakePlanningWorker : StubWorkerClient
|
||||||
{
|
{
|
||||||
public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>();
|
public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>();
|
||||||
@@ -36,7 +43,7 @@ public class DiffViewerViewModelTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CommitRange_NullHeadCommit_ShowsUnavailable()
|
public async Task CommitRange_NullHeadCommit_ShowsUnavailable()
|
||||||
{
|
{
|
||||||
var vm = new DiffViewerViewModel(null!, new FakePlanningWorker());
|
var vm = new DiffViewerViewModel(null!, new FakePlanningWorker(), TestSettings());
|
||||||
vm.ConfigureCommitRange("/some/repo", "abc123", null);
|
vm.ConfigureCommitRange("/some/repo", "abc123", null);
|
||||||
|
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
@@ -49,7 +56,7 @@ public class DiffViewerViewModelTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CommitRange_NullBaseRef_ShowsUnavailable()
|
public async Task CommitRange_NullBaseRef_ShowsUnavailable()
|
||||||
{
|
{
|
||||||
var vm = new DiffViewerViewModel(null!, new FakePlanningWorker());
|
var vm = new DiffViewerViewModel(null!, new FakePlanningWorker(), TestSettings());
|
||||||
vm.ConfigureCommitRange("/some/repo", null, "def456");
|
vm.ConfigureCommitRange("/some/repo", null, "def456");
|
||||||
|
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
@@ -104,7 +111,7 @@ public class DiffViewerViewModelTests
|
|||||||
new SubtaskDiffDto("s2", "Second", "branch-2", "base2", "head2", "+2 -1", "diff2"),
|
new SubtaskDiffDto("s2", "Second", "branch-2", "base2", "head2", "+2 -1", "diff2"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var vm = new DiffViewerViewModel(null!, fake);
|
var vm = new DiffViewerViewModel(null!, fake, TestSettings());
|
||||||
vm.ConfigurePlanning("plan-1", "main");
|
vm.ConfigurePlanning("plan-1", "main");
|
||||||
|
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
@@ -124,7 +131,7 @@ public class DiffViewerViewModelTests
|
|||||||
new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, "DIFF-B"),
|
new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, "DIFF-B"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var vm = new DiffViewerViewModel(null!, fake);
|
var vm = new DiffViewerViewModel(null!, fake, TestSettings());
|
||||||
vm.ConfigurePlanning("plan-1", "main");
|
vm.ConfigurePlanning("plan-1", "main");
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
|
|
||||||
@@ -141,7 +148,7 @@ public class DiffViewerViewModelTests
|
|||||||
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
||||||
CombinedResult = new CombinedDiffResultDto(true, "integration-branch", "COMBINED-DIFF", null, null),
|
CombinedResult = new CombinedDiffResultDto(true, "integration-branch", "COMBINED-DIFF", null, null),
|
||||||
};
|
};
|
||||||
var vm = new DiffViewerViewModel(null!, fake);
|
var vm = new DiffViewerViewModel(null!, fake, TestSettings());
|
||||||
vm.ConfigurePlanning("plan-1", "main");
|
vm.ConfigurePlanning("plan-1", "main");
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
|
|
||||||
@@ -162,7 +169,7 @@ public class DiffViewerViewModelTests
|
|||||||
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
||||||
CombinedResult = new CombinedDiffResultDto(false, null, null, "subtask-42", new[] { "a.cs", "b.cs" }),
|
CombinedResult = new CombinedDiffResultDto(false, null, null, "subtask-42", new[] { "a.cs", "b.cs" }),
|
||||||
};
|
};
|
||||||
var vm = new DiffViewerViewModel(null!, fake);
|
var vm = new DiffViewerViewModel(null!, fake, TestSettings());
|
||||||
vm.ConfigurePlanning("plan-1", "main");
|
vm.ConfigurePlanning("plan-1", "main");
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
|
|
||||||
@@ -184,7 +191,7 @@ public class DiffViewerViewModelTests
|
|||||||
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
||||||
CombinedResult = null,
|
CombinedResult = null,
|
||||||
};
|
};
|
||||||
var vm = new DiffViewerViewModel(null!, fake);
|
var vm = new DiffViewerViewModel(null!, fake, TestSettings());
|
||||||
vm.ConfigurePlanning("plan-1", "main");
|
vm.ConfigurePlanning("plan-1", "main");
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
|
|
||||||
@@ -205,7 +212,7 @@ public class DiffViewerViewModelTests
|
|||||||
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
||||||
CombinedException = "planning task not found",
|
CombinedException = "planning task not found",
|
||||||
};
|
};
|
||||||
var vm = new DiffViewerViewModel(null!, fake);
|
var vm = new DiffViewerViewModel(null!, fake, TestSettings());
|
||||||
vm.ConfigurePlanning("plan-1", "main");
|
vm.ConfigurePlanning("plan-1", "main");
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user