diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 7d2dabe2..c85e0934 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -33,5 +33,8 @@ namespace SourceGit.Converters public static readonly FuncValueConverter ToBookmarkBrush = new FuncValueConverter(v => Models.Bookmarks.Get(v) ?? App.Current?.FindResource("Brush.FG1") as IBrush); + + public static readonly FuncValueConverter ToUnsolvedDesc = + new FuncValueConverter(v => v == 0 ? App.Text("MergeConflictEditor.AllResolved") : App.Text("MergeConflictEditor.ConflictsRemaining", v)); } } diff --git a/src/ViewModels/MergeConflictEditor.cs b/src/ViewModels/MergeConflictEditor.cs index 582f121e..057a21fb 100644 --- a/src/ViewModels/MergeConflictEditor.cs +++ b/src/ViewModels/MergeConflictEditor.cs @@ -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 ConflictRegions => _conflictRegions; - public bool HasUnresolvedConflicts => _unresolvedConflictCount > 0; - public bool HasUnsavedChanges => _unresolvedConflictCount < _conflictRegions.Count; + public IReadOnlyList 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(); + var unsolved = new List(); 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 _oursDiffLines = []; private List _theirsDiffLines = []; diff --git a/src/Views/MergeConflictEditor.axaml b/src/Views/MergeConflictEditor.axaml index 868dd287..99e8298a 100644 --- a/src/Views/MergeConflictEditor.axaml +++ b/src/Views/MergeConflictEditor.axaml @@ -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}"> - + @@ -78,9 +80,9 @@ - + - + @@ -163,7 +165,7 @@ - + @@ -247,6 +249,14 @@ + + + + + diff --git a/src/Views/MergeConflictEditor.axaml.cs b/src/Views/MergeConflictEditor.axaml.cs index f8643964..4394faae 100644 --- a/src/Views/MergeConflictEditor.axaml.cs +++ b/src/Views/MergeConflictEditor.axaml.cs @@ -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 DisplayRangeProperty = + AvaloniaProperty.Register(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 DisplayRangeProperty = + AvaloniaProperty.Register(nameof(DisplayRange)); + + public ViewModels.TextDiffDisplayRange DisplayRange + { + get => GetValue(DisplayRangeProperty); + set => SetValue(DisplayRangeProperty, value); + } + + public static readonly StyledProperty UnsolvedCountProperty = + AvaloniaProperty.Register(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(); + 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;