From 12a79d634affb4631f3792348b9ad5775b725f50 Mon Sep 17 00:00:00 2001 From: Bert Verhelst Date: Mon, 26 Oct 2026 19:17:04 +0100 Subject: [PATCH 1/3] feature: per-line picks, editable result, collapsible unchanged lines in merge conflict editor - Checkbox margin in Mine/Theirs panels to pick individual lines (result keeps click order) - Result panel is now an editable document; hand-edited hunks resolve once markers are gone - Toggle to collapse unchanged lines around conflicts - Prev/next conflict navigation wraps, supports F7/Shift+F7, shows position - Fix row alignment for diff3-style conflicts Co-Authored-By: Claude Opus 5.5 --- src/Models/Conflict.cs | 30 +- src/Resources/Locales/en_US.axaml | 4 + src/ViewModels/MergeConflictEditor.cs | 637 ++++++++++++++++--------- src/Views/MergeConflictEditor.axaml | 31 +- src/Views/MergeConflictEditor.axaml.cs | 600 +++++++++++++++++------ 5 files changed, 917 insertions(+), 385 deletions(-) diff --git a/src/Models/Conflict.cs b/src/Models/Conflict.cs index fd9c690308..68b59c9d4a 100644 --- a/src/Models/Conflict.cs +++ b/src/Models/Conflict.cs @@ -33,24 +33,26 @@ public enum ConflictLineType Marker, Ours, Theirs, + Edited, } - public enum ConflictLineState - { - Normal, - ConflictBlockStart, - ConflictBlock, - ConflictBlockEnd, - ResolvedBlockStart, - ResolvedBlock, - ResolvedBlockEnd, - } + public record struct ConflictLineDecoration( + ConflictLineType Type, + bool InRegion, + bool IsResolved, + bool IsFirst, + bool IsLast + ); + + public record ConflictPick(ConflictPanelType Side, int Index); public class ConflictLine { public ConflictLineType Type { get; set; } = ConflictLineType.None; public string Content { get; set; } = string.Empty; public string LineNumber { get; set; } = string.Empty; + public int RegionIndex { get; set; } = -1; + public int IndexInSide { get; set; } = -1; public ConflictLine() { @@ -78,8 +80,8 @@ bool IsResolved public class ConflictRegion { - public int StartLineInOriginal { get; set; } - public int EndLineInOriginal { get; set; } + public int StartRow { get; set; } + public int EndRow { get; set; } public string StartMarker { get; set; } = "<<<<<<<"; public string SeparatorMarker { get; set; } = "======="; @@ -88,7 +90,9 @@ public class ConflictRegion public List OursContent { get; set; } = new(); public List TheirsContent { get; set; } = new(); + public List Picks { get; set; } = new(); + public bool HasChoice { get; set; } = false; + public bool IsCustom { get; set; } = false; public bool IsResolved { get; set; } = false; - public ConflictResolution ResolutionType { get; set; } = ConflictResolution.None; } } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 9abb3bdcc9..c850a299e8 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -607,10 +607,14 @@ First Theirs, then Mine USE BOTH All conflicts resolved + Collapse Unchanged Lines + Conflict {0} of {1} {0} conflict(s) remaining + {0} unchanged lines MINE Next Conflict Previous Conflict + RESET RESULT SAVE & STAGE THEIRS diff --git a/src/ViewModels/MergeConflictEditor.cs b/src/ViewModels/MergeConflictEditor.cs index fc9d0f632d..96ec3607ab 100644 --- a/src/ViewModels/MergeConflictEditor.cs +++ b/src/ViewModels/MergeConflictEditor.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -using Avalonia; +using AvaloniaEdit.Document; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -44,11 +44,10 @@ public List TheirsLines private set => SetProperty(ref _theirsLines, value); } - public List ResultLines + public TextDocument ResultDocument { - get => _resultLines; - private set => SetProperty(ref _resultLines, value); - } + get; + } = new TextDocument(); public int MaxLineNumber { @@ -59,13 +58,43 @@ public int MaxLineNumber public int UnsolvedCount { get => _unsolvedCount; - private set => SetProperty(ref _unsolvedCount, value); + private set + { + if (SetProperty(ref _unsolvedCount, value)) + OnPropertyChanged(nameof(NavigationDesc)); + } + } + + public int CurrentConflictIndex + { + get => _currentConflictIndex; + private set + { + if (SetProperty(ref _currentConflictIndex, value)) + OnPropertyChanged(nameof(NavigationDesc)); + } + } + + public string NavigationDesc + { + get + { + var unsolved = _unsolvedCount == 0 ? + App.Text("MergeConflictEditor.AllResolved") : + App.Text("MergeConflictEditor.ConflictsRemaining", _unsolvedCount); + + if (_currentConflictIndex < 0 || _conflictRegions.Count == 0) + return unsolved; + + var position = App.Text("MergeConflictEditor.ConflictPosition", _currentConflictIndex + 1, _conflictRegions.Count); + return $"{position} · {unsolved}"; + } } - public Vector ScrollOffset + public bool IsDirty { - get => _scrollOffset; - set => SetProperty(ref _scrollOffset, value); + get; + private set; } public Models.ConflictSelectedChunk SelectedChunk @@ -79,6 +108,8 @@ public IReadOnlyList ConflictRegions get => _conflictRegions; } + public event Action RegionsChanged; + public MergeConflictEditor(Repository repo, Models.Commit head, string filePath) { _repo = repo; @@ -105,99 +136,210 @@ public MergeConflictEditor(Repository repo, Models.Commit head, string filePath) } ParseOriginalContent(workingCopyContent); - RefreshDisplayData(); + BuildResultDocument(); + RefreshRegionStates(); + + ResultDocument.Changing += OnResultDocumentChanging; + ResultDocument.Changed += OnResultDocumentChanged; } - public Models.ConflictLineState GetLineState(int line) + public Models.ConflictLineDecoration GetRowDecoration(int row) { - if (line >= 0 && line < _lineStates.Count) - return _lineStates[line]; - return Models.ConflictLineState.Normal; + if (row < 0 || row >= _oursLines.Count) + return default; + + var regionIdx = _oursLines[row].RegionIndex; + if (regionIdx < 0) + return default; + + var r = _conflictRegions[regionIdx]; + return new(Models.ConflictLineType.None, true, r.IsResolved, row == r.StartRow, row == r.EndRow); } - public void Resolve(object param) + public Models.ConflictLineDecoration GetResultLineDecoration(int line) { - if (_selectedChunk == null) - return; + if (line >= 0 && line < _resultDecorations.Length) + return _resultDecorations[line]; + return default; + } - var region = _conflictRegions[_selectedChunk.ConflictIndex]; - if (param is not Models.ConflictResolution resolution) - return; + public (int FirstLine, int LineCount) GetResultRange(int regionIdx) + { + var range = _ranges[regionIdx]; + return (range.FirstLine, range.LineCount); + } + + public int FindRegionByResultLine(int line) + { + for (var i = 0; i < _ranges.Count; i++) + { + var range = _ranges[i]; + if (line >= range.FirstLine && line < range.FirstLine + range.LineCount) + return i; + } + + return -1; + } + + public int RowToResultLine(int row) + { + var prevEndRow = -1; + var prevEndLine = 0; + for (var i = 0; i < _conflictRegions.Count; i++) + { + var r = _conflictRegions[i]; + var range = _ranges[i]; + if (row < r.StartRow) + break; + + if (row <= r.EndRow) + return range.FirstLine + Math.Min(row - r.StartRow, Math.Max(range.LineCount - 1, 0)); + + prevEndRow = r.EndRow; + prevEndLine = range.FirstLine + range.LineCount; + } + + return Math.Clamp(prevEndLine + (row - prevEndRow - 1), 0, Math.Max(ResultDocument.LineCount - 1, 0)); + } + + public int ResultLineToRow(int line) + { + var prevEndRow = -1; + var prevEndLine = 0; + for (var i = 0; i < _conflictRegions.Count; i++) + { + var r = _conflictRegions[i]; + var range = _ranges[i]; + if (line < range.FirstLine) + break; + + if (line < range.FirstLine + range.LineCount) + return r.StartRow + Math.Min(line - range.FirstLine, r.EndRow - r.StartRow); + + prevEndRow = r.EndRow; + prevEndLine = range.FirstLine + range.LineCount; + } + + return Math.Clamp(prevEndRow + 1 + (line - prevEndLine), 0, Math.Max(_oursLines.Count - 1, 0)); + } - // Try to resolve a resolved region. - if (resolution != Models.ConflictResolution.None && region.IsResolved) + public bool IsPicked(int regionIdx, Models.ConflictPanelType side, int index, out int order) + { + var picks = _conflictRegions[regionIdx].Picks; + order = picks.FindIndex(p => p.Side == side && p.Index == index); + return order >= 0; + } + + public void TogglePick(int regionIdx, Models.ConflictPanelType side, int index) + { + var r = _conflictRegions[regionIdx]; + if (r.IsCustom) return; - // Try to undo an unresolved region. - if (resolution == Models.ConflictResolution.None && !region.IsResolved) + var existing = r.Picks.FindIndex(p => p.Side == side && p.Index == index); + if (existing >= 0) + r.Picks.RemoveAt(existing); + else + r.Picks.Add(new(side, index)); + + r.HasChoice = r.Picks.Count > 0; + ApplyRegion(regionIdx); + CurrentConflictIndex = regionIdx; + OnRegionsChanged(); + } + + public void Resolve(object param) + { + if (_selectedChunk == null || param is not Models.ConflictResolution resolution) return; - region.IsResolved = resolution != Models.ConflictResolution.None; - region.ResolutionType = resolution; - RefreshDisplayData(); + ResolveRegion(_selectedChunk.ConflictIndex, resolution); } - public async Task SaveAndStageAsync() + public void ResolveRegion(int regionIdx, Models.ConflictResolution resolution) + { + var r = _conflictRegions[regionIdx]; + r.Picks.Clear(); + r.IsCustom = false; + r.HasChoice = resolution != Models.ConflictResolution.None; + + if (resolution is Models.ConflictResolution.UseOurs or Models.ConflictResolution.UseBothMineFirst) + AddAllPicks(r, Models.ConflictPanelType.Ours); + + if (resolution is Models.ConflictResolution.UseTheirs or Models.ConflictResolution.UseBothMineFirst or Models.ConflictResolution.UseBothTheirsFirst) + AddAllPicks(r, Models.ConflictPanelType.Theirs); + + if (resolution == Models.ConflictResolution.UseBothTheirsFirst) + AddAllPicks(r, Models.ConflictPanelType.Ours); + + ApplyRegion(regionIdx); + CurrentConflictIndex = regionIdx; + SelectedChunk = null; + OnRegionsChanged(); + } + + public int GotoConflict(bool next) { if (_conflictRegions.Count == 0) - return true; + return -1; - if (_unsolvedCount > 0) + var candidates = new List(); + for (var i = 0; i < _conflictRegions.Count; i++) { - Error = "Cannot save: there are still unresolved conflicts."; - return false; + if (!_conflictRegions[i].IsResolved) + candidates.Add(i); } - var lines = _originalContent.Split('\n', StringSplitOptions.None); - var builder = new StringBuilder(); - var lastLineIdx = 0; - - foreach (var r in _conflictRegions) + if (candidates.Count == 0) { - for (var i = lastLineIdx; i < r.StartLineInOriginal; i++) - builder.Append(lines[i]).Append('\n'); + for (var i = 0; i < _conflictRegions.Count; i++) + candidates.Add(i); + } - if (r.ResolutionType == Models.ConflictResolution.UseOurs) + var target = next ? candidates[0] : candidates[^1]; + if (next) + { + foreach (var c in candidates) { - foreach (var l in r.OursContent) - builder.Append(l).Append('\n'); + if (c > _currentConflictIndex) + { + target = c; + break; + } } - else if (r.ResolutionType == Models.ConflictResolution.UseTheirs) + } + else + { + for (var i = candidates.Count - 1; i >= 0; i--) { - foreach (var l in r.TheirsContent) - builder.Append(l).Append('\n'); + if (candidates[i] < _currentConflictIndex) + { + target = candidates[i]; + break; + } } - else if (r.ResolutionType == Models.ConflictResolution.UseBothMineFirst) - { - foreach (var l in r.OursContent) - builder.Append(l).Append('\n'); + } - foreach (var l in r.TheirsContent) - builder.Append(l).Append('\n'); - } - else if (r.ResolutionType == Models.ConflictResolution.UseBothTheirsFirst) - { - foreach (var l in r.TheirsContent) - builder.Append(l).Append('\n'); + CurrentConflictIndex = target; + return target; + } - foreach (var l in r.OursContent) - builder.Append(l).Append('\n'); - } + public async Task SaveAndStageAsync() + { + if (_conflictRegions.Count == 0 && !IsDirty) + return true; - lastLineIdx = r.EndLineInOriginal + 1; + if (_unsolvedCount > 0) + { + Error = "Cannot save: there are still unresolved conflicts."; + return false; } - for (var j = lastLineIdx; j < lines.Length; j++) - builder.Append(lines[j]).Append('\n'); - - if (builder.Length > 1) - builder.Length--; // Remove extra '\n' - try { // Write merged content to file var fullPath = Path.Combine(_repo.FullPath, _filePath); - await File.WriteAllTextAsync(fullPath, builder.ToString()); + await File.WriteAllTextAsync(fullPath, ResultDocument.Text); // Stage the file var pathSpecFile = Path.GetTempFileName(); @@ -222,7 +364,6 @@ public void ClearErrorMessage() private void ParseOriginalContent(string content) { - _originalContent = content; _conflictRegions.Clear(); if (string.IsNullOrEmpty(content)) @@ -241,14 +382,15 @@ private void ParseOriginalContent(string content) if (line.StartsWith("<<<<<<<", StringComparison.Ordinal)) { + var regionIdx = _conflictRegions.Count; var region = new Models.ConflictRegion { - StartLineInOriginal = i, + StartRow = oursLines.Count, StartMarker = line, }; - oursLines.Add(new()); - theirsLines.Add(new()); + oursLines.Add(new() { RegionIndex = regionIdx }); + theirsLines.Add(new() { RegionIndex = regionIdx }); i++; // Collect ours content @@ -257,9 +399,9 @@ private void ParseOriginalContent(string content) !lines[i].StartsWith("=======", StringComparison.Ordinal)) { line = lines[i]; + oursLines.Add(new(Models.ConflictLineType.Ours, line, oursLineNumber++) { RegionIndex = regionIdx, IndexInSide = region.OursContent.Count }); + theirsLines.Add(new() { RegionIndex = regionIdx }); region.OursContent.Add(line); - oursLines.Add(new(Models.ConflictLineType.Ours, line, oursLineNumber++)); - theirsLines.Add(new()); i++; } @@ -274,8 +416,8 @@ private void ParseOriginalContent(string content) // Capture separator marker if (i < lines.Length && lines[i].StartsWith("=======", StringComparison.Ordinal)) { - oursLines.Add(new()); - theirsLines.Add(new()); + oursLines.Add(new() { RegionIndex = regionIdx }); + theirsLines.Add(new() { RegionIndex = regionIdx }); region.SeparatorMarker = lines[i]; i++; } @@ -284,23 +426,22 @@ private void ParseOriginalContent(string content) while (i < lines.Length && !lines[i].StartsWith(">>>>>>>", StringComparison.Ordinal)) { line = lines[i]; + oursLines.Add(new() { RegionIndex = regionIdx }); + theirsLines.Add(new(Models.ConflictLineType.Theirs, line, theirsLineNumber++) { RegionIndex = regionIdx, IndexInSide = region.TheirsContent.Count }); region.TheirsContent.Add(line); - oursLines.Add(new()); - theirsLines.Add(new(Models.ConflictLineType.Theirs, line, theirsLineNumber++)); i++; } // Capture end marker (e.g., ">>>>>>> feature-branch") if (i < lines.Length && lines[i].StartsWith(">>>>>>>", StringComparison.Ordinal)) { - oursLines.Add(new()); - theirsLines.Add(new()); - + oursLines.Add(new() { RegionIndex = regionIdx }); + theirsLines.Add(new() { RegionIndex = regionIdx }); region.EndMarker = lines[i]; - region.EndLineInOriginal = i; i++; } + region.EndRow = oursLines.Count - 1; _conflictRegions.Add(region); } else @@ -318,166 +459,236 @@ private void ParseOriginalContent(string content) TheirsLines = theirsLines; } - private void RefreshDisplayData() + private void BuildResultDocument() { - var resultLines = new List(); - _lineStates.Clear(); + var builder = new StringBuilder(); + var offsets = new List<(int Start, int End)>(); + var row = 0; - if (_oursLines == null || _oursLines.Count == 0) + _ranges.Clear(); + while (row < _oursLines.Count) { - ResultLines = resultLines; - return; + var regionIdx = _ranges.Count; + if (regionIdx < _conflictRegions.Count && _conflictRegions[regionIdx].StartRow == row) + { + var r = _conflictRegions[regionIdx]; + var range = new RegionRange(); + var start = builder.Length; + foreach (var line in ComposeRegion(r, range.LineTypes)) + builder.Append(line).Append('\n'); + + offsets.Add((start, builder.Length)); + _ranges.Add(range); + row = r.EndRow + 1; + } + else + { + builder.Append(_oursLines[row].Content).Append('\n'); + row++; + } } - int resultLineNumber = 1; - int currentLine = 0; - int conflictIdx = 0; + if (builder.Length > 0) + builder.Length--; // Remove extra '\n' + + ResultDocument.Text = builder.ToString(); + ResultDocument.UndoStack.ClearAll(); - while (currentLine < _oursLines.Count) + for (var i = 0; i < _ranges.Count; i++) { - // Check if we're at a conflict region - Models.ConflictRegion currentRegion = null; - if (conflictIdx < _conflictRegions.Count) - { - var region = _conflictRegions[conflictIdx]; - if (region.StartLineInOriginal == currentLine) - currentRegion = region; - } + _ranges[i].Start = CreateAnchor(offsets[i].Start); + _ranges[i].End = CreateAnchor(Math.Min(offsets[i].End, builder.Length)); + } + } - if (currentRegion != null) - { - int regionLines = currentRegion.EndLineInOriginal - currentRegion.StartLineInOriginal + 1; - if (currentRegion.IsResolved) - { - var oldLineCount = resultLines.Count; - var resolveType = currentRegion.ResolutionType; - - // Resolved - show resolved content with color based on resolution type - if (resolveType == Models.ConflictResolution.UseBothMineFirst) - { - int mineCount = currentRegion.OursContent.Count; - for (int i = 0; i < mineCount; i++) - { - resultLines.Add(new(Models.ConflictLineType.Ours, currentRegion.OursContent[i], resultLineNumber)); - resultLineNumber++; - } - - int theirsCount = currentRegion.TheirsContent.Count; - for (int i = 0; i < theirsCount; i++) - { - resultLines.Add(new(Models.ConflictLineType.Theirs, currentRegion.TheirsContent[i], resultLineNumber)); - resultLineNumber++; - } - } - else if (resolveType == Models.ConflictResolution.UseBothTheirsFirst) - { - int theirsCount = currentRegion.TheirsContent.Count; - for (int i = 0; i < theirsCount; i++) - { - resultLines.Add(new(Models.ConflictLineType.Theirs, currentRegion.TheirsContent[i], resultLineNumber)); - resultLineNumber++; - } - - int mineCount = currentRegion.OursContent.Count; - for (int i = 0; i < mineCount; i++) - { - resultLines.Add(new(Models.ConflictLineType.Ours, currentRegion.OursContent[i], resultLineNumber)); - resultLineNumber++; - } - } - else if (resolveType == Models.ConflictResolution.UseOurs) - { - int mineCount = currentRegion.OursContent.Count; - for (int i = 0; i < mineCount; i++) - { - resultLines.Add(new(Models.ConflictLineType.Ours, currentRegion.OursContent[i], resultLineNumber)); - resultLineNumber++; - } - } - else if (resolveType == Models.ConflictResolution.UseTheirs) - { - int theirsCount = currentRegion.TheirsContent.Count; - for (int i = 0; i < theirsCount; i++) - { - resultLines.Add(new(Models.ConflictLineType.Theirs, currentRegion.TheirsContent[i], resultLineNumber)); - resultLineNumber++; - } - } - - // Pad with empty lines to match Mine/Theirs panel height - int added = resultLines.Count - oldLineCount; - int padding = regionLines - added; - for (int p = 0; p < padding; p++) - resultLines.Add(new()); - - int blockSize = resultLines.Count - oldLineCount - 2; - _lineStates.Add(Models.ConflictLineState.ResolvedBlockStart); - for (var i = 0; i < blockSize; i++) - _lineStates.Add(Models.ConflictLineState.ResolvedBlock); - _lineStates.Add(Models.ConflictLineState.ResolvedBlockEnd); - } - else - { - resultLines.Add(new(Models.ConflictLineType.Marker, currentRegion.StartMarker)); - _lineStates.Add(Models.ConflictLineState.ConflictBlockStart); - - foreach (var line in currentRegion.OursContent) - { - resultLines.Add(new(Models.ConflictLineType.Ours, line, resultLineNumber++)); - _lineStates.Add(Models.ConflictLineState.ConflictBlock); - } - - resultLines.Add(new(Models.ConflictLineType.Marker, currentRegion.SeparatorMarker)); - _lineStates.Add(Models.ConflictLineState.ConflictBlock); - - foreach (var line in currentRegion.TheirsContent) - { - resultLines.Add(new(Models.ConflictLineType.Theirs, line, resultLineNumber++)); - _lineStates.Add(Models.ConflictLineState.ConflictBlock); - } - - resultLines.Add(new(Models.ConflictLineType.Marker, currentRegion.EndMarker)); - _lineStates.Add(Models.ConflictLineState.ConflictBlockEnd); - } + private List ComposeRegion(Models.ConflictRegion r, List types) + { + var lines = new List(); + types.Clear(); - currentLine = currentRegion.EndLineInOriginal + 1; - conflictIdx++; - } - else + if (r.HasChoice) + { + foreach (var pick in r.Picks) { - var oursLine = _oursLines[currentLine]; - resultLines.Add(new(oursLine.Type, oursLine.Content, resultLineNumber)); - _lineStates.Add(Models.ConflictLineState.Normal); - resultLineNumber++; - currentLine++; + var isOurs = pick.Side == Models.ConflictPanelType.Ours; + lines.Add(isOurs ? r.OursContent[pick.Index] : r.TheirsContent[pick.Index]); + types.Add(isOurs ? Models.ConflictLineType.Ours : Models.ConflictLineType.Theirs); } + + return lines; } - SelectedChunk = null; - ResultLines = resultLines; + lines.Add(r.StartMarker); + types.Add(Models.ConflictLineType.Marker); - var unsolved = new List(); - for (var i = 0; i < _conflictRegions.Count; i++) + foreach (var l in r.OursContent) + { + lines.Add(l); + types.Add(Models.ConflictLineType.Ours); + } + + lines.Add(r.SeparatorMarker); + types.Add(Models.ConflictLineType.Marker); + + foreach (var l in r.TheirsContent) + { + lines.Add(l); + types.Add(Models.ConflictLineType.Theirs); + } + + lines.Add(r.EndMarker); + types.Add(Models.ConflictLineType.Marker); + return lines; + } + + private void ApplyRegion(int regionIdx) + { + var r = _conflictRegions[regionIdx]; + var range = _ranges[regionIdx]; + + var builder = new StringBuilder(); + foreach (var line in ComposeRegion(r, range.LineTypes)) + builder.Append(line).Append('\n'); + + // The last region of a file has no trailing line break of its own. + if (r.EndRow == _oursLines.Count - 1 && builder.Length > 0) + builder.Length--; + + var text = builder.ToString(); + var start = range.Start.Offset; + + _isApplying = true; + ResultDocument.Replace(start, range.End.Offset - start, text); + _isApplying = false; + + range.Start = CreateAnchor(start); + range.End = CreateAnchor(start + text.Length); + } + + private void AddAllPicks(Models.ConflictRegion r, Models.ConflictPanelType side) + { + var count = side == Models.ConflictPanelType.Ours ? r.OursContent.Count : r.TheirsContent.Count; + for (var i = 0; i < count; i++) + r.Picks.Add(new(side, i)); + } + + private TextAnchor CreateAnchor(int offset) + { + var anchor = ResultDocument.CreateAnchor(offset); + anchor.MovementType = AnchorMovementType.BeforeInsertion; + anchor.SurviveDeletion = true; + return anchor; + } + + private void OnResultDocumentChanging(object sender, DocumentChangeEventArgs e) + { + if (_isApplying) + return; + + _touchedRegions.Clear(); + for (var i = 0; i < _ranges.Count; i++) + { + var start = _ranges[i].Start.Offset; + var end = _ranges[i].End.Offset; + var removed = e.RemovalLength > 0 && start < e.Offset + e.RemovalLength && e.Offset < end; + var inserted = e.InsertionLength > 0 && start <= e.Offset && e.Offset < end; + if (removed || inserted) + _touchedRegions.Add(i); + } + } + + private void OnResultDocumentChanged(object sender, DocumentChangeEventArgs e) + { + if (_isApplying) + return; + + foreach (var i in _touchedRegions) + { + var r = _conflictRegions[i]; + r.IsCustom = true; + r.HasChoice = false; + r.Picks.Clear(); + _ranges[i].LineTypes.Clear(); + } + + _touchedRegions.Clear(); + OnRegionsChanged(); + } + + private void OnRegionsChanged() + { + IsDirty = true; + RefreshRegionStates(); + RegionsChanged?.Invoke(); + } + + private void RefreshRegionStates() + { + var doc = ResultDocument; + var decorations = new Models.ConflictLineDecoration[doc.LineCount]; + var unsolved = 0; + + for (var i = 0; i < _ranges.Count; i++) { var r = _conflictRegions[i]; + var range = _ranges[i]; + var start = range.Start.Offset; + var end = range.End.Offset; + + range.FirstLine = doc.GetLineByOffset(start).LineNumber - 1; + range.LineCount = end > start ? doc.GetLineByOffset(end - 1).LineNumber - range.FirstLine : 0; + + r.IsResolved = r.IsCustom ? !ContainsMarkers(range) : r.HasChoice; if (!r.IsResolved) - unsolved.Add(i); + unsolved++; + + for (var k = 0; k < range.LineCount; k++) + { + var type = !r.IsCustom && k < range.LineTypes.Count ? range.LineTypes[k] : Models.ConflictLineType.Edited; + decorations[range.FirstLine + k] = new(type, true, r.IsResolved, k == 0, k == range.LineCount - 1); + } } - UnsolvedCount = unsolved.Count; + _resultDecorations = decorations; + UnsolvedCount = unsolved; + } + + private bool ContainsMarkers(RegionRange range) + { + var doc = ResultDocument; + for (var k = 0; k < range.LineCount; k++) + { + var text = doc.GetText(doc.GetLineByNumber(range.FirstLine + k + 1)); + if (text.StartsWith("<<<<<<<", StringComparison.Ordinal) || + text.StartsWith("=======", StringComparison.Ordinal) || + text.StartsWith(">>>>>>>", StringComparison.Ordinal)) + return true; + } + + return false; + } + + private class RegionRange + { + public TextAnchor Start { get; set; } + public TextAnchor End { get; set; } + public int FirstLine { get; set; } + public int LineCount { get; set; } + public List LineTypes { get; } = []; } private readonly Repository _repo; private readonly string _filePath; - private string _originalContent = string.Empty; private int _unsolvedCount = 0; private int _maxLineNumber = 0; + private int _currentConflictIndex = -1; + private bool _isApplying = false; private List _oursLines = []; private List _theirsLines = []; - private List _resultLines = []; private List _conflictRegions = []; - private List _lineStates = []; - private Vector _scrollOffset = Vector.Zero; + private List _ranges = []; + private List _touchedRegions = []; + private Models.ConflictLineDecoration[] _resultDecorations = []; private Models.ConflictSelectedChunk _selectedChunk; private string _error = string.Empty; } diff --git a/src/Views/MergeConflictEditor.axaml b/src/Views/MergeConflictEditor.axaml index e5b4379661..ed150255e2 100644 --- a/src/Views/MergeConflictEditor.axaml +++ b/src/Views/MergeConflictEditor.axaml @@ -37,7 +37,7 @@ - + - - - @@ -221,7 +231,7 @@