feat(merge): in-app 3-way merge editor (chunk 2b)
Replace the whole-file conflict resolver with a real 3-way merge editor built on the line-level hunk pipeline. - ConflictModels: MergeFile/MergeFileSegment/MergeConflictBlock with Compose() that reassembles stable text + chosen resolutions - ConflictResolverViewModel (same seam contract): loads conflict documents, flattens conflicts for one-at-a-time navigation, per-block Accept Ours/Base/Theirs/Both + editable result, binary files block continue - ConflictResolverView: 3-column Base|Ours|Theirs + editable result via AvaloniaEdit with TextMate syntax highlighting by file extension; editors synced in code-behind - add Avalonia.AvaloniaEdit + AvaloniaEdit.TextMate + TextMateSharp.Grammars; AvaloniaEdit theme StyleInclude in App.axaml - rewrite ConflictResolverViewModel tests (load/gating/compose/nav/binary/abort)
This commit is contained in:
@@ -1,19 +1,102 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using Avalonia.Controls;
|
||||
using AvaloniaEdit;
|
||||
using AvaloniaEdit.TextMate;
|
||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||
using TextMateSharp.Grammars;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Conflicts;
|
||||
|
||||
public partial class ConflictResolverView : Window
|
||||
{
|
||||
private ConflictResolverViewModel? _vm;
|
||||
private RegistryOptions? _registry;
|
||||
private TextMate.Installation? _baseTm, _oursTm, _theirsTm, _resultTm;
|
||||
private MergeConflictBlock? _hooked;
|
||||
private bool _reloading;
|
||||
|
||||
public ConflictResolverView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(System.EventArgs e)
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is ConflictResolverViewModel vm)
|
||||
vm.CloseRequested = Close;
|
||||
|
||||
if (_vm is not null) _vm.CurrentChanged -= ReloadEditors;
|
||||
_vm = DataContext as ConflictResolverViewModel;
|
||||
if (_vm is null) return;
|
||||
|
||||
_vm.CloseRequested = Close;
|
||||
EnsureTextMate();
|
||||
_vm.CurrentChanged += ReloadEditors;
|
||||
ResultEditor.TextChanged += OnResultEditorChanged;
|
||||
ReloadEditors();
|
||||
}
|
||||
|
||||
private void EnsureTextMate()
|
||||
{
|
||||
if (_registry is not null) return;
|
||||
_registry = new RegistryOptions(ThemeName.DarkPlus);
|
||||
_baseTm = BaseEditor.InstallTextMate(_registry);
|
||||
_oursTm = OursEditor.InstallTextMate(_registry);
|
||||
_theirsTm = TheirsEditor.InstallTextMate(_registry);
|
||||
_resultTm = ResultEditor.InstallTextMate(_registry);
|
||||
}
|
||||
|
||||
private void ReloadEditors()
|
||||
{
|
||||
if (_vm is null) return;
|
||||
_reloading = true;
|
||||
try
|
||||
{
|
||||
if (_hooked is not null) _hooked.PropertyChanged -= OnCurrentResolutionChanged;
|
||||
_hooked = _vm.Current;
|
||||
if (_hooked is not null) _hooked.PropertyChanged += OnCurrentResolutionChanged;
|
||||
|
||||
BaseEditor.Text = _vm.Current?.Base ?? "";
|
||||
OursEditor.Text = _vm.Current?.Ours ?? "";
|
||||
TheirsEditor.Text = _vm.Current?.Theirs ?? "";
|
||||
ResultEditor.Text = _vm.Current?.Resolution ?? "";
|
||||
ApplyGrammar(_vm.CurrentPath);
|
||||
}
|
||||
finally { _reloading = false; }
|
||||
}
|
||||
|
||||
// User edits in the result editor flow back to the current conflict's resolution.
|
||||
private void OnResultEditorChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_reloading || _vm?.Current is null) return;
|
||||
_vm.Current.Resolution = ResultEditor.Text;
|
||||
}
|
||||
|
||||
// Accept-buttons set Resolution on the VM; mirror that into the result editor.
|
||||
private void OnCurrentResolutionChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_reloading || e.PropertyName != nameof(MergeConflictBlock.Resolution)) return;
|
||||
var resolved = _vm?.Current?.Resolution ?? "";
|
||||
if (ResultEditor.Text == resolved) return;
|
||||
_reloading = true;
|
||||
try { ResultEditor.Text = resolved; }
|
||||
finally { _reloading = false; }
|
||||
}
|
||||
|
||||
private void ApplyGrammar(string? path)
|
||||
{
|
||||
if (_registry is null || string.IsNullOrEmpty(path)) return;
|
||||
var ext = Path.GetExtension(path);
|
||||
if (string.IsNullOrEmpty(ext)) return;
|
||||
|
||||
var language = _registry.GetLanguageByExtension(ext);
|
||||
if (language is null) return;
|
||||
|
||||
var scope = _registry.GetScopeByLanguageId(language.Id);
|
||||
_baseTm?.SetGrammar(scope);
|
||||
_oursTm?.SetGrammar(scope);
|
||||
_theirsTm?.SetGrammar(scope);
|
||||
_resultTm?.SetGrammar(scope);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user