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.
208 lines
11 KiB
XML
208 lines
11 KiB
XML
<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>
|