Browse Source

Add Debug Steps support for the C# AST pipeline

The Debug Steps pane previously only served the ILAst language (one node per
IL transform). The C# decompiler's intermediate AST states were instead
exposed as a row of extra "C# - after TRANSFORM" dropdown languages, which
cluttered the language list and only let you jump to one fixed step at a time.

Generalise the pane to an IDebugStepProvider interface: ILAst keeps its IL
steps, and the C# language now contributes one step per AST transform, mapping
a selected step onto options.StepLimit (already honoured by CreateDecompiler).
Each language also supplies its own options object, so the writing-options
checkboxes show for ILAst and nothing for C#. The per-transform dropdown
languages are removed in favour of this.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 4 weeks ago
parent
commit
4ae0b18cd8
  1. 86
      ILSpy.Tests/Languages/CSharpDebugTransformLanguagesTests.cs
  2. 84
      ILSpy/Languages/CSharpLanguage.DebugSteps.cs
  3. 75
      ILSpy/Languages/CSharpLanguage.cs
  4. 52
      ILSpy/Languages/IDebugStepProvider.cs
  5. 5
      ILSpy/Languages/ILAstLanguage.cs
  6. 8
      ILSpy/Languages/LanguageService.cs
  7. 28
      ILSpy/ViewModels/DebugStepsPaneModel.cs
  8. 29
      ILSpy/Views/DebugSteps.axaml

86
ILSpy.Tests/Languages/CSharpDebugTransformLanguagesTests.cs

@ -1,86 +0,0 @@
// 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 Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.Decompiler.CSharp;
using ILSpy.AppEnv;
using ILSpy.Languages;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Languages;
/// <summary>
/// Pins the WPF parity feature: in Debug builds the C# language pipeline contributes one
/// extra language per AST transform, named <c>"C# - no transforms"</c>, <c>"C# - after
/// <em>TransformName</em>"</c>, … so a developer can pick the dropdown entry and see the
/// decompiler's intermediate AST at that stage. Each variant sets <c>showAllMembers</c>
/// so compiler-generated members aren't hidden — visibility into the synthetic ones is
/// the whole point of the feature.
/// </summary>
[TestFixture]
public class CSharpDebugTransformLanguagesTests
{
[Test]
public void GetDebugLanguages_Yields_A_Pipeline_Step_Per_Ast_Transform_Plus_The_No_Transforms_Baseline()
{
var transforms = CSharpDecompiler.GetAstTransforms().ToList();
transforms.Should().NotBeEmpty(
"the decompiler must publish at least one AST transform — otherwise the debug-languages feature would be meaningless");
var debugLanguages = CSharpLanguage.GetDebugLanguages().ToList();
// One baseline ("no transforms"), one variant per transform, plus a final "after
// <last transform>" entry. So count = transforms.Count + 1.
debugLanguages.Should().HaveCount(transforms.Count + 1,
"one variant for the baseline + one per transform step (the last yields the fully-transformed AST)");
debugLanguages.Select(l => l.Name).Should().Contain("C# - no transforms",
"the first dropdown entry is the baseline before any transform runs");
// Spot-check the second entry is named after the first transform, matching WPF —
// each subsequent variant is named after the transform that just ran.
debugLanguages[1].Name.Should().Be("C# - after " + transforms[0].GetType().Name);
}
[AvaloniaTest]
public void LanguageService_Registers_The_Debug_Transform_Languages_In_The_Dropdown()
{
// MEF-side wiring: LanguageService aggregates [Export(Language)]-resolved instances
// plus the manually-yielded debug variants under #if DEBUG. Without the registration
// step the variants never reach the toolbar — even though GetDebugLanguages itself
// would return a populated list.
var languageService = AppComposition.Current.GetExport<LanguageService>();
var names = languageService.Languages.Select(l => l.Name).ToList();
names.Should().Contain("C# - no transforms",
"LanguageService must include the no-transforms baseline in its Languages list");
names.Should().Contain(n => n.StartsWith("C# - after "),
"LanguageService must include at least one 'after <TransformName>' variant");
}
}
#endif

84
ILSpy/Languages/CSharpLanguage.DebugSteps.cs

@ -0,0 +1,84 @@
// 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 ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.IL.Transforms;
using ILSpy.AppEnv;
using ILSpy.Docking;
using ILSpy.TextView;
using ILSpy.ViewModels;
namespace ILSpy.Languages
{
/// <summary>
/// Debug Steps support for the C# language: coarse, one step per AST transform, shown in the
/// Debug Steps pane like the ILAst language already does for IL transforms. The step list is
/// static -- the C# AST pipeline (<see cref="CSharpDecompiler.GetAstTransforms"/>) is the same
/// for every member -- so a selected step's index maps straight onto <see
/// cref="DecompilationOptions.StepLimit"/>, which CreateDecompiler turns into the number of AST
/// transforms to keep before re-rendering.
/// </summary>
partial class CSharpLanguage : IDebugStepProvider
{
Stepper? stepper;
public Stepper Stepper => stepper ??= BuildStepper();
public event EventHandler? StepperUpdated;
// The C# AST step view has no options of its own (yet); the pane shows nothing above the
// tree for C#, unlike ILAst's writing-options checkboxes.
public object? StepOptions => null;
// One node per AST transform, in pipeline order. Stepper.Step assigns BeginStep=i /
// EndStep=i+1 automatically, so "show state before step i" re-decompiles with StepLimit=i
// (run i transforms) and "after" with StepLimit=i+1 -- exactly the cap CreateDecompiler wants.
static Stepper BuildStepper()
{
var s = new Stepper();
foreach (var transform in CSharpDecompiler.GetAstTransforms())
s.Step(transform.GetType().Name);
return s;
}
partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options)
{
// The button always shows so the pane is one click away; mirrors the ILAst language.
// DockWorkspace is resolved lazily (an ImportingConstructor import would form a MEF
// cycle via LanguageService -> Languages).
(output as ISmartTextOutput)?.AddButton(Images.Images.ViewCode, "Show Steps", delegate {
AppComposition.TryGetExport<DockWorkspace>()?.ShowToolPane(DebugStepsPaneModel.PaneContentId);
});
// Only a full run refreshes the step list; a step-limited re-decompile (triggered by the
// pane itself) must leave the tree and the user's selection intact.
if (options.StepLimit == int.MaxValue)
{
_ = Stepper;
StepperUpdated?.Invoke(this, EventArgs.Empty);
}
}
}
}
#endif

75
ILSpy/Languages/CSharpLanguage.cs

@ -51,20 +51,9 @@ namespace ILSpy.Languages
{ {
[Export(typeof(Language))] [Export(typeof(Language))]
[Shared] [Shared]
public sealed class CSharpLanguage : Language public sealed partial class CSharpLanguage : Language
{ {
// Per-instance fields so the DEBUG-only AST-pipeline-step variants (see public override string Name => "C#";
// GetDebugLanguages below) can each carry their own name, transform-stop point, and
// member-visibility override without subclassing. The single MEF-resolved Release
// instance keeps the defaults: Name="C#", all transforms run, ShowMember honours
// the hide-compiler-generated filter.
string name = "C#";
int transformCount = int.MaxValue;
// Initialiser silences CS0649 in Release builds (where GetDebugLanguages is
// excluded by #if DEBUG, leaving no write site).
bool showAllMembers = false;
public override string Name => name;
public override string FileExtension => ".cs"; public override string FileExtension => ".cs";
@ -95,42 +84,6 @@ namespace ILSpy.Languages
new(CSharpLanguageVersion.CSharp15_0.ToString(), "C# 15.0 / VS 202x.yy"), new(CSharpLanguageVersion.CSharp15_0.ToString(), "C# 15.0 / VS 202x.yy"),
}; };
#if DEBUG
/// <summary>
/// Generates one additional <see cref="CSharpLanguage"/> per AST transform step in
/// the C# decompiler pipeline. The dropdown entries are named "C# - no transforms",
/// "C# - after <em>FirstTransformName</em>", … "C# - after <em>LastTransformName</em>".
/// Selecting a step makes <see cref="CreateDecompiler"/> stop running AST transforms
/// at that point, so the editor renders the AST as it looked mid-pipeline — handy
/// for diagnosing transform regressions. Each variant also flips
/// <see cref="showAllMembers"/> so compiler-generated members stay visible in the
/// tree even when a transform would normally have hidden them by this stage.
/// Compiled only under DEBUG; Release builds keep a single "C#" entry.
/// </summary>
internal static IEnumerable<CSharpLanguage> GetDebugLanguages()
{
string lastTransformName = "no transforms";
int transformCount = 0;
foreach (var transform in CSharpDecompiler.GetAstTransforms())
{
yield return new CSharpLanguage {
transformCount = transformCount,
name = "C# - " + lastTransformName,
showAllMembers = true,
};
lastTransformName = "after " + transform.GetType().Name;
transformCount++;
}
// One final variant whose transformCount equals the full transform list length —
// equivalent output to the regular "C#" language but with showAllMembers on, so
// the tree exposes everything that lived through the entire pipeline.
yield return new CSharpLanguage {
name = "C# - " + lastTransformName,
showAllMembers = true,
};
}
#endif
static CSharpAmbience CreateAmbience() => new() { static CSharpAmbience CreateAmbience() => new() {
ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList, ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList,
}; };
@ -304,10 +257,7 @@ namespace ILSpy.Languages
var assembly = member.ParentModule?.MetadataFile; var assembly = member.ParentModule?.MetadataFile;
if (assembly == null) if (assembly == null)
return true; return true;
// showAllMembers is set by the DEBUG pipeline-step variants — bypassing the return !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, new DecompilerSettings());
// hide-compiler-generated filter lets the developer see synthetic members
// (closure classes, fixed-buffer helpers, …) the transforms consume.
return showAllMembers || !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, new DecompilerSettings());
} }
public override RichText GetRichTextTooltip(IEntity entity) public override RichText GetRichTextTooltip(IEntity entity)
@ -329,11 +279,10 @@ namespace ILSpy.Languages
CancellationToken = options.CancellationToken, CancellationToken = options.CancellationToken,
DebugInfoProvider = module.GetDebugInfoOrNull(), DebugInfoProvider = module.GetDebugInfoOrNull(),
}; };
// Pop AST transforms from the end until the count matches transformCount. // The Debug Steps pane stops the AST pipeline at a chosen step by re-decompiling with
// transformCount is int.MaxValue for the regular C# language (no pops), but the // options.StepLimit = number of AST transforms to keep; pop the rest from the end.
// DEBUG pipeline-step variants set it to 0, 1, 2 … N to stop the AST mid-pipeline // StepLimit is int.MaxValue for a normal decompile, so nothing is removed.
// so the user sees the tree as it looked before/after a specific transform. while (decompiler.AstTransforms.Count > options.StepLimit)
while (decompiler.AstTransforms.Count > transformCount)
decompiler.AstTransforms.RemoveAt(decompiler.AstTransforms.Count - 1); decompiler.AstTransforms.RemoveAt(decompiler.AstTransforms.Count - 1);
if (options.EscapeInvalidIdentifiers) if (options.EscapeInvalidIdentifiers)
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
@ -368,13 +317,19 @@ namespace ILSpy.Languages
{ {
WriteCode(output, options.DecompilerSettings, decompiler.Decompile(method.MetadataToken), decompiler.TypeSystem); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(method.MetadataToken), decompiler.TypeSystem);
} }
OnCSharpDecompiled(output, options);
} }
// Implemented only under DEBUG (CSharpLanguage.DebugSteps.cs) to feed the Debug Steps pane;
// a no-op partial in Release.
partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options);
public override void DecompileProperty(IProperty property, ITextOutput output, DecompilationOptions options) public override void DecompileProperty(IProperty property, ITextOutput output, DecompilationOptions options)
{ {
CSharpDecompiler decompiler = BeginDecompile(property, output, options); CSharpDecompiler decompiler = BeginDecompile(property, output, options);
WriteCommentLine(output, TypeToString(property.DeclaringType)); WriteCommentLine(output, TypeToString(property.DeclaringType));
WriteCode(output, options.DecompilerSettings, decompiler.Decompile(property.MetadataToken), decompiler.TypeSystem); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(property.MetadataToken), decompiler.TypeSystem);
OnCSharpDecompiled(output, options);
} }
public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options) public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options)
@ -392,6 +347,7 @@ namespace ILSpy.Languages
decompiler.AstTransforms.Add(new SelectFieldTransform(resolvedField)); decompiler.AstTransforms.Add(new SelectFieldTransform(resolvedField));
WriteCode(output, options.DecompilerSettings, decompiler.Decompile(members), decompiler.TypeSystem); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(members), decompiler.TypeSystem);
} }
OnCSharpDecompiled(output, options);
} }
/// <summary> /// <summary>
@ -419,6 +375,7 @@ namespace ILSpy.Languages
WriteCommentLine(output, TypeToString(commentType, WriteCommentLine(output, TypeToString(commentType,
ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations)); ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations));
WriteCode(output, options.DecompilerSettings, decompiler.DecompileExtension(extension.MetadataToken), decompiler.TypeSystem); WriteCode(output, options.DecompilerSettings, decompiler.DecompileExtension(extension.MetadataToken), decompiler.TypeSystem);
OnCSharpDecompiled(output, options);
} }
public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options) public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options)
@ -426,6 +383,7 @@ namespace ILSpy.Languages
CSharpDecompiler decompiler = BeginDecompile(ev, output, options); CSharpDecompiler decompiler = BeginDecompile(ev, output, options);
WriteCommentLine(output, TypeToString(ev.DeclaringType)); WriteCommentLine(output, TypeToString(ev.DeclaringType));
WriteCode(output, options.DecompilerSettings, decompiler.Decompile(ev.MetadataToken), decompiler.TypeSystem); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(ev.MetadataToken), decompiler.TypeSystem);
OnCSharpDecompiled(output, options);
} }
public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options) public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options)
@ -433,6 +391,7 @@ namespace ILSpy.Languages
CSharpDecompiler decompiler = BeginDecompile(type, output, options); CSharpDecompiler decompiler = BeginDecompile(type, output, options);
WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames)); WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames));
WriteCode(output, options.DecompilerSettings, decompiler.Decompile(type.MetadataToken), decompiler.TypeSystem); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(type.MetadataToken), decompiler.TypeSystem);
OnCSharpDecompiled(output, options);
} }
public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options)

52
ILSpy/Languages/IDebugStepProvider.cs

@ -0,0 +1,52 @@
// 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 ICSharpCode.Decompiler.IL.Transforms;
namespace ILSpy.Languages
{
/// <summary>
/// A language that surfaces a <see cref="Stepper"/> of decompiler-pipeline steps for the
/// Debug Steps pane to visualise. Implemented by <see cref="ILAstLanguage"/> (one node per IL
/// transform step) and <see cref="CSharpLanguage"/> (one node per C# AST transform). The pane
/// binds to whichever current language is an <see cref="IDebugStepProvider"/>, so it no longer has to
/// know the concrete language type.
/// </summary>
internal interface IDebugStepProvider
{
/// <summary>The step tree produced by the most recent full decompile.</summary>
Stepper Stepper { get; }
/// <summary>Raised after a full (non step-limited) decompile refreshes <see cref="Stepper"/>.</summary>
event EventHandler? StepperUpdated;
/// <summary>
/// Language-specific options object the Debug Steps pane renders above the step tree
/// (e.g. ILAst's writing-options checkboxes), or <see langword="null"/> when the language
/// has no step options. The pane picks a template by the object's runtime type, so each
/// language contributes its own controls.
/// </summary>
object? StepOptions { get; }
}
}
#endif

5
ILSpy/Languages/ILAstLanguage.cs

@ -48,7 +48,7 @@ namespace ILSpy.Languages
/// Compiled only when DEBUG is defined — the language list is identical to Release /// Compiled only when DEBUG is defined — the language list is identical to Release
/// otherwise. /// otherwise.
/// </summary> /// </summary>
public abstract class ILAstLanguage : Language public abstract class ILAstLanguage : Language, IDebugStepProvider
{ {
readonly string name; readonly string name;
@ -74,6 +74,9 @@ namespace ILSpy.Languages
public Stepper Stepper { get; set; } = new(); public Stepper Stepper { get; set; } = new();
// ILAst contributes the shared writing-options checkboxes to the Debug Steps pane.
public object? StepOptions => DebugStepsPaneModel.WritingOptions;
public override string Name => name; public override string Name => name;
public override string FileExtension => ".il"; public override string FileExtension => ".il";

8
ILSpy/Languages/LanguageService.cs

@ -56,14 +56,6 @@ namespace ILSpy.Languages
List<Language> ordered; List<Language> ordered;
using (ILSpy.AppEnv.AppLog.Phase("LanguageService: materialise languages.OrderBy")) using (ILSpy.AppEnv.AppLog.Phase("LanguageService: materialise languages.OrderBy"))
ordered = languages.OrderBy(l => l.Name).ToList(); ordered = languages.OrderBy(l => l.Name).ToList();
#if DEBUG
// Under DEBUG, append one C# language per AST transform step so the dropdown
// surfaces the decompiler's intermediate output. Mirrors WPF's LanguageService —
// the variants aren't [Export]-ed because they're factory-built from the live
// CSharpDecompiler.GetAstTransforms() list, which only the static helper knows.
using (ILSpy.AppEnv.AppLog.Phase("LanguageService: CSharpLanguage.GetDebugLanguages()"))
ordered.AddRange(CSharpLanguage.GetDebugLanguages());
#endif
ILSpy.AppEnv.AppLog.Mark($"LanguageService: {ordered.Count} languages resolved"); ILSpy.AppEnv.AppLog.Mark($"LanguageService: {ordered.Count} languages resolved");
Languages = ordered; Languages = ordered;
var saved = settingsService.SessionSettings.ActiveLanguageName; var saved = settingsService.SessionSettings.ActiveLanguageName;

28
ILSpy/ViewModels/DebugStepsPaneModel.cs

@ -39,11 +39,12 @@ using ILSpy.Util;
namespace ILSpy.ViewModels namespace ILSpy.ViewModels
{ {
/// <summary> /// <summary>
/// Bottom-aligned tool pane that surfaces the Stepper output from /// Bottom-aligned tool pane that surfaces the step tree from the active
/// <see cref="Languages.BlockILLanguage"/>. The ViewModel owns the cross-language / /// <see cref="Languages.IDebugStepProvider"/> language — ILAst (one step per IL transform) or
/// cross-decompile state (active language, current Stepper.Steps list) so it doesn't /// C# (one step per AST transform). The ViewModel owns the cross-language / cross-decompile
/// state (active language, current Stepper.Steps list, per-language options) so it doesn't
/// matter when the matching View materialises — the View just binds to <see cref="Steps"/> /// 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. /// and lights up whenever the current language is a step provider and a decompile finishes.
/// ///
/// Compiled only in Debug builds — Release users don't see the pane or the languages /// Compiled only in Debug builds — Release users don't see the pane or the languages
/// that populate it. /// that populate it.
@ -57,7 +58,7 @@ namespace ILSpy.ViewModels
readonly LanguageService? languageService; readonly LanguageService? languageService;
ILAstLanguage? activeLanguage; IDebugStepProvider? activeLanguage;
int lastSelectedStep = int.MaxValue; int lastSelectedStep = int.MaxValue;
/// <summary> /// <summary>
@ -71,12 +72,17 @@ namespace ILSpy.ViewModels
UseLogicOperationSugar = true, UseLogicOperationSugar = true,
}; };
/// <summary>Instance accessor for XAML binding (Avalonia's static-source binding is awkward).</summary> /// <summary>
public ILAstWritingOptions Options => WritingOptions; /// Options controls for the active step-provider language (e.g. ILAst's writing options),
/// or null when the language has none. The view selects a template by runtime type, so the
/// options shown swap with the language.
/// </summary>
[ObservableProperty]
object? options;
/// <summary> /// <summary>
/// Currently displayed list of recorded transform steps. Re-assigned (not mutated) /// Currently displayed list of recorded transform steps. Re-assigned (not mutated)
/// whenever the active ILAstLanguage's <see cref="Stepper"/> reports a new run, so /// whenever the active step provider's <see cref="Stepper"/> reports a new run, so
/// late-binding views pick up the latest list via the observable change. /// late-binding views pick up the latest list via the observable change.
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty]
@ -143,13 +149,13 @@ namespace ILSpy.ViewModels
void TryAttachToCurrentLanguage() void TryAttachToCurrentLanguage()
{ {
if (languageService?.CurrentLanguage is ILAstLanguage il) if (languageService?.CurrentLanguage is IDebugStepProvider il)
AttachToLanguage(il); AttachToLanguage(il);
else else
DetachFromLanguage(); DetachFromLanguage();
} }
void AttachToLanguage(ILAstLanguage language) void AttachToLanguage(IDebugStepProvider language)
{ {
if (ReferenceEquals(activeLanguage, language)) if (ReferenceEquals(activeLanguage, language))
{ {
@ -162,6 +168,7 @@ namespace ILSpy.ViewModels
activeLanguage = language; activeLanguage = language;
language.StepperUpdated += OnStepperUpdated; language.StepperUpdated += OnStepperUpdated;
Steps = language.Stepper.Steps; Steps = language.Stepper.Steps;
Options = language.StepOptions;
} }
void DetachFromLanguage() void DetachFromLanguage()
@ -170,6 +177,7 @@ namespace ILSpy.ViewModels
{ {
activeLanguage.StepperUpdated -= OnStepperUpdated; activeLanguage.StepperUpdated -= OnStepperUpdated;
activeLanguage = null; activeLanguage = null;
Options = null;
} }
} }

29
ILSpy/Views/DebugSteps.axaml

@ -3,20 +3,29 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ILSpy.ViewModels" xmlns:vm="using:ILSpy.ViewModels"
xmlns:il="using:ICSharpCode.Decompiler.IL"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300"
x:Class="ILSpy.Views.DebugSteps" x:Class="ILSpy.Views.DebugSteps"
x:DataType="vm:DebugStepsPaneModel"> x:DataType="vm:DebugStepsPaneModel">
<DockPanel> <DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="2"> <!-- Options are contributed by the active step-provider language and selected by type:
<CheckBox Margin="3" Content="Field sugar" ILAst renders its writing-options checkboxes; C# (null Options) renders nothing. -->
IsChecked="{Binding Options.UseFieldSugar, Mode=TwoWay}" /> <ContentControl DockPanel.Dock="Top" Margin="2" Content="{Binding Options}">
<CheckBox Margin="3" Content="Logic operation sugar" <ContentControl.DataTemplates>
IsChecked="{Binding Options.UseLogicOperationSugar, Mode=TwoWay}" /> <DataTemplate DataType="il:ILAstWritingOptions">
<CheckBox Margin="3" Content="Show IL ranges" <StackPanel Orientation="Horizontal">
IsChecked="{Binding Options.ShowILRanges, Mode=TwoWay}" /> <CheckBox Margin="3" Content="Field sugar"
<CheckBox Margin="3" Content="Show child index in block" IsChecked="{Binding UseFieldSugar, Mode=TwoWay}" />
IsChecked="{Binding Options.ShowChildIndexInBlock, Mode=TwoWay}" /> <CheckBox Margin="3" Content="Logic operation sugar"
</StackPanel> IsChecked="{Binding UseLogicOperationSugar, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show IL ranges"
IsChecked="{Binding ShowILRanges, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show child index in block"
IsChecked="{Binding ShowChildIndexInBlock, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<TreeView Name="StepsTree" <TreeView Name="StepsTree"
ItemsSource="{Binding Steps}" ItemsSource="{Binding Steps}"
SelectedItem="{Binding SelectedStep, Mode=TwoWay}" SelectedItem="{Binding SelectedStep, Mode=TwoWay}"

Loading…
Cancel
Save