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