mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
The Options page is non-modal and live-apply, so -- unlike the WPF host, which ran a full assembly-list Refresh() when its modal Options dialog closed -- a setting that changes the decompiler/disassembler output (brace folding, member/using expansion, debug info, IL detail, indentation) had nothing to make it take effect; toggling it did nothing until the user re-navigated. Classify every DisplaySettings property in one table (DisplaySettingReactions: editor-live / tree-text / tree-shape / re-decompile / none), grounded in the actual consumers (ApplyDisplaySettings, GetIndentationString, the IL/mixed languages). OnSettingsChanged dispatches on it, and a coverage test asserts the table spans every settable property so a newly-added one can't silently fall through. Fixing the re-decompile case exposed that ForceRefreshActiveTab was itself a no-op for an unchanged node -- ShowSelectedNode re-sets CurrentNodes, whose setter dedups -- so RefreshDecompiledView (also used after dependency resolution) never actually re-ran. Add DecompilerTabPageModel.Redecompile() to force past the dedup and call it from ForceRefreshActiveTab. Also drop the EnableSmoothScrolling setting: it drove TomsToolbox's AdvancedScrollWheelBehavior in WPF, which the Avalonia port doesn't use and never replaced, so the checkbox persisted a value nothing read. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3755/head
9 changed files with 259 additions and 38 deletions
@ -0,0 +1,110 @@
@@ -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; |
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<TypeTreeNode>( |
||||
"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<SettingsService>().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"); |
||||
} |
||||
} |
||||
@ -0,0 +1,102 @@
@@ -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 |
||||
{ |
||||
/// <summary>
|
||||
/// How a live <see cref="DisplaySettings"/> 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
|
||||
/// <c>Refresh()</c> when its modal Options dialog closed -- every setting has to drive its own
|
||||
/// reaction or it silently does nothing.
|
||||
/// </summary>
|
||||
public enum DisplaySettingReaction |
||||
{ |
||||
/// <summary>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.</summary>
|
||||
EditorLive, |
||||
|
||||
/// <summary>Re-pull of member-node text only (the metadata-token suffix).</summary>
|
||||
TreeText, |
||||
|
||||
/// <summary>In-place rebuild of a tree subtree whose shape the setting changes
|
||||
/// (nested namespaces, hidden empty metadata tables).</summary>
|
||||
TreeShape, |
||||
|
||||
/// <summary>Baked into the decompiler / disassembler output, so the active tab must be
|
||||
/// re-decompiled for the change to show.</summary>
|
||||
Redecompile, |
||||
|
||||
/// <summary>Deliberately no model-side reaction (window chrome, search ordering).</summary>
|
||||
None, |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Single source of truth classifying every <see cref="DisplaySettings"/> property by its live
|
||||
/// reaction. <c>AssemblyTreeModel</c> dispatches on it; <c>DisplaySettingsReactionTests</c>
|
||||
/// asserts the table covers every setting, so a newly-added one can't fall through unhandled.
|
||||
/// </summary>
|
||||
public static class DisplaySettingReactions |
||||
{ |
||||
static readonly Dictionary<string, DisplaySettingReaction> 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, |
||||
}; |
||||
|
||||
/// <summary>Every classified property name. The coverage test asserts this equals the set of
|
||||
/// settable <see cref="DisplaySettings"/> properties.</summary>
|
||||
public static IReadOnlyCollection<string> ClassifiedProperties => reactions.Keys; |
||||
|
||||
/// <summary>
|
||||
/// The reaction for <paramref name="propertyName"/>, or <see cref="DisplaySettingReaction.None"/>
|
||||
/// for an unclassified one (the coverage test prevents that case from reaching production).
|
||||
/// </summary>
|
||||
public static DisplaySettingReaction For(string? propertyName) |
||||
=> propertyName != null && reactions.TryGetValue(propertyName, out var reaction) |
||||
? reaction |
||||
: DisplaySettingReaction.None; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue