mirror of
https://fastgit.cc/github.com/sourcegit-scm/sourcegit
synced 2026-04-21 21:30:37 +08:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user