code_review: PR #2070

- Remove unused code
- Add translations for Chinese (Simplified & Traditional)
- UI/UX changes
- Fix sync-scroll function sometimes does not work
- Re-arrange context menu items for local change

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2026-01-27 13:34:06 +08:00
parent d25faca618
commit ea9517eef9
11 changed files with 605 additions and 863 deletions

View File

@@ -7,10 +7,8 @@ namespace SourceGit.Models
None,
UseOurs,
UseTheirs,
UseBoth,
UseBothMineFirst,
UseBothTheirsFirst,
Manual,
}
public class MergeConflictRegion

View File

@@ -93,6 +93,8 @@
<x:String x:Key="Text.ChangeCM.CheckoutFirstParentRevision" xml:space="preserve">Reset to Parent Revision</x:String>
<x:String x:Key="Text.ChangeCM.CheckoutThisRevision" xml:space="preserve">Reset to This Revision</x:String>
<x:String x:Key="Text.ChangeCM.GenerateCommitMessage" xml:space="preserve">Generate commit message</x:String>
<x:String x:Key="Text.ChangeCM.Merge" xml:space="preserve">Merge (Built-in)</x:String>
<x:String x:Key="Text.ChangeCM.MergeExternal" xml:space="preserve">Merge (External)</x:String>
<x:String x:Key="Text.ChangeDisplayMode" xml:space="preserve">CHANGE DISPLAY MODE</x:String>
<x:String x:Key="Text.ChangeDisplayMode.Grid" xml:space="preserve">Show as File and Dir List</x:String>
<x:String x:Key="Text.ChangeDisplayMode.List" xml:space="preserve">Show as Path List</x:String>
@@ -536,6 +538,24 @@
<x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String>
<x:String x:Key="Text.Merge.Source" xml:space="preserve">Source:</x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.MineFirst" xml:space="preserve">First Mine, then Theirs</x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.TheirsFirst" xml:space="preserve">First Theirs, then Mine</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseBoth" xml:space="preserve">USE BOTH</x:String>
<x:String x:Key="Text.MergeConflictEditor.AllResolved" xml:space="preserve">All conflicts resolved</x:String>
<x:String x:Key="Text.MergeConflictEditor.ConflictsRemaining" xml:space="preserve">{0} conflict(s) remaining</x:String>
<x:String x:Key="Text.MergeConflictEditor.Mine" xml:space="preserve">MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.NextConflict" xml:space="preserve">Next Conflict</x:String>
<x:String x:Key="Text.MergeConflictEditor.PrevConflict" xml:space="preserve">Previous Conflict</x:String>
<x:String x:Key="Text.MergeConflictEditor.Result" xml:space="preserve">RESULT</x:String>
<x:String x:Key="Text.MergeConflictEditor.SaveAndStage" xml:space="preserve">SAVE &amp; STAGE</x:String>
<x:String x:Key="Text.MergeConflictEditor.Theirs" xml:space="preserve">THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.Title" xml:space="preserve">Merge Conflicts</x:String>
<x:String x:Key="Text.MergeConflictEditor.UnsavedChanges" xml:space="preserve">Discard unsaved changes?</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseMine" xml:space="preserve">USE MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseMine.Tip" xml:space="preserve">Resolve current conflict using Mine version</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseTheirs" xml:space="preserve">USE THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseTheirs.Tip" xml:space="preserve">Resolve current conflict using Theirs version</x:String>
<x:String x:Key="Text.MergeConflictEditor.Undo" xml:space="preserve">UNDO</x:String>
<x:String x:Key="Text.MergeMultiple" xml:space="preserve">Merge (Multiple)</x:String>
<x:String x:Key="Text.MergeMultiple.CommitChanges" xml:space="preserve">Commit all changes</x:String>
<x:String x:Key="Text.MergeMultiple.Strategy" xml:space="preserve">Strategy:</x:String>
@@ -552,8 +572,7 @@
<x:String x:Key="Text.Open.SystemDefaultEditor" xml:space="preserve">Default Editor (System)</x:String>
<x:String x:Key="Text.OpenAppDataDir" xml:space="preserve">Open Data Storage Directory</x:String>
<x:String x:Key="Text.OpenFile" xml:space="preserve">Open File</x:String>
<x:String x:Key="Text.OpenInExternalMergeTool" xml:space="preserve">Open in Merge Tool</x:String>
<x:String x:Key="Text.OpenInBuiltinMergeTool" xml:space="preserve">Open in Built-in Merge Tool</x:String>
<x:String x:Key="Text.OpenInExternalMergeTool" xml:space="preserve">Open in External Merge Tool</x:String>
<x:String x:Key="Text.Optional" xml:space="preserve">Optional.</x:String>
<x:String x:Key="Text.PageTabBar.New" xml:space="preserve">Create New Tab</x:String>
<x:String x:Key="Text.PageTabBar.Tab.Bookmark" xml:space="preserve">Bookmark</x:String>
@@ -875,24 +894,6 @@
<x:String x:Key="Text.TagCM.DeleteMultiple" xml:space="preserve">Delete selected {0} tags...</x:String>
<x:String x:Key="Text.TagCM.Merge" xml:space="preserve">Merge ${0}$ into ${1}$...</x:String>
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">Push ${0}$...</x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.MineFirst" xml:space="preserve">First Mine, then Theirs</x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.TheirsFirst" xml:space="preserve">First Theirs, then Mine</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseBoth" xml:space="preserve">USE BOTH</x:String>
<x:String x:Key="Text.MergeConflictEditor.AllResolved" xml:space="preserve">All conflicts resolved</x:String>
<x:String x:Key="Text.MergeConflictEditor.ConflictsRemaining" xml:space="preserve">{0} conflict(s) remaining</x:String>
<x:String x:Key="Text.MergeConflictEditor.Mine" xml:space="preserve">MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.NextConflict" xml:space="preserve">Next Conflict</x:String>
<x:String x:Key="Text.MergeConflictEditor.PrevConflict" xml:space="preserve">Previous Conflict</x:String>
<x:String x:Key="Text.MergeConflictEditor.Result" xml:space="preserve">RESULT</x:String>
<x:String x:Key="Text.MergeConflictEditor.SaveAndStage" xml:space="preserve">SAVE &amp; STAGE</x:String>
<x:String x:Key="Text.MergeConflictEditor.Theirs" xml:space="preserve">THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.Title" xml:space="preserve">Merge Conflict - {0}</x:String>
<x:String x:Key="Text.MergeConflictEditor.UnsavedChanges" xml:space="preserve">Discard unsaved changes?</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseMine" xml:space="preserve">USE MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseMine.Tip" xml:space="preserve">Resolve current conflict using Mine version</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseTheirs" xml:space="preserve">USE THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseTheirs.Tip" xml:space="preserve">Resolve current conflict using Theirs version</x:String>
<x:String x:Key="Text.MergeConflictEditor.Undo" xml:space="preserve">UNDO</x:String>
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">Update Submodules</x:String>
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">All submodules</x:String>
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">Initialize as needed</x:String>
@@ -936,8 +937,8 @@
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithDetachedHead">You are creating commit on a detached HEAD. Do you want to continue?</x:String>
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithFilter">You have staged {0} file(s) but only {1} file(s) displayed ({2} files are filtered out). Do you want to continue?</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">CONFLICTS DETECTED</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenBuiltinMergeTool" xml:space="preserve">OPEN MERGE TOOL</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeTool" xml:space="preserve">OPEN EXTERNAL MERGETOOL</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Merge" xml:space="preserve">MERGE</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.MergeExternal" xml:space="preserve">OPEN EXTERNAL MERGETOOL</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts" xml:space="preserve">OPEN ALL CONFLICTS IN EXTERNAL MERGETOOL</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Resolved" xml:space="preserve">FILE CONFLICTS ARE RESOLVED</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">USE MINE</x:String>

View File

@@ -97,6 +97,8 @@
<x:String x:Key="Text.ChangeCM.CheckoutFirstParentRevision" xml:space="preserve">重置文件到上一版本</x:String>
<x:String x:Key="Text.ChangeCM.CheckoutThisRevision" xml:space="preserve">重置文件到该版本</x:String>
<x:String x:Key="Text.ChangeCM.GenerateCommitMessage" xml:space="preserve">生成提交信息</x:String>
<x:String x:Key="Text.ChangeCM.Merge" xml:space="preserve">解决冲突(内部工具)</x:String>
<x:String x:Key="Text.ChangeCM.MergeExternal" xml:space="preserve">解决冲突(外部工具)</x:String>
<x:String x:Key="Text.ChangeDisplayMode" xml:space="preserve">切换变更显示模式</x:String>
<x:String x:Key="Text.ChangeDisplayMode.Grid" xml:space="preserve">文件名+路径列表模式</x:String>
<x:String x:Key="Text.ChangeDisplayMode.List" xml:space="preserve">全路径列表模式</x:String>
@@ -540,6 +542,22 @@
<x:String x:Key="Text.Merge.Into" xml:space="preserve">目标分支 </x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合并方式 </x:String>
<x:String x:Key="Text.Merge.Source" xml:space="preserve">合并目标 </x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.MineFirst" xml:space="preserve">先应用 MINE 后 THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.TheirsFirst" xml:space="preserve">先应用 THEIRS 后 MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseBoth" xml:space="preserve">应用全部</x:String>
<x:String x:Key="Text.MergeConflictEditor.AllResolved" xml:space="preserve">所有冲突已解决</x:String>
<x:String x:Key="Text.MergeConflictEditor.ConflictsRemaining" xml:space="preserve">{0} 个冲突未解决</x:String>
<x:String x:Key="Text.MergeConflictEditor.Mine" xml:space="preserve">MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.NextConflict" xml:space="preserve">下一个冲突</x:String>
<x:String x:Key="Text.MergeConflictEditor.PrevConflict" xml:space="preserve">上一个冲突</x:String>
<x:String x:Key="Text.MergeConflictEditor.Result" xml:space="preserve">合并结果</x:String>
<x:String x:Key="Text.MergeConflictEditor.SaveAndStage" xml:space="preserve">保存并暂存</x:String>
<x:String x:Key="Text.MergeConflictEditor.Theirs" xml:space="preserve">THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.Title" xml:space="preserve">合并冲突</x:String>
<x:String x:Key="Text.MergeConflictEditor.UnsavedChanges" xml:space="preserve">放弃所有更改?</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseMine" xml:space="preserve">仅应用 MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseTheirs" xml:space="preserve">仅应用 THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.Undo" xml:space="preserve">撤销更改</x:String>
<x:String x:Key="Text.MergeMultiple" xml:space="preserve">合并(多目标)</x:String>
<x:String x:Key="Text.MergeMultiple.CommitChanges" xml:space="preserve">提交变化</x:String>
<x:String x:Key="Text.MergeMultiple.Strategy" xml:space="preserve">合并策略 </x:String>
@@ -921,7 +939,8 @@
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithDetachedHead">您正在向一个游离的 HEAD 提交变更,是否继续提交?</x:String>
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithFilter" xml:space="preserve">当前有 {0} 个文件在暂存区中,但仅显示了 {1} 个文件({2} 个文件被过滤掉了),是否继续提交?</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">检测到冲突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeTool" xml:space="preserve">打开合并工具</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Merge" xml:space="preserve">解决冲突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.MergeExternal" xml:space="preserve">使用外部工具解决冲突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts" xml:space="preserve">打开合并工具解决冲突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Resolved" xml:space="preserve">文件冲突已解决</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">使用 MINE</x:String>

View File

@@ -97,6 +97,8 @@
<x:String x:Key="Text.ChangeCM.CheckoutFirstParentRevision" xml:space="preserve">重設檔案到上一版本</x:String>
<x:String x:Key="Text.ChangeCM.CheckoutThisRevision" xml:space="preserve">重設檔案為此版本</x:String>
<x:String x:Key="Text.ChangeCM.GenerateCommitMessage" xml:space="preserve">產生提交訊息</x:String>
<x:String x:Key="Text.ChangeCM.Merge" xml:space="preserve">解決衝突 (內建工具)</x:String>
<x:String x:Key="Text.ChangeCM.MergeExternal" xml:space="preserve">解決衝突 (外部工具)</x:String>
<x:String x:Key="Text.ChangeDisplayMode" xml:space="preserve">切換變更顯示模式</x:String>
<x:String x:Key="Text.ChangeDisplayMode.Grid" xml:space="preserve">檔案名稱 + 路徑列表模式</x:String>
<x:String x:Key="Text.ChangeDisplayMode.List" xml:space="preserve">全路徑列表模式</x:String>
@@ -540,6 +542,22 @@
<x:String x:Key="Text.Merge.Into" xml:space="preserve">目標分支:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合併方式:</x:String>
<x:String x:Key="Text.Merge.Source" xml:space="preserve">合併來源:</x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.MineFirst" xml:space="preserve">先應用 MINE再應用 THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.AcceptBoth.TheirsFirst" xml:space="preserve">先應用 THEIRS再應用 MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseBoth" xml:space="preserve">應用兩側</x:String>
<x:String x:Key="Text.MergeConflictEditor.AllResolved" xml:space="preserve">所有衝突已經解決</x:String>
<x:String x:Key="Text.MergeConflictEditor.ConflictsRemaining" xml:space="preserve">{0} 個衝突尚未解決</x:String>
<x:String x:Key="Text.MergeConflictEditor.Mine" xml:space="preserve">MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.NextConflict" xml:space="preserve">下一個衝突</x:String>
<x:String x:Key="Text.MergeConflictEditor.PrevConflict" xml:space="preserve">上一個衝突</x:String>
<x:String x:Key="Text.MergeConflictEditor.Result" xml:space="preserve">合併结果</x:String>
<x:String x:Key="Text.MergeConflictEditor.SaveAndStage" xml:space="preserve">保存並暫存</x:String>
<x:String x:Key="Text.MergeConflictEditor.Theirs" xml:space="preserve">THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.Title" xml:space="preserve">解決衝突</x:String>
<x:String x:Key="Text.MergeConflictEditor.UnsavedChanges" xml:space="preserve">放棄所有更改?</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseMine" xml:space="preserve">僅應用 MINE</x:String>
<x:String x:Key="Text.MergeConflictEditor.UseTheirs" xml:space="preserve">僅應用 THEIRS</x:String>
<x:String x:Key="Text.MergeConflictEditor.Undo" xml:space="preserve">撤銷更改</x:String>
<x:String x:Key="Text.MergeMultiple" xml:space="preserve">合併 (多個來源)</x:String>
<x:String x:Key="Text.MergeMultiple.CommitChanges" xml:space="preserve">提交變更</x:String>
<x:String x:Key="Text.MergeMultiple.Strategy" xml:space="preserve">合併策略:</x:String>
@@ -921,7 +939,8 @@
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithDetachedHead">您正在向一个分離狀態的 HEAD 提交變更,您確定要繼續提交嗎?</x:String>
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithFilter" xml:space="preserve">您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎?</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">偵測到衝突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeTool" xml:space="preserve">使用外部合併工具開啟</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Merge" xml:space="preserve">解決衝突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.MergeExternal" xml:space="preserve">使用外部工具解決衝突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts" xml:space="preserve">使用外部合併工具開啟</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Resolved" xml:space="preserve">檔案衝突已解決</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">使用我方版本 (ours)</x:String>

View File

@@ -54,17 +54,12 @@ namespace SourceGit.ViewModels
private set;
} = false;
public bool CanUseExternalMergeTool
public bool CanMerge
{
get;
private set;
} = false;
public bool CanUseBuiltinMergeTool
{
get => CanUseExternalMergeTool;
}
public string FilePath
{
get => _change.Path;
@@ -79,7 +74,7 @@ namespace SourceGit.ViewModels
var isSubmodule = repo.Submodules.Find(x => x.Path.Equals(change.Path, StringComparison.Ordinal)) != null;
if (!isSubmodule && (_change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified))
{
CanUseExternalMergeTool = true;
CanMerge = true;
IsResolved = new Commands.IsConflictResolved(repo.FullPath, change).GetResult();
}
@@ -123,14 +118,20 @@ namespace SourceGit.ViewModels
await _wc.UseMineAsync([_change]);
}
public async Task OpenExternalMergeToolAsync()
public async Task MergeAsync()
{
await _wc.UseExternalMergeToolAsync(_change);
if (CanMerge)
{
var ctx = new MergeConflictEditor(_repo, _change.Path);
await ctx.LoadAsync();
await App.ShowDialog(ctx);
}
}
public MergeConflictEditor CreateBuiltinMergeViewModel()
public async Task MergeExternalAsync()
{
return new MergeConflictEditor(_repo, _change.Path);
if (CanMerge)
await _wc.UseExternalMergeToolAsync(_change);
}
private Repository _repo = null;

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
@@ -52,8 +51,6 @@ namespace SourceGit.ViewModels
public class MergeConflictEditor : ObservableObject
{
public string Title => App.Text("MergeConflictEditor.Title", _filePath);
public string FilePath
{
get => _filePath;
@@ -566,96 +563,6 @@ namespace SourceGit.ViewModels
ResultDiffLines = resultLines;
}
public void AcceptOurs()
{
if (_conflictRegions.Count == 0)
return;
bool anyResolved = false;
foreach (var region in _conflictRegions)
{
if (!region.IsResolved)
{
region.ResolvedContent = new List<string>(region.OursContent);
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseOurs;
anyResolved = true;
}
}
if (anyResolved)
{
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
}
public void AcceptTheirs()
{
if (_conflictRegions.Count == 0)
return;
bool anyResolved = false;
foreach (var region in _conflictRegions)
{
if (!region.IsResolved)
{
region.ResolvedContent = new List<string>(region.TheirsContent);
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseTheirs;
anyResolved = true;
}
}
if (anyResolved)
{
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
}
public void AcceptCurrentOurs()
{
if (_currentConflictIndex < 0 || _currentConflictIndex >= _conflictRegions.Count)
return;
var region = _conflictRegions[_currentConflictIndex];
if (region.IsResolved)
return;
region.ResolvedContent = new List<string>(region.OursContent);
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseOurs;
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
public void AcceptCurrentTheirs()
{
if (_currentConflictIndex < 0 || _currentConflictIndex >= _conflictRegions.Count)
return;
var region = _conflictRegions[_currentConflictIndex];
if (region.IsResolved)
return;
region.ResolvedContent = new List<string>(region.TheirsContent);
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseTheirs;
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
public IReadOnlyList<ConflictRegion> GetConflictRegions() => _conflictRegions;
public void AcceptOursAtIndex(int conflictIndex)
@@ -671,10 +578,10 @@ namespace SourceGit.ViewModels
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseOurs;
IsModified = true;
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
public void AcceptTheirsAtIndex(int conflictIndex)
@@ -690,66 +597,10 @@ namespace SourceGit.ViewModels
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseTheirs;
IsModified = true;
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
public void AcceptBothMineFirst()
{
if (_conflictRegions.Count == 0)
return;
bool anyResolved = false;
foreach (var region in _conflictRegions)
{
if (!region.IsResolved)
{
var combined = new List<string>(region.OursContent);
combined.AddRange(region.TheirsContent);
region.ResolvedContent = combined;
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseBothMineFirst;
anyResolved = true;
}
}
if (anyResolved)
{
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
}
public void AcceptBothTheirsFirst()
{
if (_conflictRegions.Count == 0)
return;
bool anyResolved = false;
foreach (var region in _conflictRegions)
{
if (!region.IsResolved)
{
var combined = new List<string>(region.TheirsContent);
combined.AddRange(region.OursContent);
region.ResolvedContent = combined;
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseBothTheirsFirst;
anyResolved = true;
}
}
if (anyResolved)
{
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
}
public void AcceptBothMineFirstAtIndex(int conflictIndex)
@@ -767,10 +618,10 @@ namespace SourceGit.ViewModels
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseBothMineFirst;
IsModified = true;
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
public void AcceptBothTheirsFirstAtIndex(int conflictIndex)
@@ -788,10 +639,10 @@ namespace SourceGit.ViewModels
region.IsResolved = true;
region.ResolutionType = Models.ConflictResolution.UseBothTheirsFirst;
IsModified = true;
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
public void UndoResolutionAtIndex(int conflictIndex)
@@ -807,10 +658,10 @@ namespace SourceGit.ViewModels
region.IsResolved = false;
region.ResolutionType = Models.ConflictResolution.None;
IsModified = true;
RebuildResultContent();
BuildAlignedResultPanel();
UpdateConflictInfo();
IsModified = true;
}
public void GotoPrevConflict()
@@ -910,7 +761,6 @@ namespace SourceGit.ViewModels
if (string.IsNullOrEmpty(_resultContent))
{
UnresolvedConflictCount = 0;
_totalConflicts = 0;
CurrentConflictIndex = -1;
UpdateResolvedRanges();
return;
@@ -918,14 +768,16 @@ namespace SourceGit.ViewModels
// Count unresolved conflicts in current content
var markers = Commands.QueryConflictContent.GetConflictMarkers(_resultContent);
var conflictStarts = markers.Where(m => m.Type == Models.ConflictMarkerType.Start).ToList();
var conflictStarts = new List<Models.ConflictMarkerInfo>();
foreach (var m in markers)
{
if (m.Type == Models.ConflictMarkerType.Start)
conflictStarts.Add(m);
}
int unresolvedCount = conflictStarts.Count;
UnresolvedConflictCount = unresolvedCount;
// Total conflicts is the original count (never changes)
_totalConflicts = _conflictRegions.Count;
// Mark which original conflicts are resolved
// A conflict is resolved if its start marker no longer exists in _resultContent
var currentLines = _resultContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
@@ -952,8 +804,20 @@ namespace SourceGit.ViewModels
j++;
}
if (currentOurs.Count == region.OursContent.Count &&
currentOurs.SequenceEqual(region.OursContent))
if (currentOurs.Count != region.OursContent.Count)
continue;
var allEquals = true;
for (var k = 0; k < currentOurs.Count; k++)
{
if (!currentOurs[k].Equals(region.OursContent[k], StringComparison.Ordinal))
{
allEquals = false;
break;
}
}
if (allEquals)
{
region.IsResolved = false;
break;
@@ -1127,7 +991,6 @@ namespace SourceGit.ViewModels
private int _currentConflictLine = -1;
private int _currentConflictStartLine = -1;
private int _currentConflictEndLine = -1;
private int _totalConflicts = 0;
private List<Models.TextDiffLine> _oursDiffLines = [];
private List<Models.TextDiffLine> _theirsDiffLines = [];
private List<Models.TextDiffLine> _resultDiffLines = [];

View File

@@ -124,26 +124,82 @@
<TextBlock Margin="6,0,0,0" Text="{DynamicResource Text.WorkingCopy.Conflicts.UseMine}" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Classes="flat primary"
Margin="8,0,0,0"
Click="OnOpenBuiltinMergeTool"
IsVisible="{Binding CanUseBuiltinMergeTool}"
HotKey="{OnPlatform Ctrl+Shift+M, macOS=⌘+Shift+M}">
<SplitButton Height="28"
Margin="8,0,0,0" Padding="8,0"
Click="OnMerge"
IsVisible="{Binding CanMerge, Mode=OneWay}">
<SplitButton.Styles>
<Style Selector="SplitButton">
<Setter Property="MinHeight" Value="24"/>
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,1,Auto">
<Button x:Name="PART_PrimaryButton"
Grid.Column="0"
Classes="flat primary"
MinWidth="32"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}"
CornerRadius="3,0,0,3"
Padding="{TemplateBinding Padding}"
Focusable="False"
KeyboardNavigation.IsTabStop="False" />
<Button x:Name="PART_SecondaryButton"
Grid.Column="2"
Classes="flat primary"
Width="32"
CornerRadius="0,3,3,0"
Padding="0"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
KeyboardNavigation.IsTabStop="False">
<Path Height="12" Width="12"
Margin="0,4,0,0"
Fill="{DynamicResource AccentButtonForeground}"
Data="{DynamicResource Icons.Down}"/>
</Button>
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^:disabled /template/ Button">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border2}"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="^:disabled TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG2}"/>
</Style>
<Style Selector="^:disabled Path">
<Setter Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</Style>
</Style>
</SplitButton.Styles>
<SplitButton.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="{DynamicResource Text.WorkingCopy.Conflicts.MergeExternal}"
Click="OnMergeExternal">
<MenuItem.Icon>
<Path Width="12" Height="12" Data="{StaticResource Icons.OpenWith}"/>
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</SplitButton.Flyout>
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Conflict}"/>
<TextBlock Margin="6,0,0,0" Text="{DynamicResource Text.WorkingCopy.Conflicts.OpenBuiltinMergeTool}" VerticalAlignment="Center"/>
<TextBlock Margin="6,0,0,0" Text="{DynamicResource Text.WorkingCopy.Conflicts.Merge}"/>
</StackPanel>
</Button>
<Button Classes="flat"
Margin="8,0,0,0"
Click="OnOpenExternalMergeTool"
IsVisible="{Binding CanUseExternalMergeTool}"
HotKey="{OnPlatform Ctrl+Shift+D, macOS=⌘+Shift+D}">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.OpenWith}"/>
<TextBlock Margin="6,0,0,0" Text="{DynamicResource Text.WorkingCopy.Conflicts.OpenExternalMergeTool}" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</SplitButton>
</StackPanel>
</StackPanel>

View File

@@ -37,25 +37,18 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void OnOpenExternalMergeTool(object _, RoutedEventArgs e)
private async void OnMerge(object _, RoutedEventArgs e)
{
if (DataContext is ViewModels.Conflict vm)
await vm.OpenExternalMergeToolAsync();
await vm.MergeAsync();
e.Handled = true;
}
private async void OnOpenBuiltinMergeTool(object _, RoutedEventArgs e)
private async void OnMergeExternal(object _, RoutedEventArgs e)
{
if (DataContext is ViewModels.Conflict vm)
{
var mergeVm = vm.CreateBuiltinMergeViewModel();
await mergeVm.LoadAsync();
var window = TopLevel.GetTopLevel(this) as Window;
var mergeWindow = new MergeConflictEditor { DataContext = mergeVm };
await mergeWindow.ShowDialog(window);
}
await vm.MergeExternalAsync();
e.Handled = true;
}

View File

@@ -9,9 +9,9 @@
x:DataType="vm:MergeConflictEditor"
x:Name="ThisControl"
Icon="/App.ico"
Title="{Binding Title}"
Title="{DynamicResource Text.MergeConflictEditor.Title}"
MinWidth="1024" MinHeight="600">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid RowDefinitions="Auto,32,*,Auto">
<!-- TitleBar -->
<Grid Grid.Row="0" Height="28" IsVisible="{Binding !#ThisControl.UseSystemWindowFrame}">
<Border Background="{DynamicResource Brush.TitleBar}"
@@ -26,7 +26,7 @@
IsVisible="{OnPlatform True, macOS=False}"/>
<TextBlock Classes="bold"
Text="{Binding Title}"
Text="{DynamicResource Text.MergeConflictEditor.Title}"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsHitTestVisible="False"/>
@@ -34,163 +34,76 @@
</Grid>
<!-- Toolbar -->
<Border Grid.Row="1" Padding="8" Background="{DynamicResource Brush.ToolBar}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border2}">
<Border Grid.Row="1" Padding="8,0" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border2}">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- File Path -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Path Width="14" Height="14" Data="{StaticResource Icons.File}"/>
<TextBlock Margin="8,0,0,0" Text="{Binding FilePath}" VerticalAlignment="Center"/>
<TextBlock Margin="8" Text="{Binding FilePath}" VerticalAlignment="Center"/>
<v:LoadingIcon Width="14" Height="14" IsVisible="{Binding IsLoading}"/>
</StackPanel>
<!-- Navigation and Actions -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<!-- Navigation -->
<Button Classes="icon_button"
Width="24"
ToolTip.Tip="{DynamicResource Text.MergeConflictEditor.PrevConflict}"
Click="OnGotoPrevConflict"
IsEnabled="{Binding HasPrevConflict}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Up}" Margin="0,6,0,0"/>
<Path Width="12" Height="12" Margin="0,6,0,0" Data="{StaticResource Icons.Up}"/>
</Button>
<Button Classes="icon_button"
Width="24"
ToolTip.Tip="{DynamicResource Text.MergeConflictEditor.NextConflict}"
Click="OnGotoNextConflict"
IsEnabled="{Binding HasNextConflict}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Down}" Margin="0,6,0,0"/>
<Path Width="12" Height="12" Margin="0,6,0,0" Data="{StaticResource Icons.Down}"/>
</Button>
<!-- Info Bar -->
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center"/>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" Margin="4,4"/>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" Margin="4,6"/>
<!-- Save -->
<Button Classes="flat primary"
Margin="0,2" Padding="6,3"
Content="{DynamicResource Text.MergeConflictEditor.SaveAndStage}"
Click="OnSaveAndStage"
IsEnabled="{Binding CanSave}"/>
IsEnabled="{Binding CanSave, Mode=OneWay}"/>
</StackPanel>
</Grid>
</Border>
<!-- Main Content -->
<Grid Grid.Row="2">
<!-- Loading Indicator -->
<v:LoadingIcon Width="48" Height="48" IsVisible="{Binding IsLoading}"/>
<!-- Editor Panels -->
<Grid RowDefinitions="*,*" IsVisible="{Binding !IsLoading}">
<!-- Mine and Theirs Panels (Side-by-Side) -->
<Grid Grid.Row="0" ColumnDefinitions="*,*" Margin="4,4,4,2">
<!-- Mine (Ours) Panel -->
<Border Grid.Column="0" Margin="0,0,2,0">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.Diff.MineHeader}">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Mine}"
Foreground="White"
FontWeight="Bold"/>
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="OursPresenter"
PanelType="Mine"
DiffLines="{Binding OursDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
IsOldSide="True"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.AddedBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.MineBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.AddedHighlight}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.MineBG}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"/>
<Border x:Name="MinePopup"
IsVisible="False"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Background="{DynamicResource Brush.ToolBar}"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"
CornerRadius="4"
Padding="4,2"
BoxShadow="0 2 8 0 #40000000">
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseMine}"
Click="OnUseMineFromHover"/>
</Border>
</Grid>
</Grid>
</Border>
<!-- Theirs Panel -->
<Border Grid.Column="1" Margin="2,0,0,0">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.Diff.TheirsHeader}">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Theirs}"
Foreground="White"
FontWeight="Bold"/>
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="TheirsPresenter"
PanelType="Theirs"
DiffLines="{Binding TheirsDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
IsOldSide="False"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.TheirsBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.TheirsBG}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"/>
<Border x:Name="TheirsPopup"
IsVisible="False"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Background="{DynamicResource Brush.ToolBar}"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"
CornerRadius="4"
Padding="4,2"
BoxShadow="0 2 8 0 #40000000">
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseTheirs}"
Click="OnUseTheirsFromHover"/>
</Border>
</Grid>
</Grid>
</Border>
</Grid>
<!-- Result Panel -->
<Border Grid.Row="1" Margin="4,2,4,4">
<Grid Grid.Row="2" RowDefinitions="*,*">
<!-- Mine and Theirs Panels (Side-by-Side) -->
<Grid Grid.Row="0" ColumnDefinitions="*,*" Margin="4,4,4,2">
<!-- Mine (Ours) Panel -->
<Border Grid.Column="0" Margin="0,0,2,0">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.FG1}">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Result}"
Foreground="{DynamicResource Brush.Window}"
FontWeight="Bold"/>
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.Diff.MineHeader}">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Mine}"
Foreground="White"
FontWeight="Bold"/>
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="ResultPresenter"
PanelType="Result"
DiffLines="{Binding ResultDiffLines, Mode=OneWay}"
<v:MergeDiffPresenter x:Name="OursPresenter"
PanelType="Mine"
DiffLines="{Binding OursDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
IsOldSide="False"
IsResultPanel="True"
IsOldSide="True"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.AddedBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.MineBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.AddedHighlight}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}"
IndicatorBackground="{DynamicResource Brush.Diff.EmptyBG}"
MineContentBackground="{DynamicResource Brush.Diff.MineBG}"
TheirsContentBackground="{DynamicResource Brush.Diff.TheirsBG}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.MineBG}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"/>
<Border x:Name="ResultPopup"
BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}"/>
<Border x:Name="MinePopup"
IsVisible="False"
VerticalAlignment="Top"
HorizontalAlignment="Right"
@@ -200,42 +113,39 @@
CornerRadius="4"
Padding="4,2"
BoxShadow="0 2 8 0 #40000000">
<StackPanel Orientation="Horizontal" Spacing="4">
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseMine}"
Click="OnUseMineFromHover"/>
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseTheirs}"
Click="OnUseTheirsFromHover"/>
<Button Classes="flat primary">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.UseBoth}"/>
<Path Width="8" Height="8" Data="{StaticResource Icons.Down}" VerticalAlignment="Center"/>
</StackPanel>
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Click="OnUseBothMineFirstFromHover">
<MenuItem.Icon>
<Path Width="12" Height="12" Data="{StaticResource Icons.February}" Fill="{DynamicResource Brush.Diff.MineHeader}"/>
</MenuItem.Icon>
<MenuItem.Header>
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.AcceptBoth.MineFirst}"/>
</MenuItem.Header>
</MenuItem>
<MenuItem Click="OnUseBothTheirsFirstFromHover">
<MenuItem.Icon>
<Path Width="12" Height="12" Data="{StaticResource Icons.February}" Fill="{DynamicResource Brush.Diff.TheirsHeader}"/>
</MenuItem.Icon>
<MenuItem.Header>
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.AcceptBoth.TheirsFirst}"/>
</MenuItem.Header>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseMine}"
Click="OnUseMine"/>
</Border>
<Border x:Name="ResultUndoPopup"
</Grid>
</Grid>
</Border>
<!-- Theirs Panel -->
<Border Grid.Column="1" Margin="2,0,0,0">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.Diff.TheirsHeader}">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Theirs}"
Foreground="White"
FontWeight="Bold"/>
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="TheirsPresenter"
PanelType="Theirs"
DiffLines="{Binding TheirsDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
IsOldSide="False"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.TheirsBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.TheirsBG}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}"/>
<Border x:Name="TheirsPopup"
IsVisible="False"
VerticalAlignment="Top"
HorizontalAlignment="Right"
@@ -245,16 +155,105 @@
CornerRadius="4"
Padding="4,2"
BoxShadow="0 2 8 0 #40000000">
<Button Classes="flat"
Content="{DynamicResource Text.MergeConflictEditor.Undo}"
Click="OnUndoResolution"/>
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseTheirs}"
Click="OnUseTheirs"/>
</Border>
</Grid>
</Grid>
</Border>
</Grid>
</Grid>
<!-- Result Panel -->
<Border Grid.Row="1" Margin="4,2,4,4">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="8,4" Background="{DynamicResource Brush.FG1}">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.Result}"
Foreground="{DynamicResource Brush.Window}"
FontWeight="Bold"/>
</Border>
<Grid Grid.Row="1">
<v:MergeDiffPresenter x:Name="ResultPresenter"
PanelType="Result"
DiffLines="{Binding ResultDiffLines, Mode=OneWay}"
MaxLineNumber="{Binding DiffMaxLineNumber}"
FileName="{Binding FilePath}"
IsOldSide="False"
IsResultPanel="True"
FontFamily="{DynamicResource Fonts.Monospace}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.AddedBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.AddedHighlight}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}"
IndicatorBackground="{DynamicResource Brush.Diff.EmptyBG}"
MineContentBackground="{DynamicResource Brush.Diff.MineBG}"
TheirsContentBackground="{DynamicResource Brush.Diff.TheirsBG}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}"/>
<Border x:Name="ResultPopup"
IsVisible="False"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Background="{DynamicResource Brush.ToolBar}"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"
CornerRadius="4"
Padding="4,2"
BoxShadow="0 2 8 0 #40000000">
<StackPanel Orientation="Horizontal" Spacing="4">
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseMine}"
Click="OnUseMine"/>
<Button Classes="flat primary"
Content="{DynamicResource Text.MergeConflictEditor.UseTheirs}"
Click="OnUseTheirs"/>
<Button Classes="flat primary">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.UseBoth}"/>
<Path Width="12" Height="12" Margin="4,4,0,0" Data="{StaticResource Icons.Down}"/>
</StackPanel>
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Click="OnUseBothMineFirst">
<MenuItem.Icon>
<Path Width="12" Height="12" Data="{StaticResource Icons.February}" Fill="{DynamicResource Brush.Diff.MineHeader}"/>
</MenuItem.Icon>
<MenuItem.Header>
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.AcceptBoth.MineFirst}"/>
</MenuItem.Header>
</MenuItem>
<MenuItem Click="OnUseBothTheirsFirst">
<MenuItem.Icon>
<Path Width="12" Height="12" Data="{StaticResource Icons.February}" Fill="{DynamicResource Brush.Diff.TheirsHeader}"/>
</MenuItem.Icon>
<MenuItem.Header>
<TextBlock Text="{DynamicResource Text.MergeConflictEditor.AcceptBoth.TheirsFirst}"/>
</MenuItem.Header>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
</Border>
<Border x:Name="ResultUndoPopup"
IsVisible="False"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Background="{DynamicResource Brush.ToolBar}"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"
CornerRadius="4"
Padding="4,2"
BoxShadow="0 2 8 0 #40000000">
<Button Classes="flat"
Content="{DynamicResource Text.MergeConflictEditor.Undo}"
Click="OnUndoResolution"/>
</Border>
</Grid>
</Grid>
</Border>
</Grid>
</Grid>
</v:ChromelessWindow>

View File

@@ -2,14 +2,15 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.VisualTree;
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
@@ -188,11 +189,23 @@ namespace SourceGit.Views
TextArea.TextView.Margin = new Thickness(4, 0);
TextArea.LeftMargins.Add(new MergeDiffLineNumberMargin(this));
TextArea.LeftMargins.Add(new MergeDiffVerticalSeparatorMargin(this));
TextArea.LeftMargins.Add(new MergeDiffVerticalSeparatorMargin());
TextArea.TextView.BackgroundRenderers.Add(new MergeDiffLineBackgroundRenderer(this));
TextArea.TextView.LineTransformers.Add(new MergeDiffIndicatorTransformer(this));
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged += OnTextViewScrollChanged;
_scrollViewer.Bind(ScrollViewer.OffsetProperty, new Binding("ScrollOffset", BindingMode.OneWay));
}
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
@@ -208,16 +221,8 @@ namespace SourceGit.Views
TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged;
}
public ScrollViewer GetScrollViewer()
{
_scrollViewer ??= this.FindDescendantOfType<ScrollViewer>();
return _scrollViewer;
}
protected override void OnUnloaded(RoutedEventArgs e)
{
_scrollViewer = null;
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
TextArea.TextView.PointerMoved -= OnTextViewPointerMoved;
TextArea.TextView.PointerExited -= OnTextViewPointerExited;
@@ -310,8 +315,7 @@ namespace SourceGit.Views
private void OnTextViewPointerMoved(object sender, PointerEventArgs e)
{
var window = this.FindAncestorOfType<MergeConflictEditor>();
if (window?.DataContext is not ViewModels.MergeConflictEditor vm)
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
if (vm.IsLoading)
@@ -430,8 +434,7 @@ namespace SourceGit.Views
private void OnTextViewPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
var window = this.FindAncestorOfType<MergeConflictEditor>();
if (window?.DataContext is not ViewModels.MergeConflictEditor vm)
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
if (vm.SelectedChunk == null || vm.SelectedChunk.Panel != PanelType)
@@ -443,8 +446,7 @@ namespace SourceGit.Views
private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
{
var window = this.FindAncestorOfType<MergeConflictEditor>();
if (window?.DataContext is not ViewModels.MergeConflictEditor vm)
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
if (vm.SelectedChunk == null || vm.SelectedChunk.Panel != PanelType)
@@ -454,6 +456,23 @@ namespace SourceGit.Views
UpdateSelectedChunkPosition(vm);
}
private void OnTextViewScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_scrollViewer == null || DataContext is not ViewModels.MergeConflictEditor vm)
return;
if (vm.ScrollOffset.NearlyEquals(_scrollViewer.Offset))
return;
if (IsPointerOver || e.OffsetDelta.SquaredLength > 1.0f)
{
vm.ScrollOffset = _scrollViewer.Offset;
if (!TextArea.TextView.IsPointerOver)
vm.SelectedChunk = null;
}
}
private void UpdateSelectedChunkPosition(ViewModels.MergeConflictEditor vm)
{
var chunk = vm.SelectedChunk;
@@ -616,11 +635,6 @@ namespace SourceGit.Views
public class MergeDiffVerticalSeparatorMargin : AbstractMargin
{
public MergeDiffVerticalSeparatorMargin(MergeDiffPresenter presenter)
{
_presenter = presenter;
}
public override void Render(DrawingContext context)
{
var pen = new Pen(Brushes.DarkGray);
@@ -631,8 +645,6 @@ namespace SourceGit.Views
{
return new Size(1, 0);
}
private readonly MergeDiffPresenter _presenter;
}
public class MergeDiffIndicatorTransformer : DocumentColorizingTransformer
@@ -817,107 +829,93 @@ namespace SourceGit.Views
InitializeComponent();
}
protected override void OnOpened(EventArgs e)
protected override void OnDataContextChanged(EventArgs e)
{
base.OnOpened(e);
// Get presenter references
_oursPresenter = this.FindControl<MergeDiffPresenter>("OursPresenter");
_theirsPresenter = this.FindControl<MergeDiffPresenter>("TheirsPresenter");
_resultPresenter = this.FindControl<MergeDiffPresenter>("ResultPresenter");
// Get popup references
_minePopup = this.FindControl<Border>("MinePopup");
_theirsPopup = this.FindControl<Border>("TheirsPopup");
_resultPopup = this.FindControl<Border>("ResultPopup");
_resultUndoPopup = this.FindControl<Border>("ResultUndoPopup");
// Defer scroll sync setup to ensure ScrollViewers are available in the visual tree
Avalonia.Threading.Dispatcher.UIThread.Post(SetupScrollSync,
Avalonia.Threading.DispatcherPriority.Loaded);
base.OnDataContextChanged(e);
if (DataContext is ViewModels.MergeConflictEditor vm)
{
ScrollToCurrentConflict();
vm.PropertyChanged += OnViewModelPropertyChanged;
}
}
private void SetupScrollSync()
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
var oursScroll = _oursPresenter?.GetScrollViewer();
var theirsScroll = _theirsPresenter?.GetScrollViewer();
var resultScroll = _resultPresenter?.GetScrollViewer();
base.OnSizeChanged(e);
// Wheel events for scroll sync
if (_oursPresenter != null)
_oursPresenter.AddHandler(PointerWheelChangedEvent, OnPresenterPointerWheelChanged, RoutingStrategies.Tunnel);
if (_theirsPresenter != null)
_theirsPresenter.AddHandler(PointerWheelChangedEvent, OnPresenterPointerWheelChanged, RoutingStrategies.Tunnel);
if (_resultPresenter != null)
_resultPresenter.AddHandler(PointerWheelChangedEvent, OnPresenterPointerWheelChanged, RoutingStrategies.Tunnel);
// ScrollChanged for scrollbar drag sync
if (oursScroll != null)
oursScroll.ScrollChanged += OnScrollChanged;
if (theirsScroll != null)
theirsScroll.ScrollChanged += OnScrollChanged;
if (resultScroll != null)
resultScroll.ScrollChanged += OnScrollChanged;
}
private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_isSyncingScroll || sender is not ScrollViewer source)
return;
// Sync on any scroll change (scrollbar drag, programmatic, etc.)
SyncAllScrollViewers(source.Offset);
}
private void OnPresenterPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
var oursScroll = _oursPresenter?.GetScrollViewer();
var theirsScroll = _theirsPresenter?.GetScrollViewer();
var resultScroll = _resultPresenter?.GetScrollViewer();
var delta = e.Delta.Y * 50;
var currentOffset = oursScroll?.Offset ?? Vector.Zero;
var newOffset = new Vector(currentOffset.X, Math.Max(0, currentOffset.Y - delta));
SyncAllScrollViewers(newOffset);
e.Handled = true;
}
private void SyncAllScrollViewers(Vector offset)
{
if (_isSyncingScroll)
return;
_isSyncingScroll = true;
try
if (!_execSizeChanged)
{
var oursScroll = _oursPresenter?.GetScrollViewer();
var theirsScroll = _theirsPresenter?.GetScrollViewer();
var resultScroll = _resultPresenter?.GetScrollViewer();
_execSizeChanged = true;
ScrollToCurrentConflict();
}
}
// Direct offset assignment for immediate sync
if (oursScroll != null)
oursScroll.Offset = offset;
if (theirsScroll != null)
theirsScroll.Offset = offset;
if (resultScroll != null)
resultScroll.Offset = offset;
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
// Also update ViewModel for state tracking
if (e.Handled)
return;
var vm = DataContext as ViewModels.MergeConflictEditor;
if (vm == null)
return;
var modifier = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
if (e.KeyModifiers == modifier)
{
if (e.Key == Key.S && vm.CanSave)
{
OnSaveAndStage(null, null);
e.Handled = true;
}
else if (e.Key == Key.Up && vm.HasPrevConflict)
{
vm.GotoPrevConflict();
UpdateCurrentConflictHighlight();
ScrollToCurrentConflict();
e.Handled = true;
}
else if (e.Key == Key.Down && vm.HasNextConflict)
{
vm.GotoNextConflict();
UpdateCurrentConflictHighlight();
ScrollToCurrentConflict();
e.Handled = true;
}
}
}
protected override async void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
if (_forceClose)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
vm.ScrollOffset = offset;
vm.PropertyChanged -= OnViewModelPropertyChanged;
return;
}
finally
e.Cancel = true;
var result = await App.AskConfirmAsync(App.Text("MergeConflictEditor.UnsavedChanges"));
if (result)
{
_isSyncingScroll = false;
_forceClose = true;
Close();
}
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
GC.Collect();
}
private void OnViewModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewModels.MergeConflictEditor.IsLoading))
@@ -946,204 +944,6 @@ namespace SourceGit.Views
}
}
private void UpdateResolvedRanges()
{
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
var allRanges = vm.AllConflictRanges;
if (_oursPresenter != null)
_oursPresenter.AllConflictRanges = allRanges;
if (_theirsPresenter != null)
_theirsPresenter.AllConflictRanges = allRanges;
// Note: Result panel doesn't use conflict ranges since it shows current state
}
private void UpdateCurrentConflictHighlight()
{
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
var startLine = vm.CurrentConflictStartLine;
var endLine = vm.CurrentConflictEndLine;
if (_oursPresenter != null)
{
_oursPresenter.CurrentConflictStartLine = startLine;
_oursPresenter.CurrentConflictEndLine = endLine;
}
if (_theirsPresenter != null)
{
_theirsPresenter.CurrentConflictStartLine = startLine;
_theirsPresenter.CurrentConflictEndLine = endLine;
}
if (_resultPresenter != null)
{
_resultPresenter.CurrentConflictStartLine = startLine;
_resultPresenter.CurrentConflictEndLine = endLine;
}
}
private void UpdatePopupVisibility()
{
// Hide all popups first
if (_minePopup != null)
_minePopup.IsVisible = false;
if (_theirsPopup != null)
_theirsPopup.IsVisible = false;
if (_resultPopup != null)
_resultPopup.IsVisible = false;
if (_resultUndoPopup != null)
_resultUndoPopup.IsVisible = false;
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
var chunk = vm.SelectedChunk;
if (chunk == null)
return;
// Get the presenter for bounds checking
MergeDiffPresenter presenter = chunk.Panel switch
{
ViewModels.MergeConflictPanelType.Mine => _oursPresenter,
ViewModels.MergeConflictPanelType.Theirs => _theirsPresenter,
ViewModels.MergeConflictPanelType.Result => _resultPresenter,
_ => null
};
// Show the appropriate popup based on panel type and resolved state
Border popup;
if (chunk.Panel == ViewModels.MergeConflictPanelType.Result && chunk.IsResolved)
{
// Show Undo popup for resolved conflicts in Result panel
popup = _resultUndoPopup;
}
else
{
popup = chunk.Panel switch
{
ViewModels.MergeConflictPanelType.Mine => _minePopup,
ViewModels.MergeConflictPanelType.Theirs => _theirsPopup,
ViewModels.MergeConflictPanelType.Result => _resultPopup,
_ => null
};
}
if (popup != null && presenter != null)
{
// Position popup - clamp to visible area
var top = chunk.Y + (chunk.Height >= 36 ? 8 : 2);
// Clamp top to ensure popup is visible
var popupHeight = popup.Bounds.Height > 0 ? popup.Bounds.Height : 32;
var presenterHeight = presenter.Bounds.Height;
top = Math.Max(4, Math.Min(top, presenterHeight - popupHeight - 4));
popup.Margin = new Thickness(0, top, 24, 0);
popup.IsVisible = true;
}
}
private void OnUseMineFromHover(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
var savedOffset = SaveScrollOffset();
vm.AcceptOursAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
vm.SelectedChunk = null;
}
e.Handled = true;
}
private void OnUseTheirsFromHover(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
var savedOffset = SaveScrollOffset();
vm.AcceptTheirsAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
vm.SelectedChunk = null;
}
e.Handled = true;
}
private void OnUndoResolution(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
var savedOffset = SaveScrollOffset();
vm.UndoResolutionAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
vm.SelectedChunk = null;
}
e.Handled = true;
}
protected override async void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
if (_forceClose)
return;
if (DataContext is ViewModels.MergeConflictEditor vm && vm.HasUnsavedChanges())
{
e.Cancel = true;
var result = await App.AskConfirmAsync(App.Text("MergeConflictEditor.UnsavedChanges"));
if (result)
{
_forceClose = true;
Close();
}
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Handled)
return;
var vm = DataContext as ViewModels.MergeConflictEditor;
if (vm == null)
return;
var modifier = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
if (e.KeyModifiers == modifier)
{
if (e.Key == Key.S && vm.CanSave)
{
_ = SaveAndCloseAsync();
e.Handled = true;
}
else if (e.Key == Key.Up && vm.HasPrevConflict)
{
vm.GotoPrevConflict();
UpdateCurrentConflictHighlight();
ScrollToCurrentConflict();
e.Handled = true;
}
else if (e.Key == Key.Down && vm.HasNextConflict)
{
vm.GotoNextConflict();
UpdateCurrentConflictHighlight();
ScrollToCurrentConflict();
e.Handled = true;
}
}
}
private void OnGotoPrevConflict(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.HasPrevConflict)
@@ -1166,135 +966,77 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnUseCurrentMine(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
{
var savedOffset = SaveScrollOffset();
vm.AcceptCurrentOurs();
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
}
e.Handled = true;
}
private void OnUseCurrentTheirs(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
{
var savedOffset = SaveScrollOffset();
vm.AcceptCurrentTheirs();
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
}
e.Handled = true;
}
private void OnAcceptMine(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
{
var savedOffset = SaveScrollOffset();
vm.AcceptOurs();
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
}
e.Handled = true;
}
private void OnAcceptTheirs(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
{
var savedOffset = SaveScrollOffset();
vm.AcceptTheirs();
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
}
e.Handled = true;
}
private void OnAcceptBothMineFirst(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
{
var savedOffset = SaveScrollOffset();
vm.AcceptBothMineFirst();
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
}
e.Handled = true;
}
private void OnAcceptBothTheirsFirst(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
{
var savedOffset = SaveScrollOffset();
vm.AcceptBothTheirsFirst();
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
}
e.Handled = true;
}
private void OnUseBothMineFirstFromHover(object sender, RoutedEventArgs e)
private void OnUseMine(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
var savedOffset = SaveScrollOffset();
var savedOffset = vm.ScrollOffset;
vm.AcceptOursAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
vm.SelectedChunk = null;
vm.ScrollOffset = savedOffset;
}
e.Handled = true;
}
private void OnUseTheirs(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
var savedOffset = vm.ScrollOffset;
vm.AcceptTheirsAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
vm.SelectedChunk = null;
vm.ScrollOffset = savedOffset;
}
e.Handled = true;
}
private void OnUseBothMineFirst(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
var savedOffset = vm.ScrollOffset;
vm.AcceptBothMineFirstAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
vm.SelectedChunk = null;
vm.ScrollOffset = savedOffset;
}
e.Handled = true;
}
private void OnUseBothTheirsFirstFromHover(object sender, RoutedEventArgs e)
private void OnUseBothTheirsFirst(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
var savedOffset = SaveScrollOffset();
var savedOffset = vm.ScrollOffset;
vm.AcceptBothTheirsFirstAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
RestoreScrollOffset(savedOffset);
vm.SelectedChunk = null;
vm.ScrollOffset = savedOffset;
}
e.Handled = true;
}
private Vector SaveScrollOffset()
private void OnUndoResolution(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
return vm.ScrollOffset;
return new Vector(0, 0);
}
private void RestoreScrollOffset(Vector offset)
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
if (DataContext is ViewModels.MergeConflictEditor vm && vm.SelectedChunk is { } chunk)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
vm.ScrollOffset = offset;
}, Avalonia.Threading.DispatcherPriority.Loaded);
}
private async void OnSaveAndStage(object sender, RoutedEventArgs e)
{
await SaveAndCloseAsync();
var savedOffset = vm.ScrollOffset;
vm.UndoResolutionAtIndex(chunk.ConflictIndex);
UpdateCurrentConflictHighlight();
UpdateResolvedRanges();
vm.SelectedChunk = null;
vm.ScrollOffset = savedOffset;
}
e.Handled = true;
}
private async Task SaveAndCloseAsync()
private async void OnSaveAndStage(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MergeConflictEditor vm)
{
@@ -1309,50 +1051,97 @@ namespace SourceGit.Views
private void ScrollToCurrentConflict()
{
if (DataContext is ViewModels.MergeConflictEditor vm && vm.CurrentConflictLine >= 0)
if (IsLoaded && DataContext is ViewModels.MergeConflictEditor vm && vm.CurrentConflictLine >= 0)
{
if (_oursPresenter != null)
{
var lineHeight = _oursPresenter.TextArea.TextView.DefaultLineHeight;
var vOffset = lineHeight * vm.CurrentConflictLine;
var targetOffset = new Vector(0, Math.Max(0, vOffset - _oursPresenter.Bounds.Height * 0.3));
SyncAllScrollViewers(targetOffset);
}
var lineHeight = OursPresenter.TextArea.TextView.DefaultLineHeight;
var vOffset = lineHeight * vm.CurrentConflictLine;
vm.ScrollOffset = new Vector(0, Math.Max(0, vOffset - OursPresenter.Bounds.Height * 0.3));
}
}
protected override void OnClosed(EventArgs e)
private void UpdateResolvedRanges()
{
var oursScroll = _oursPresenter?.GetScrollViewer();
var theirsScroll = _theirsPresenter?.GetScrollViewer();
var resultScroll = _resultPresenter?.GetScrollViewer();
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
if (_oursPresenter != null)
_oursPresenter.RemoveHandler(PointerWheelChangedEvent, OnPresenterPointerWheelChanged);
if (_theirsPresenter != null)
_theirsPresenter.RemoveHandler(PointerWheelChangedEvent, OnPresenterPointerWheelChanged);
if (_resultPresenter != null)
_resultPresenter.RemoveHandler(PointerWheelChangedEvent, OnPresenterPointerWheelChanged);
var allRanges = vm.AllConflictRanges;
OursPresenter.AllConflictRanges = allRanges;
TheirsPresenter.AllConflictRanges = allRanges;
}
if (oursScroll != null)
oursScroll.ScrollChanged -= OnScrollChanged;
if (theirsScroll != null)
theirsScroll.ScrollChanged -= OnScrollChanged;
if (resultScroll != null)
resultScroll.ScrollChanged -= OnScrollChanged;
private void UpdateCurrentConflictHighlight()
{
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
base.OnClosed(e);
GC.Collect();
var startLine = vm.CurrentConflictStartLine;
var endLine = vm.CurrentConflictEndLine;
OursPresenter.CurrentConflictStartLine = startLine;
OursPresenter.CurrentConflictEndLine = endLine;
TheirsPresenter.CurrentConflictStartLine = startLine;
TheirsPresenter.CurrentConflictEndLine = endLine;
ResultPresenter.CurrentConflictStartLine = startLine;
ResultPresenter.CurrentConflictEndLine = endLine;
}
private void UpdatePopupVisibility()
{
// Hide all popups first
MinePopup.IsVisible = false;
TheirsPopup.IsVisible = false;
ResultPopup.IsVisible = false;
ResultUndoPopup.IsVisible = false;
if (DataContext is not ViewModels.MergeConflictEditor vm)
return;
var chunk = vm.SelectedChunk;
if (chunk == null)
return;
// Get the presenter for bounds checking
MergeDiffPresenter presenter = chunk.Panel switch
{
ViewModels.MergeConflictPanelType.Mine => OursPresenter,
ViewModels.MergeConflictPanelType.Theirs => TheirsPresenter,
ViewModels.MergeConflictPanelType.Result => ResultPresenter,
_ => null
};
// Show the appropriate popup based on panel type and resolved state
Border popup;
if (chunk.Panel == ViewModels.MergeConflictPanelType.Result && chunk.IsResolved)
{
// Show Undo popup for resolved conflicts in Result panel
popup = ResultUndoPopup;
}
else
{
popup = chunk.Panel switch
{
ViewModels.MergeConflictPanelType.Mine => MinePopup,
ViewModels.MergeConflictPanelType.Theirs => TheirsPopup,
ViewModels.MergeConflictPanelType.Result => ResultPopup,
_ => null
};
}
if (popup != null && presenter != null)
{
// Position popup - clamp to visible area
var top = chunk.Y + (chunk.Height >= 36 ? 8 : 2);
// Clamp top to ensure popup is visible
var popupHeight = popup.Bounds.Height > 0 ? popup.Bounds.Height : 32;
var presenterHeight = presenter.Bounds.Height;
top = Math.Max(4, Math.Min(top, presenterHeight - popupHeight - 4));
popup.Margin = new Thickness(0, top, 24, 0);
popup.IsVisible = true;
}
}
private bool _forceClose = false;
private bool _isSyncingScroll = false;
private MergeDiffPresenter _oursPresenter;
private MergeDiffPresenter _theirsPresenter;
private MergeDiffPresenter _resultPresenter;
private Border _minePopup;
private Border _theirsPopup;
private Border _resultPopup;
private Border _resultUndoPopup;
private bool _execSizeChanged = false;
}
}

View File

@@ -260,48 +260,22 @@ namespace SourceGit.Views
{
var change = selectedUnstaged[0];
var path = Native.OS.GetAbsPath(repo.FullPath, change.Path);
TryAddOpenFileToContextMenu(menu, path);
if (!change.IsConflicted || change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified)
if (!change.IsConflicted)
{
if (change.IsConflicted)
TryAddOpenFileToContextMenu(menu, path);
var diffWithMerger = new MenuItem();
diffWithMerger.Header = App.Text("OpenInExternalMergeTool");
diffWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
diffWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
diffWithMerger.Click += (_, ev) =>
{
var isBinary = new Commands.IsBinary(repo.FullPath, "HEAD", change.Path).GetResultAsync().GetAwaiter().GetResult();
if (!isBinary)
{
var openBuiltinMerger = new MenuItem();
openBuiltinMerger.Header = App.Text("OpenInBuiltinMergeTool");
openBuiltinMerger.Icon = App.CreateMenuIcon("Icons.Conflict");
openBuiltinMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+M" : "Ctrl+Shift+M";
openBuiltinMerger.Click += async (_, e) =>
{
var mergeVm = new ViewModels.MergeConflictEditor(repo, change.Path);
await mergeVm.LoadAsync();
var window = TopLevel.GetTopLevel(this) as Window;
var mergeWindow = new MergeConflictEditor { DataContext = mergeVm };
await mergeWindow.ShowDialog(window);
e.Handled = true;
};
menu.Items.Add(openBuiltinMerger);
}
}
var openMerger = new MenuItem();
openMerger.Header = App.Text("OpenInExternalMergeTool");
openMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
openMerger.Click += async (_, e) =>
{
if (change.IsConflicted)
await vm.UseExternalMergeToolAsync(change);
else
vm.UseExternalDiffTool(change, true);
e.Handled = true;
vm.UseExternalDiffTool(change, false);
ev.Handled = true;
};
menu.Items.Add(openMerger);
menu.Items.Add(diffWithMerger);
}
var explore = new MenuItem();
@@ -361,6 +335,36 @@ namespace SourceGit.Views
menu.Items.Add(useTheirs);
menu.Items.Add(useMine);
if (change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified)
{
var isBinary = new Commands.IsBinary(repo.FullPath, "HEAD", change.Path).GetResultAsync().GetAwaiter().GetResult();
if (!isBinary)
{
var mergeBuiltin = new MenuItem();
mergeBuiltin.Header = App.Text("ChangeCM.Merge");
mergeBuiltin.Icon = App.CreateMenuIcon("Icons.Conflict");
mergeBuiltin.Click += async (_, e) =>
{
var ctx = new ViewModels.MergeConflictEditor(repo, change.Path);
await ctx.LoadAsync();
await App.ShowDialog(ctx);
e.Handled = true;
};
menu.Items.Add(mergeBuiltin);
}
var mergeExternal = new MenuItem();
mergeExternal.Header = App.Text("ChangeCM.MergeExternal");
mergeExternal.Icon = App.CreateMenuIcon("Icons.OpenWith");
mergeExternal.Click += async (_, e) =>
{
await vm.UseExternalMergeToolAsync(change);
e.Handled = true;
};
menu.Items.Add(mergeExternal);
}
menu.Items.Add(new MenuItem() { Header = "-" });
}
else