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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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