From 7ad4ccc025a4df8561d8ab390ff7f302f18f276b Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 29 Apr 2025 00:49:25 +0200 Subject: [PATCH] Add CompareView and FrozenContent flag --- ILSpy/AssemblyTree/AssemblyTreeModel.cs | 5 + ILSpy/Commands/CompareContextMenuEntry.cs | 58 +++ ILSpy/Commands/SearchMsdnContextMenuEntry.cs | 1 - ILSpy/Docking/DockWorkspace.cs | 6 +- ILSpy/Images/Images.cs | 23 + ILSpy/TreeNodes/MethodTreeNode.cs | 21 - ILSpy/ViewModels/CompareViewModel.cs | 434 +++++++++++++++++++ ILSpy/ViewModels/TabPageModel.cs | 7 + ILSpy/Views/CompareView.xaml | 63 +++ ILSpy/Views/CompareView.xaml.cs | 26 ++ ILSpy/Views/DebugSteps.xaml.cs | 7 +- 11 files changed, 626 insertions(+), 25 deletions(-) create mode 100644 ILSpy/Commands/CompareContextMenuEntry.cs create mode 100644 ILSpy/ViewModels/CompareViewModel.cs create mode 100644 ILSpy/Views/CompareView.xaml create mode 100644 ILSpy/Views/CompareView.xaml.cs diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index dd5ba6fba..48fc52b4e 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -794,6 +794,11 @@ namespace ICSharpCode.ILSpy.AssemblyTree { var activeTabPage = DockWorkspace.ActiveTabPage; + if (activeTabPage.FrozenContent) + { + activeTabPage = DockWorkspace.AddTabPage(); + } + activeTabPage.SupportsLanguageSwitching = true; if (SelectedItems.Length == 1) diff --git a/ILSpy/Commands/CompareContextMenuEntry.cs b/ILSpy/Commands/CompareContextMenuEntry.cs new file mode 100644 index 000000000..a4b12d82d --- /dev/null +++ b/ILSpy/Commands/CompareContextMenuEntry.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2025 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. + +using System.Composition; + +using ICSharpCode.ILSpy.AssemblyTree; +using ICSharpCode.ILSpy.Docking; +using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.ILSpy.ViewModels; +using ICSharpCode.ILSpy.Views; + +namespace ICSharpCode.ILSpy +{ + [ExportContextMenuEntry(Header = "Compare...", Order = 9999)] + [Shared] + internal sealed class CompareContextMenuEntry(AssemblyTreeModel assemblyTreeModel, DockWorkspace dockWorkspace) : IContextMenuEntry + { + public void Execute(TextViewContext context) + { + var left = ((AssemblyTreeNode)context.SelectedTreeNodes[0]).LoadedAssembly; + var right = ((AssemblyTreeNode)context.SelectedTreeNodes[1]).LoadedAssembly; + + var tabPage = dockWorkspace.AddTabPage(); + + tabPage.Title = $"Compare {left.Text} - {right.Text}"; + tabPage.SupportsLanguageSwitching = false; + tabPage.FrozenContent = true; + var compareView = new CompareView(); + compareView.DataContext = new CompareViewModel(assemblyTreeModel, left, right); + tabPage.Content = compareView; + } + + public bool IsEnabled(TextViewContext context) + { + return true; + } + + public bool IsVisible(TextViewContext context) + { + return context.SelectedTreeNodes is [AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true }, AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true }]; + } + } +} \ No newline at end of file diff --git a/ILSpy/Commands/SearchMsdnContextMenuEntry.cs b/ILSpy/Commands/SearchMsdnContextMenuEntry.cs index c6a8babad..e8b54357a 100644 --- a/ILSpy/Commands/SearchMsdnContextMenuEntry.cs +++ b/ILSpy/Commands/SearchMsdnContextMenuEntry.cs @@ -17,7 +17,6 @@ // DEALINGS IN THE SOFTWARE. using System.Linq; -using System.Threading; using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.TreeNodes; diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index 2fe0d0fc2..b5986fa50 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -118,9 +118,11 @@ namespace ICSharpCode.ILSpy.Docking } } - public void AddTabPage(TabPageModel tabPage = null) + public TabPageModel AddTabPage(TabPageModel tabPage = null) { - tabPages.Add(tabPage ?? exportProvider.GetExportedValue()); + TabPageModel item = tabPage ?? exportProvider.GetExportedValue(); + tabPages.Add(item); + return item; } public ReadOnlyObservableCollection TabPages { get; } diff --git a/ILSpy/Images/Images.cs b/ILSpy/Images/Images.cs index 49dfde0fd..a4e0aa064 100644 --- a/ILSpy/Images/Images.cs +++ b/ILSpy/Images/Images.cs @@ -25,6 +25,8 @@ using System.Windows.Media.Imaging; namespace ICSharpCode.ILSpy { + using ICSharpCode.Decompiler.TypeSystem; + static class Images { private static readonly Rect iconRect = new Rect(0, 0, 16, 16); @@ -212,6 +214,27 @@ namespace ICSharpCode.ILSpy return memberIconCache.GetIcon(icon, overlay, isStatic); } + public static AccessOverlayIcon GetOverlayIcon(Accessibility accessibility) + { + switch (accessibility) + { + case Accessibility.Public: + return AccessOverlayIcon.Public; + case Accessibility.Internal: + return AccessOverlayIcon.Internal; + case Accessibility.ProtectedAndInternal: + return AccessOverlayIcon.PrivateProtected; + case Accessibility.Protected: + return AccessOverlayIcon.Protected; + case Accessibility.ProtectedOrInternal: + return AccessOverlayIcon.ProtectedInternal; + case Accessibility.Private: + return AccessOverlayIcon.Private; + default: + return AccessOverlayIcon.CompilerControlled; + } + } + private static ImageSource GetIcon(string baseImage, string overlay = null, bool isStatic = false) { ImageSource baseImageSource = Load(baseImage); diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs index 1658ac714..a78c9d80b 100644 --- a/ILSpy/TreeNodes/MethodTreeNode.cs +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -73,27 +73,6 @@ namespace ICSharpCode.ILSpy.TreeNodes GetOverlayIcon(method.Accessibility), method.IsStatic); } - internal static AccessOverlayIcon GetOverlayIcon(Accessibility accessibility) - { - switch (accessibility) - { - case Accessibility.Public: - return AccessOverlayIcon.Public; - case Accessibility.Internal: - return AccessOverlayIcon.Internal; - case Accessibility.ProtectedAndInternal: - return AccessOverlayIcon.PrivateProtected; - case Accessibility.Protected: - return AccessOverlayIcon.Protected; - case Accessibility.ProtectedOrInternal: - return AccessOverlayIcon.ProtectedInternal; - case Accessibility.Private: - return AccessOverlayIcon.Private; - default: - return AccessOverlayIcon.CompilerControlled; - } - } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileMethod(MethodDefinition, output, options); diff --git a/ILSpy/ViewModels/CompareViewModel.cs b/ILSpy/ViewModels/CompareViewModel.cs new file mode 100644 index 000000000..0e415407e --- /dev/null +++ b/ILSpy/ViewModels/CompareViewModel.cs @@ -0,0 +1,434 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata; + +using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.ILSpy.AssemblyTree; +using ICSharpCode.ILSpyX; + +using TomsToolbox.Wpf; + +namespace ICSharpCode.ILSpy.ViewModels +{ + using System.Linq; + + using ICSharpCode.Decompiler; + using ICSharpCode.Decompiler.TypeSystem; + using ICSharpCode.ILSpy.TreeNodes; + + class CompareViewModel : ObservableObject + { + private readonly AssemblyTreeModel assemblyTreeModel; + private LoadedAssembly leftAssembly; + private LoadedAssembly rightAssembly; + private LoadedAssembly[] assemblies; + private ComparisonEntryTreeNode root; + + public CompareViewModel(AssemblyTreeModel assemblyTreeModel, LoadedAssembly left, LoadedAssembly right) + { + MessageBus.Subscribers += (sender, e) => this.Assemblies = assemblyTreeModel.AssemblyList.GetAssemblies(); + + leftAssembly = left; + rightAssembly = right; + assemblies = assemblyTreeModel.AssemblyList.GetAssemblies(); + + var leftTree = CreateEntityTree(new DecompilerTypeSystem(leftAssembly.GetMetadataFileOrNull(), leftAssembly.GetAssemblyResolver())); + var rightTree = CreateEntityTree(new DecompilerTypeSystem(rightAssembly.GetMetadataFileOrNull(), rightAssembly.GetAssemblyResolver())); + + this.root = new ComparisonEntryTreeNode(MergeTrees(leftTree.Item2, rightTree.Item2)); + } + + public LoadedAssembly[] Assemblies { + get => assemblies; + set { + if (assemblies != value) + { + assemblies = value; + OnPropertyChanged(); + } + } + } + + public LoadedAssembly LeftAssembly { + get => leftAssembly; + set { + if (leftAssembly != value) + { + leftAssembly = value; + OnPropertyChanged(); + } + } + } + + public LoadedAssembly RightAssembly { + get => rightAssembly; + set { + if (rightAssembly != value) + { + rightAssembly = value; + OnPropertyChanged(); + } + } + } + + public ComparisonEntryTreeNode RootEntry { + get => root; + set { + if (root != value) + { + root = value; + OnPropertyChanged(); + } + } + } + + Entry MergeTrees(Entry a, Entry b) + { + var m = new Entry() { + Entity = a.Entity, + Signature = a.Signature, + }; + + if (a.Children?.Count > 0 && b.Children?.Count > 0) + { + var diff = CalculateDiff(a.Children, b.Children); + m.Children ??= new(); + + foreach (var (left, right) in diff) + { + if (left != null && right != null) + m.Children.Add(MergeTrees(left, right)); + else if (left != null) + m.Children.Add(left); + else if (right != null) + m.Children.Add(right); + else + Debug.Fail("wh00t?"); + m.Children[^1].Parent = m; + } + } + else if (a.Children?.Count > 0) + { + m.Children ??= new(); + foreach (var child in a.Children) + { + child.Parent = m; + m.Children.Add(child); + } + } + else if (b.Children?.Count > 0) + { + m.Children ??= new(); + foreach (var child in b.Children) + { + child.Parent = m; + m.Children.Add(child); + } + } + + return m; + } + + (List, Entry) CreateEntityTree(DecompilerTypeSystem typeSystem) + { + var module = typeSystem.MainModule; + var metadata = module.MetadataFile.Metadata; + var ambience = new CSharpAmbience(); + ambience.ConversionFlags = ICSharpCode.Decompiler.Output.ConversionFlags.All & ~ICSharpCode.Decompiler.Output.ConversionFlags.ShowDeclaringType; + + List results = new(); + Dictionary typeEntries = new(); + Dictionary namespaceEntries = new(StringComparer.Ordinal); + + Entry root = new Entry { Entity = null!, Signature = module.FullAssemblyName }; + + // typeEntries need a different signature: must include list of base types + + Entry? TryCreateEntry(IEntity entity) + { + if (entity.EffectiveAccessibility() != Accessibility.Public) + return null; + + Entry? parent = null; + + if (entity.DeclaringTypeDefinition != null + && !typeEntries.TryGetValue((TypeDefinitionHandle)entity.DeclaringTypeDefinition.MetadataToken, out parent)) + { + return null; + } + + var entry = new Entry { + Signature = ambience.ConvertSymbol(entity), + Entity = entity, + Parent = parent, + }; + + if (parent != null) + { + parent.Children ??= new(); + parent.Children.Add(entry); + } + + return entry; + } + + foreach (var typeDefHandle in metadata.TypeDefinitions) + { + var typeDef = module.GetDefinition(typeDefHandle); + + if (typeDef.EffectiveAccessibility() != Accessibility.Public) + continue; + + var entry = typeEntries[typeDefHandle] = new Entry { + Signature = ambience.ConvertSymbol(typeDef), + Entity = typeDef + }; + + if (typeDef.DeclaringType == null) + { + if (!namespaceEntries.TryGetValue(typeDef.Namespace, out var nsEntry)) + { + namespaceEntries[typeDef.Namespace] = nsEntry = new Entry { Parent = root, Signature = typeDef.Namespace, Entity = null! }; + root.Children ??= new(); + root.Children.Add(nsEntry); + } + + entry.Parent = nsEntry; + nsEntry.Children ??= new(); + nsEntry.Children.Add(entry); + } + } + + foreach (var fieldHandle in metadata.FieldDefinitions) + { + var fieldDef = module.GetDefinition(fieldHandle); + var entry = TryCreateEntry(fieldDef); + + if (entry != null) + results.Add(entry); + } + + foreach (var eventHandle in metadata.EventDefinitions) + { + var eventDef = module.GetDefinition(eventHandle); + var entry = TryCreateEntry(eventDef); + + if (entry != null) + results.Add(entry); + } + + foreach (var propertyHandle in metadata.PropertyDefinitions) + { + var propertyDef = module.GetDefinition(propertyHandle); + var entry = TryCreateEntry(propertyDef); + + if (entry != null) + results.Add(entry); + } + + foreach (var methodHandle in metadata.MethodDefinitions) + { + var methodDef = module.GetDefinition(methodHandle); + + if (methodDef.AccessorOwner != null) + continue; + + var entry = TryCreateEntry(methodDef); + + if (entry != null) + results.Add(entry); + } + + return (results, root); + } + + List<(Entry? Left, Entry? Right)> CalculateDiff(List left, List right) + { + Dictionary> leftMap = new(); + Dictionary> rightMap = new(); + + foreach (var item in left) + { + string key = item.Signature; + if (leftMap.ContainsKey(key)) + leftMap[key].Add(item); + else + leftMap[key] = [item]; + } + + foreach (var item in right) + { + string key = item.Signature; + if (rightMap.ContainsKey(key)) + rightMap[key].Add(item); + else + rightMap[key] = [item]; + } + + List<(Entry? Left, Entry? Right)> results = new(); + + foreach (var (key, items) in leftMap) + { + if (rightMap.TryGetValue(key, out var rightEntries)) + { + foreach (var item in items) + { + var other = rightEntries.Find(_ => EntryComparer.Instance.Equals(_, item)); + results.Add((item, other)); + if (other == null) + { + item.Kind = DiffKind.Remove; + } + } + } + else + { + foreach (var item in items) + { + item.Kind = DiffKind.Remove; + results.Add((item, null)); + } + } + } + + foreach (var (key, items) in rightMap) + { + if (leftMap.TryGetValue(key, out var leftEntries)) + { + foreach (var item in items) + { + if (!leftEntries.Any(_ => EntryComparer.Instance.Equals(_, item))) + { + results.Add((null, item)); + item.Kind = DiffKind.Add; + } + } + } + else + { + foreach (var item in items) + { + item.Kind = DiffKind.Add; + results.Add((null, item)); + } + } + } + + return results; + } + } + + [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] + public class Entry + { + private DiffKind kind = DiffKind.None; + + public DiffKind Kind { + get { + if (Children == null || Children.Count == 0) + { + return kind; + } + + int addCount = 0, removeCount = 0, updateCount = 0; + + foreach (var item in Children) + { + switch (item.Kind) + { + case DiffKind.Add: + addCount++; + break; + case DiffKind.Remove: + removeCount++; + break; + case DiffKind.Update: + updateCount++; + break; + } + } + + if (addCount == Children.Count) + return DiffKind.Add; + if (removeCount == Children.Count) + return DiffKind.Remove; + if (addCount > 0 || removeCount > 0 || updateCount > 0) + return DiffKind.Update; + return DiffKind.None; + } + set { + if (Children == null || Children.Count == 0) + { + kind = value; + } + } + } + public required string Signature { get; init; } + public required IEntity Entity { get; init; } + + public Entry? Parent { get; set; } + public List? Children { get; set; } + + private string GetDebuggerDisplay() + { + return $"Entry{Kind}{Entity?.ToString() ?? Signature}"; + } + } + + public class EntryComparer : IEqualityComparer + { + public static EntryComparer Instance = new(); + + public bool Equals(Entry? x, Entry? y) + { + return x?.Signature == y?.Signature; + } + + public int GetHashCode([DisallowNull] Entry obj) + { + return obj.Signature.GetHashCode(); + } + } + + public enum DiffKind + { + None = ' ', + Add = '+', + Remove = '-', + Update = '~' + } + + class ComparisonEntryTreeNode : ILSpyTreeNode + { + private readonly Entry entry; + + public ComparisonEntryTreeNode(Entry entry) + { + this.entry = entry; + this.LazyLoading = entry.Children != null; + } + + protected override void LoadChildren() + { + if (entry.Children == null) + return; + + foreach (var item in entry.Children) + { + this.Children.Add(new ComparisonEntryTreeNode(item)); + } + } + + public override object Text => entry.Signature; + + public override object Icon => Images.GetIcon(; + + public DiffKind Difference => entry.Kind; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + } + } +} diff --git a/ILSpy/ViewModels/TabPageModel.cs b/ILSpy/ViewModels/TabPageModel.cs index 73746af41..8afc7e35f 100644 --- a/ILSpy/ViewModels/TabPageModel.cs +++ b/ILSpy/ViewModels/TabPageModel.cs @@ -51,6 +51,13 @@ namespace ICSharpCode.ILSpy.ViewModels set => SetProperty(ref supportsLanguageSwitching, value); } + private bool frozenContent; + + public bool FrozenContent { + get => frozenContent; + set => SetProperty(ref frozenContent, value); + } + private object? content; public object? Content { diff --git a/ILSpy/Views/CompareView.xaml b/ILSpy/Views/CompareView.xaml new file mode 100644 index 000000000..bdfeb056c --- /dev/null +++ b/ILSpy/Views/CompareView.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + diff --git a/ILSpy/Views/CompareView.xaml.cs b/ILSpy/Views/CompareView.xaml.cs new file mode 100644 index 000000000..e702b924d --- /dev/null +++ b/ILSpy/Views/CompareView.xaml.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace ICSharpCode.ILSpy.Views +{ + /// + /// Interaction logic for CompareView.xaml + /// + public partial class CompareView : UserControl + { + public CompareView() + { + InitializeComponent(); + } + } +} diff --git a/ILSpy/Views/DebugSteps.xaml.cs b/ILSpy/Views/DebugSteps.xaml.cs index c341c37ab..f86566b36 100644 --- a/ILSpy/Views/DebugSteps.xaml.cs +++ b/ILSpy/Views/DebugSteps.xaml.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.Composition; using System.Windows; using System.Windows.Controls; @@ -134,6 +133,12 @@ namespace ICSharpCode.ILSpy void DecompileAsync(int step, bool isDebug = false) { lastSelectedStep = step; + + if (dockWorkspace.ActiveTabPage.FrozenContent) + { + dockWorkspace.ActiveTabPage = dockWorkspace.AddTabPage(); + } + var state = dockWorkspace.ActiveTabPage.GetState(); dockWorkspace.ActiveTabPage.ShowTextViewAsync(textView => textView.DecompileAsync(assemblyTreeModel.CurrentLanguage, assemblyTreeModel.SelectedNodes, new DecompilationOptions(assemblyTreeModel.CurrentLanguageVersion, settingsService.DecompilerSettings, settingsService.DisplaySettings) {