Skip to content

Commit 2ec2bc3

Browse files
committed
feat: undo button
1 parent 593c605 commit 2ec2bc3

File tree

4 files changed

+118
-22
lines changed

4 files changed

+118
-22
lines changed

src/Resources/Locales/en_US.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,7 @@
892892
<x:String x:Key="Text.MergeConflictEditor.UseMine.Tip" xml:space="preserve">Resolve current conflict using Mine version</x:String>
893893
<x:String x:Key="Text.MergeConflictEditor.UseTheirs" xml:space="preserve">USE THEIRS</x:String>
894894
<x:String x:Key="Text.MergeConflictEditor.UseTheirs.Tip" xml:space="preserve">Resolve current conflict using Theirs version</x:String>
895+
<x:String x:Key="Text.MergeConflictEditor.Undo" xml:space="preserve">UNDO</x:String>
895896
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">Update Submodules</x:String>
896897
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">All submodules</x:String>
897898
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">Initialize as needed</x:String>

src/ViewModels/MergeConflictEditor.cs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public record MergeConflictSelectedChunk(
2121
double Y,
2222
double Height,
2323
int ConflictIndex,
24-
MergeConflictPanelType Panel
24+
MergeConflictPanelType Panel,
25+
bool IsResolved
2526
);
2627

2728
// Represents a single conflict region with its original content and panel positions
@@ -39,6 +40,14 @@ public class ConflictRegion
3940

4041
// Content chosen when resolved (null = unresolved, empty list = deleted)
4142
public List<string> ResolvedContent { get; set; } = null;
43+
44+
// Real markers from the file
45+
public string StartMarker { get; set; } = "<<<<<<<";
46+
public string SeparatorMarker { get; set; } = "=======";
47+
public string EndMarker { get; set; } = ">>>>>>>";
48+
49+
// Track the type of resolution
50+
public Models.ConflictResolution ResolutionType { get; set; } = Models.ConflictResolution.None;
4251
}
4352

4453
public class MergeConflictEditor : ObservableObject
@@ -251,6 +260,8 @@ private void ParseOriginalConflicts(string content)
251260
if (line.StartsWith("<<<<<<<", StringComparison.Ordinal))
252261
{
253262
var region = new ConflictRegion { StartLineInOriginal = i };
263+
// Capture the start marker (e.g., "<<<<<<< HEAD")
264+
region.StartMarker = line;
254265
i++;
255266

256267
// Collect ours content
@@ -270,9 +281,12 @@ private void ParseOriginalConflicts(string content)
270281
i++;
271282
}
272283

273-
// Skip separator
284+
// Capture separator marker
274285
if (i < lines.Length && lines[i].StartsWith("=======", StringComparison.Ordinal))
286+
{
287+
region.SeparatorMarker = lines[i];
275288
i++;
289+
}
276290

277291
// Collect theirs content
278292
while (i < lines.Length && !lines[i].StartsWith(">>>>>>>", StringComparison.Ordinal))
@@ -281,9 +295,10 @@ private void ParseOriginalConflicts(string content)
281295
i++;
282296
}
283297

284-
// End marker
298+
// Capture end marker (e.g., ">>>>>>> feature-branch")
285299
if (i < lines.Length && lines[i].StartsWith(">>>>>>>", StringComparison.Ordinal))
286300
{
301+
region.EndMarker = lines[i];
287302
region.EndLineInOriginal = i;
288303
i++;
289304
}
@@ -446,11 +461,18 @@ private void BuildAlignedResultPanel()
446461

447462
if (currentRegion.ResolvedContent != null)
448463
{
449-
// Resolved - show resolved content + padding
464+
// Resolved - show resolved content with color based on resolution type
465+
var lineType = currentRegion.ResolutionType switch
466+
{
467+
Models.ConflictResolution.UseOurs => Models.TextDiffLineType.Deleted, // Mine color
468+
Models.ConflictResolution.UseTheirs => Models.TextDiffLineType.Added, // Theirs color
469+
_ => Models.TextDiffLineType.Normal
470+
};
471+
450472
foreach (var line in currentRegion.ResolvedContent)
451473
{
452474
resultLines.Add(new Models.TextDiffLine(
453-
Models.TextDiffLineType.Normal, line, resultLineNumber, resultLineNumber));
475+
lineType, line, resultLineNumber, resultLineNumber));
454476
resultLineNumber++;
455477
}
456478
// Pad with empty lines to match Mine/Theirs panel height
@@ -461,11 +483,11 @@ private void BuildAlignedResultPanel()
461483
else
462484
{
463485
// Unresolved - show conflict markers with content, aligned with Mine/Theirs
464-
// First line: start marker placeholder (matches <<<<<< line)
465-
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, "<<<<<<< (unresolved)", 0, 0));
486+
// First line: start marker (use real marker from file)
487+
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, currentRegion.StartMarker, 0, 0));
466488

467-
// Second line: separator placeholder (matches ======= line)
468-
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, "=======", 0, 0));
489+
// Second line: separator marker
490+
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, currentRegion.SeparatorMarker, 0, 0));
469491

470492
// Mine content lines (matches the deleted lines in Ours panel)
471493
foreach (var line in currentRegion.OursContent)
@@ -481,8 +503,8 @@ private void BuildAlignedResultPanel()
481503
Models.TextDiffLineType.Added, line, 0, resultLineNumber++));
482504
}
483505

484-
// End marker placeholder (matches >>>>>>> line)
485-
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, ">>>>>>> (unresolved)", 0, 0));
506+
// End marker (use real marker from file)
507+
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, currentRegion.EndMarker, 0, 0));
486508
}
487509

488510
currentLine = currentRegion.PanelEndLine + 1;
@@ -523,6 +545,7 @@ public void AcceptOurs()
523545
{
524546
region.ResolvedContent = new List<string>(region.OursContent);
525547
region.IsResolved = true;
548+
region.ResolutionType = Models.ConflictResolution.UseOurs;
526549
anyResolved = true;
527550
}
528551
}
@@ -548,6 +571,7 @@ public void AcceptTheirs()
548571
{
549572
region.ResolvedContent = new List<string>(region.TheirsContent);
550573
region.IsResolved = true;
574+
region.ResolutionType = Models.ConflictResolution.UseTheirs;
551575
anyResolved = true;
552576
}
553577
}
@@ -572,6 +596,7 @@ public void AcceptCurrentOurs()
572596

573597
region.ResolvedContent = new List<string>(region.OursContent);
574598
region.IsResolved = true;
599+
region.ResolutionType = Models.ConflictResolution.UseOurs;
575600

576601
RebuildResultContent();
577602
BuildAlignedResultPanel();
@@ -590,6 +615,7 @@ public void AcceptCurrentTheirs()
590615

591616
region.ResolvedContent = new List<string>(region.TheirsContent);
592617
region.IsResolved = true;
618+
region.ResolutionType = Models.ConflictResolution.UseTheirs;
593619

594620
RebuildResultContent();
595621
BuildAlignedResultPanel();
@@ -610,6 +636,7 @@ public void AcceptOursAtIndex(int conflictIndex)
610636

611637
region.ResolvedContent = new List<string>(region.OursContent);
612638
region.IsResolved = true;
639+
region.ResolutionType = Models.ConflictResolution.UseOurs;
613640

614641
RebuildResultContent();
615642
BuildAlignedResultPanel();
@@ -628,6 +655,26 @@ public void AcceptTheirsAtIndex(int conflictIndex)
628655

629656
region.ResolvedContent = new List<string>(region.TheirsContent);
630657
region.IsResolved = true;
658+
region.ResolutionType = Models.ConflictResolution.UseTheirs;
659+
660+
RebuildResultContent();
661+
BuildAlignedResultPanel();
662+
UpdateConflictInfo();
663+
IsModified = true;
664+
}
665+
666+
public void UndoResolutionAtIndex(int conflictIndex)
667+
{
668+
if (conflictIndex < 0 || conflictIndex >= _conflictRegions.Count)
669+
return;
670+
671+
var region = _conflictRegions[conflictIndex];
672+
if (!region.IsResolved)
673+
return;
674+
675+
region.ResolvedContent = null;
676+
region.IsResolved = false;
677+
region.ResolutionType = Models.ConflictResolution.None;
631678

632679
RebuildResultContent();
633680
BuildAlignedResultPanel();

src/Views/MergeConflictEditor.axaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,20 @@
223223
Click="OnUseTheirsFromHover"/>
224224
</StackPanel>
225225
</Border>
226+
<Border x:Name="ResultUndoPopup"
227+
IsVisible="False"
228+
VerticalAlignment="Top"
229+
HorizontalAlignment="Right"
230+
Background="{DynamicResource Brush.ToolBar}"
231+
BorderBrush="{DynamicResource Brush.Border2}"
232+
BorderThickness="1"
233+
CornerRadius="4"
234+
Padding="4,2"
235+
BoxShadow="0 2 8 0 #40000000">
236+
<Button Classes="flat"
237+
Content="{DynamicResource Text.MergeConflictEditor.Undo}"
238+
Click="OnUndoResolution"/>
239+
</Border>
226240
</Grid>
227241
</Grid>
228242
</Border>

src/Views/MergeConflictEditor.axaml.cs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,11 @@ private void OnTextViewPointerMoved(object sender, PointerEventArgs e)
342342
for (int i = 0; i < conflictRegions.Count; i++)
343343
{
344344
var region = conflictRegions[i];
345-
if (region.IsResolved || region.PanelStartLine < 0 || region.PanelEndLine < 0)
345+
// For Result panel, allow hover on resolved conflicts (for undo)
346+
// For Mine/Theirs panels, skip resolved conflicts
347+
if (region.PanelStartLine < 0 || region.PanelEndLine < 0)
348+
continue;
349+
if (region.IsResolved && PanelType != ViewModels.MergeConflictPanelType.Result)
346350
continue;
347351

348352
// Get the visual bounds of this conflict region
@@ -400,12 +404,13 @@ private void OnTextViewPointerMoved(object sender, PointerEventArgs e)
400404
if (isWithinRegion)
401405
{
402406
var newChunk = new ViewModels.MergeConflictSelectedChunk(
403-
viewportY, height, i, PanelType);
407+
viewportY, height, i, PanelType, region.IsResolved);
404408

405409
// Only update if changed
406410
if (currentChunk == null ||
407411
currentChunk.ConflictIndex != newChunk.ConflictIndex ||
408412
currentChunk.Panel != newChunk.Panel ||
413+
currentChunk.IsResolved != newChunk.IsResolved ||
409414
Math.Abs(currentChunk.Y - newChunk.Y) > 1 ||
410415
Math.Abs(currentChunk.Height - newChunk.Height) > 1)
411416
{
@@ -465,7 +470,9 @@ private void UpdateSelectedChunkPosition(ViewModels.MergeConflictEditor vm)
465470
return;
466471

467472
var region = conflictRegions[chunk.ConflictIndex];
468-
if (region.IsResolved)
473+
// For Result panel, keep showing chunk for resolved conflicts (for undo)
474+
// For Mine/Theirs panels, clear if resolved
475+
if (region.IsResolved && PanelType != ViewModels.MergeConflictPanelType.Result)
469476
{
470477
vm.SelectedChunk = null;
471478
return;
@@ -521,7 +528,7 @@ private void UpdateSelectedChunkPosition(ViewModels.MergeConflictEditor vm)
521528

522529
// Update chunk with new position
523530
var newChunk = new ViewModels.MergeConflictSelectedChunk(
524-
viewportY, height, chunk.ConflictIndex, PanelType);
531+
viewportY, height, chunk.ConflictIndex, PanelType, region.IsResolved);
525532

526533
if (Math.Abs(chunk.Y - newChunk.Y) > 1 || Math.Abs(chunk.Height - newChunk.Height) > 1)
527534
{
@@ -824,6 +831,7 @@ protected override void OnOpened(EventArgs e)
824831
_minePopup = this.FindControl<Border>("MinePopup");
825832
_theirsPopup = this.FindControl<Border>("TheirsPopup");
826833
_resultPopup = this.FindControl<Border>("ResultPopup");
834+
_resultUndoPopup = this.FindControl<Border>("ResultUndoPopup");
827835

828836
// Set up scroll synchronization
829837
SetupScrollSync();
@@ -984,6 +992,8 @@ private void UpdatePopupVisibility()
984992
_theirsPopup.IsVisible = false;
985993
if (_resultPopup != null)
986994
_resultPopup.IsVisible = false;
995+
if (_resultUndoPopup != null)
996+
_resultUndoPopup.IsVisible = false;
987997

988998
if (DataContext is not ViewModels.MergeConflictEditor vm)
989999
return;
@@ -1001,14 +1011,23 @@ private void UpdatePopupVisibility()
10011011
_ => null
10021012
};
10031013

1004-
// Show the appropriate popup based on panel type
1005-
Border popup = chunk.Panel switch
1014+
// Show the appropriate popup based on panel type and resolved state
1015+
Border popup;
1016+
if (chunk.Panel == ViewModels.MergeConflictPanelType.Result && chunk.IsResolved)
10061017
{
1007-
ViewModels.MergeConflictPanelType.Mine => _minePopup,
1008-
ViewModels.MergeConflictPanelType.Theirs => _theirsPopup,
1009-
ViewModels.MergeConflictPanelType.Result => _resultPopup,
1010-
_ => null
1011-
};
1018+
// Show Undo popup for resolved conflicts in Result panel
1019+
popup = _resultUndoPopup;
1020+
}
1021+
else
1022+
{
1023+
popup = chunk.Panel switch
1024+
{
1025+
ViewModels.MergeConflictPanelType.Mine => _minePopup,
1026+
ViewModels.MergeConflictPanelType.Theirs => _theirsPopup,
1027+
ViewModels.MergeConflictPanelType.Result => _resultPopup,
1028+
_ => null
1029+
};
1030+
}
10121031

10131032
if (popup != null && presenter != null)
10141033
{
@@ -1053,6 +1072,20 @@ private void OnUseTheirsFromHover(object sender, RoutedEventArgs e)
10531072
e.Handled = true;
10541073
}
10551074

1075+
private void OnUndoResolution(object sender, RoutedEventArgs e)
1076+
{
1077+
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
1078+
{
1079+
var savedOffset = SaveScrollOffset();
1080+
vm.UndoResolutionAtIndex(chunk.ConflictIndex);
1081+
UpdateCurrentConflictHighlight();
1082+
UpdateResolvedRanges();
1083+
RestoreScrollOffset(savedOffset);
1084+
vm.SelectedChunk = null;
1085+
}
1086+
e.Handled = true;
1087+
}
1088+
10561089
protected override async void OnClosing(WindowClosingEventArgs e)
10571090
{
10581091
base.OnClosing(e);
@@ -1264,5 +1297,6 @@ protected override void OnClosed(EventArgs e)
12641297
private Border _minePopup;
12651298
private Border _theirsPopup;
12661299
private Border _resultPopup;
1300+
private Border _resultUndoPopup;
12671301
}
12681302
}

0 commit comments

Comments
 (0)