diff --git a/Extensions/ILSpy.Decompiler/ICSharpCode.Decompiler b/Extensions/ILSpy.Decompiler/ICSharpCode.Decompiler index 4a5d661149..22b67d3ccc 160000 --- a/Extensions/ILSpy.Decompiler/ICSharpCode.Decompiler +++ b/Extensions/ILSpy.Decompiler/ICSharpCode.Decompiler @@ -1 +1 @@ -Subproject commit 4a5d661149aefd35ae55cedc46e469f47fb465f8 +Subproject commit 22b67d3cccfa4efd7118b1923e7aa7203bcdc1a8 diff --git a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/ILAst/ILAstDecompiler.cs b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/ILAst/ILAstDecompiler.cs index 2908c00e02..811d6691fb 100644 --- a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/ILAst/ILAstDecompiler.cs +++ b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/ILAst/ILAstDecompiler.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text; using dnlib.DotNet; using dnSpy.Contracts.Decompiler; @@ -66,8 +65,13 @@ sealed class ILAstDecompiler : DecompilerBase { readonly string uniqueNameUI; Guid uniqueGuid; + // Hacky fields and properties for Stepper functionality + MethodDef? lastMethod; + public Stepper Stepper { get; set; } = new Stepper(); + public event EventHandler? StepperUpdated; + void OnStepperUpdated(EventArgs? e = null) => StepperUpdated?.Invoke(this, e ?? EventArgs.Empty); + public override DecompilerSettingsBase Settings { get; } - const int settingsVersion = 1; ILAstDecompiler(ILAstDecompilerSettings langSettings, double orderUI, string uniqueNameUI) { Settings = langSettings; @@ -96,7 +100,7 @@ public override void Decompile(MethodDef method, IDecompilerOutput output, Decom var bodyInfo = StartKeywordBlock(output, ".body", method); var ts = new DecompilerTypeSystem(new PEFile(method.Module), TypeSystemOptions.Default); - var reader = new ILReader(ts.MainModule) { CalculateILSpans = true }; + var reader = new ILReader(ts.MainModule) { UseDebugSymbols = true, CalculateILSpans = true }; var il = reader.ReadIL(method, kind: ILFunctionKind.TopLevelFunction, cancellationToken: ctx.CancellationToken); var settings = new DecompilerSettings(LanguageVersion.Latest); @@ -104,9 +108,11 @@ public override void Decompile(MethodDef method, IDecompilerOutput output, Decom var context = run.CreateILTransformContext(il); context.CalculateILSpans = true; - //context.Stepper.StepLimit = options.StepLimit; - context.Stepper.IsDebug = Debugger.IsAttached; + if (lastMethod != method && Settings is ILAstDecompilerSettings s) + s.StepLimit = int.MaxValue; + int stepLimit = (Settings as ILAstDecompilerSettings)?.StepLimit ?? int.MaxValue; + context.Stepper.StepLimit = stepLimit; try { @@ -125,18 +131,19 @@ public override void Decompile(MethodDef method, IDecompilerOutput output, Decom } finally { - // update stepper even if a transform crashed unexpectedly - // if (options.StepLimit == int.MaxValue) - // { - // Stepper = context.Stepper; - // OnStepperUpdated(new EventArgs()); - // } + if (stepLimit == int.MaxValue) + { + Stepper = context.Stepper; + OnStepperUpdated(); + } } output.WriteLine(); - il.WriteTo(output, new ILAstWritingOptions()); + il.WriteTo(output, new ILAstWritingOptions() {ShowILRanges = true}); EndKeywordBlock(output, bodyInfo, CodeBracesRangeFlags.MethodBraces, true); + + lastMethod = method; } struct BraceInfo { diff --git a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/ILAst/StepperConstants.cs b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/ILAst/StepperConstants.cs new file mode 100644 index 0000000000..8dd5cb7e1f --- /dev/null +++ b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/ILAst/StepperConstants.cs @@ -0,0 +1,29 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System; + +#if DEBUG +namespace dnSpy.Decompiler.ILSpy.Core.ILAst { + public static class StepperConstants { + public static readonly Guid ToolWindowGuid = new Guid("38FBE664-81E4-4456-961D-37CFB7A9FB8E"); + public static readonly Guid TreeViewGuid = new Guid("D3707DFF-3728-4B4D-BB81-A9D85793FD65"); + } +} +#endif diff --git a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/Settings/ILAstDecompilerSettings.cs b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/Settings/ILAstDecompilerSettings.cs index 1469df3712..a9c929ae77 100644 --- a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/Settings/ILAstDecompilerSettings.cs +++ b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy.Core/Settings/ILAstDecompilerSettings.cs @@ -20,27 +20,47 @@ You should have received a copy of the GNU General Public License #if DEBUG using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; using dnSpy.Contracts.Decompiler; namespace dnSpy.Decompiler.ILSpy.Core.Settings { sealed class ILAstDecompilerSettings : DecompilerSettingsBase { - public override int Version => 0; - public override event EventHandler? VersionChanged { add { } remove { } } + public override int Version => settingsVersion; + int settingsVersion; - public ILAstDecompilerSettings() { + public override event EventHandler? VersionChanged; + + public override IEnumerable Options { + get { yield break; } } - ILAstDecompilerSettings(ILAstDecompilerSettings other) { + public int StepLimit { + get => stepLimit; + set { + if (stepLimit == value) + return; + stepLimit = value; + OnPropertyChanged(); + } } + int stepLimit = int.MaxValue; - public override DecompilerSettingsBase Clone() => new ILAstDecompilerSettings(this); + public ILAstDecompilerSettings() { } - public override IEnumerable Options { - get { yield break; } + ILAstDecompilerSettings(ILAstDecompilerSettings other) => stepLimit = other.stepLimit; + + void OnPropertyChanged([CallerMemberName] string? propertyName = null) { + Interlocked.Increment(ref settingsVersion); + VersionChanged?.Invoke(this, EventArgs.Empty); } - public override bool Equals(object? obj) => obj is ILAstDecompilerSettings; - public override int GetHashCode() => 0; + public override DecompilerSettingsBase Clone() => new ILAstDecompilerSettings(this); + + public override bool Equals(object? obj) => obj is ILAstDecompilerSettings settings && settings.stepLimit == stepLimit; + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => stepLimit; } } #endif diff --git a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/ILAstStepperToolWindowContent.cs b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/ILAstStepperToolWindowContent.cs new file mode 100644 index 0000000000..c566a3f0f8 --- /dev/null +++ b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/ILAstStepperToolWindowContent.cs @@ -0,0 +1,152 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using dnSpy.Contracts.Controls; +using dnSpy.Contracts.Decompiler; +using dnSpy.Contracts.Documents; +using dnSpy.Contracts.Documents.Tabs; +using dnSpy.Contracts.ToolWindows; +using dnSpy.Contracts.ToolWindows.App; +using dnSpy.Contracts.TreeView; +using dnSpy.Contracts.TreeView.Text; +using dnSpy.Decompiler.ILSpy.Core.ILAst; +using dnSpy.Decompiler.ILSpy.Core.Settings; +using ICSharpCode.Decompiler.IL.Transforms; + +#if DEBUG +namespace dnSpy.Decompiler.ILSpy.ILAst { + [Export(typeof(IToolWindowContentProvider))] + sealed class AnalyzerToolWindowContentProvider : IToolWindowContentProvider { + readonly IDecompilerService decompilerService; + readonly IDocumentTabService documentTabService; + readonly ITreeViewService treeViewService; + readonly ITreeViewNodeTextElementProvider treeViewNodeTextElementProvider; + + public ILAstStepperToolWindowContent DocumentTreeViewWindowContent => analyzerToolWindowContent ??= new ILAstStepperToolWindowContent(decompilerService, documentTabService, treeViewService, treeViewNodeTextElementProvider); + ILAstStepperToolWindowContent? analyzerToolWindowContent; + + [ImportingConstructor] + AnalyzerToolWindowContentProvider(IDecompilerService decompilerService, IDocumentTabService documentTabService, ITreeViewService treeViewService, ITreeViewNodeTextElementProvider treeViewNodeTextElementProvider) { + this.decompilerService = decompilerService; + this.documentTabService = documentTabService; + this.treeViewService = treeViewService; + this.treeViewNodeTextElementProvider = treeViewNodeTextElementProvider; + } + + public IEnumerable ContentInfos { + get { yield return new ToolWindowContentInfo(StepperConstants.ToolWindowGuid); } + } + + public ToolWindowContent? GetOrCreate(Guid guid) => guid == StepperConstants.ToolWindowGuid ? DocumentTreeViewWindowContent : null; + } + + sealed class ILAstStepperToolWindowContent : ToolWindowContent, IFocusable, IStepperTreeNodeDataContext { + readonly IDecompilerService decompilerService; + readonly IDocumentTabService documentTabService; + readonly ITreeViewNodeTextElementProvider treeViewNodeTextElementProvider; + readonly ITreeView treeView; + ILAstDecompiler? decompiler; + + public override object UIObject => treeView.UIObject; + public override IInputElement? FocusedElement => null; + public override FrameworkElement ZoomElement => treeView.UIObject; + public override Guid Guid => StepperConstants.ToolWindowGuid; + public override string Title => "ILAst Stepper"; + public bool CanFocus => true; + public ITreeView TreeView => treeView; + public ITreeViewNodeTextElementProvider TreeViewNodeTextElementProvider => treeViewNodeTextElementProvider; + public Stepper.Node? CurrentState { get; private set; } + + public ILAstStepperToolWindowContent(IDecompilerService decompilerService, IDocumentTabService documentTabService, ITreeViewService treeViewService, ITreeViewNodeTextElementProvider treeViewNodeTextElementProvider) { + this.decompilerService = decompilerService; + this.documentTabService = documentTabService; + this.treeViewNodeTextElementProvider = treeViewNodeTextElementProvider; + decompilerService.DecompilerChanged += DecompilerService_DecompilerChanged; + + var options = new TreeViewOptions { + CanDragAndDrop = false, + SelectionMode = SelectionMode.Single, + }; + treeView = treeViewService.Create(StepperConstants.TreeViewGuid, options); + treeView.UIObject.Padding = new Thickness(0, 2, 0, 2); + treeView.UIObject.BorderThickness = new Thickness(1); + + OnDecompilerSelected(decompilerService.Decompiler); + } + + void DecompilerService_DecompilerChanged(object? sender, EventArgs e) => + OnDecompilerSelected(decompilerService.Decompiler); + + void OnDecompilerSelected(IDecompiler newDecompiler) { + if (decompiler is not null) + decompiler.StepperUpdated -= ILAstStepperUpdated; + decompiler = newDecompiler as ILAstDecompiler; + if (decompiler is not null) + decompiler.StepperUpdated += ILAstStepperUpdated; + else { + CurrentState = null; + treeView.UIObject.Dispatcher.Invoke(() => treeView.Root.Children.Clear()); + } + } + + void ILAstStepperUpdated(object? sender, EventArgs e) { + if (decompiler is null) + return; + + CurrentState = null; + + treeView.UIObject.Dispatcher.Invoke(() => { + treeView.Root.Children.Clear(); + for (var i = 0; i < decompiler.Stepper.Steps.Count; i++) + treeView.Root.AddChild(treeView.Create(new StepperNodeTreeNodeData(this, decompiler.Stepper.Steps[i]))); + }); + } + + public void ShowStateAfter(Stepper.Node node) { + CurrentState = node; + SetNewStepLimit(node.EndStep); + RefreshTabs(); + treeView.RefreshAllNodes(); + } + + void SetNewStepLimit(int stepLimit) { + if (decompiler is not null && decompiler.Settings is ILAstDecompilerSettings settings) + settings.StepLimit = stepLimit; + } + + void RefreshTabs() { + var toRefresh = new HashSet(); + foreach (var tab in documentTabService.VisibleFirstTabs) { + var decomp = (tab.Content as IDecompilerTabContent)?.Decompiler; + if (decomp is not null && decomp == decompiler) + toRefresh.Add(tab); + } + documentTabService.Refresh(toRefresh.ToArray()); + } + + public void Focus() => treeView.Focus(); + } +} +#endif diff --git a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/IStepperTreeNodeDataContext.cs b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/IStepperTreeNodeDataContext.cs new file mode 100644 index 0000000000..d5621ba542 --- /dev/null +++ b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/IStepperTreeNodeDataContext.cs @@ -0,0 +1,33 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using dnSpy.Contracts.TreeView; +using dnSpy.Contracts.TreeView.Text; +using ICSharpCode.Decompiler.IL.Transforms; + +#if DEBUG +namespace dnSpy.Decompiler.ILSpy.ILAst { + interface IStepperTreeNodeDataContext { + ITreeView TreeView { get; } + ITreeViewNodeTextElementProvider TreeViewNodeTextElementProvider { get; } + Stepper.Node? CurrentState { get; } + void ShowStateAfter(Stepper.Node node); + } +} +#endif diff --git a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/ShowStepperToolWindowLoader.cs b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/ShowStepperToolWindowLoader.cs new file mode 100644 index 0000000000..eb999f6bc9 --- /dev/null +++ b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/ShowStepperToolWindowLoader.cs @@ -0,0 +1,34 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System.ComponentModel.Composition; +using dnSpy.Contracts.Extension; +using dnSpy.Contracts.ToolWindows.App; +using dnSpy.Decompiler.ILSpy.Core.ILAst; + +#if DEBUG +namespace dnSpy.Decompiler.ILSpy.ILAst { + [ExportAutoLoaded] + sealed class ShowStepperToolWindowLoader : IAutoLoaded { + [ImportingConstructor] + public ShowStepperToolWindowLoader(IDsToolWindowService toolWindowService) => + toolWindowService.Show(StepperConstants.ToolWindowGuid); + } +} +#endif diff --git a/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/StepperNodeTreeNodeData.cs b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/StepperNodeTreeNodeData.cs new file mode 100644 index 0000000000..c286f919c9 --- /dev/null +++ b/Extensions/ILSpy.Decompiler/dnSpy.Decompiler.ILSpy/ILAst/StepperNodeTreeNodeData.cs @@ -0,0 +1,79 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System; +using System.Collections.Generic; +using dnSpy.Contracts.Images; +using dnSpy.Contracts.Text; +using dnSpy.Contracts.Text.Classification; +using dnSpy.Contracts.TreeView; +using dnSpy.Contracts.TreeView.Text; +using ICSharpCode.Decompiler.IL.Transforms; + +#if DEBUG +namespace dnSpy.Decompiler.ILSpy.ILAst { + sealed class StepperNodeTreeNodeData : TreeNodeData { + readonly IStepperTreeNodeDataContext context; + readonly Stepper.Node node; + + public override Guid Guid => Guid.Empty; + + static class Cache { + static readonly TextClassifierTextColorWriter writer = new TextClassifierTextColorWriter(); + public static TextClassifierTextColorWriter GetWriter() => writer; + public static void FreeWriter(TextClassifierTextColorWriter writer) => writer.Clear(); + } + + public override object Text { + get { + var writer = Cache.GetWriter(); + try { + writer.Write(node == context.CurrentState ? BoxedTextColor.Blue : BoxedTextColor.Text, node.Description); + var classifierContext = new TreeViewNodeClassifierContext(writer.Text, context.TreeView, this, isToolTip: false, colorize: true, colors: writer.Colors); + return context.TreeViewNodeTextElementProvider.CreateTextElement(classifierContext, TreeViewContentTypes.TreeViewNode, TextElementFlags.FilterOutNewLines); + } + finally { + Cache.FreeWriter(writer); + } + } + } + + public override object? ToolTip => null; + + public override ImageReference Icon => ImageReference.None; + + public StepperNodeTreeNodeData(IStepperTreeNodeDataContext context, Stepper.Node node) { + this.context = context; + this.node = node; + } + + public override IEnumerable CreateChildren() { + for (int i = 0; i < node.Children.Count; i++) + yield return new StepperNodeTreeNodeData(context, node.Children[i]); + } + + public override bool Activate() { + context.ShowStateAfter(node); + return true; + } + + public override void OnRefreshUI() { } + } +} +#endif