diff --git a/ILSpy.Tests/Languages/CSharpDebugTransformLanguagesTests.cs b/ILSpy.Tests/Languages/CSharpDebugTransformLanguagesTests.cs deleted file mode 100644 index 50ce213d8..000000000 --- a/ILSpy.Tests/Languages/CSharpDebugTransformLanguagesTests.cs +++ /dev/null @@ -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; - -/// -/// Pins the WPF parity feature: in Debug builds the C# language pipeline contributes one -/// extra language per AST transform, named "C# - no transforms", "C# - after -/// TransformName", … so a developer can pick the dropdown entry and see the -/// decompiler's intermediate AST at that stage. Each variant sets showAllMembers -/// so compiler-generated members aren't hidden — visibility into the synthetic ones is -/// the whole point of the feature. -/// -[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 - // " 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(); - 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 ' variant"); - } -} - -#endif diff --git a/ILSpy/Languages/CSharpLanguage.DebugSteps.cs b/ILSpy/Languages/CSharpLanguage.DebugSteps.cs new file mode 100644 index 000000000..2198f68b4 --- /dev/null +++ b/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 +{ + /// + /// 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 () is the same + /// for every member -- so a selected step's index maps straight onto , which CreateDecompiler turns into the number of AST + /// transforms to keep before re-rendering. + /// + 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()?.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 diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index c2a48f872..55f9e6811 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -51,20 +51,9 @@ namespace ILSpy.Languages { [Export(typeof(Language))] [Shared] - public sealed class CSharpLanguage : Language + public sealed partial class CSharpLanguage : Language { - // Per-instance fields so the DEBUG-only AST-pipeline-step variants (see - // 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 Name => "C#"; public override string FileExtension => ".cs"; @@ -95,42 +84,6 @@ namespace ILSpy.Languages new(CSharpLanguageVersion.CSharp15_0.ToString(), "C# 15.0 / VS 202x.yy"), }; -#if DEBUG - /// - /// Generates one additional per AST transform step in - /// the C# decompiler pipeline. The dropdown entries are named "C# - no transforms", - /// "C# - after FirstTransformName", … "C# - after LastTransformName". - /// Selecting a step makes 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 - /// 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. - /// - internal static IEnumerable 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() { ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList, }; @@ -304,10 +257,7 @@ namespace ILSpy.Languages var assembly = member.ParentModule?.MetadataFile; if (assembly == null) return true; - // showAllMembers is set by the DEBUG pipeline-step variants — bypassing the - // 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()); + return !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, new DecompilerSettings()); } public override RichText GetRichTextTooltip(IEntity entity) @@ -329,11 +279,10 @@ namespace ILSpy.Languages CancellationToken = options.CancellationToken, DebugInfoProvider = module.GetDebugInfoOrNull(), }; - // Pop AST transforms from the end until the count matches transformCount. - // transformCount is int.MaxValue for the regular C# language (no pops), but the - // DEBUG pipeline-step variants set it to 0, 1, 2 … N to stop the AST mid-pipeline - // so the user sees the tree as it looked before/after a specific transform. - while (decompiler.AstTransforms.Count > transformCount) + // The Debug Steps pane stops the AST pipeline at a chosen step by re-decompiling with + // options.StepLimit = number of AST transforms to keep; pop the rest from the end. + // StepLimit is int.MaxValue for a normal decompile, so nothing is removed. + while (decompiler.AstTransforms.Count > options.StepLimit) decompiler.AstTransforms.RemoveAt(decompiler.AstTransforms.Count - 1); if (options.EscapeInvalidIdentifiers) decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); @@ -368,13 +317,19 @@ namespace ILSpy.Languages { 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) { CSharpDecompiler decompiler = BeginDecompile(property, output, options); WriteCommentLine(output, TypeToString(property.DeclaringType)); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(property.MetadataToken), decompiler.TypeSystem); + OnCSharpDecompiled(output, options); } public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options) @@ -392,6 +347,7 @@ namespace ILSpy.Languages decompiler.AstTransforms.Add(new SelectFieldTransform(resolvedField)); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(members), decompiler.TypeSystem); } + OnCSharpDecompiled(output, options); } /// @@ -419,6 +375,7 @@ namespace ILSpy.Languages WriteCommentLine(output, TypeToString(commentType, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations)); WriteCode(output, options.DecompilerSettings, decompiler.DecompileExtension(extension.MetadataToken), decompiler.TypeSystem); + OnCSharpDecompiled(output, options); } public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options) @@ -426,6 +383,7 @@ namespace ILSpy.Languages CSharpDecompiler decompiler = BeginDecompile(ev, output, options); WriteCommentLine(output, TypeToString(ev.DeclaringType)); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(ev.MetadataToken), decompiler.TypeSystem); + OnCSharpDecompiled(output, options); } public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options) @@ -433,6 +391,7 @@ namespace ILSpy.Languages CSharpDecompiler decompiler = BeginDecompile(type, output, options); WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames)); WriteCode(output, options.DecompilerSettings, decompiler.Decompile(type.MetadataToken), decompiler.TypeSystem); + OnCSharpDecompiled(output, options); } public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) diff --git a/ILSpy/Languages/IDebugStepProvider.cs b/ILSpy/Languages/IDebugStepProvider.cs new file mode 100644 index 000000000..156011a1c --- /dev/null +++ b/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 +{ + /// + /// A language that surfaces a of decompiler-pipeline steps for the + /// Debug Steps pane to visualise. Implemented by (one node per IL + /// transform step) and (one node per C# AST transform). The pane + /// binds to whichever current language is an , so it no longer has to + /// know the concrete language type. + /// + internal interface IDebugStepProvider + { + /// The step tree produced by the most recent full decompile. + Stepper Stepper { get; } + + /// Raised after a full (non step-limited) decompile refreshes . + event EventHandler? StepperUpdated; + + /// + /// Language-specific options object the Debug Steps pane renders above the step tree + /// (e.g. ILAst's writing-options checkboxes), or 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. + /// + object? StepOptions { get; } + } +} + +#endif diff --git a/ILSpy/Languages/ILAstLanguage.cs b/ILSpy/Languages/ILAstLanguage.cs index 478377896..d509fa85b 100644 --- a/ILSpy/Languages/ILAstLanguage.cs +++ b/ILSpy/Languages/ILAstLanguage.cs @@ -48,7 +48,7 @@ namespace ILSpy.Languages /// Compiled only when DEBUG is defined — the language list is identical to Release /// otherwise. /// - public abstract class ILAstLanguage : Language + public abstract class ILAstLanguage : Language, IDebugStepProvider { readonly string name; @@ -74,6 +74,9 @@ namespace ILSpy.Languages 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 FileExtension => ".il"; diff --git a/ILSpy/Languages/LanguageService.cs b/ILSpy/Languages/LanguageService.cs index 5a333b21d..8eb50ba67 100644 --- a/ILSpy/Languages/LanguageService.cs +++ b/ILSpy/Languages/LanguageService.cs @@ -56,14 +56,6 @@ namespace ILSpy.Languages List ordered; using (ILSpy.AppEnv.AppLog.Phase("LanguageService: materialise languages.OrderBy")) 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"); Languages = ordered; var saved = settingsService.SessionSettings.ActiveLanguageName; diff --git a/ILSpy/ViewModels/DebugStepsPaneModel.cs b/ILSpy/ViewModels/DebugStepsPaneModel.cs index acf782fef..eb963f79a 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -39,11 +39,12 @@ using ILSpy.Util; namespace ILSpy.ViewModels { /// - /// Bottom-aligned tool pane that surfaces the Stepper output from - /// . The ViewModel owns the cross-language / - /// cross-decompile state (active language, current Stepper.Steps list) so it doesn't + /// Bottom-aligned tool pane that surfaces the step tree from the active + /// language — ILAst (one step per IL transform) or + /// 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 - /// 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 /// that populate it. @@ -57,7 +58,7 @@ namespace ILSpy.ViewModels readonly LanguageService? languageService; - ILAstLanguage? activeLanguage; + IDebugStepProvider? activeLanguage; int lastSelectedStep = int.MaxValue; /// @@ -71,12 +72,17 @@ namespace ILSpy.ViewModels UseLogicOperationSugar = true, }; - /// Instance accessor for XAML binding (Avalonia's static-source binding is awkward). - 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. + /// + [ObservableProperty] + object? options; /// /// Currently displayed list of recorded transform steps. Re-assigned (not mutated) - /// whenever the active ILAstLanguage's reports a new run, so + /// whenever the active step provider's reports a new run, so /// late-binding views pick up the latest list via the observable change. /// [ObservableProperty] @@ -143,13 +149,13 @@ namespace ILSpy.ViewModels void TryAttachToCurrentLanguage() { - if (languageService?.CurrentLanguage is ILAstLanguage il) + if (languageService?.CurrentLanguage is IDebugStepProvider il) AttachToLanguage(il); else DetachFromLanguage(); } - void AttachToLanguage(ILAstLanguage language) + void AttachToLanguage(IDebugStepProvider language) { if (ReferenceEquals(activeLanguage, language)) { @@ -162,6 +168,7 @@ namespace ILSpy.ViewModels activeLanguage = language; language.StepperUpdated += OnStepperUpdated; Steps = language.Stepper.Steps; + Options = language.StepOptions; } void DetachFromLanguage() @@ -170,6 +177,7 @@ namespace ILSpy.ViewModels { activeLanguage.StepperUpdated -= OnStepperUpdated; activeLanguage = null; + Options = null; } } diff --git a/ILSpy/Views/DebugSteps.axaml b/ILSpy/Views/DebugSteps.axaml index 42c0e11e4..fe6e7dbad 100644 --- a/ILSpy/Views/DebugSteps.axaml +++ b/ILSpy/Views/DebugSteps.axaml @@ -3,20 +3,29 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:ILSpy.ViewModels" + xmlns:il="using:ICSharpCode.Decompiler.IL" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300" x:Class="ILSpy.Views.DebugSteps" x:DataType="vm:DebugStepsPaneModel"> - - - - - - + + + + + + + + + + + + +