From 5d6481f59ea763ea110251b288e9f4b1808f2a61 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 1 Oct 2026 07:55:33 +0200 Subject: [PATCH] Fix live line number margin toggle AvaloniaEdit inserts the line number margin before ILSpy's bookmark gutter and can render it before its cached font size is initialized. Keep the gutter order stable after live toggles and prepare the margin before the first render. Assisted-by: OpenCode:openai/gpt-5.5:OpenCode --- ILSpy.Tests/Editor/MarginCursorTests.cs | 35 ++++++++++++++++++ ILSpy/TextView/DecompilerTextEditor.cs | 12 +++++++ ILSpy/TextView/DecompilerTextView.axaml.cs | 41 ++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/ILSpy.Tests/Editor/MarginCursorTests.cs b/ILSpy.Tests/Editor/MarginCursorTests.cs index 786898286..6ecec59ab 100644 --- a/ILSpy.Tests/Editor/MarginCursorTests.cs +++ b/ILSpy.Tests/Editor/MarginCursorTests.cs @@ -17,9 +17,11 @@ // DEALINGS IN THE SOFTWARE. using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Headless.NUnit; using AvaloniaEdit.Editing; @@ -75,4 +77,37 @@ public class MarginCursorTests $"the {margin.GetType().Name} is a click target, so it must show the arrow rather than the text I-beam"); } } + + [AvaloniaTest] + public async Task Toggling_Line_Numbers_On_Live_Keeps_Gutter_Order_And_Positive_Font_Size() + { + var (window, vm) = await TestHarness.BootAsync(1); + var settings = AppComposition.Current.GetExport().DisplaySettings; + settings.ShowLineNumbers = false; + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var objectNode = vm.AssemblyTreeModel.FindNode(coreLibName, "System", "System.Object"); + vm.AssemblyTreeModel.SelectNode(objectNode); + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + var view = await window.WaitForComponent(); + + settings.ShowLineNumbers = true; + var margins = view.Editor.TextArea.LeftMargins; + var lineNumberMargin = margins.OfType() + .Should().ContainSingle("line numbers should render after being toggled on live").Which; + margins.IndexOf(margins.OfType().Single()) + .Should().Be(margins.IndexOf(lineNumberMargin) - 1, + "the bookmark gutter must stay left of line numbers when line numbers are toggled on live"); + GetLineNumberMarginEmSize(lineNumberMargin).Should().BeGreaterThan(0); + + window.UpdateLayout(); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + } + + static double GetLineNumberMarginEmSize(LineNumberMargin margin) + { + return (double)typeof(LineNumberMargin) + .GetProperty("EmSize", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(margin)!; + } } diff --git a/ILSpy/TextView/DecompilerTextEditor.cs b/ILSpy/TextView/DecompilerTextEditor.cs index 8df849bc0..d6c6ce057 100644 --- a/ILSpy/TextView/DecompilerTextEditor.cs +++ b/ILSpy/TextView/DecompilerTextEditor.cs @@ -18,6 +18,7 @@ using System; using System.ComponentModel; +using System.Linq; using Avalonia; using Avalonia.Controls; @@ -100,6 +101,17 @@ namespace ICSharpCode.ILSpy.TextView FontFamily = new FontFamily(displaySettings.SelectedFont); if (displaySettings.SelectedFontSize > 0) FontSize = EditorZoom.EffectiveFontSize(displaySettings); + PrepareLineNumberMarginsForRender(); + } + + internal void PrepareLineNumberMarginsForRender() + { + foreach (var margin in TextArea.LeftMargins.OfType()) + { + margin.SetValue(TextBlock.FontFamilyProperty, FontFamily); + margin.SetValue(TextBlock.FontSizeProperty, FontSize); + margin.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } } static DisplaySettings? TryGetDisplaySettings() diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 4cc7feba1..3293f14da 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -34,6 +34,7 @@ using Avalonia.Threading; using Avalonia.VisualTree; using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; using AvaloniaEdit.Folding; using AvaloniaEdit.Highlighting; @@ -179,6 +180,41 @@ namespace ICSharpCode.ILSpy.TextView margin.Cursor = ArrowCursor; } + void KeepBookmarkMarginBeforeLineNumbers() + { + if (bookmarkMargin == null) + return; + + var margins = Editor.TextArea.LeftMargins; + var lineNumberIndex = -1; + for (var i = 0; i < margins.Count; i++) + { + if (margins[i] is LineNumberMargin) + { + lineNumberIndex = i; + break; + } + } + var bookmarkIndex = margins.IndexOf(bookmarkMargin); + if (lineNumberIndex < 0 || bookmarkIndex < 0 || bookmarkIndex < lineNumberIndex) + return; + + var lineNumberMargin = margins[lineNumberIndex]; + Control? separator = null; + if (lineNumberIndex + 1 < margins.Count + && margins[lineNumberIndex + 1] is global::Avalonia.Controls.Shapes.Line) + separator = margins[lineNumberIndex + 1]; + + if (separator != null) + margins.Remove(separator); + margins.Remove(lineNumberMargin); + + bookmarkIndex = margins.IndexOf(bookmarkMargin); + margins.Insert(bookmarkIndex + 1, lineNumberMargin); + if (separator != null) + margins.Insert(bookmarkIndex + 2, separator); + } + void OnPreviewKeyDownForOmnibar(object? sender, KeyEventArgs e) { if (e.Key == Key.L && e.KeyModifiers == KeyModifiers.Control && Omnibar.IsVisible) @@ -649,6 +685,11 @@ namespace ICSharpCode.ILSpy.TextView { case nameof(DisplaySettings.ShowLineNumbers): Editor.ShowLineNumbers = s.ShowLineNumbers; + if (Editor.ShowLineNumbers) + { + KeepBookmarkMarginBeforeLineNumbers(); + Editor.PrepareLineNumberMarginsForRender(); + } break; case nameof(DisplaySettings.EnableWordWrap): Editor.WordWrap = s.EnableWordWrap;