diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 5aa6b39c..41797703 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -358,8 +358,11 @@ "filesHeader": "Dateien", "binary": "Binärdatei — kein Text-Diff", "empty": "Kein Inhalt", + "unifiedView": "Zusammen", "splitView": "Nebeneinander", - "wrapLines": "Zeilenumbruch" + "wrapLines": "Zeilenumbruch", + "paneBase": "BASIS", + "paneWorktree": "WORKTREE" }, "worktreesOverview": { "refresh": "Aktualisieren", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 41213be1..c8f18116 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -358,8 +358,11 @@ "filesHeader": "Files", "binary": "Binary file — no text diff", "empty": "No content", + "unifiedView": "Unified", "splitView": "Side by side", - "wrapLines": "Wrap lines" + "wrapLines": "Wrap lines", + "paneBase": "BASE", + "paneWorktree": "WORKTREE" }, "worktreesOverview": { "refresh": "Refresh", diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml index 41d37329..c6870765 100644 --- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml +++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml @@ -108,6 +108,9 @@ M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.03 7.03 0 0 0-1.69-.98l-.38-2.65a.5.5 0 0 0-.5-.42h-4a.5.5 0 0 0-.5.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65z + + M3 5 H21 V7 H3 Z M3 11 H17 A4 4 0 0 1 17 19 H12 V21 L7 18 L12 15 V17 H17 A2 2 0 0 0 17 13 H3 Z + F0 M12 2 C7 2 4 5.5 4 10 C4 13.5 6 16 8 17.5 L8 19 C8 20 8.9 21 10 21 L10 18.5 L14 18.5 L14 21 C15.1 21 16 20 16 19 L16 17.5 C18 16 20 13.5 20 10 C20 5.5 17 2 12 2 Z M8.5 8 L8.5 12 L11 12 L11 8 Z M13 8 L13 12 L15.5 12 L15.5 8 Z @@ -323,8 +326,9 @@ - - - + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs index 21838697..b6670ebc 100644 --- a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs @@ -1,19 +1,14 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using Avalonia; using Avalonia.Controls; using Avalonia.Media; -using Avalonia.Threading; -using Avalonia.VisualTree; -using AvaloniaEdit; -using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using AvaloniaEdit.Rendering; using AvaloniaEdit.TextMate; using ClaudeDo.Ui.ViewModels.Modals; -using TextMateSharp.Grammars; +using Seg = ClaudeDo.Ui.Views.Controls.DiffEditorSetup.Seg; namespace ClaudeDo.Ui.Views.Controls; @@ -52,9 +47,6 @@ public partial class DiffTextView : UserControl set => SetValue(WrapLinesProperty, value); } - // Grammars and themes are process-wide; loading the registry per control would be wasteful. - private static readonly RegistryOptions Registry = new(ThemeName.DarkPlus); - private TextMate.Installation? _leftTm, _rightTm; // Row lookup per editor, indexed by document line number (1-based). Populated on rebuild @@ -66,18 +58,16 @@ public partial class DiffTextView : UserControl private DiffLineNumberMargin? _leftMargin, _rightMargin; - private ScrollViewer? _leftScroll, _rightScroll; - private bool _scrollHooked; - private bool _syncing; private bool _renderersInstalled; public DiffTextView() { InitializeComponent(); - _leftTm = LeftEditor.InstallTextMate(Registry); - _rightTm = RightEditor.InstallTextMate(Registry); + _leftTm = DiffEditorSetup.InstallHighlighting(LeftEditor); + _rightTm = DiffEditorSetup.InstallHighlighting(RightEditor); ReloadFile(); - HookScrollSync(); + new VerticalScrollSync(new[] { LeftEditor, RightEditor }, + isActive: () => IsSplit, anchorByLine: () => WrapLines); } /// Brushes and the mono typeface only resolve once the control is in the visual tree — @@ -121,6 +111,7 @@ public partial class DiffTextView : UserControl var split = IsSplit; RightEditor.IsVisible = split; PaneDivider.IsVisible = split; + PaneHeader.IsVisible = split; Grid.SetColumnSpan(LeftEditor, split ? 1 : 3); if (split) @@ -146,7 +137,7 @@ public partial class DiffTextView : UserControl RebuildMargins(); ApplyWrap(); - ApplyGrammar(File?.Path); + DiffEditorSetup.ApplyGrammar(File?.Path, _leftTm, _rightTm); InvalidateRenderers(); } @@ -171,21 +162,6 @@ public partial class DiffTextView : UserControl RightEditor.WordWrap = WrapLines; } - /// Only hunks are in the document, not whole files, so TextMate's line-by-line state can - /// be wrong at a fragment boundary (a line inside a block comment may highlight as code). - /// Accepted — every fragment-based diff viewer has this. - private void ApplyGrammar(string? path) - { - if (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); - _leftTm?.SetGrammar(scope); - _rightTm?.SetGrammar(scope); - } - /// The margin's column layout depends on split vs unified, so it is rebuilt rather than /// reconfigured whenever the layout changes. private void RebuildMargins() @@ -205,10 +181,7 @@ public partial class DiffTextView : UserControl RightEditor.TextArea.LeftMargins.Insert(0, _rightMargin); } - private IBrush Brush(string key, Color fallback) => - this.TryFindResource(key, out var value) && value is IBrush brush - ? brush - : new SolidColorBrush(fallback); + private IBrush Brush(string key, Color fallback) => DiffEditorSetup.Brush(this, key, fallback); private void InstallRenderers() { @@ -231,58 +204,6 @@ public partial class DiffTextView : UserControl RightEditor.TextArea.TextView.InvalidateVisual(); } - /// The editors' ScrollViewers only exist once the template has been applied. - private void HookScrollSync() - { - if (_scrollHooked) return; - _scrollHooked = true; - Dispatcher.UIThread.Post(() => - { - _leftScroll = LeftEditor.FindDescendantOfType(); - _rightScroll = RightEditor.FindDescendantOfType(); - if (_leftScroll is not null) _leftScroll.ScrollChanged += OnPaneScroll; - if (_rightScroll is not null) _rightScroll.ScrollChanged += OnPaneScroll; - }, DispatcherPriority.Loaded); - } - - private void OnPaneScroll(object? sender, ScrollChangedEventArgs e) - { - if (_syncing || !IsSplit || _leftScroll is null || _rightScroll is null) return; - var fromLeft = ReferenceEquals(sender, _leftScroll); - var source = fromLeft ? _leftScroll : _rightScroll; - var target = fromLeft ? _rightScroll : _leftScroll; - var targetEditor = fromLeft ? RightEditor : LeftEditor; - var sourceEditor = fromLeft ? LeftEditor : RightEditor; - - _syncing = true; - try - { - if (WrapLines) - { - // Line heights differ once lines wrap, so anchor on the top visible line. - // ScrollToLine/ScrollTo are "bring into view" primitives (they park the line - // near mid-viewport past a hysteresis threshold) — top-align by computing the - // target's own vertical offset for that document line and assigning it - // directly, the same way the non-wrap branch assigns Offset below. That is - // synchronous, so the same offset-comparison guard makes the echo self-terminate. - var topLine = sourceEditor.TextArea.TextView.VisualLines is { Count: > 0 } visualLines - ? visualLines[0].FirstDocumentLine.LineNumber - : (int?)null; - if (topLine is { } line && line >= 1 && line <= targetEditor.Document.LineCount) - { - var targetTop = targetEditor.TextArea.TextView.GetVisualTopByDocumentLine(line); - if (Math.Abs(target.Offset.Y - targetTop) > 0.5) - target.Offset = new Vector(target.Offset.X, targetTop); - } - } - else if (Math.Abs(target.Offset.Y - source.Offset.Y) > 0.5) - { - target.Offset = new Vector(target.Offset.X, source.Offset.Y); - } - } - finally { _syncing = false; } - } - /// What one document line represents, for the margin and the background renderers. internal sealed record RowInfo( AlignedSide Kind, int? OldNo, int? NewNo, IReadOnlyList Spans); @@ -428,13 +349,4 @@ public partial class DiffTextView : UserControl } } } - - /// A minimal for geometry queries. - private readonly struct Seg : ISegment - { - public Seg(int offset, int length) { Offset = offset; Length = length; } - public int Offset { get; } - public int Length { get; } - public int EndOffset => Offset + Length; - } } diff --git a/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml b/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml index 003cdaf2..89fbd4e1 100644 --- a/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml @@ -30,10 +30,24 @@ - + - - + + +