feat(merge): diff Merge opens the 3-pane editor + conflict overview ruler

- The Merge button in the Diff window now hands a conflicting merge to the in-app
  3-pane editor (MergeModal routes 'conflict' through RequestConflictResolution,
  the same seam Approve uses) instead of dead-ending on a conflict message.
- Add a conflict overview ruler right of the Result pane: a proportional map of
  every conflict in the file, recolored by resolved state, click a tick to jump —
  so conflicts are findable in long files without scrolling.
- New MergeResolvedEdgeBrush token + conflictMap en/de key. Ui 128 + Loc 16 green.
This commit is contained in:
Mika Kuns
2026-06-19 11:31:34 +02:00
parent ca4377e641
commit 29a294b7f3
9 changed files with 79 additions and 8 deletions

View File

@@ -149,6 +149,9 @@
<Border Classes="col-head" DockPanel.Dock="Top">
<TextBlock Classes="eyebrow" Text="{loc:Tr conflictResolver.result}"/>
</Border>
<Canvas Name="ConflictMap" DockPanel.Dock="Right" Width="13"
Background="{DynamicResource Surface2Brush}"
ToolTip.Tip="{loc:Tr conflictResolver.conflictMap}"/>
<ae:TextEditor Name="ResultEditor" ShowLineNumbers="True"/>
</DockPanel>
</Border>

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
@@ -251,6 +252,7 @@ public partial class ConflictResolverView : Window
private void PositionGutters()
{
ClearGutters();
PopulateConflictMap();
if (_vm?.ActiveFile is null) return;
var tv = ResultEditor.TextArea.TextView;
if (!tv.VisualLinesValid)
@@ -298,6 +300,47 @@ public partial class ConflictResolverView : Window
canvas.Children.Add(b);
}
// ── Conflict overview ruler (right of the result pane) ───────────────────
// A proportional map of every conflict in the active file so they're findable in
// long files without scrolling; ticks recolor by resolved state and jump on click.
private void PopulateConflictMap()
{
ConflictMap.Children.Clear();
if (_vm?.ActiveFile is null || _resultRegions.Count == 0) return;
var h = ConflictMap.Bounds.Height;
if (h <= 1) return;
var doc = ResultEditor.Document;
var totalLines = Math.Max(1, doc.LineCount);
var unresolved = BrushRes("MergeConflictEdgeBrush", Color.Parse("#80C87060"));
var resolved = BrushRes("MergeResolvedEdgeBrush", Color.Parse("#806FA86B"));
foreach (var region in _resultRegions)
{
var line = doc.GetLineByOffset(region.Start.Offset).LineNumber;
var y = (line - 1) / (double)totalLines * h;
var tick = new Rectangle
{
Width = 9,
Height = 4,
Fill = region.Block.IsResolved ? resolved : unresolved,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand),
};
Canvas.SetLeft(tick, 2);
Canvas.SetTop(tick, Math.Min(h - 4, Math.Max(0, y)));
var r = region;
tick.PointerPressed += (_, _) => JumpToRegion(r);
ConflictMap.Children.Add(tick);
}
}
private void JumpToRegion(ResultRegion region)
{
var line = ResultEditor.Document.GetLineByOffset(region.Start.Offset).LineNumber;
ResultEditor.ScrollToLine(line);
QueueGutters();
}
private static string Tr(string key) => ClaudeDo.Ui.Localization.Loc.T(key);
// ── Synced vertical scroll across the three panes ─────────────────────────
@@ -345,7 +388,7 @@ public partial class ConflictResolverView : Window
private void ApplyGrammar(string? path)
{
if (_registry is null || string.IsNullOrEmpty(path)) return;
var ext = Path.GetExtension(path);
var ext = System.IO.Path.GetExtension(path);
if (string.IsNullOrEmpty(ext)) return;
var language = _registry.GetLanguageByExtension(ext);
if (language is null) return;