code_review: PR #2070

- Remove duplicated attributes in `Models.ConflictRegion`
- Add `ViewModels.MergeConflictEditor.Resolve(object resolution)` to replace `AcceptOurs/AcceptTheirs/AcceptBothMineFirst/AcceptBothTheirsFirst/Undo`
- Use command-binding instead of listening `Click` event for command buttons
- Remove `IsOldSide` and `IsResult` properties since `PanelType` is enough
- Rewrite the way to calculate current hovered conflict chunk

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2026-01-28 11:48:04 +08:00
parent 4f97983730
commit 0b0882b460
4 changed files with 183 additions and 423 deletions

View File

@@ -57,23 +57,15 @@ namespace SourceGit.Models
{
public int StartLineInOriginal { get; set; }
public int EndLineInOriginal { get; set; }
public List<string> OursContent { get; set; } = new();
public List<string> TheirsContent { get; set; } = new();
public bool IsResolved { get; set; } = false;
// Line indices in the built static panels (0-based)
public int PanelStartLine { get; set; } = -1;
public int PanelEndLine { get; set; } = -1;
// Content chosen when resolved (null = unresolved, empty list = deleted)
public List<string> ResolvedContent { get; set; } = null;
// Real markers from the file
public string StartMarker { get; set; } = "<<<<<<<";
public string SeparatorMarker { get; set; } = "=======";
public string EndMarker { get; set; } = ">>>>>>>";
// Track the type of resolution
public List<string> OursContent { get; set; } = new();
public List<string> TheirsContent { get; set; } = new();
public bool IsResolved { get; set; } = false;
public ConflictResolution ResolutionType { get; set; } = ConflictResolution.None;
}
}

View File

@@ -124,82 +124,25 @@ namespace SourceGit.ViewModels
return Models.ConflictLineState.Normal;
}
public void AcceptOurs()
public void Resolve(object param)
{
if (_selectedChunk == null)
return;
var region = _conflictRegions[_selectedChunk.ConflictIndex];
if (region.IsResolved)
if (param is not Models.ConflictResolution resolution)
return;
region.ResolvedContent = new List<string>(region.OursContent);
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseOurs;
RefreshDisplayData();
}
public void AcceptTheirs()
{
if (_selectedChunk == null)
// Try to resolve a resolved region.
if (resolution != Models.ConflictResolution.None && region.IsResolved)
return;
var region = _conflictRegions[_selectedChunk.ConflictIndex];
if (region.IsResolved)
// Try to undo an unresolved region.
if (resolution == Models.ConflictResolution.None && !region.IsResolved)
return;
region.ResolvedContent = new List<string>(region.TheirsContent);
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseTheirs;
RefreshDisplayData();
}
public void AcceptBothMineFirst()
{
if (_selectedChunk == null)
return;
var region = _conflictRegions[_selectedChunk.ConflictIndex];
if (region.IsResolved)
return;
var combined = new List<string>(region.OursContent);
combined.AddRange(region.TheirsContent);
region.ResolvedContent = combined;
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseBothMineFirst;
RefreshDisplayData();
}
public void AcceptBothTheirsFirst()
{
if (_selectedChunk == null)
return;
var region = _conflictRegions[_selectedChunk.ConflictIndex];
if (region.IsResolved)
return;
var combined = new List<string>(region.TheirsContent);
combined.AddRange(region.OursContent);
region.ResolvedContent = combined;
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseBothTheirsFirst;
RefreshDisplayData();
}
public void Undo()
{
if (_selectedChunk == null)
return;
var region = _conflictRegions[_selectedChunk.ConflictIndex];
if (!region.IsResolved)
return;
region.ResolvedContent = null;
region.IsResolved = false;
region.ResolutionType = Models.ConflictResolution.None;
region.IsResolved = resolution != Models.ConflictResolution.None;
region.ResolutionType = resolution;
RefreshDisplayData();
}
@@ -223,8 +166,32 @@ namespace SourceGit.ViewModels
for (var i = lastLineIdx; i < r.StartLineInOriginal; i++)
builder.Append(lines[i]).Append('\n');
foreach (var l in r.ResolvedContent)
builder.Append(l).Append('\n');
if (r.ResolutionType == Models.ConflictResolution.UseOurs)
{
foreach (var l in r.OursContent)
builder.Append(l).Append('\n');
}
else if (r.ResolutionType == Models.ConflictResolution.UseTheirs)
{
foreach (var l in r.TheirsContent)
builder.Append(l).Append('\n');
}
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');
foreach (var l in r.OursContent)
builder.Append(l).Append('\n');
}
lastLineIdx = r.EndLineInOriginal + 1;
}
@@ -278,7 +245,6 @@ namespace SourceGit.ViewModels
var region = new Models.ConflictRegion
{
StartLineInOriginal = i,
PanelStartLine = oursLines.Count,
StartMarker = line,
};
@@ -339,7 +305,6 @@ namespace SourceGit.ViewModels
region.EndMarker = lines[i];
region.EndLineInOriginal = i;
region.PanelEndLine = oursLines.Count - 1;
i++;
}
@@ -383,71 +348,79 @@ namespace SourceGit.ViewModels
if (conflictIdx < _conflictRegions.Count)
{
var region = _conflictRegions[conflictIdx];
if (region.PanelStartLine == currentLine)
if (region.StartLineInOriginal == currentLine)
currentRegion = region;
}
if (currentRegion != null)
{
int regionLines = currentRegion.PanelEndLine - currentRegion.PanelStartLine + 1;
if (currentRegion.ResolvedContent != 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 (currentRegion.ResolutionType == Models.ConflictResolution.UseBothMineFirst)
if (resolveType == Models.ConflictResolution.UseBothMineFirst)
{
// First portion is Mine (Deleted color), second is Theirs (Added color)
int mineCount = currentRegion.OursContent.Count;
for (int i = 0; i < currentRegion.ResolvedContent.Count; i++)
for (int i = 0; i < mineCount; i++)
{
var lineType = i < mineCount
? Models.TextDiffLineType.Deleted
: Models.TextDiffLineType.Added;
resultLines.Add(new Models.TextDiffLine(
lineType, currentRegion.ResolvedContent[i], resultLineNumber, resultLineNumber));
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, currentRegion.OursContent[i], resultLineNumber, resultLineNumber));
resultLineNumber++;
}
}
else if (currentRegion.ResolutionType == Models.ConflictResolution.UseBothTheirsFirst)
{
// First portion is Theirs (Added color), second is Mine (Deleted color)
int theirsCount = currentRegion.TheirsContent.Count;
for (int i = 0; i < currentRegion.ResolvedContent.Count; i++)
{
var lineType = i < theirsCount
? Models.TextDiffLineType.Added
: Models.TextDiffLineType.Deleted;
resultLines.Add(new Models.TextDiffLine(
lineType, currentRegion.ResolvedContent[i], resultLineNumber, resultLineNumber));
resultLineNumber++;
}
}
else
{
var lineType = currentRegion.ResolutionType switch
{
Models.ConflictResolution.UseOurs => Models.TextDiffLineType.Deleted, // Mine color
Models.ConflictResolution.UseTheirs => Models.TextDiffLineType.Added, // Theirs color
_ => Models.TextDiffLineType.Normal
};
foreach (var line in currentRegion.ResolvedContent)
int theirsCount = currentRegion.TheirsContent.Count;
for (int i = 0; i < theirsCount; i++)
{
resultLines.Add(new Models.TextDiffLine(
lineType, line, resultLineNumber, resultLineNumber));
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, currentRegion.TheirsContent[i], resultLineNumber, resultLineNumber));
resultLineNumber++;
}
}
else if (resolveType == Models.ConflictResolution.UseBothTheirsFirst)
{
int theirsCount = currentRegion.TheirsContent.Count;
for (int i = 0; i < theirsCount; i++)
{
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, currentRegion.TheirsContent[i], resultLineNumber, resultLineNumber));
resultLineNumber++;
}
int mineCount = currentRegion.OursContent.Count;
for (int i = 0; i < mineCount; i++)
{
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, currentRegion.OursContent[i], resultLineNumber, resultLineNumber));
resultLineNumber++;
}
}
else if (resolveType == Models.ConflictResolution.UseOurs)
{
int mineCount = currentRegion.OursContent.Count;
for (int i = 0; i < mineCount; i++)
{
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, currentRegion.OursContent[i], resultLineNumber, resultLineNumber));
resultLineNumber++;
}
}
else if (resolveType == Models.ConflictResolution.UseTheirs)
{
int theirsCount = currentRegion.TheirsContent.Count;
for (int i = 0; i < theirsCount; i++)
{
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, currentRegion.TheirsContent[i], resultLineNumber, resultLineNumber));
resultLineNumber++;
}
}
// Pad with empty lines to match Mine/Theirs panel height
int padding = regionLines - currentRegion.ResolvedContent.Count;
int added = resultLines.Count - oldLineCount;
int padding = regionLines - added;
for (int p = 0; p < padding; p++)
resultLines.Add(new Models.TextDiffLine());
int added = resultLines.Count - oldLineCount;
int blockSize = resultLines.Count - oldLineCount - 2;
_lineStates.Add(Models.ConflictLineState.ResolvedBlockStart);
for (var i = 0; i < added - 2; i++)
for (var i = 0; i < blockSize; i++)
_lineStates.Add(Models.ConflictLineState.ResolvedBlock);
_lineStates.Add(Models.ConflictLineState.ResolvedBlockEnd);
}
@@ -461,8 +434,7 @@ namespace SourceGit.ViewModels
// Mine content lines (matches the deleted lines in Ours panel)
foreach (var line in currentRegion.OursContent)
{
resultLines.Add(new Models.TextDiffLine(
Models.TextDiffLineType.Deleted, line, 0, resultLineNumber++));
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line, 0, resultLineNumber++));
_lineStates.Add(Models.ConflictLineState.ConflictBlock);
}
@@ -473,8 +445,7 @@ namespace SourceGit.ViewModels
// Theirs content lines (matches the added lines in Theirs panel)
foreach (var line in currentRegion.TheirsContent)
{
resultLines.Add(new Models.TextDiffLine(
Models.TextDiffLineType.Added, line, 0, resultLineNumber++));
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line, 0, resultLineNumber++));
_lineStates.Add(Models.ConflictLineState.ConflictBlock);
}
@@ -483,7 +454,7 @@ namespace SourceGit.ViewModels
_lineStates.Add(Models.ConflictLineState.ConflictBlockEnd);
}
currentLine = currentRegion.PanelEndLine + 1;
currentLine = currentRegion.EndLineInOriginal + 1;
conflictIdx++;
}
else
@@ -492,9 +463,7 @@ namespace SourceGit.ViewModels
var oursLine = _oursDiffLines[currentLine];
if (oursLine.Type == Models.TextDiffLineType.Normal)
{
resultLines.Add(new Models.TextDiffLine(
Models.TextDiffLineType.Normal, oursLine.Content,
resultLineNumber, resultLineNumber));
resultLines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, oursLine.Content, resultLineNumber, resultLineNumber));
resultLineNumber++;
}
else

View File

@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
@@ -89,11 +90,11 @@
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="OursPresenter"
PanelType="Mine"
DiffLines="{Binding OursDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
SelectedChunk="{Binding SelectedChunk}"
IsOldSide="True"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.TheirsBG}"
@@ -112,7 +113,8 @@
BoxShadow="0 2 8 0 #40000000">
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseMine}"
Click="OnUseMine"/>
Command="{Binding Resolve}"
CommandParameter="{x:Static m:ConflictResolution.UseOurs}"/>
</Border>
</Grid>
</Grid>
@@ -128,11 +130,11 @@
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="TheirsPresenter"
PanelType="Theirs"
DiffLines="{Binding TheirsDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
SelectedChunk="{Binding SelectedChunk}"
IsOldSide="False"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.TheirsBG}"
@@ -151,7 +153,8 @@
BoxShadow="0 2 8 0 #40000000">
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseTheirs}"
Click="OnUseTheirs"/>
Command="{Binding Resolve}"
CommandParameter="{x:Static m:ConflictResolution.UseTheirs}"/>
</Border>
</Grid>
</Grid>
@@ -166,12 +169,11 @@
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="ResultPresenter"
PanelType="Result"
DiffLines="{Binding ResultDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
SelectedChunk="{Binding SelectedChunk}"
IsOldSide="False"
IsResultPanel="True"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.TheirsBG}"
@@ -192,10 +194,12 @@
<StackPanel Orientation="Horizontal" Spacing="4">
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseMine}"
Click="OnUseMine"/>
Command="{Binding Resolve}"
CommandParameter="{x:Static m:ConflictResolution.UseOurs}"/>
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseTheirs}"
Click="OnUseTheirs"/>
Command="{Binding Resolve}"
CommandParameter="{x:Static m:ConflictResolution.UseTheirs}"/>
<Button Classes="flat primary">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.UseBoth}" Foreground="White"/>
@@ -203,7 +207,7 @@
</StackPanel>
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Click="OnUseBothMineFirst">
<MenuItem Command="{Binding Resolve}" CommandParameter="{x:Static m:ConflictResolution.UseBothMineFirst}">
<MenuItem.Icon>
<Path Width="12" Height="12" Data="{StaticResource Icons.February}" Fill="{DynamicResource Brush.Diff.MineHeader}"/>
</MenuItem.Icon>
@@ -211,7 +215,7 @@
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.AcceptBoth.MineFirst}"/>
</MenuItem.Header>
</MenuItem>
<MenuItem Click="OnUseBothTheirsFirst">
<MenuItem Command="{Binding Resolve}" CommandParameter="{x:Static m:ConflictResolution.UseBothTheirsFirst}">
<MenuItem.Icon>
<Path Width="12" Height="12" Data="{StaticResource Icons.February}" Fill="{DynamicResource Brush.Diff.TheirsHeader}"/>
</MenuItem.Icon>
@@ -236,7 +240,8 @@
BoxShadow="0 2 8 0 #40000000">
<Button Classes="flat"
Content="{DynamicResource Text.MergeConflictEditor.Undo}"
Click="OnUndo"/>
Command="{Binding Resolve}"
CommandParameter="{x:Static m:ConflictResolution.None}"/>
</Border>
</Grid>
</Grid>

View File

@@ -11,6 +11,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using AvaloniaEdit;
using AvaloniaEdit.Document;
@@ -32,6 +33,15 @@ namespace SourceGit.Views
set => SetValue(FileNameProperty, value);
}
public static readonly StyledProperty<Models.ConflictPanelType> PanelTypeProperty =
AvaloniaProperty.Register<MergeDiffPresenter, Models.ConflictPanelType>(nameof(PanelType));
public Models.ConflictPanelType PanelType
{
get => GetValue(PanelTypeProperty);
set => SetValue(PanelTypeProperty, value);
}
public static readonly StyledProperty<List<Models.TextDiffLine>> DiffLinesProperty =
AvaloniaProperty.Register<MergeDiffPresenter, List<Models.TextDiffLine>>(nameof(DiffLines));
@@ -50,24 +60,6 @@ namespace SourceGit.Views
set => SetValue(MaxLineNumberProperty, value);
}
public static readonly StyledProperty<bool> IsOldSideProperty =
AvaloniaProperty.Register<MergeDiffPresenter, bool>(nameof(IsOldSide));
public bool IsOldSide
{
get => GetValue(IsOldSideProperty);
set => SetValue(IsOldSideProperty, value);
}
public static readonly StyledProperty<bool> IsResultPanelProperty =
AvaloniaProperty.Register<MergeDiffPresenter, bool>(nameof(IsResultPanel), false);
public bool IsResultPanel
{
get => GetValue(IsResultPanelProperty);
set => SetValue(IsResultPanelProperty, value);
}
public static readonly StyledProperty<IBrush> EmptyContentBackgroundProperty =
AvaloniaProperty.Register<MergeDiffPresenter, IBrush>(nameof(EmptyContentBackground), new SolidColorBrush(Color.FromArgb(60, 0, 0, 0)));
@@ -113,18 +105,6 @@ namespace SourceGit.Views
set => SetValue(SelectedChunkProperty, value);
}
protected Models.ConflictPanelType PanelType
{
get
{
if (IsResultPanel)
return Models.ConflictPanelType.Result;
if (IsOldSide)
return Models.ConflictPanelType.Mine;
return Models.ConflictPanelType.Theirs;
}
}
protected override Type StyleKeyOverride => typeof(TextEditor);
public MergeDiffPresenter() : base(new TextArea(), new TextDocument())
@@ -164,20 +144,18 @@ namespace SourceGit.Views
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName);
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
TextArea.TextView.PointerMoved += OnTextViewPointerMoved;
TextArea.TextView.PointerExited += OnTextViewPointerExited;
TextArea.TextView.PointerEntered += OnTextViewPointerChanged;
TextArea.TextView.PointerMoved += OnTextViewPointerChanged;
TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged;
TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged;
TextArea.TextView.LineTransformers.Add(new MergeDiffIndicatorTransformer(this));
}
protected override void OnUnloaded(RoutedEventArgs e)
{
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
TextArea.TextView.PointerMoved -= OnTextViewPointerMoved;
TextArea.TextView.PointerExited -= OnTextViewPointerExited;
TextArea.TextView.PointerEntered -= OnTextViewPointerChanged;
TextArea.TextView.PointerMoved -= OnTextViewPointerChanged;
TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged;
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
if (_textMate != null)
{
@@ -252,125 +230,15 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnTextViewPointerMoved(object sender, PointerEventArgs e)
private void OnTextViewPointerChanged(object sender, PointerEventArgs e)
{
if (DataContext is not ViewModels.MergeConflictEditor vm)
if (DataContext is not ViewModels.MergeConflictEditor { IsLoading: false } vm)
return;
if (vm.IsLoading)
if (sender is not TextView view)
return;
var textView = TextArea.TextView;
if (!textView.VisualLinesValid)
return;
// Check if pointer is still within current chunk bounds (like TextDiffView does)
var currentChunk = vm.SelectedChunk;
var panelType = PanelType;
if (currentChunk != null && currentChunk.Panel == panelType)
{
var rect = new Rect(0, currentChunk.Y, Bounds.Width, currentChunk.Height);
if (rect.Contains(e.GetPosition(this)))
return; // Still within chunk, don't update
}
var conflictRegions = vm.ConflictRegions;
if (conflictRegions == null || conflictRegions.Count == 0)
return;
var isResultPanel = IsResultPanel;
var position = e.GetPosition(textView);
var y = position.Y + textView.VerticalOffset;
// Find which conflict region contains this Y position
for (int i = 0; i < conflictRegions.Count; i++)
{
var region = conflictRegions[i];
// For Result panel, allow hover on resolved conflicts (for undo)
// For Mine/Theirs panels, skip resolved conflicts
if (region.PanelStartLine < 0 || region.PanelEndLine < 0)
continue;
if (region.IsResolved && !isResultPanel)
continue;
// Get the visual bounds of this conflict region
var startLine = region.PanelStartLine + 1; // Document lines are 1-indexed
var endLine = region.PanelEndLine + 1;
if (startLine > Document.LineCount || endLine > Document.LineCount)
continue;
var startVisualLine = textView.GetVisualLine(startLine);
var endVisualLine = textView.GetVisualLine(endLine);
// Handle partially visible conflicts (same pattern as UpdateSelectedChunkPosition)
double viewportY, height;
bool isWithinRegion;
if (startVisualLine != null && endVisualLine != null)
{
// Both lines visible
var regionStartY = startVisualLine.GetTextLineVisualYPosition(
startVisualLine.TextLines[0], VisualYPosition.LineTop);
var regionEndY = endVisualLine.GetTextLineVisualYPosition(
endVisualLine.TextLines[^1], VisualYPosition.LineBottom);
isWithinRegion = y >= regionStartY && y <= regionEndY;
viewportY = regionStartY - textView.VerticalOffset;
height = regionEndY - regionStartY;
}
else if (startVisualLine == null && endVisualLine != null)
{
// Start scrolled out, end visible - clamp to top
var regionEndY = endVisualLine.GetTextLineVisualYPosition(
endVisualLine.TextLines[^1], VisualYPosition.LineBottom);
isWithinRegion = y <= regionEndY;
viewportY = 0;
height = regionEndY - textView.VerticalOffset;
}
else if (startVisualLine != null && endVisualLine == null)
{
// Start visible, end scrolled out - clamp to bottom
var regionStartY = startVisualLine.GetTextLineVisualYPosition(
startVisualLine.TextLines[0], VisualYPosition.LineTop);
isWithinRegion = y >= regionStartY;
viewportY = regionStartY - textView.VerticalOffset;
height = textView.Bounds.Height - viewportY;
}
else
{
// Both scrolled out - conflict not visible
continue;
}
if (isWithinRegion)
{
var newChunk = new Models.ConflictSelectedChunk(
viewportY, height, i, panelType, region.IsResolved);
// Only update if changed
if (currentChunk == null ||
currentChunk.ConflictIndex != newChunk.ConflictIndex ||
currentChunk.Panel != newChunk.Panel ||
currentChunk.IsResolved != newChunk.IsResolved ||
Math.Abs(currentChunk.Y - newChunk.Y) > 1 ||
Math.Abs(currentChunk.Height - newChunk.Height) > 1)
{
vm.SelectedChunk = newChunk;
}
return;
}
}
// Not hovering over any unresolved conflict - clear chunk
vm.SelectedChunk = null;
}
private void OnTextViewPointerExited(object sender, PointerEventArgs e)
{
// Don't clear here - the chunk stays visible until pointer moves to non-conflict area
UpdateSelectedChunkPosition(vm, e.GetPosition(view).Y + view.VerticalOffset);
}
private void OnTextViewPointerWheelChanged(object sender, PointerWheelEventArgs e)
@@ -378,23 +246,11 @@ namespace SourceGit.Views
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
if (vm.SelectedChunk == null || vm.SelectedChunk.Panel != PanelType)
if (sender is not TextView view)
return;
// Update chunk position after scroll
Avalonia.Threading.Dispatcher.UIThread.Post(() => UpdateSelectedChunkPosition(vm));
}
private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
{
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
if (vm.SelectedChunk == null || vm.SelectedChunk.Panel != PanelType)
return;
// Update chunk position when visual lines change
UpdateSelectedChunkPosition(vm);
var y = e.GetPosition(view).Y + view.VerticalOffset;
Dispatcher.UIThread.Post(() => UpdateSelectedChunkPosition(vm, y));
}
private void OnTextViewScrollChanged(object sender, ScrollChangedEventArgs e)
@@ -414,86 +270,65 @@ namespace SourceGit.Views
}
}
private void UpdateSelectedChunkPosition(ViewModels.MergeConflictEditor vm)
private void UpdateSelectedChunkPosition(ViewModels.MergeConflictEditor vm, double y)
{
var chunk = vm.SelectedChunk;
var panelType = PanelType;
if (chunk == null || chunk.Panel != panelType)
return;
var lines = DiffLines;
var panel = PanelType;
var view = TextArea.TextView;
var lineIdx = -1;
foreach (var line in view.VisualLines)
{
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
continue;
var textView = TextArea.TextView;
if (!textView.VisualLinesValid)
return;
var index = line.FirstDocumentLine.LineNumber;
if (index > lines.Count)
break;
var conflictRegions = vm.ConflictRegions;
if (conflictRegions == null || chunk.ConflictIndex >= conflictRegions.Count)
return;
var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.TextBottom);
if (endY > y)
{
lineIdx = index - 1;
break;
}
}
var region = conflictRegions[chunk.ConflictIndex];
// For Result panel, keep showing chunk for resolved conflicts (for undo)
// For Mine/Theirs panels, clear if resolved
if (region.IsResolved && !IsResultPanel)
if (lineIdx == -1)
{
vm.SelectedChunk = null;
return;
}
var startLine = region.PanelStartLine + 1;
var endLine = region.PanelEndLine + 1;
if (startLine > Document.LineCount || endLine > Document.LineCount)
return;
var startVisualLine = textView.GetVisualLine(startLine);
var endVisualLine = textView.GetVisualLine(endLine);
// Calculate visible portion of the conflict
double viewportY, height;
if (startVisualLine != null && endVisualLine != null)
for (var i = 0; i < vm.ConflictRegions.Count; i++)
{
// Both lines visible
var regionStartY = startVisualLine.GetTextLineVisualYPosition(
startVisualLine.TextLines[0], VisualYPosition.LineTop);
var regionEndY = endVisualLine.GetTextLineVisualYPosition(
endVisualLine.TextLines[^1], VisualYPosition.LineBottom);
var r = vm.ConflictRegions[i];
if (r.StartLineInOriginal <= lineIdx && r.EndLineInOriginal >= lineIdx)
{
if (r.IsResolved && panel != Models.ConflictPanelType.Result)
{
vm.SelectedChunk = null;
return;
}
viewportY = regionStartY - textView.VerticalOffset;
height = regionEndY - regionStartY;
}
else if (startVisualLine == null && endVisualLine != null)
{
// Start scrolled out, end visible - clamp to top
var regionEndY = endVisualLine.GetTextLineVisualYPosition(
endVisualLine.TextLines[^1], VisualYPosition.LineBottom);
var startLine = r.StartLineInOriginal + 1;
var endLine = r.EndLineInOriginal + 1;
if (startLine > Document.LineCount || endLine > Document.LineCount)
{
vm.SelectedChunk = null;
return;
}
viewportY = 0;
height = regionEndY - textView.VerticalOffset;
}
else if (startVisualLine != null && endVisualLine == null)
{
// Start visible, end scrolled out - clamp to bottom
var regionStartY = startVisualLine.GetTextLineVisualYPosition(
startVisualLine.TextLines[0], VisualYPosition.LineTop);
viewportY = regionStartY - textView.VerticalOffset;
height = textView.Bounds.Height - viewportY;
}
else
{
// Both scrolled out - conflict not visible, clear chunk
vm.SelectedChunk = null;
return;
var vOffset = view.VerticalOffset;
var startVisualLine = view.GetVisualLine(startLine);
var endVisualLine = view.GetVisualLine(endLine);
var topY = startVisualLine?.GetTextLineVisualYPosition(startVisualLine.TextLines[0], VisualYPosition.LineTop) ?? vOffset;
var bottomY = endVisualLine?.GetTextLineVisualYPosition(endVisualLine.TextLines[^1], VisualYPosition.LineBottom) ?? (view.Bounds.Height + vOffset);
vm.SelectedChunk = new Models.ConflictSelectedChunk(topY - vOffset, bottomY - topY, i, panel, r.IsResolved);
return;
}
}
// Update chunk with new position
var newChunk = new Models.ConflictSelectedChunk(
viewportY, height, chunk.ConflictIndex, panelType, region.IsResolved);
if (Math.Abs(chunk.Y - newChunk.Y) > 1 || Math.Abs(chunk.Height - newChunk.Height) > 1)
{
vm.SelectedChunk = newChunk;
}
vm.SelectedChunk = null;
}
private TextMate.Installation _textMate;
@@ -519,8 +354,7 @@ namespace SourceGit.Views
if (view is not { VisualLinesValid: true })
return;
var isOld = _presenter.IsOldSide;
var isResult = _presenter.IsResultPanel;
var panel = _presenter.PanelType;
var typeface = view.CreateTypeface();
foreach (var line in view.VisualLines)
@@ -534,11 +368,11 @@ namespace SourceGit.Views
var info = lines[index - 1];
string lineNumber;
if (isResult)
lineNumber = info.NewLine;
else
lineNumber = isOld ? info.OldLine : info.NewLine;
string lineNumber = panel switch
{
Models.ConflictPanelType.Mine => info.OldLine,
_ => info.NewLine,
};
if (string.IsNullOrEmpty(lineNumber))
continue;
@@ -808,9 +642,9 @@ namespace SourceGit.Views
for (var i = vm.ConflictRegions.Count - 1; i >= 0; i--)
{
var r = vm.ConflictRegions[i];
if (r.PanelStartLine < minLineIdx && !r.IsResolved)
if (r.StartLineInOriginal < minLineIdx && !r.IsResolved)
{
OursPresenter.ScrollToLine(r.PanelStartLine + 1);
OursPresenter.ScrollToLine(r.StartLineInOriginal + 1);
break;
}
}
@@ -851,9 +685,9 @@ namespace SourceGit.Views
for (var i = 0; i < vm.ConflictRegions.Count; i++)
{
var r = vm.ConflictRegions[i];
if (r.PanelStartLine > maxLineIdx && !r.IsResolved)
if (r.StartLineInOriginal > maxLineIdx && !r.IsResolved)
{
OursPresenter.ScrollToLine(r.PanelStartLine + 1);
OursPresenter.ScrollToLine(r.StartLineInOriginal + 1);
break;
}
}
@@ -863,46 +697,6 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnUseMine(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
vm.AcceptOurs();
e.Handled = true;
}
private void OnUseTheirs(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
vm.AcceptTheirs();
e.Handled = true;
}
private void OnUseBothMineFirst(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
vm.AcceptBothMineFirst();
e.Handled = true;
}
private void OnUseBothTheirsFirst(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
vm.AcceptBothTheirsFirst();
e.Handled = true;
}
private void OnUndo(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
vm.Undo();
e.Handled = true;
}
private async void OnSaveAndStage(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)