From 1bd3670bed2832d2e62693e8b12b847195081e8c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 11 May 2026 09:35:37 +0200 Subject: [PATCH] DisplaySettings live-wires the decompiler editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DecompilerTextView subscribes to SettingsService.DisplaySettings.PropertyChanged and mirrors the new value into the AvaloniaEdit TextEditor immediately — no re-decompile, no Apply. Properties wired: Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy.Tests/Options/OptionsTabTests.cs | 34 +++++++++++ ILSpy/Languages/LanguageService.cs | 3 + ILSpy/TextView/DecompilerTextView.axaml.cs | 66 ++++++++++++++++++++++ ILSpy/TreeNodes/ILSpyTreeNode.cs | 9 +++ 4 files changed, 112 insertions(+) diff --git a/ILSpy.Tests/Options/OptionsTabTests.cs b/ILSpy.Tests/Options/OptionsTabTests.cs index c4ad0eedb..4a66c040d 100644 --- a/ILSpy.Tests/Options/OptionsTabTests.cs +++ b/ILSpy.Tests/Options/OptionsTabTests.cs @@ -30,6 +30,8 @@ using ILSpy.AppEnv; using ILSpy.Commands; using ILSpy.Docking; using ILSpy.Options; +using ILSpy.TextView; +using ILSpy.TreeNodes; using ILSpy.ViewModels; using ILSpy.Views; @@ -199,4 +201,36 @@ public class OptionsTabTests // Reset must restore the panel's settings to `new DecompilerSettings()` defaults. refreshedItem.IsEnabled.Should().Be(defaultValue); } + + [AvaloniaTest] + public async Task Display_Setting_Change_Live_Updates_Decompiler_Editor() + { + // DisplaySettings → DecompilerTextView wire-up: mutating SelectedFontSize on the live + // settings instance flows immediately into Editor.FontSize without any Apply or + // re-decompile step. Covers the broader live-bind contract for font / size / line + // numbers / word wrap / highlight current line / indentation. + var window = AppComposition.Current.GetExport(); + window.Show(); + var settings = AppComposition.Current.GetExport(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + // Materialise a DecompilerTextView by selecting a node. + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + vm.AssemblyTreeModel.SelectNode(typeNode); + var view = await window.WaitForComponent(); + var editor = await view.WaitForComponent(); + + var originalSize = settings.DisplaySettings.SelectedFontSize; + var newSize = originalSize + 5; + + // Act — change the live setting; subscriber should write through to Editor.FontSize. + settings.DisplaySettings.SelectedFontSize = newSize; + + editor.FontSize.Should().Be(newSize); + + // Clean-up: restore so later tests see the original. + settings.DisplaySettings.SelectedFontSize = originalSize; + } } diff --git a/ILSpy/Languages/LanguageService.cs b/ILSpy/Languages/LanguageService.cs index cea0d10f1..ac0f2d8e2 100644 --- a/ILSpy/Languages/LanguageService.cs +++ b/ILSpy/Languages/LanguageService.cs @@ -72,6 +72,9 @@ namespace ILSpy.Languages public Language GetLanguage(string? name) => Languages.FirstOrDefault(l => l.Name == name) ?? Languages.First(); + // The MVVM-toolkit source generator declares the partial method with nullable + // reference types (the backing field is a nullable T). Match the signature exactly + // to avoid a CS8611 nullability mismatch. partial void OnCurrentLanguageChanged(Language? oldValue, Language newValue) { if (newValue == null) diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 386a75425..9f9e7cdce 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -44,6 +44,7 @@ using ICSharpCode.ILSpyX; using ILSpy; using ILSpy.AppEnv; +using ILSpy.Options; namespace ILSpy.TextView { @@ -152,6 +153,71 @@ namespace ILSpy.TextView richPopup.Closed += OnRichPopupClosed; // Popup.Open uses PlacementTarget to find its TopLevel, so the popup itself doesn't // need to be in any visual tree of ours. + + WireUpDisplaySettings(); + } + + /// + /// Mirrors the live into the editor on construction and on + /// every PropertyChanged after that. Lets the Options Display panel produce + /// immediate visual feedback without a re-decompile. + /// + void WireUpDisplaySettings() + { + var settings = TryGetDisplaySettings(); + if (settings == null) + return; + ApplyAllDisplaySettings(settings); + settings.PropertyChanged += (_, e) => ApplyDisplaySetting(settings, e.PropertyName); + } + + static DisplaySettings? TryGetDisplaySettings() + { + try + { return AppComposition.Current.GetExport().DisplaySettings; } + catch { return null; } + } + + void ApplyAllDisplaySettings(DisplaySettings s) + { + ApplyDisplaySetting(s, nameof(DisplaySettings.SelectedFont)); + ApplyDisplaySetting(s, nameof(DisplaySettings.SelectedFontSize)); + ApplyDisplaySetting(s, nameof(DisplaySettings.ShowLineNumbers)); + ApplyDisplaySetting(s, nameof(DisplaySettings.EnableWordWrap)); + ApplyDisplaySetting(s, nameof(DisplaySettings.HighlightCurrentLine)); + ApplyDisplaySetting(s, nameof(DisplaySettings.IndentationSize)); + ApplyDisplaySetting(s, nameof(DisplaySettings.IndentationUseTabs)); + } + + void ApplyDisplaySetting(DisplaySettings s, string? propertyName) + { + switch (propertyName) + { + case nameof(DisplaySettings.SelectedFont): + if (!string.IsNullOrEmpty(s.SelectedFont)) + Editor.FontFamily = new FontFamily(s.SelectedFont); + break; + case nameof(DisplaySettings.SelectedFontSize): + if (s.SelectedFontSize > 0) + Editor.FontSize = s.SelectedFontSize; + break; + case nameof(DisplaySettings.ShowLineNumbers): + Editor.ShowLineNumbers = s.ShowLineNumbers; + break; + case nameof(DisplaySettings.EnableWordWrap): + Editor.WordWrap = s.EnableWordWrap; + break; + case nameof(DisplaySettings.HighlightCurrentLine): + Editor.Options.HighlightCurrentLine = s.HighlightCurrentLine; + break; + case nameof(DisplaySettings.IndentationSize): + if (s.IndentationSize > 0) + Editor.Options.IndentationSize = s.IndentationSize; + break; + case nameof(DisplaySettings.IndentationUseTabs): + Editor.Options.ConvertTabsToSpaces = !s.IndentationUseTabs; + break; + } } static IReadOnlyList TryGetContextMenuEntries() diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs index 8dfa63f1f..8f9b7c944 100644 --- a/ILSpy/TreeNodes/ILSpyTreeNode.cs +++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs @@ -76,6 +76,15 @@ namespace ILSpy.TreeNodes public Language Language => LanguageService.CurrentLanguage; + /// + /// Long-form label used in navigation surfaces — back/forward history dropdowns, future + /// breadcrumb rendering, etc. Defaults to ; override on + /// nodes whose is too generic to be useful out of + /// context (e.g. "Base Types" → "Base Types (System.Exception)") so the user can tell + /// dropdown entries apart at a glance. + /// + public override object? NavigationText => Text; + /// /// True for nodes that represent assemblies the user did not open explicitly — the /// decompiler resolved them as part of another assembly's references and added them to