Browse Source

Port the Debug Steps pane and its ILAst languages (DEBUG-only)

Brings the WPF decompiler-pipeline-stepper feature across. Available only
in Debug builds — the entire feature set is gated behind `#if DEBUG`, so
Release users see neither the pane nor the ILAst languages in the picker.
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
61f3081217
  1. 5
      ILSpy.Tests/Docking/DockWorkspaceTests.cs
  2. 86
      ILSpy.Tests/Views/DebugStepsTests.cs
  3. 3
      ILSpy/App.axaml
  4. 4
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  5. 1
      ILSpy/Assets/Icons/ViewCode.svg
  6. 15
      ILSpy/DecompilationOptions.cs
  7. 9
      ILSpy/ILSpy.csproj
  8. 1
      ILSpy/Images.cs
  9. 195
      ILSpy/Languages/ILAstLanguage.cs
  10. 16
      ILSpy/SettingsService.cs
  11. 39
      ILSpy/TextView/DecompilerTabPageModel.cs
  12. 63
      ILSpy/ViewModels/DebugStepsPaneModel.cs
  13. 41
      ILSpy/Views/DebugSteps.axaml
  14. 171
      ILSpy/Views/DebugSteps.axaml.cs

5
ILSpy.Tests/Docking/DockWorkspaceTests.cs

@ -42,7 +42,12 @@ public class DockWorkspaceTests @@ -42,7 +42,12 @@ public class DockWorkspaceTests
workspace.Layout.Should().NotBeNull("ILSpyDockFactory.CreateLayout() wires the root dock in the ctor.");
workspace.Factory.Should().NotBeNull();
#if DEBUG
workspace.ToolPaneMenuItems.Should().HaveCount(4,
"AssemblyTree, Search, Analyzers, and the Debug Steps pane (Debug-only) are wired at this point.");
#else
workspace.ToolPaneMenuItems.Should().HaveCount(3,
"AssemblyTree, Search, and Analyzers are the three tool panes wired at this point.");
#endif
}
}

86
ILSpy.Tests/Views/DebugStepsTests.cs

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
// 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.
#if DEBUG
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ILSpy.AppEnv;
using ILSpy.Languages;
using ILSpy.ViewModels;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Views;
/// <summary>
/// Pins the Debug Steps wiring at the composition layer. Tests are gated by DEBUG since
/// the feature is itself DEBUG-only — Release builds wouldn't have the types these
/// assertions reference.
/// </summary>
[TestFixture]
public class DebugStepsTests
{
[AvaloniaTest]
public Task DebugStepsPaneModel_Is_Exported_As_A_ToolPane()
{
// MEF tool-pane registry contract: DebugStepsPaneModel registers under its
// PaneContentId so DockWorkspace.ShowToolPane(...) can surface it.
var pane = AppComposition.Current.GetExport<DebugStepsPaneModel>();
((object?)pane).Should().NotBeNull("DebugStepsPaneModel must resolve as a [Shared] export");
pane.Id.Should().Be(DebugStepsPaneModel.PaneContentId);
pane.Title.Should().Be("Debug Steps");
return Task.CompletedTask;
}
[AvaloniaTest]
public Task DebugStepsPaneModel_WritingOptions_Default_Enables_Field_And_LogicOperation_Sugar()
{
// The default writing options match the WPF baseline: field sugar and
// logic-operation sugar on; IL ranges and child-index-in-block off. The four
// CheckBoxes in DebugSteps.axaml bind two-way against these defaults.
var options = DebugStepsPaneModel.WritingOptions;
options.UseFieldSugar.Should().BeTrue();
options.UseLogicOperationSugar.Should().BeTrue();
options.ShowILRanges.Should().BeFalse();
options.ShowChildIndexInBlock.Should().BeFalse();
return Task.CompletedTask;
}
[AvaloniaTest]
public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds()
{
// Two ILAstLanguage subclasses: BlockILLanguage ("ILAst") drives the stepper,
// TypedILLanguage ("Typed IL") writes type-annotated raw IL without transforms.
// Both register via [Export(typeof(Language))]; the language picker uses them
// in addition to C# and the disassembler-IL language.
var languageService = AppComposition.Current.GetExport<LanguageService>();
languageService.Languages.OfType<ILAstLanguage>().Should().HaveCount(2,
"both BlockIL and TypedIL must be registered when DEBUG is defined");
languageService.Languages.Should().Contain(l => l.Name == "ILAst");
languageService.Languages.Should().Contain(l => l.Name == "Typed IL");
return Task.CompletedTask;
}
}
#endif

3
ILSpy/App.axaml

@ -54,6 +54,9 @@ @@ -54,6 +54,9 @@
<DataTemplate DataType="vm:MetadataTablePageModel">
<metaViews:MetadataTablePage />
</DataTemplate>
<DataTemplate DataType="vm:DebugStepsPaneModel">
<metaViews:DebugSteps />
</DataTemplate>
<local:ViewLocator/>
</Application.DataTemplates>

4
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -130,6 +130,10 @@ namespace ILSpy.AssemblyTree @@ -130,6 +130,10 @@ namespace ILSpy.AssemblyTree
// notified, and the saved path must follow the new primary.
OnPropertyChanged(nameof(SelectedItem));
settingsService.SessionSettings.ActiveTreeViewPath = GetPathForNode(SelectedItem);
// Pub-sub fan-out: panes (e.g. Debug Steps) that need to invalidate their state
// when the selection moves listen on this message.
Util.MessageBus.Send(this, new Util.AssemblyTreeSelectionChangedEventArgs());
}
// Walks already-materialized children and re-raises Text PropertyChanged so the cell

1
ILSpy/Assets/Icons/ViewCode.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;} .icon-vs-action-blue{fill:#00539C;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M10 3v.879l-3.646-3.647-2.122 2.122 1.647 1.646h-5.879v3h5.879l-1.647 1.646.354.354h-2.586v3h4v3h9v-3h-1v-3h1v-3h1v-3h-6zm-1.879 6l.879-.879v.879h-.879z" id="outline"/><path class="icon-vs-bg" d="M15 4v1h-4v-1h4zm-1 3h-4v1h4v-1zm-1 3h-10v1h10v-1zm-6 4h7v-1h-7v1z" id="iconBg"/><path class="icon-vs-action-blue" d="M10.207 5.5l-3.853 3.854-.708-.708 2.647-2.646h-7.293v-1h7.293l-2.647-2.646.707-.707 3.854 3.853z" id="colorAction"/></svg>

After

Width:  |  Height:  |  Size: 808 B

15
ILSpy/DecompilationOptions.cs

@ -42,6 +42,21 @@ namespace ILSpy @@ -42,6 +42,21 @@ namespace ILSpy
/// <summary>Escape invalid C# identifiers so the output compiles.</summary>
public bool EscapeInvalidIdentifiers { get; set; }
/// <summary>
/// Stop the IL-transform pipeline after this many steps. <see cref="int.MaxValue"/>
/// means "run all transforms". The Debug Steps pane sets this to the index of a
/// chosen step so it can show the partial state at that point. Honoured by
/// <see cref="Languages.BlockILLanguage"/>; ignored by every other language.
/// </summary>
public int StepLimit { get; set; } = int.MaxValue;
/// <summary>
/// When true, transforms emit verbose debug information about their behaviour. Only
/// meaningful in combination with <see cref="StepLimit"/> — the Debug Steps pane sets
/// it on the "Debug this step" context-menu action.
/// </summary>
public bool IsDebug { get; set; }
public DecompilationOptions(DecompilerSettings settings)
{
DecompilerSettings = settings;

9
ILSpy/ILSpy.csproj

@ -75,4 +75,13 @@ @@ -75,4 +75,13 @@
<ProjectReference Include="..\ICSharpCode.ILSpyX\ICSharpCode.ILSpyX.csproj" />
<ProjectReference Include="..\ICSharpCode.Decompiler\ICSharpCode.Decompiler.csproj" />
</ItemGroup>
<!-- The Debug Steps pane is a DEBUG-only diagnostic. The code-behind and viewmodel
are wrapped in `#if DEBUG`, so the C# type vanishes under Release; but Avalonia
compiles every .axaml file regardless of preprocessor state, and the XamlIl
compiler then fails resolving x:Class="ILSpy.Views.DebugSteps". Removing the
AXAML from the build under non-Debug configurations matches the C# gating. -->
<ItemGroup Condition="'$(Configuration)' != 'Debug'">
<AvaloniaXaml Remove="Views\DebugSteps.axaml" />
</ItemGroup>
</Project>

1
ILSpy/Images.cs

@ -108,6 +108,7 @@ namespace ILSpy.Images @@ -108,6 +108,7 @@ namespace ILSpy.Images
public static readonly IImage Property = LoadSvg(nameof(Property));
public static readonly IImage Event = LoadSvg(nameof(Event));
public static readonly IImage Literal = LoadSvg(nameof(Literal));
public static readonly IImage ViewCode = LoadSvg(nameof(ViewCode));
// Reference overlays (small badge layered onto a base icon for TypeRef/MemberRef nodes).
internal static readonly IImage ReferenceOverlay = LoadSvg(nameof(ReferenceOverlay));

195
ILSpy/Languages/ILAstLanguage.cs

@ -0,0 +1,195 @@ @@ -0,0 +1,195 @@
// 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.
#if DEBUG
using System;
using System.Collections.Generic;
using System.Composition;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ILSpy.AppEnv;
using ILSpy.Docking;
using ILSpy.TextView;
using ILSpy.ViewModels;
using SRM = System.Reflection.Metadata;
namespace ILSpy.Languages
{
/// <summary>
/// Debug-only language that surfaces the decompiler pipeline's intermediate state.
/// Two concrete variants ship: <see cref="TypedIL"/> renders raw IL with type
/// annotations; <see cref="BlockIL"/> runs the decompiler's IL transforms with a
/// <see cref="Stepper"/> attached so the Debug Steps pane can replay each transform.
/// Compiled only when DEBUG is defined — the language list is identical to Release
/// otherwise.
/// </summary>
public abstract class ILAstLanguage : Language
{
readonly string name;
protected ILAstLanguage(string name)
{
this.name = name;
}
/// <summary>
/// Fires after a <see cref="DecompileMethod"/> run installs a fresh <see cref="Stepper"/>.
/// The Debug Steps pane subscribes here so it can rebind its TreeView to the new
/// step list whenever the user reruns decompilation.
/// </summary>
public event EventHandler? StepperUpdated;
protected virtual void OnStepperUpdated(EventArgs? e = null)
=> StepperUpdated?.Invoke(this, e ?? EventArgs.Empty);
public Stepper Stepper { get; set; } = new();
public override string Name => name;
public override string FileExtension => ".il";
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options)
{
base.DecompileMethod(method, output, options);
new ReflectionDisassembler(output, options.CancellationToken)
.DisassembleMethodHeader(method.ParentModule!.MetadataFile, (SRM.MethodDefinitionHandle)method.MetadataToken);
output.WriteLine();
output.WriteLine();
}
}
/// <summary>
/// Raw IL with type annotations on each instruction. No transforms, no stepper — the
/// Debug Steps pane stays empty under this language because there's nothing to step
/// through. Useful as a sanity-check view for the IL reader itself.
/// </summary>
[Export(typeof(Language))]
[Shared]
public sealed class TypedILLanguage : ILAstLanguage
{
public TypedILLanguage() : base("Typed IL") { }
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options)
{
base.DecompileMethod(method, output, options);
var module = method.ParentModule!.MetadataFile!;
var methodDef = module.Metadata.GetMethodDefinition((SRM.MethodDefinitionHandle)method.MetadataToken);
if (!methodDef.HasBody())
return;
var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver());
var reader = new ILReader(typeSystem.MainModule);
var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress);
reader.WriteTypedIL((SRM.MethodDefinitionHandle)method.MetadataToken, methodBody, output, cancellationToken: options.CancellationToken);
}
}
/// <summary>
/// Runs the full C# decompiler's IL transforms and writes the resulting <see cref="ILFunction"/>.
/// Each transform is recorded by a <see cref="Stepper"/> hooked into the
/// <see cref="ILTransformContext"/>; the resulting step tree is what the Debug Steps
/// pane visualises. The "Show Steps" button on the output reveals the pane.
/// </summary>
[Export(typeof(Language))]
[Shared]
public sealed class BlockILLanguage : ILAstLanguage
{
readonly IReadOnlyList<IILTransform> transforms;
public BlockILLanguage() : base("ILAst")
{
this.transforms = CSharpDecompiler.GetILTransforms();
}
// DockWorkspace is resolved lazily — taking it as an [ImportingConstructor] argument
// creates a composition cycle (DockWorkspace already imports LanguageService, which
// imports the registered Languages). Lazy lookup at decompile time breaks the cycle
// and Costs only one MEF GetExport call per "Show Steps" button click.
DockWorkspace? TryGetDockWorkspace()
{
try
{ return AppComposition.Current.GetExport<DockWorkspace>(); }
catch { return null; }
}
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options)
{
base.DecompileMethod(method, output, options);
var module = method.ParentModule!.MetadataFile!;
var metadata = module.Metadata;
var methodDef = metadata.GetMethodDefinition((SRM.MethodDefinitionHandle)method.MetadataToken);
if (!methodDef.HasBody())
return;
IAssemblyResolver assemblyResolver = module.GetAssemblyResolver();
var typeSystem = new DecompilerTypeSystem(module, assemblyResolver);
var reader = new ILReader(typeSystem.MainModule) {
UseDebugSymbols = options.DecompilerSettings.UseDebugSymbols,
UseRefLocalsForAccurateOrderOfEvaluation = options.DecompilerSettings.UseRefLocalsForAccurateOrderOfEvaluation,
};
var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress);
ILFunction il = reader.ReadIL((SRM.MethodDefinitionHandle)method.MetadataToken, methodBody,
kind: ILFunctionKind.TopLevelFunction, cancellationToken: options.CancellationToken);
var decompiler = new CSharpDecompiler(typeSystem, options.DecompilerSettings) { CancellationToken = options.CancellationToken };
ILTransformContext context = decompiler.CreateILTransformContext(il);
context.Stepper.StepLimit = options.StepLimit;
context.Stepper.IsDebug = options.IsDebug;
try
{
il.RunTransforms(transforms, context);
}
catch (StepLimitReachedException)
{
// Expected when the Debug Steps pane asked to halt after a specific step.
}
catch (Exception ex)
{
output.WriteLine(ex.ToString());
output.WriteLine();
output.WriteLine("ILAst after the crash:");
}
finally
{
// Capture the populated stepper even when a transform crashed, so the user
// can see the partial step tree leading up to the failure point. Only when
// the run was full-fidelity (no StepLimit) — partial runs leave the stepper
// alone so the previously-shown tree stays visible.
if (options.StepLimit == int.MaxValue)
{
Stepper = context.Stepper;
OnStepperUpdated();
}
}
(output as ISmartTextOutput)?.AddButton(Images.Images.ViewCode, "Show Steps", delegate {
TryGetDockWorkspace()?.ShowToolPane(DebugStepsPaneModel.PaneContentId);
});
output.WriteLine();
il.WriteTo(output, DebugStepsPaneModel.WritingOptions);
}
}
}
#endif

16
ILSpy/SettingsService.cs

@ -16,12 +16,14 @@ @@ -16,12 +16,14 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.ComponentModel;
using System.Composition;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.Settings;
using ILSpy.Options;
using ILSpy.Util;
namespace ILSpy
{
@ -33,6 +35,20 @@ namespace ILSpy @@ -33,6 +35,20 @@ namespace ILSpy
{
}
/// <summary>
/// Override the base's per-section PropertyChanged forwarder to also fan out via
/// <see cref="MessageBus{T}"/>. Panes (Debug Steps, future updater banner, …) that
/// react to settings changes without holding a reference to the SettingsService
/// subscribe on <see cref="SettingsChangedEventArgs"/>. The sender is the section
/// instance (DecompilerSettings / DisplaySettings / LanguageSettings / …) so
/// subscribers can dispatch on which section changed.
/// </summary>
protected override void Section_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
base.Section_PropertyChanged(sender, e);
MessageBus.Send(sender, new SettingsChangedEventArgs(e));
}
public SessionSettings SessionSettings => GetSettings<SessionSettings>();
public DecompilerSettings DecompilerSettings => GetSettings<DecompilerSettings>();

39
ILSpy/TextView/DecompilerTabPageModel.cs

@ -238,6 +238,26 @@ namespace ILSpy.TextView @@ -238,6 +238,26 @@ namespace ILSpy.TextView
public Language Language { get; set; } = null!;
// Per-run overrides consumed by DecompileAsync. Reset to defaults at the start of
// every run; set non-default by RestartDecompileWithStepLimit before kicking off a
// debug-stepper decompile. Set on the UI thread only — no inter-thread access.
int pendingStepLimit = int.MaxValue;
bool pendingIsDebug;
/// <summary>
/// Force a fresh decompile of the same nodes with the IL-transform pipeline halted
/// after <paramref name="stepLimit"/> steps. Used by the Debug Steps pane to render
/// the partial IL after a chosen step (or full pipeline when <paramref name="stepLimit"/>
/// is <see cref="int.MaxValue"/>). <paramref name="isDebug"/> toggles the transforms'
/// verbose-debug emission. No-op when there's nothing currently being decompiled.
/// </summary>
public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug)
{
pendingStepLimit = stepLimit;
pendingIsDebug = isDebug;
_ = DecompileAsync();
}
async Task DecompileAsync()
{
activeCts?.Cancel();
@ -266,11 +286,26 @@ namespace ILSpy.TextView @@ -266,11 +286,26 @@ namespace ILSpy.TextView
// reach the next decompile without restart. Cloning isolates this run's
// settings from concurrent edits in an open Options tab.
var decompilerSettings = TryGetLiveDecompilerSettings();
// Snapshot the per-run debug-stepper overrides so the inner closure sees a
// stable value, and reset the fields to defaults so the NEXT decompile runs
// at full fidelity unless RestartDecompileWithStepLimit sets them again.
var stepLimit = pendingStepLimit;
var isDebug = pendingIsDebug;
pendingStepLimit = int.MaxValue;
pendingIsDebug = false;
var (output, _) = await Task.Run(() => {
var output = new AvaloniaEditTextOutput();
var options = decompilerSettings != null
? new DecompilationOptions(decompilerSettings) { CancellationToken = cts.Token }
: new DecompilationOptions { CancellationToken = cts.Token };
? new DecompilationOptions(decompilerSettings) {
CancellationToken = cts.Token,
StepLimit = stepLimit,
IsDebug = isDebug,
}
: new DecompilationOptions {
CancellationToken = cts.Token,
StepLimit = stepLimit,
IsDebug = isDebug,
};
try
{
for (int i = 0; i < nodes.Count; i++)

63
ILSpy/ViewModels/DebugStepsPaneModel.cs

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
// 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.
#if DEBUG
using System.Composition;
using ICSharpCode.Decompiler.IL;
using ILSpy.Commands;
namespace ILSpy.ViewModels
{
/// <summary>
/// Bottom-aligned tool pane that surfaces the Stepper output from
/// <see cref="Languages.BlockILLanguage"/>. Compiled only in Debug builds — Release
/// users don't see the pane or the languages that populate it.
/// </summary>
[Export]
[ExportToolPane(ContentId = PaneContentId, Alignment = ToolPaneAlignment.Bottom, Order = 1, IsVisibleByDefault = false)]
[Shared]
public sealed class DebugStepsPaneModel : ToolPaneModel
{
public const string PaneContentId = "DebugSteps";
/// <summary>
/// App-wide ILAst writing options shared between the BlockIL language (which reads
/// them while emitting the transformed IL) and the DebugSteps view (whose four
/// checkboxes toggle their values). Static singleton state because the language is
/// MEF-shared and decompiles on background tasks that have no view-model reference.
/// </summary>
public static ILAstWritingOptions WritingOptions { get; } = new() {
UseFieldSugar = true,
UseLogicOperationSugar = true,
};
public DebugStepsPaneModel()
{
Id = PaneContentId;
Title = "Debug Steps";
}
/// <summary>Instance accessor for XAML binding (Avalonia's static-source binding is awkward).</summary>
public ILAstWritingOptions Options => WritingOptions;
}
}
#endif

41
ILSpy/Views/DebugSteps.axaml

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ILSpy.ViewModels"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300"
x:Class="ILSpy.Views.DebugSteps"
x:DataType="vm:DebugStepsPaneModel">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="2">
<CheckBox Margin="3" Content="Field sugar"
IsChecked="{Binding Options.UseFieldSugar, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Logic operation sugar"
IsChecked="{Binding Options.UseLogicOperationSugar, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show IL ranges"
IsChecked="{Binding Options.ShowILRanges, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show child index in block"
IsChecked="{Binding Options.ShowChildIndexInBlock, Mode=TwoWay}" />
</StackPanel>
<TreeView Name="StepsTree"
DoubleTapped="OnShowStateAfter"
KeyDown="OnStepsTreeKeyDown"
x:CompileBindings="False">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Description}" />
</TreeDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Show state before this step (Shift+Enter)"
Click="OnShowStateBefore" />
<MenuItem Header="Show state after this step (Enter)"
Click="OnShowStateAfter" />
<MenuItem Header="Debug this step"
Click="OnDebugStep" />
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</DockPanel>
</UserControl>

171
ILSpy/Views/DebugSteps.axaml.cs

@ -0,0 +1,171 @@ @@ -0,0 +1,171 @@
// 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.
#if DEBUG
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using ICSharpCode.Decompiler.IL.Transforms;
using ILSpy.AppEnv;
using ILSpy.Docking;
using ILSpy.Languages;
using ILSpy.TextView;
using ILSpy.Util;
using ILSpy.ViewModels;
namespace ILSpy.Views
{
public partial class DebugSteps : UserControl
{
ILAstLanguage? activeLanguage;
LanguageService? languageService;
int lastSelectedStep = int.MaxValue;
public DebugSteps()
{
InitializeComponent();
MessageBus<AssemblyTreeSelectionChangedEventArgs>.Subscribers += OnSelectionChanged;
DebugStepsPaneModel.WritingOptions.PropertyChanged += OnWritingOptionsChanged;
// Language flips go through LanguageService.CurrentLanguage (not through
// LanguageSettings.LanguageId), so subscribe directly here — the MessageBus path
// would only cover settings-section changes.
languageService = TryGetExport<LanguageService>();
if (languageService != null)
languageService.PropertyChanged += OnLanguageServiceChanged;
TryAttachToCurrentLanguage();
}
void OnLanguageServiceChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LanguageService.CurrentLanguage))
Dispatcher.UIThread.Post(TryAttachToCurrentLanguage);
}
void TryAttachToCurrentLanguage()
{
var languageService = TryGetExport<LanguageService>();
if (languageService?.CurrentLanguage is ILAstLanguage il)
AttachToLanguage(il);
}
void AttachToLanguage(ILAstLanguage language)
{
DetachFromLanguage();
activeLanguage = language;
language.StepperUpdated += OnStepperUpdated;
RebindStepsTree(language.Stepper);
}
void DetachFromLanguage()
{
if (activeLanguage != null)
{
activeLanguage.StepperUpdated -= OnStepperUpdated;
activeLanguage = null;
}
}
void OnStepperUpdated(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(() => {
if (activeLanguage != null)
RebindStepsTree(activeLanguage.Stepper);
lastSelectedStep = int.MaxValue;
});
}
void RebindStepsTree(Stepper stepper)
{
StepsTree.ItemsSource = stepper.Steps;
}
void OnSelectionChanged(object? sender, AssemblyTreeSelectionChangedEventArgs e)
{
// When the user picks a new tree node, the previous run's stepper is stale —
// blank the tree until the next decompile populates it.
Dispatcher.UIThread.Post(() => {
StepsTree.ItemsSource = null;
lastSelectedStep = int.MaxValue;
});
}
void OnWritingOptionsChanged(object? sender, PropertyChangedEventArgs e)
{
// Toggling a checkbox should refresh the editor view so the user sees the effect
// immediately. Triggers the same path as picking "Show state after this step" on
// whatever step was last viewed (defaults to int.MaxValue → full run).
RequestRedecompile(lastSelectedStep, isDebug: false);
}
void OnShowStateBefore(object? sender, RoutedEventArgs e)
{
if (StepsTree.SelectedItem is Stepper.Node node)
RequestRedecompile(node.BeginStep, isDebug: false);
}
void OnShowStateAfter(object? sender, RoutedEventArgs e)
{
if (StepsTree.SelectedItem is Stepper.Node node)
RequestRedecompile(node.EndStep, isDebug: false);
}
void OnDebugStep(object? sender, RoutedEventArgs e)
{
if (StepsTree.SelectedItem is Stepper.Node node)
RequestRedecompile(node.BeginStep, isDebug: true);
}
void OnStepsTreeKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key != Key.Enter && e.Key != Key.Return)
return;
if ((e.KeyModifiers & KeyModifiers.Shift) != 0)
OnShowStateBefore(sender, e);
else
OnShowStateAfter(sender, e);
e.Handled = true;
}
void RequestRedecompile(int stepLimit, bool isDebug)
{
lastSelectedStep = stepLimit;
var activeTab = TryGetExport<DockWorkspace>()?.ActiveDecompilerTab;
activeTab?.RestartDecompileWithStepLimit(stepLimit, isDebug);
}
static T? TryGetExport<T>() where T : class
{
try
{ return AppComposition.Current.GetExport<T>(); }
catch
{ return null; }
}
}
}
#endif
Loading…
Cancel
Save