Skip to content

Commit bc2d13a

Browse files
committed
feature: add a minimap to merge conflict editor
Signed-off-by: leo <longshuang@msn.cn>
1 parent 7c2053a commit bc2d13a

File tree

4 files changed

+167
-26
lines changed

4 files changed

+167
-26
lines changed

src/Converters/IntConverters.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,8 @@ public static class IntConverters
3333

3434
public static readonly FuncValueConverter<int, IBrush> ToBookmarkBrush =
3535
new FuncValueConverter<int, IBrush>(v => Models.Bookmarks.Get(v) ?? App.Current?.FindResource("Brush.FG1") as IBrush);
36+
37+
public static readonly FuncValueConverter<int, string> ToUnsolvedDesc =
38+
new FuncValueConverter<int, string>(v => v == 0 ? App.Text("MergeConflictEditor.AllResolved") : App.Text("MergeConflictEditor.ConflictsRemaining", v));
3639
}
3740
}

src/ViewModels/MergeConflictEditor.cs

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,10 @@ public int DiffMaxLineNumber
4646
private set => SetProperty(ref _diffMaxLineNumber, value);
4747
}
4848

49-
public string StatusText
49+
public int UnsolvedCount
5050
{
51-
get
52-
{
53-
if (_unresolvedConflictCount > 0)
54-
return App.Text("MergeConflictEditor.ConflictsRemaining", _unresolvedConflictCount);
55-
return App.Text("MergeConflictEditor.AllResolved");
56-
}
51+
get => _unsolvedCount;
52+
private set => SetProperty(ref _unsolvedCount, value);
5753
}
5854

5955
public Vector ScrollOffset
@@ -68,9 +64,10 @@ public Models.ConflictSelectedChunk SelectedChunk
6864
set => SetProperty(ref _selectedChunk, value);
6965
}
7066

71-
public IReadOnlyList<Models.ConflictRegion> ConflictRegions => _conflictRegions;
72-
public bool HasUnresolvedConflicts => _unresolvedConflictCount > 0;
73-
public bool HasUnsavedChanges => _unresolvedConflictCount < _conflictRegions.Count;
67+
public IReadOnlyList<Models.ConflictRegion> ConflictRegions
68+
{
69+
get => _conflictRegions;
70+
}
7471

7572
public MergeConflictEditor(Repository repo, string filePath)
7673
{
@@ -126,7 +123,7 @@ public async Task<bool> SaveAndStageAsync()
126123
if (_conflictRegions.Count == 0)
127124
return true;
128125

129-
if (_unresolvedConflictCount > 0)
126+
if (_unsolvedCount > 0)
130127
{
131128
Error = "Cannot save: there are still unresolved conflicts.";
132129
return false;
@@ -460,23 +457,21 @@ private void RefreshDisplayData()
460457
SelectedChunk = null;
461458
ResultDiffLines = resultLines;
462459

463-
var unresolved = new List<int>();
460+
var unsolved = new List<int>();
464461
for (var i = 0; i < _conflictRegions.Count; i++)
465462
{
466463
var r = _conflictRegions[i];
467464
if (!r.IsResolved)
468-
unresolved.Add(i);
465+
unsolved.Add(i);
469466
}
470467

471-
_unresolvedConflictCount = unresolved.Count;
472-
OnPropertyChanged(nameof(StatusText));
473-
OnPropertyChanged(nameof(HasUnresolvedConflicts));
468+
UnsolvedCount = unsolved.Count;
474469
}
475470

476471
private readonly Repository _repo;
477472
private readonly string _filePath;
478473
private string _originalContent = string.Empty;
479-
private int _unresolvedConflictCount = 0;
474+
private int _unsolvedCount = 0;
480475
private int _diffMaxLineNumber = 0;
481476
private List<Models.TextDiffLine> _oursDiffLines = [];
482477
private List<Models.TextDiffLine> _theirsDiffLines = [];

src/Views/MergeConflictEditor.axaml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
xmlns:m="using:SourceGit.Models"
66
xmlns:vm="using:SourceGit.ViewModels"
77
xmlns:v="using:SourceGit.Views"
8+
xmlns:c="using:SourceGit.Converters"
89
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
910
x:Class="SourceGit.Views.MergeConflictEditor"
1011
x:DataType="vm:MergeConflictEditor"
@@ -51,20 +52,21 @@
5152
ToolTip.Tip="{DynamicResource Text.MergeConflictEditor.PrevConflict}"
5253
Click="OnGotoPrevConflict"
5354
HotKey="{OnPlatform Ctrl+Up, macOS=⌘+Up}"
54-
IsEnabled="{Binding HasUnresolvedConflicts, Mode=OneWay}">
55+
IsEnabled="{Binding UnsolvedCount, Converter={x:Static c:IntConverters.IsGreaterThanZero}, Mode=OneWay}">
5556
<Path Width="12" Height="12" Margin="0,6,0,0" Data="{StaticResource Icons.Up}"/>
5657
</Button>
5758
<Button Classes="icon_button"
5859
Width="24"
5960
ToolTip.Tip="{DynamicResource Text.MergeConflictEditor.NextConflict}"
6061
Click="OnGotoNextConflict"
6162
HotKey="{OnPlatform Ctrl+Down, macOS=⌘+Down}"
62-
IsEnabled="{Binding HasUnresolvedConflicts, Mode=OneWay}">
63+
IsEnabled="{Binding UnsolvedCount, Converter={x:Static c:IntConverters.IsGreaterThanZero}, Mode=OneWay}">
6364
<Path Width="12" Height="12" Margin="0,6,0,0" Data="{StaticResource Icons.Down}"/>
6465
</Button>
6566

6667
<!-- Info Bar -->
67-
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center"/>
68+
<TextBlock Text="{Binding UnsolvedCount, Converter={x:Static c:IntConverters.ToUnsolvedDesc}, Mode=OneWay}"
69+
VerticalAlignment="Center"/>
6870
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" Margin="4,6"/>
6971

7072
<!-- Save -->
@@ -78,9 +80,9 @@
7880
</Border>
7981

8082
<!-- Main Content -->
81-
<Grid Grid.Row="2" RowDefinitions="*,*">
83+
<Grid Grid.Row="2" RowDefinitions="*,*" ColumnDefinitions="*,20">
8284
<!-- Mine and Theirs Panels (Side-by-Side) -->
83-
<Grid Grid.Row="0" ColumnDefinitions="*,*" Margin="4,4,4,2">
85+
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="*,*" Margin="4,4,4,2">
8486
<!-- Mine (Ours) Panel -->
8587
<Border Grid.Column="0" Margin="0,0,2,0">
8688
<Grid RowDefinitions="Auto,*" Background="{DynamicResource Brush.Contents}">
@@ -163,7 +165,7 @@
163165
</Grid>
164166

165167
<!-- Result Panel -->
166-
<Border Grid.Row="1" Margin="4,2,4,4">
168+
<Border Grid.Row="1" Grid.Column="0" Margin="4,2,4,4">
167169
<Grid RowDefinitions="Auto,*" Background="{DynamicResource Brush.Contents}">
168170
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.ToolBar}" BorderThickness="1,1,1,0" BorderBrush="{DynamicResource Brush.Border2}">
169171
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Result}" FontWeight="Bold"/>
@@ -247,6 +249,14 @@
247249
</Grid>
248250
</Grid>
249251
</Border>
252+
253+
<!-- Minimap -->
254+
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
255+
Margin="0,0,4,4"
256+
BorderThickness="1,0,1,1" BorderBrush="{DynamicResource Brush.Border2}">
257+
<v:MergeConflictMinimap DisplayRange="{Binding #ResultPresenter.DisplayRange, Mode=OneWay}"
258+
UnsolvedCount="{Binding UnsolvedCount, Mode=OneWay}"/>
259+
</Border>
250260
</Grid>
251261

252262
<!-- Error -->

src/Views/MergeConflictEditor.axaml.cs

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Avalonia.Interactivity;
1313
using Avalonia.Media;
1414
using Avalonia.Threading;
15+
using Avalonia.VisualTree;
1516

1617
using AvaloniaEdit;
1718
using AvaloniaEdit.Document;
@@ -105,6 +106,15 @@ public Models.ConflictSelectedChunk SelectedChunk
105106
set => SetValue(SelectedChunkProperty, value);
106107
}
107108

109+
public static readonly StyledProperty<ViewModels.TextDiffDisplayRange> DisplayRangeProperty =
110+
AvaloniaProperty.Register<MergeDiffPresenter, ViewModels.TextDiffDisplayRange>(nameof(DisplayRange));
111+
112+
public ViewModels.TextDiffDisplayRange DisplayRange
113+
{
114+
get => GetValue(DisplayRangeProperty);
115+
set => SetValue(DisplayRangeProperty, value);
116+
}
117+
108118
protected override Type StyleKeyOverride => typeof(TextEditor);
109119

110120
public MergeDiffPresenter() : base(new TextArea(), new TextDocument())
@@ -147,7 +157,10 @@ protected override void OnLoaded(RoutedEventArgs e)
147157
TextArea.TextView.PointerEntered += OnTextViewPointerChanged;
148158
TextArea.TextView.PointerMoved += OnTextViewPointerChanged;
149159
TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged;
160+
TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged;
150161
TextArea.TextView.LineTransformers.Add(new MergeDiffIndicatorTransformer(this));
162+
163+
OnTextViewVisualLinesChanged(null, null);
151164
}
152165

153166
protected override void OnUnloaded(RoutedEventArgs e)
@@ -156,6 +169,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
156169
TextArea.TextView.PointerEntered -= OnTextViewPointerChanged;
157170
TextArea.TextView.PointerMoved -= OnTextViewPointerChanged;
158171
TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged;
172+
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
159173

160174
if (_textMate != null)
161175
{
@@ -253,6 +267,34 @@ private void OnTextViewPointerWheelChanged(object sender, PointerWheelEventArgs
253267
Dispatcher.UIThread.Post(() => UpdateSelectedChunkPosition(vm, y));
254268
}
255269

270+
private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
271+
{
272+
if (!TextArea.TextView.VisualLinesValid)
273+
{
274+
SetCurrentValue(DisplayRangeProperty, null);
275+
return;
276+
}
277+
278+
var lines = DiffLines;
279+
var start = int.MaxValue;
280+
var count = 0;
281+
foreach (var line in TextArea.TextView.VisualLines)
282+
{
283+
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
284+
continue;
285+
286+
var index = line.FirstDocumentLine.LineNumber - 1;
287+
if (index >= lines.Count)
288+
continue;
289+
290+
count++;
291+
if (start > index)
292+
start = index;
293+
}
294+
295+
SetCurrentValue(DisplayRangeProperty, new ViewModels.TextDiffDisplayRange(start, start + count));
296+
}
297+
256298
private void OnTextViewScrollChanged(object sender, ScrollChangedEventArgs e)
257299
{
258300
if (_scrollViewer == null || DataContext is not ViewModels.MergeConflictEditor vm)
@@ -525,6 +567,97 @@ private IBrush GetBrushByLineType(Models.TextDiffLineType type)
525567
private readonly MergeDiffPresenter _presenter;
526568
}
527569

570+
public class MergeConflictMinimap : Control
571+
{
572+
public static readonly StyledProperty<ViewModels.TextDiffDisplayRange> DisplayRangeProperty =
573+
AvaloniaProperty.Register<MergeConflictMinimap, ViewModels.TextDiffDisplayRange>(nameof(DisplayRange));
574+
575+
public ViewModels.TextDiffDisplayRange DisplayRange
576+
{
577+
get => GetValue(DisplayRangeProperty);
578+
set => SetValue(DisplayRangeProperty, value);
579+
}
580+
581+
public static readonly StyledProperty<int> UnsolvedCountProperty =
582+
AvaloniaProperty.Register<MergeConflictMinimap, int>(nameof(UnsolvedCount));
583+
584+
public int UnsolvedCount
585+
{
586+
get => GetValue(UnsolvedCountProperty);
587+
set => SetValue(UnsolvedCountProperty, value);
588+
}
589+
590+
public override void Render(DrawingContext context)
591+
{
592+
context.DrawRectangle(Brushes.Transparent, null, new Rect(0, 0, Bounds.Width, Bounds.Height));
593+
594+
if (DataContext is not ViewModels.MergeConflictEditor vm)
595+
return;
596+
597+
var total = vm.OursDiffLines.Count;
598+
var unitHeight = Bounds.Height / (total * 1.0);
599+
var conflicts = vm.ConflictRegions;
600+
var blockBGs = new SolidColorBrush[] { new SolidColorBrush(Colors.Red, 0.6), new SolidColorBrush(Colors.Green, 0.6) };
601+
foreach (var c in conflicts)
602+
{
603+
var topY = c.StartLineInOriginal * unitHeight;
604+
var bottomY = (c.EndLineInOriginal + 1) * unitHeight;
605+
var bg = blockBGs[c.IsResolved ? 1 : 0];
606+
context.DrawRectangle(bg, null, new Rect(0, topY, Bounds.Width, bottomY - topY));
607+
}
608+
609+
var range = DisplayRange;
610+
if (range == null || range.End == 0)
611+
return;
612+
613+
var startY = range.Start * unitHeight;
614+
var endY = range.End * unitHeight;
615+
var color = (Color)this.FindResource("SystemAccentColor");
616+
var brush = new SolidColorBrush(color, 0.2);
617+
var pen = new Pen(color.ToUInt32());
618+
var rect = new Rect(0, startY, Bounds.Width, endY - startY);
619+
620+
context.DrawRectangle(brush, null, rect);
621+
context.DrawLine(pen, rect.TopLeft, rect.TopRight);
622+
context.DrawLine(pen, rect.BottomLeft, rect.BottomRight);
623+
}
624+
625+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
626+
{
627+
base.OnPropertyChanged(change);
628+
629+
if (change.Property == DisplayRangeProperty ||
630+
change.Property == UnsolvedCountProperty ||
631+
change.Property.Name.Equals(nameof(ActualThemeVariant), StringComparison.Ordinal))
632+
InvalidateVisual();
633+
}
634+
635+
protected override void OnPointerPressed(PointerPressedEventArgs e)
636+
{
637+
base.OnPointerPressed(e);
638+
639+
if (DataContext is not ViewModels.MergeConflictEditor vm)
640+
return;
641+
642+
var total = vm.OursDiffLines.Count;
643+
var range = DisplayRange;
644+
if (range == null || range.End == 0)
645+
return;
646+
647+
var unitHeight = Bounds.Height / (total * 1.0);
648+
var startY = range.Start * unitHeight;
649+
var endY = range.End * unitHeight;
650+
var pressedY = e.GetPosition(this).Y;
651+
if (pressedY >= startY && pressedY <= endY)
652+
return;
653+
654+
var line = Math.Max(1, Math.Min(total, (int)Math.Ceiling(pressedY / unitHeight)));
655+
var editor = this.FindAncestorOfType<MergeConflictEditor>();
656+
if (editor != null)
657+
editor.OursPresenter.ScrollToLine(line);
658+
}
659+
}
660+
528661
public partial class MergeConflictEditor : ChromelessWindow
529662
{
530663
public MergeConflictEditor()
@@ -547,7 +680,7 @@ protected override async void OnClosing(WindowClosingEventArgs e)
547680
if (DataContext is not ViewModels.MergeConflictEditor vm)
548681
return;
549682

550-
if (_forceClose || !vm.HasUnsavedChanges)
683+
if (_forceClose || vm.UnsolvedCount < vm.ConflictRegions.Count)
551684
{
552685
vm.PropertyChanged -= OnViewModelPropertyChanged;
553686
return;
@@ -580,7 +713,7 @@ private void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs
580713

581714
private void OnGotoPrevConflict(object sender, RoutedEventArgs e)
582715
{
583-
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.HasUnresolvedConflicts)
716+
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.UnsolvedCount > 0)
584717
{
585718
var view = OursPresenter.TextArea?.TextView;
586719
var lines = vm.OursDiffLines;
@@ -623,7 +756,7 @@ private void OnGotoPrevConflict(object sender, RoutedEventArgs e)
623756

624757
private void OnGotoNextConflict(object sender, RoutedEventArgs e)
625758
{
626-
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.HasUnresolvedConflicts)
759+
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.UnsolvedCount > 0)
627760
{
628761
var view = OursPresenter.TextArea?.TextView;
629762
var lines = vm.OursDiffLines;

0 commit comments

Comments
 (0)