Files
ClaudeDo/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml
T
Mika Kuns 9fc8ed391e fix(ui): offload UnifiedDiffParser.Parse off the UI thread in DiffViewerViewModel
Both call sites (LoadFilesAsync, OnDisplayedDiffChanged) ran the parser
synchronously on the dispatcher, freezing the window on a large diff. The
Files-mode parse+tree-build now runs via Task.Run behind a ParseOp
OperationStatus wired to the toolbar; the planning-mode parse is
fire-and-forget with a generation counter so a fast second DisplayedDiff
change can't write stale PlanningFiles.
2026-08-12 13:19:20 +02:00

208 lines
11 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.DiffViewerView"
x:DataType="vm:DiffViewerViewModel"
Title="{loc:Tr modals.diff.windowTitle}"
Width="1200" Height="800" MinWidth="700" MinHeight="450"
CanResize="True"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}" ShowMaximizeButton="True">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="{loc:Tr modals.diff.merge}"
Command="{Binding MergeCommand}"
IsVisible="{Binding ShowMerge}"/>
</StackPanel>
</ctl:ModalShell.Footer>
<DockPanel>
<!-- View toolbar: layout mode (segmented) + wrap (icon toggle), both persisted.
Wrap is orthogonal to the layout mode, so it stays a separate control. -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="16,8,16,0">
<Border Classes="segmented">
<StackPanel Orientation="Horizontal">
<Button Classes="segment" Classes.active="{Binding !IsSplitView}"
Content="{loc:Tr modals.diff.unifiedView}"
Command="{Binding ShowUnifiedCommand}"/>
<Button Classes="segment" Classes.active="{Binding IsSplitView}"
Content="{loc:Tr modals.diff.splitView}"
Command="{Binding ShowSplitCommand}"/>
</StackPanel>
</Border>
<ToggleButton Classes="icon-btn" IsChecked="{Binding WrapLines}"
VerticalAlignment="Center"
ToolTip.Tip="{loc:Tr modals.diff.wrapLines}">
<PathIcon Data="{StaticResource Icon.Wrap}" Width="14" Height="14"/>
</ToggleButton>
<ctl:OperationIndicator Status="{Binding ParseOp}"/>
</StackPanel>
<!-- Planning toolbar: combined-mode toggle + warning/loading -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="16,8,16,0"
IsVisible="{Binding IsPlanning}">
<ToggleButton Content="{loc:Tr planning.diff.previewCombined}" IsChecked="{Binding IsCombinedMode}"/>
<TextBlock Text="{Binding CombinedWarning}"
Foreground="{DynamicResource BloodBrush}"
VerticalAlignment="Center"
IsVisible="{Binding CombinedWarning, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock Text="{loc:Tr planning.diff.loading}"
VerticalAlignment="Center"
IsVisible="{Binding IsLoadingCombined}"/>
</StackPanel>
<!-- Body: nav | splitter | diff -->
<Grid ColumnDefinitions="280,4,*" Margin="16">
<!-- Left nav: file tree (Files) OR subtask list (Planning) -->
<Border Grid.Column="0" Classes="island">
<DockPanel>
<Border DockPanel.Dock="Top" Classes="island-header" IsVisible="{Binding !IsPlanning}">
<TextBlock Classes="eyebrow" Text="{loc:Tr modals.diff.filesHeader}"/>
</Border>
<Panel>
<!-- Files: folder tree -->
<TreeView x:Name="FileTree"
ItemsSource="{Binding FileTree}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
Background="Transparent"
BorderThickness="0"
IsVisible="{Binding !IsPlanning}">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:DiffTreeNodeViewModel">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate DataType="vm:DiffTreeNodeViewModel" ItemsSource="{Binding Children}">
<Border Background="Transparent" Tapped="OnNodeTapped" Cursor="Hand" Padding="0,2">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Tag="{Binding StatusCode}" CornerRadius="3" Padding="4,0" Margin="0,0,6,0"
VerticalAlignment="Center"
IsVisible="{Binding StatusCode, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding StatusCode}"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
<TextBlock Grid.Column="1" Classes="meta" Text="{Binding Name}"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center"
Margin="0,0,8,0"
IsVisible="{Binding ShowStats}">
<TextBlock Foreground="{DynamicResource MossBrightBrush}" FontSize="{StaticResource FontSizeEyebrow}"
Text="{Binding Additions, StringFormat='+{0}'}"/>
<TextBlock Foreground="{DynamicResource BloodBrush}" FontSize="{StaticResource FontSizeEyebrow}"
Text="{Binding Deletions, StringFormat='−{0}'}"/>
</StackPanel>
</Grid>
</Border>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Planning: subtask list -->
<ListBox ItemsSource="{Binding Subtasks}"
SelectedItem="{Binding SelectedSubtask}"
IsEnabled="{Binding !IsCombinedMode}"
IsVisible="{Binding IsPlanning}"
Background="Transparent"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SubtaskDiffRow">
<Border Padding="10,8" Background="Transparent">
<StackPanel Spacing="2">
<TextBlock Classes="title" Text="{Binding Title}" TextTrimming="CharacterEllipsis"/>
<TextBlock Classes="meta" Text="{Binding DiffStat}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Panel>
</DockPanel>
</Border>
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Background="{DynamicResource LineBrush}"/>
<!-- Right: diff content (Files per-file pane OR Planning flat diff) -->
<Border Grid.Column="2" Classes="island">
<Panel>
<!-- Files mode: per-file pane with header + placeholders -->
<DockPanel IsVisible="{Binding !IsPlanning}">
<Border DockPanel.Dock="Top" Classes="island-header"
IsVisible="{Binding SelectedFile, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid ColumnDefinitions="Auto,*">
<Border Grid.Column="0" Tag="{Binding SelectedFile.StatusCode}"
CornerRadius="3" Padding="4,0" Margin="0,0,8,0" VerticalAlignment="Center">
<TextBlock Text="{Binding SelectedFile.StatusCode}"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
<TextBlock Grid.Column="1" Classes="path-mono" Text="{Binding SelectedFile.HeaderPath}"
VerticalAlignment="Center" TextTrimming="PrefixCharacterEllipsis"/>
</Grid>
</Border>
<Grid Background="{DynamicResource VoidBrush}">
<TextBlock Classes="body" Text="{Binding StatusMessage}"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Classes="body" Text="{loc:Tr modals.diff.binary}"
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding SelectedFile.IsBinary}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Classes="body" Text="{loc:Tr modals.diff.empty}"
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding SelectedFile.IsEmptyContent}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<ctl:DiffTextView IsVisible="{Binding SelectedFile.HasLines}"
File="{Binding SelectedFile}"
IsSplit="{Binding IsSplitView}"
WrapLines="{Binding WrapLines}"/>
</Grid>
</DockPanel>
<!-- Planning mode: one editor per file so each gets its own grammar -->
<Grid Background="{DynamicResource VoidBrush}" IsVisible="{Binding IsPlanning}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding PlanningFiles}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DiffFileViewModel">
<StackPanel Margin="0,0,0,12">
<Border Classes="island-header">
<TextBlock Classes="path-mono" Text="{Binding HeaderPath}"
TextTrimming="PrefixCharacterEllipsis"/>
</Border>
<ctl:DiffTextView File="{Binding}"
IsSplit="{Binding $parent[Window].((vm:DiffViewerViewModel)DataContext).IsSplitView}"
WrapLines="{Binding $parent[Window].((vm:DiffViewerViewModel)DataContext).WrapLines}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Panel>
</Border>
</Grid>
</DockPanel>
</ctl:ModalShell>
</Window>