@ -18,26 +18,48 @@
@@ -18,26 +18,48 @@
#if DEBUG
using System.Collections.Generic ;
using System.ComponentModel ;
using System.Composition ;
using Avalonia.Threading ;
using CommunityToolkit.Mvvm.ComponentModel ;
using CommunityToolkit.Mvvm.Input ;
using ICSharpCode.Decompiler.IL ;
using ICSharpCode.Decompiler.IL.Transforms ;
using ILSpy.AppEnv ;
using ILSpy.Commands ;
using ILSpy.Docking ;
using ILSpy.Languages ;
using ILSpy.Util ;
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.
/// <see cref="Languages.BlockILLanguage"/>. The ViewModel owns the cross-language /
/// cross-decompile state (active language, current Stepper.Steps list) so it doesn't
/// matter when the matching View materialises — the View just binds to <see cref="Steps"/>
/// and lights up whenever the language switches to ILAst and a decompile finishes.
///
/// 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 sealed partial class DebugStepsPaneModel : ToolPaneModel
{
public const string PaneContentId = "DebugSteps" ;
readonly LanguageService ? languageService ;
ILAstLanguage ? activeLanguage ;
int lastSelectedStep = int . MaxValue ;
/// <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
@ -49,14 +71,139 @@ namespace ILSpy.ViewModels
@@ -49,14 +71,139 @@ namespace ILSpy.ViewModels
UseLogicOperationSugar = true ,
} ;
/// <summary>Instance accessor for XAML binding (Avalonia's static-source binding is awkward).</summary>
public ILAstWritingOptions Options = > WritingOptions ;
/// <summary>
/// Currently displayed list of recorded transform steps. Re-assigned (not mutated)
/// whenever the active ILAstLanguage's <see cref="Stepper"/> reports a new run, so
/// late-binding views pick up the latest list via the observable change.
/// </summary>
[ObservableProperty]
IList < Stepper . Node > ? steps ;
/// <summary>Two-way bound to the TreeView's selected item.</summary>
[ObservableProperty]
Stepper . Node ? selectedStep ;
public IRelayCommand ShowStateBeforeCommand { get ; }
public IRelayCommand ShowStateAfterCommand { get ; }
public IRelayCommand DebugStepCommand { get ; }
/// <summary>Design-time / fallback ctor — no dependencies wired.</summary>
public DebugStepsPaneModel ( )
{
Id = PaneContentId ;
Title = "Debug Steps" ;
ShowStateBeforeCommand = new RelayCommand ( ( ) = > RequestRedecompile ( SelectedStep ? . BeginStep ? ? int . MaxValue , isDebug : false ) ) ;
ShowStateAfterCommand = new RelayCommand ( ( ) = > RequestRedecompile ( SelectedStep ? . EndStep ? ? int . MaxValue , isDebug : false ) ) ;
DebugStepCommand = new RelayCommand ( ( ) = > RequestRedecompile ( SelectedStep ? . BeginStep ? ? int . MaxValue , isDebug : true ) ) ;
}
/// <summary>Instance accessor for XAML binding (Avalonia's static-source binding is awkward).</summary>
public ILAstWritingOptions Options = > WritingOptions ;
[ImportingConstructor]
public DebugStepsPaneModel ( LanguageService languageService ) : this ( )
{
this . languageService = languageService ;
// DockWorkspace is resolved lazily inside RequestRedecompile — taking it as an
// ImportingConstructor argument creates a MEF cycle (DockWorkspace imports
// ToolPaneRegistry which materialises this VM which would import DockWorkspace).
// Lazy lookup at command-execution time breaks the cycle.
// Language flips go through LanguageService.CurrentLanguage; the BlockILLanguage
// pumps StepperUpdated when its decompile finishes. The selection-changed event
// is the signal that the user picked a new tree node — clear the step list so
// the previous run's nodes aren't shown against a fresh selection. All three
// subscriptions live here in the long-lived VM so the View's lifecycle (lazy /
// deferred / re-realised) can't lose them.
languageService . PropertyChanged + = OnLanguageServiceChanged ;
MessageBus < AssemblyTreeSelectionChangedEventArgs > . Subscribers + = OnSelectionChanged ;
WritingOptions . PropertyChanged + = OnWritingOptionsChanged ;
TryAttachToCurrentLanguage ( ) ;
}
void OnLanguageServiceChanged ( object? sender , PropertyChangedEventArgs e )
{
if ( e . PropertyName = = nameof ( LanguageService . CurrentLanguage ) )
Dispatcher . UIThread . Post ( TryAttachToCurrentLanguage ) ;
}
void TryAttachToCurrentLanguage ( )
{
if ( languageService ? . CurrentLanguage is ILAstLanguage il )
AttachToLanguage ( il ) ;
else
DetachFromLanguage ( ) ;
}
void AttachToLanguage ( ILAstLanguage language )
{
if ( ReferenceEquals ( activeLanguage , language ) )
{
// Same language instance — just refresh the steps in case a decompile happened
// while we were detached.
Steps = activeLanguage ! . Stepper . Steps ;
return ;
}
DetachFromLanguage ( ) ;
activeLanguage = language ;
language . StepperUpdated + = OnStepperUpdated ;
Steps = language . Stepper . Steps ;
}
void DetachFromLanguage ( )
{
if ( activeLanguage ! = null )
{
activeLanguage . StepperUpdated - = OnStepperUpdated ;
activeLanguage = null ;
}
}
void OnStepperUpdated ( object? sender , System . EventArgs e )
{
Dispatcher . UIThread . Post ( ( ) = > {
if ( activeLanguage ! = null )
{
Steps = activeLanguage . Stepper . Steps ;
lastSelectedStep = int . MaxValue ;
}
} ) ;
}
void OnSelectionChanged ( object? sender , AssemblyTreeSelectionChangedEventArgs e )
{
// User picked a new tree node — the previous run's stepper is stale until the
// next decompile populates it.
Dispatcher . UIThread . Post ( ( ) = > {
Steps = 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 RequestRedecompile ( int stepLimit , bool isDebug )
{
lastSelectedStep = stepLimit ;
DockWorkspace ? dock ;
try
{
dock = AppComposition . Current . GetExport < DockWorkspace > ( ) ;
}
catch
{
// Composition unavailable in design-time previews; the gesture is a no-op there.
return ;
}
dock . ActiveDecompilerTab ? . RestartDecompileWithStepLimit ( stepLimit , isDebug ) ;
}
}
}