mirror of
https://fastgit.cc/github.com/sourcegit-scm/sourcegit
synced 2026-04-30 13:51:53 +08:00
feature: add a minimap to merge conflict editor
Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
@@ -33,5 +33,8 @@ namespace SourceGit.Converters
|
||||
|
||||
public static readonly FuncValueConverter<int, IBrush> ToBookmarkBrush =
|
||||
new FuncValueConverter<int, IBrush>(v => Models.Bookmarks.Get(v) ?? App.Current?.FindResource("Brush.FG1") as IBrush);
|
||||
|
||||
public static readonly FuncValueConverter<int, string> ToUnsolvedDesc =
|
||||
new FuncValueConverter<int, string>(v => v == 0 ? App.Text("MergeConflictEditor.AllResolved") : App.Text("MergeConflictEditor.ConflictsRemaining", v));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +46,10 @@ namespace SourceGit.ViewModels
|
||||
private set => SetProperty(ref _diffMaxLineNumber, value);
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
public int UnsolvedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_unresolvedConflictCount > 0)
|
||||
return App.Text("MergeConflictEditor.ConflictsRemaining", _unresolvedConflictCount);
|
||||
return App.Text("MergeConflictEditor.AllResolved");
|
||||
}
|
||||
get => _unsolvedCount;
|
||||
private set => SetProperty(ref _unsolvedCount, value);
|
||||
}
|
||||
|
||||
public Vector ScrollOffset
|
||||
@@ -68,9 +64,10 @@ namespace SourceGit.ViewModels
|
||||
set => SetProperty(ref _selectedChunk, value);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Models.ConflictRegion> ConflictRegions => _conflictRegions;
|
||||
public bool HasUnresolvedConflicts => _unresolvedConflictCount > 0;
|
||||
public bool HasUnsavedChanges => _unresolvedConflictCount < _conflictRegions.Count;
|
||||
public IReadOnlyList<Models.ConflictRegion> ConflictRegions
|
||||
{
|
||||
get => _conflictRegions;
|
||||
}
|
||||
|
||||
public MergeConflictEditor(Repository repo, string filePath)
|
||||
{
|
||||
@@ -126,7 +123,7 @@ namespace SourceGit.ViewModels
|
||||
if (_conflictRegions.Count == 0)
|
||||
return true;
|
||||
|
||||
if (_unresolvedConflictCount > 0)
|
||||
if (_unsolvedCount > 0)
|
||||
{
|
||||
Error = "Cannot save: there are still unresolved conflicts.";
|
||||
return false;
|
||||
@@ -460,23 +457,21 @@ namespace SourceGit.ViewModels
|
||||
SelectedChunk = null;
|
||||
ResultDiffLines = resultLines;
|
||||
|
||||
var unresolved = new List<int>();
|
||||
var unsolved = new List<int>();
|
||||
for (var i = 0; i < _conflictRegions.Count; i++)
|
||||
{
|
||||
var r = _conflictRegions[i];
|
||||
if (!r.IsResolved)
|
||||
unresolved.Add(i);
|
||||
unsolved.Add(i);
|
||||
}
|
||||
|
||||
_unresolvedConflictCount = unresolved.Count;
|
||||
OnPropertyChanged(nameof(StatusText));
|
||||
OnPropertyChanged(nameof(HasUnresolvedConflicts));
|
||||
UnsolvedCount = unsolved.Count;
|
||||
}
|
||||
|
||||
private readonly Repository _repo;
|
||||
private readonly string _filePath;
|
||||
private string _originalContent = string.Empty;
|
||||
private int _unresolvedConflictCount = 0;
|
||||
private int _unsolvedCount = 0;
|
||||
private int _diffMaxLineNumber = 0;
|
||||
private List<Models.TextDiffLine> _oursDiffLines = [];
|
||||
private List<Models.TextDiffLine> _theirsDiffLines = [];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:m="using:SourceGit.Models"
|
||||
xmlns:vm="using:SourceGit.ViewModels"
|
||||
xmlns:v="using:SourceGit.Views"
|
||||
xmlns:c="using:SourceGit.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
|
||||
x:Class="SourceGit.Views.MergeConflictEditor"
|
||||
x:DataType="vm:MergeConflictEditor"
|
||||
@@ -51,7 +52,7 @@
|
||||
ToolTip.Tip="{DynamicResource Text.MergeConflictEditor.PrevConflict}"
|
||||
Click="OnGotoPrevConflict"
|
||||
HotKey="{OnPlatform Ctrl+Up, macOS=⌘+Up}"
|
||||
IsEnabled="{Binding HasUnresolvedConflicts, Mode=OneWay}">
|
||||
IsEnabled="{Binding UnsolvedCount, Converter={x:Static c:IntConverters.IsGreaterThanZero}, Mode=OneWay}">
|
||||
<Path Width="12" Height="12" Margin="0,6,0,0" Data="{StaticResource Icons.Up}"/>
|
||||
</Button>
|
||||
<Button Classes="icon_button"
|
||||
@@ -59,12 +60,13 @@
|
||||
ToolTip.Tip="{DynamicResource Text.MergeConflictEditor.NextConflict}"
|
||||
Click="OnGotoNextConflict"
|
||||
HotKey="{OnPlatform Ctrl+Down, macOS=⌘+Down}"
|
||||
IsEnabled="{Binding HasUnresolvedConflicts, Mode=OneWay}">
|
||||
IsEnabled="{Binding UnsolvedCount, Converter={x:Static c:IntConverters.IsGreaterThanZero}, Mode=OneWay}">
|
||||
<Path Width="12" Height="12" Margin="0,6,0,0" Data="{StaticResource Icons.Down}"/>
|
||||
</Button>
|
||||
|
||||
<!-- Info Bar -->
|
||||
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding UnsolvedCount, Converter={x:Static c:IntConverters.ToUnsolvedDesc}, Mode=OneWay}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" Margin="4,6"/>
|
||||
|
||||
<!-- Save -->
|
||||
@@ -78,9 +80,9 @@
|
||||
</Border>
|
||||
|
||||
<!-- Main Content -->
|
||||
<Grid Grid.Row="2" RowDefinitions="*,*">
|
||||
<Grid Grid.Row="2" RowDefinitions="*,*" ColumnDefinitions="*,20">
|
||||
<!-- Mine and Theirs Panels (Side-by-Side) -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,*" Margin="4,4,4,2">
|
||||
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="*,*" Margin="4,4,4,2">
|
||||
<!-- Mine (Ours) Panel -->
|
||||
<Border Grid.Column="0" Margin="0,0,2,0">
|
||||
<Grid RowDefinitions="Auto,*" Background="{DynamicResource Brush.Contents}">
|
||||
@@ -163,7 +165,7 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Result Panel -->
|
||||
<Border Grid.Row="1" Margin="4,2,4,4">
|
||||
<Border Grid.Row="1" Grid.Column="0" Margin="4,2,4,4">
|
||||
<Grid RowDefinitions="Auto,*" Background="{DynamicResource Brush.Contents}">
|
||||
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.ToolBar}" BorderThickness="1,1,1,0" BorderBrush="{DynamicResource Brush.Border2}">
|
||||
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Result}" FontWeight="Bold"/>
|
||||
@@ -247,6 +249,14 @@
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Minimap -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
|
||||
Margin="0,0,4,4"
|
||||
BorderThickness="1,0,1,1" BorderBrush="{DynamicResource Brush.Border2}">
|
||||
<v:MergeConflictMinimap DisplayRange="{Binding #ResultPresenter.DisplayRange, Mode=OneWay}"
|
||||
UnsolvedCount="{Binding UnsolvedCount, Mode=OneWay}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Error -->
|
||||
|
||||
@@ -12,6 +12,7 @@ using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
using AvaloniaEdit;
|
||||
using AvaloniaEdit.Document;
|
||||
@@ -105,6 +106,15 @@ namespace SourceGit.Views
|
||||
set => SetValue(SelectedChunkProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ViewModels.TextDiffDisplayRange> DisplayRangeProperty =
|
||||
AvaloniaProperty.Register<MergeDiffPresenter, ViewModels.TextDiffDisplayRange>(nameof(DisplayRange));
|
||||
|
||||
public ViewModels.TextDiffDisplayRange DisplayRange
|
||||
{
|
||||
get => GetValue(DisplayRangeProperty);
|
||||
set => SetValue(DisplayRangeProperty, value);
|
||||
}
|
||||
|
||||
protected override Type StyleKeyOverride => typeof(TextEditor);
|
||||
|
||||
public MergeDiffPresenter() : base(new TextArea(), new TextDocument())
|
||||
@@ -147,7 +157,10 @@ namespace SourceGit.Views
|
||||
TextArea.TextView.PointerEntered += OnTextViewPointerChanged;
|
||||
TextArea.TextView.PointerMoved += OnTextViewPointerChanged;
|
||||
TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged;
|
||||
TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged;
|
||||
TextArea.TextView.LineTransformers.Add(new MergeDiffIndicatorTransformer(this));
|
||||
|
||||
OnTextViewVisualLinesChanged(null, null);
|
||||
}
|
||||
|
||||
protected override void OnUnloaded(RoutedEventArgs e)
|
||||
@@ -156,6 +169,7 @@ namespace SourceGit.Views
|
||||
TextArea.TextView.PointerEntered -= OnTextViewPointerChanged;
|
||||
TextArea.TextView.PointerMoved -= OnTextViewPointerChanged;
|
||||
TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged;
|
||||
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
|
||||
|
||||
if (_textMate != null)
|
||||
{
|
||||
@@ -253,6 +267,34 @@ namespace SourceGit.Views
|
||||
Dispatcher.UIThread.Post(() => UpdateSelectedChunkPosition(vm, y));
|
||||
}
|
||||
|
||||
private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!TextArea.TextView.VisualLinesValid)
|
||||
{
|
||||
SetCurrentValue(DisplayRangeProperty, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = DiffLines;
|
||||
var start = int.MaxValue;
|
||||
var count = 0;
|
||||
foreach (var line in TextArea.TextView.VisualLines)
|
||||
{
|
||||
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
|
||||
continue;
|
||||
|
||||
var index = line.FirstDocumentLine.LineNumber - 1;
|
||||
if (index >= lines.Count)
|
||||
continue;
|
||||
|
||||
count++;
|
||||
if (start > index)
|
||||
start = index;
|
||||
}
|
||||
|
||||
SetCurrentValue(DisplayRangeProperty, new ViewModels.TextDiffDisplayRange(start, start + count));
|
||||
}
|
||||
|
||||
private void OnTextViewScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (_scrollViewer == null || DataContext is not ViewModels.MergeConflictEditor vm)
|
||||
@@ -525,6 +567,97 @@ namespace SourceGit.Views
|
||||
private readonly MergeDiffPresenter _presenter;
|
||||
}
|
||||
|
||||
public class MergeConflictMinimap : Control
|
||||
{
|
||||
public static readonly StyledProperty<ViewModels.TextDiffDisplayRange> DisplayRangeProperty =
|
||||
AvaloniaProperty.Register<MergeConflictMinimap, ViewModels.TextDiffDisplayRange>(nameof(DisplayRange));
|
||||
|
||||
public ViewModels.TextDiffDisplayRange DisplayRange
|
||||
{
|
||||
get => GetValue(DisplayRangeProperty);
|
||||
set => SetValue(DisplayRangeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<int> UnsolvedCountProperty =
|
||||
AvaloniaProperty.Register<MergeConflictMinimap, int>(nameof(UnsolvedCount));
|
||||
|
||||
public int UnsolvedCount
|
||||
{
|
||||
get => GetValue(UnsolvedCountProperty);
|
||||
set => SetValue(UnsolvedCountProperty, value);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
context.DrawRectangle(Brushes.Transparent, null, new Rect(0, 0, Bounds.Width, Bounds.Height));
|
||||
|
||||
if (DataContext is not ViewModels.MergeConflictEditor vm)
|
||||
return;
|
||||
|
||||
var total = vm.OursDiffLines.Count;
|
||||
var unitHeight = Bounds.Height / (total * 1.0);
|
||||
var conflicts = vm.ConflictRegions;
|
||||
var blockBGs = new SolidColorBrush[] { new SolidColorBrush(Colors.Red, 0.6), new SolidColorBrush(Colors.Green, 0.6) };
|
||||
foreach (var c in conflicts)
|
||||
{
|
||||
var topY = c.StartLineInOriginal * unitHeight;
|
||||
var bottomY = (c.EndLineInOriginal + 1) * unitHeight;
|
||||
var bg = blockBGs[c.IsResolved ? 1 : 0];
|
||||
context.DrawRectangle(bg, null, new Rect(0, topY, Bounds.Width, bottomY - topY));
|
||||
}
|
||||
|
||||
var range = DisplayRange;
|
||||
if (range == null || range.End == 0)
|
||||
return;
|
||||
|
||||
var startY = range.Start * unitHeight;
|
||||
var endY = range.End * unitHeight;
|
||||
var color = (Color)this.FindResource("SystemAccentColor");
|
||||
var brush = new SolidColorBrush(color, 0.2);
|
||||
var pen = new Pen(color.ToUInt32());
|
||||
var rect = new Rect(0, startY, Bounds.Width, endY - startY);
|
||||
|
||||
context.DrawRectangle(brush, null, rect);
|
||||
context.DrawLine(pen, rect.TopLeft, rect.TopRight);
|
||||
context.DrawLine(pen, rect.BottomLeft, rect.BottomRight);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property == DisplayRangeProperty ||
|
||||
change.Property == UnsolvedCountProperty ||
|
||||
change.Property.Name.Equals(nameof(ActualThemeVariant), StringComparison.Ordinal))
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
if (DataContext is not ViewModels.MergeConflictEditor vm)
|
||||
return;
|
||||
|
||||
var total = vm.OursDiffLines.Count;
|
||||
var range = DisplayRange;
|
||||
if (range == null || range.End == 0)
|
||||
return;
|
||||
|
||||
var unitHeight = Bounds.Height / (total * 1.0);
|
||||
var startY = range.Start * unitHeight;
|
||||
var endY = range.End * unitHeight;
|
||||
var pressedY = e.GetPosition(this).Y;
|
||||
if (pressedY >= startY && pressedY <= endY)
|
||||
return;
|
||||
|
||||
var line = Math.Max(1, Math.Min(total, (int)Math.Ceiling(pressedY / unitHeight)));
|
||||
var editor = this.FindAncestorOfType<MergeConflictEditor>();
|
||||
if (editor != null)
|
||||
editor.OursPresenter.ScrollToLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class MergeConflictEditor : ChromelessWindow
|
||||
{
|
||||
public MergeConflictEditor()
|
||||
@@ -547,7 +680,7 @@ namespace SourceGit.Views
|
||||
if (DataContext is not ViewModels.MergeConflictEditor vm)
|
||||
return;
|
||||
|
||||
if (_forceClose || !vm.HasUnsavedChanges)
|
||||
if (_forceClose || vm.UnsolvedCount < vm.ConflictRegions.Count)
|
||||
{
|
||||
vm.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
return;
|
||||
@@ -580,7 +713,7 @@ namespace SourceGit.Views
|
||||
|
||||
private void OnGotoPrevConflict(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.HasUnresolvedConflicts)
|
||||
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.UnsolvedCount > 0)
|
||||
{
|
||||
var view = OursPresenter.TextArea?.TextView;
|
||||
var lines = vm.OursDiffLines;
|
||||
@@ -623,7 +756,7 @@ namespace SourceGit.Views
|
||||
|
||||
private void OnGotoNextConflict(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.HasUnresolvedConflicts)
|
||||
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.UnsolvedCount > 0)
|
||||
{
|
||||
var view = OursPresenter.TextArea?.TextView;
|
||||
var lines = vm.OursDiffLines;
|
||||
|
||||
Reference in New Issue
Block a user