diff --git a/ILSpy.Tests/Docking/DockWorkspaceTests.cs b/ILSpy.Tests/Docking/DockWorkspaceTests.cs
index 9c204317e..17058d688 100644
--- a/ILSpy.Tests/Docking/DockWorkspaceTests.cs
+++ b/ILSpy.Tests/Docking/DockWorkspaceTests.cs
@@ -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
}
}
diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs
new file mode 100644
index 000000000..ad8bff3bb
--- /dev/null
+++ b/ILSpy.Tests/Views/DebugStepsTests.cs
@@ -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;
+
+///
+/// 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.
+///
+[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();
+ ((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.Languages.OfType().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
diff --git a/ILSpy/App.axaml b/ILSpy/App.axaml
index 35c2d3dc2..417cd49cb 100644
--- a/ILSpy/App.axaml
+++ b/ILSpy/App.axaml
@@ -54,6 +54,9 @@
+
+
+
diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
index 1eaa0e09e..2c5a38e6c 100644
--- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -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
diff --git a/ILSpy/Assets/Icons/ViewCode.svg b/ILSpy/Assets/Icons/ViewCode.svg
new file mode 100644
index 000000000..7c0f3e087
--- /dev/null
+++ b/ILSpy/Assets/Icons/ViewCode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs
index b74f3cc9d..4354c314d 100644
--- a/ILSpy/DecompilationOptions.cs
+++ b/ILSpy/DecompilationOptions.cs
@@ -42,6 +42,21 @@ namespace ILSpy
/// Escape invalid C# identifiers so the output compiles.
public bool EscapeInvalidIdentifiers { get; set; }
+ ///
+ /// Stop the IL-transform pipeline after this many steps.
+ /// 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
+ /// ; ignored by every other language.
+ ///
+ public int StepLimit { get; set; } = int.MaxValue;
+
+ ///
+ /// When true, transforms emit verbose debug information about their behaviour. Only
+ /// meaningful in combination with — the Debug Steps pane sets
+ /// it on the "Debug this step" context-menu action.
+ ///
+ public bool IsDebug { get; set; }
+
public DecompilationOptions(DecompilerSettings settings)
{
DecompilerSettings = settings;
diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj
index c312dcdeb..e7006966c 100644
--- a/ILSpy/ILSpy.csproj
+++ b/ILSpy/ILSpy.csproj
@@ -75,4 +75,13 @@
+
+
+
+
+
diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs
index d9969966f..e7ffb5f83 100644
--- a/ILSpy/Images.cs
+++ b/ILSpy/Images.cs
@@ -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));
diff --git a/ILSpy/Languages/ILAstLanguage.cs b/ILSpy/Languages/ILAstLanguage.cs
new file mode 100644
index 000000000..15e392094
--- /dev/null
+++ b/ILSpy/Languages/ILAstLanguage.cs
@@ -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
+{
+ ///
+ /// Debug-only language that surfaces the decompiler pipeline's intermediate state.
+ /// Two concrete variants ship: renders raw IL with type
+ /// annotations; runs the decompiler's IL transforms with a
+ /// attached so the Debug Steps pane can replay each transform.
+ /// Compiled only when DEBUG is defined — the language list is identical to Release
+ /// otherwise.
+ ///
+ public abstract class ILAstLanguage : Language
+ {
+ readonly string name;
+
+ protected ILAstLanguage(string name)
+ {
+ this.name = name;
+ }
+
+ ///
+ /// Fires after a run installs a fresh .
+ /// The Debug Steps pane subscribes here so it can rebind its TreeView to the new
+ /// step list whenever the user reruns decompilation.
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ [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);
+ }
+ }
+
+ ///
+ /// Runs the full C# decompiler's IL transforms and writes the resulting .
+ /// Each transform is recorded by a hooked into the
+ /// ; the resulting step tree is what the Debug Steps
+ /// pane visualises. The "Show Steps" button on the output reveals the pane.
+ ///
+ [Export(typeof(Language))]
+ [Shared]
+ public sealed class BlockILLanguage : ILAstLanguage
+ {
+ readonly IReadOnlyList 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(); }
+ 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
diff --git a/ILSpy/SettingsService.cs b/ILSpy/SettingsService.cs
index 3ec4d6584..d84c1b9cf 100644
--- a/ILSpy/SettingsService.cs
+++ b/ILSpy/SettingsService.cs
@@ -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
{
}
+ ///
+ /// Override the base's per-section PropertyChanged forwarder to also fan out via
+ /// . Panes (Debug Steps, future updater banner, …) that
+ /// react to settings changes without holding a reference to the SettingsService
+ /// subscribe on . The sender is the section
+ /// instance (DecompilerSettings / DisplaySettings / LanguageSettings / …) so
+ /// subscribers can dispatch on which section changed.
+ ///
+ protected override void Section_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ base.Section_PropertyChanged(sender, e);
+ MessageBus.Send(sender, new SettingsChangedEventArgs(e));
+ }
+
public SessionSettings SessionSettings => GetSettings();
public DecompilerSettings DecompilerSettings => GetSettings();
diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs
index 1bfc4d6d2..221cb22c4 100644
--- a/ILSpy/TextView/DecompilerTabPageModel.cs
+++ b/ILSpy/TextView/DecompilerTabPageModel.cs
@@ -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;
+
+ ///
+ /// Force a fresh decompile of the same nodes with the IL-transform pipeline halted
+ /// after steps. Used by the Debug Steps pane to render
+ /// the partial IL after a chosen step (or full pipeline when
+ /// is ). toggles the transforms'
+ /// verbose-debug emission. No-op when there's nothing currently being decompiled.
+ ///
+ public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug)
+ {
+ pendingStepLimit = stepLimit;
+ pendingIsDebug = isDebug;
+ _ = DecompileAsync();
+ }
+
async Task DecompileAsync()
{
activeCts?.Cancel();
@@ -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++)
diff --git a/ILSpy/ViewModels/DebugStepsPaneModel.cs b/ILSpy/ViewModels/DebugStepsPaneModel.cs
new file mode 100644
index 000000000..832936d52
--- /dev/null
+++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs
@@ -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
+{
+ ///
+ /// Bottom-aligned tool pane that surfaces the Stepper output from
+ /// . Compiled only in Debug builds — Release
+ /// users don't see the pane or the languages that populate it.
+ ///
+ [Export]
+ [ExportToolPane(ContentId = PaneContentId, Alignment = ToolPaneAlignment.Bottom, Order = 1, IsVisibleByDefault = false)]
+ [Shared]
+ public sealed class DebugStepsPaneModel : ToolPaneModel
+ {
+ public const string PaneContentId = "DebugSteps";
+
+ ///
+ /// 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.
+ ///
+ public static ILAstWritingOptions WritingOptions { get; } = new() {
+ UseFieldSugar = true,
+ UseLogicOperationSugar = true,
+ };
+
+ public DebugStepsPaneModel()
+ {
+ Id = PaneContentId;
+ Title = "Debug Steps";
+ }
+
+ /// Instance accessor for XAML binding (Avalonia's static-source binding is awkward).
+ public ILAstWritingOptions Options => WritingOptions;
+ }
+}
+
+#endif
diff --git a/ILSpy/Views/DebugSteps.axaml b/ILSpy/Views/DebugSteps.axaml
new file mode 100644
index 000000000..72d183359
--- /dev/null
+++ b/ILSpy/Views/DebugSteps.axaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ILSpy/Views/DebugSteps.axaml.cs b/ILSpy/Views/DebugSteps.axaml.cs
new file mode 100644
index 000000000..cceff6aac
--- /dev/null
+++ b/ILSpy/Views/DebugSteps.axaml.cs
@@ -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.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();
+ 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();
+ 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()?.ActiveDecompilerTab;
+ activeTab?.RestartDecompileWithStepLimit(stepLimit, isDebug);
+ }
+
+ static T? TryGetExport() where T : class
+ {
+ try
+ { return AppComposition.Current.GetExport(); }
+ catch
+ { return null; }
+ }
+ }
+}
+
+#endif