mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
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
14 changed files with 647 additions and 2 deletions
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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> |
||||||
@ -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…
Reference in new issue