diff --git a/ILSpy.Tests/Options/DisplaySettingsReactionTests.cs b/ILSpy.Tests/Options/DisplaySettingsReactionTests.cs
new file mode 100644
index 000000000..5b09ca435
--- /dev/null
+++ b/ILSpy.Tests/Options/DisplaySettingsReactionTests.cs
@@ -0,0 +1,110 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+using Avalonia.Headless.NUnit;
+using Avalonia.Threading;
+
+using AwesomeAssertions;
+
+using ILSpy;
+using ILSpy.AppEnv;
+using ILSpy.Options;
+using ILSpy.TextView;
+using ILSpy.TreeNodes;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+///
+/// The Options page is non-modal and live-apply (no "OK" step), so unlike WPF -- which did a full
+/// assembly-list Refresh() when the modal dialog closed -- every display setting has to drive its own
+/// live reaction. These tests pin that a decompiler-output setting actually re-decompiles the active
+/// tab, and that every setting is classified so a newly-added one can't silently fall through.
+///
+[TestFixture]
+public class DisplaySettingsReactionTests
+{
+ static async Task Pump(int ticks = 12)
+ {
+ for (int i = 0; i < ticks; i++)
+ {
+ Dispatcher.UIThread.RunJobs();
+ await Task.Delay(20);
+ }
+ }
+
+ [AvaloniaTest]
+ public async Task Toggling_A_Decompiler_Output_Setting_Re_Decompiles_The_Active_Tab()
+ {
+ var (_, vm) = await TestHarness.BootAsync();
+ var typeNode = vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ vm.AssemblyTreeModel.SelectedItem = typeNode;
+ var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
+
+ int decompileStarts = 0;
+ void OnTab(object? _, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(DecompilerTabPageModel.IsDecompiling) && tab.IsDecompiling)
+ decompileStarts++;
+ }
+ tab.PropertyChanged += OnTab;
+ try
+ {
+ var display = AppComposition.Current.GetExport().DisplaySettings;
+ display.DecodeCustomAttributeBlobs = !display.DecodeCustomAttributeBlobs;
+ await Pump();
+ }
+ finally
+ {
+ tab.PropertyChanged -= OnTab;
+ }
+
+ decompileStarts.Should().BeGreaterThan(0,
+ "toggling a decompiler-output display setting must re-decompile the active tab "
+ + "(the live Options page has no full-refresh-on-close to fall back on)");
+ }
+
+ [Test]
+ public void Every_Display_Setting_Is_Classified()
+ {
+ // The safety net: each settable DisplaySettings property must appear in the reaction table,
+ // so adding a new setting forces a deliberate classification (editor / tree / re-decompile /
+ // none) instead of silently doing nothing. Set equality also catches stale entries left
+ // behind when a setting is removed.
+ var settable = typeof(DisplaySettings)
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(p => p is { CanRead: true, CanWrite: true })
+ .Select(p => p.Name)
+ .ToHashSet();
+ settable.Should().NotBeEmpty();
+
+ var classified = DisplaySettingReactions.ClassifiedProperties.ToHashSet();
+ settable.Should().BeSubsetOf(classified,
+ "every settable DisplaySettings property must be classified in DisplaySettingReactions");
+ classified.Should().BeSubsetOf(settable,
+ "DisplaySettingReactions must not reference settings that no longer exist");
+ }
+}
diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
index 23b6412e1..5018781f9 100644
--- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -254,33 +254,45 @@ namespace ILSpy.AssemblyTree
return;
if (Root == null)
return;
- switch (e.Inner.PropertyName)
+ // One classification table (DisplaySettingReactions) drives every reaction, so a
+ // newly-added setting can't silently fall through -- the coverage test fails until it's
+ // listed. The Options page is non-modal/live-apply, so there is no full-refresh-on-close
+ // fallback the way the WPF host had; each bucket must do its own update.
+ var name = e.Inner.PropertyName;
+ switch (Options.DisplaySettingReactions.For(name))
{
- case nameof(Options.DisplaySettings.ShowMetadataTokens):
- case nameof(Options.DisplaySettings.ShowMetadataTokensInBase10):
+ case Options.DisplaySettingReaction.TreeText:
// Text suffix on member nodes is computed at read time — just fire the
// notification so bound cell templates re-pull.
NotifyTextChanged(Root);
break;
- case nameof(Options.DisplaySettings.UseNestedNamespaceNodes):
- // Tree shape changes — every loaded AssemblyTreeNode's namespace subtree
- // needs rebuilding.
- foreach (var asm in Root.Children.OfType())
- asm.ReloadChildren();
- // AssemblyListPane caches a snapshot of each expanded node's children via
- // the HierarchicalOptions.ChildrenSelector — mid-expand mutations of
- // node.Children aren't observed. Re-raising Root forces BindTree to fire,
- // which creates a fresh HierarchicalModel that re-reads children on demand.
+ case Options.DisplaySettingReaction.TreeShape:
+ if (name == nameof(Options.DisplaySettings.UseNestedNamespaceNodes))
+ {
+ // Every loaded AssemblyTreeNode's namespace subtree needs rebuilding.
+ foreach (var asm in Root.Children.OfType())
+ asm.ReloadChildren();
+ }
+ else
+ {
+ // Visible MetadataTablesTreeNode instances get their children regenerated;
+ // untouched (lazy) ones already pick up the new value on first expand.
+ RebuildMetadataTablesIn(Root);
+ }
+ // AssemblyListPane caches a snapshot of each expanded node's children via the
+ // HierarchicalOptions.ChildrenSelector — mid-expand mutations of node.Children
+ // aren't observed. Re-raising Root forces BindTree to fire, which creates a fresh
+ // HierarchicalModel that re-reads children on demand.
OnPropertyChanged(nameof(Root));
break;
- case nameof(Options.DisplaySettings.HideEmptyMetadataTables):
- // Visible MetadataTablesTreeNode instances get their children regenerated;
- // untouched (lazy) ones already pick up the new value on first expand.
- RebuildMetadataTablesIn(Root);
- // Same DataGrid-snapshot problem as the UseNestedNamespaceNodes branch —
- // force a re-bind so the rebuilt children make it into the visible grid.
- OnPropertyChanged(nameof(Root));
+ case Options.DisplaySettingReaction.Redecompile:
+ // Baked into the decompiler/disassembler output (folding, member/using
+ // expansion, debug info, IL detail, indentation), so it only shows after a
+ // re-decompile of the active tab.
+ RefreshDecompiledView();
break;
+ // EditorLive / None: the text view applies editor settings to AvaloniaEdit itself,
+ // and the rest have no model-side reaction.
}
}
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index 9ff77513c..f903b4968 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -707,6 +707,11 @@ namespace ILSpy.Docking
{
lastShownNodes = null;
ShowSelectedNode();
+ // ShowSelectedNode re-projects the selection, but DecompilerTabPageModel.CurrentNodes
+ // dedups an unchanged node -- so a same-node refresh (a decompiler-output display setting,
+ // or freshly resolved dependencies) wouldn't actually re-run. Force the active decompiler
+ // tab to re-decompile so its output reflects the new state.
+ ActiveDecompilerTab?.Redecompile();
}
void ShowSelectedNode()
diff --git a/ILSpy/Options/DisplaySettingReactions.cs b/ILSpy/Options/DisplaySettingReactions.cs
new file mode 100644
index 000000000..2b9263b3d
--- /dev/null
+++ b/ILSpy/Options/DisplaySettingReactions.cs
@@ -0,0 +1,102 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Collections.Generic;
+
+namespace ILSpy.Options
+{
+ ///
+ /// How a live change has to be surfaced. The Options page is
+ /// non-modal and applies immediately, so -- unlike the WPF host, which ran a full assembly-list
+ /// Refresh() when its modal Options dialog closed -- every setting has to drive its own
+ /// reaction or it silently does nothing.
+ ///
+ public enum DisplaySettingReaction
+ {
+ /// The text view applies it to the editor itself (font, line numbers, word wrap,
+ /// brace highlighting, ...); the assembly tree and the decompile output are unaffected.
+ EditorLive,
+
+ /// Re-pull of member-node text only (the metadata-token suffix).
+ TreeText,
+
+ /// In-place rebuild of a tree subtree whose shape the setting changes
+ /// (nested namespaces, hidden empty metadata tables).
+ TreeShape,
+
+ /// Baked into the decompiler / disassembler output, so the active tab must be
+ /// re-decompiled for the change to show.
+ Redecompile,
+
+ /// Deliberately no model-side reaction (window chrome, search ordering).
+ None,
+ }
+
+ ///
+ /// Single source of truth classifying every property by its live
+ /// reaction. AssemblyTreeModel dispatches on it; DisplaySettingsReactionTests
+ /// asserts the table covers every setting, so a newly-added one can't fall through unhandled.
+ ///
+ public static class DisplaySettingReactions
+ {
+ static readonly Dictionary reactions = new() {
+ // Tree.
+ [nameof(DisplaySettings.ShowMetadataTokens)] = DisplaySettingReaction.TreeText,
+ [nameof(DisplaySettings.ShowMetadataTokensInBase10)] = DisplaySettingReaction.TreeText,
+ [nameof(DisplaySettings.UseNestedNamespaceNodes)] = DisplaySettingReaction.TreeShape,
+ [nameof(DisplaySettings.HideEmptyMetadataTables)] = DisplaySettingReaction.TreeShape,
+
+ // Decompiler / disassembler output (see DecompilerTabPageModel.ApplyDisplaySettings +
+ // GetIndentationString, and CSharpILMixedLanguage / ILLanguage for the IL-detail ones).
+ [nameof(DisplaySettings.FoldBraces)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.ExpandMemberDefinitions)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.ExpandUsingDeclarations)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.ShowDebugInfo)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.ShowRawOffsetsAndBytesBeforeInstruction)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.DecodeCustomAttributeBlobs)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.IndentationUseTabs)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.IndentationSize)] = DisplaySettingReaction.Redecompile,
+ [nameof(DisplaySettings.IndentationTabSize)] = DisplaySettingReaction.Redecompile,
+
+ // Editor-only (DecompilerTextView applies these directly to the AvaloniaEdit control).
+ [nameof(DisplaySettings.SelectedFont)] = DisplaySettingReaction.EditorLive,
+ [nameof(DisplaySettings.SelectedFontSize)] = DisplaySettingReaction.EditorLive,
+ [nameof(DisplaySettings.ShowLineNumbers)] = DisplaySettingReaction.EditorLive,
+ [nameof(DisplaySettings.EnableWordWrap)] = DisplaySettingReaction.EditorLive,
+ [nameof(DisplaySettings.HighlightCurrentLine)] = DisplaySettingReaction.EditorLive,
+ [nameof(DisplaySettings.HighlightMatchingBraces)] = DisplaySettingReaction.EditorLive,
+
+ // No model-side reaction.
+ [nameof(DisplaySettings.StyleWindowTitleBar)] = DisplaySettingReaction.None,
+ [nameof(DisplaySettings.SortResults)] = DisplaySettingReaction.None,
+ };
+
+ /// Every classified property name. The coverage test asserts this equals the set of
+ /// settable properties.
+ public static IReadOnlyCollection ClassifiedProperties => reactions.Keys;
+
+ ///
+ /// The reaction for , or
+ /// for an unclassified one (the coverage test prevents that case from reaching production).
+ ///
+ public static DisplaySettingReaction For(string? propertyName)
+ => propertyName != null && reactions.TryGetValue(propertyName, out var reaction)
+ ? reaction
+ : DisplaySettingReaction.None;
+ }
+}
diff --git a/ILSpy/Options/DisplaySettings.cs b/ILSpy/Options/DisplaySettings.cs
index 356431efa..a27c3f4f7 100644
--- a/ILSpy/Options/DisplaySettings.cs
+++ b/ILSpy/Options/DisplaySettings.cs
@@ -93,9 +93,6 @@ namespace ILSpy.Options
[ObservableProperty]
bool showRawOffsetsAndBytesBeforeInstruction;
- [ObservableProperty]
- bool enableSmoothScrolling = true;
-
[ObservableProperty]
bool decodeCustomAttributeBlobs;
@@ -123,7 +120,6 @@ namespace ILSpy.Options
UseNestedNamespaceNodes = (bool?)section.Attribute(nameof(UseNestedNamespaceNodes)) ?? false;
ShowRawOffsetsAndBytesBeforeInstruction = (bool?)section.Attribute(nameof(ShowRawOffsetsAndBytesBeforeInstruction)) ?? false;
StyleWindowTitleBar = (bool?)section.Attribute(nameof(StyleWindowTitleBar)) ?? false;
- EnableSmoothScrolling = (bool?)section.Attribute(nameof(EnableSmoothScrolling)) ?? true;
DecodeCustomAttributeBlobs = (bool?)section.Attribute(nameof(DecodeCustomAttributeBlobs)) ?? false;
}
@@ -150,7 +146,6 @@ namespace ILSpy.Options
section.SetAttributeValue(nameof(UseNestedNamespaceNodes), UseNestedNamespaceNodes);
section.SetAttributeValue(nameof(ShowRawOffsetsAndBytesBeforeInstruction), ShowRawOffsetsAndBytesBeforeInstruction);
section.SetAttributeValue(nameof(StyleWindowTitleBar), StyleWindowTitleBar);
- section.SetAttributeValue(nameof(EnableSmoothScrolling), EnableSmoothScrolling);
section.SetAttributeValue(nameof(DecodeCustomAttributeBlobs), DecodeCustomAttributeBlobs);
return section;
}
diff --git a/ILSpy/Options/DisplaySettingsPanel.axaml b/ILSpy/Options/DisplaySettingsPanel.axaml
index a33aff383..4e13dd4fe 100644
--- a/ILSpy/Options/DisplaySettingsPanel.axaml
+++ b/ILSpy/Options/DisplaySettingsPanel.axaml
@@ -81,8 +81,6 @@
Content="{x:Static res:Resources.ShowRawOffsetsAndBytesBeforeInstruction}" />
-
diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs
index 4d907d73b..b314262c2 100644
--- a/ILSpy/Properties/Resources.Designer.cs
+++ b/ILSpy/Properties/Resources.Designer.cs
@@ -1848,15 +1848,6 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
- ///
- /// Looks up a localized string similar to Enable smooth scrolling.
- ///
- public static string EnableSmoothScrolling {
- get {
- return ResourceManager.GetString("EnableSmoothScrolling", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Enable word wrap.
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 34cf2a046..0511a6f71 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -642,9 +642,6 @@ Are you sure you want to continue?
Enable folding on all blocks in braces
-
- Enable smooth scrolling
-
Enable word wrap
diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs
index 3c76e745e..2c80ee247 100644
--- a/ILSpy/TextView/DecompilerTabPageModel.cs
+++ b/ILSpy/TextView/DecompilerTabPageModel.cs
@@ -324,6 +324,17 @@ namespace ILSpy.TextView
StartDecompile();
}
+ ///
+ /// Forces a fresh decompilation of the current nodes, bypassing the
+ /// dedup. Used by ForceRefreshActiveTab when a display setting or freshly resolved
+ /// dependencies changed but the selected node did not. No-op when nothing is being decompiled.
+ ///
+ public void Redecompile()
+ {
+ if (CurrentNodes.Count > 0)
+ StartDecompile();
+ }
+
// Built (off the UI thread) when a decompile blows past its output limit: a short message plus
// a "Display code anyway" button (retry at the extended limit, only offered for the normal
// limit) and a "Save code" button. The button click handlers run back on the UI thread.