From 0af45643eb7924ba7f1d3665ae1eee4fb4e0873e Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 29 Apr 2025 00:49:25 +0200 Subject: [PATCH 1/4] Add CompareView and FrozenContent flag --- ICSharpCode.ILSpyX/AssemblyListSnapshot.cs | 2 +- ILSpy/AssemblyTree/AssemblyTreeModel.cs | 5 + ILSpy/Commands/CompareContextMenuEntry.cs | 53 ++ ILSpy/Commands/SearchMsdnContextMenuEntry.cs | 1 - ILSpy/Controls/TreeView/SharpTreeView.xaml | 2 +- ILSpy/Docking/DockWorkspace.cs | 6 +- ILSpy/Images/Images.cs | 23 + ILSpy/TreeNodes/EventTreeNode.cs | 2 +- ILSpy/TreeNodes/FieldTreeNode.cs | 8 +- ILSpy/TreeNodes/MethodTreeNode.cs | 31 +- ILSpy/TreeNodes/PropertyTreeNode.cs | 2 +- ILSpy/ViewModels/CompareViewModel.cs | 625 +++++++++++++++++++ ILSpy/ViewModels/TabPageModel.cs | 7 + ILSpy/Views/CompareView.xaml | 56 ++ ILSpy/Views/CompareView.xaml.cs | 33 + ILSpy/Views/DebugSteps.xaml.cs | 7 +- 16 files changed, 825 insertions(+), 38 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/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs b/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs index 485a52a08..293851b97 100644 --- a/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs +++ b/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs @@ -186,7 +186,7 @@ namespace ICSharpCode.ILSpyX foreach (var entry in folder.Entries) { - if (!entry.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + if (!entry.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !entry.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) continue; var asm = folder.ResolveFileName(entry.Name); if (asm == null) 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..462ecb5d7 --- /dev/null +++ b/ILSpy/Commands/CompareContextMenuEntry.cs @@ -0,0 +1,53 @@ +// 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 System.Threading.Tasks; + +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(); + CompareViewModel.Show(tabPage, left, right, assemblyTreeModel); + } + + 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/Controls/TreeView/SharpTreeView.xaml b/ILSpy/Controls/TreeView/SharpTreeView.xaml index 562f4bc51..e93d659e3 100644 --- a/ILSpy/Controls/TreeView/SharpTreeView.xaml +++ b/ILSpy/Controls/TreeView/SharpTreeView.xaml @@ -246,7 +246,7 @@ VerticalAlignment="Center" /> ()); + 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/EventTreeNode.cs b/ILSpy/TreeNodes/EventTreeNode.cs index 1af0a9de3..36a38c07f 100644 --- a/ILSpy/TreeNodes/EventTreeNode.cs +++ b/ILSpy/TreeNodes/EventTreeNode.cs @@ -65,7 +65,7 @@ namespace ICSharpCode.ILSpy.TreeNodes public static ImageSource GetIcon(IEvent @event) { - return Images.GetIcon(MemberIcon.Event, MethodTreeNode.GetOverlayIcon(@event.Accessibility), @event.IsStatic); + return Images.GetIcon(MemberIcon.Event, Images.GetOverlayIcon(@event.Accessibility), @event.IsStatic); } public override FilterResult Filter(LanguageSettings settings) diff --git a/ILSpy/TreeNodes/FieldTreeNode.cs b/ILSpy/TreeNodes/FieldTreeNode.cs index 4a086ceb7..2e566d079 100644 --- a/ILSpy/TreeNodes/FieldTreeNode.cs +++ b/ILSpy/TreeNodes/FieldTreeNode.cs @@ -58,15 +58,15 @@ namespace ICSharpCode.ILSpy.TreeNodes public static ImageSource GetIcon(IField field) { if (field.DeclaringType.Kind == TypeKind.Enum && field.ReturnType.Kind == TypeKind.Enum) - return Images.GetIcon(MemberIcon.EnumValue, MethodTreeNode.GetOverlayIcon(field.Accessibility), false); + return Images.GetIcon(MemberIcon.EnumValue, Images.GetOverlayIcon(field.Accessibility), false); if (field.IsConst) - return Images.GetIcon(MemberIcon.Literal, MethodTreeNode.GetOverlayIcon(field.Accessibility), false); + return Images.GetIcon(MemberIcon.Literal, Images.GetOverlayIcon(field.Accessibility), false); if (field.IsReadOnly) - return Images.GetIcon(MemberIcon.FieldReadOnly, MethodTreeNode.GetOverlayIcon(field.Accessibility), field.IsStatic); + return Images.GetIcon(MemberIcon.FieldReadOnly, Images.GetOverlayIcon(field.Accessibility), field.IsStatic); - return Images.GetIcon(MemberIcon.Field, MethodTreeNode.GetOverlayIcon(field.Accessibility), field.IsStatic); + return Images.GetIcon(MemberIcon.Field, Images.GetOverlayIcon(field.Accessibility), field.IsStatic); } public override FilterResult Filter(LanguageSettings settings) diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs index 1658ac714..ad3d1cfe8 100644 --- a/ILSpy/TreeNodes/MethodTreeNode.cs +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -58,40 +58,19 @@ namespace ICSharpCode.ILSpy.TreeNodes public static ImageSource GetIcon(IMethod method) { if (method.IsOperator) - return Images.GetIcon(MemberIcon.Operator, GetOverlayIcon(method.Accessibility), false); + return Images.GetIcon(MemberIcon.Operator, Images.GetOverlayIcon(method.Accessibility), false); if (method.IsExtensionMethod) - return Images.GetIcon(MemberIcon.ExtensionMethod, GetOverlayIcon(method.Accessibility), false); + return Images.GetIcon(MemberIcon.ExtensionMethod, Images.GetOverlayIcon(method.Accessibility), false); if (method.IsConstructor) - return Images.GetIcon(MemberIcon.Constructor, GetOverlayIcon(method.Accessibility), method.IsStatic); + return Images.GetIcon(MemberIcon.Constructor, Images.GetOverlayIcon(method.Accessibility), method.IsStatic); if (!method.HasBody && method.HasAttribute(KnownAttribute.DllImport)) - return Images.GetIcon(MemberIcon.PInvokeMethod, GetOverlayIcon(method.Accessibility), true); + return Images.GetIcon(MemberIcon.PInvokeMethod, Images.GetOverlayIcon(method.Accessibility), true); return Images.GetIcon(method.IsVirtual ? MemberIcon.VirtualMethod : MemberIcon.Method, - 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; - } + Images.GetOverlayIcon(method.Accessibility), method.IsStatic); } public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) diff --git a/ILSpy/TreeNodes/PropertyTreeNode.cs b/ILSpy/TreeNodes/PropertyTreeNode.cs index fc0a0ccbc..cdc6bd2f3 100644 --- a/ILSpy/TreeNodes/PropertyTreeNode.cs +++ b/ILSpy/TreeNodes/PropertyTreeNode.cs @@ -68,7 +68,7 @@ namespace ICSharpCode.ILSpy.TreeNodes public static ImageSource GetIcon(IProperty property) { return Images.GetIcon(property.IsIndexer ? MemberIcon.Indexer : MemberIcon.Property, - MethodTreeNode.GetOverlayIcon(property.Accessibility), property.IsStatic); + Images.GetOverlayIcon(property.Accessibility), property.IsStatic); } public override FilterResult Filter(LanguageSettings settings) diff --git a/ILSpy/ViewModels/CompareViewModel.cs b/ILSpy/ViewModels/CompareViewModel.cs new file mode 100644 index 000000000..3dc2fc53f --- /dev/null +++ b/ILSpy/ViewModels/CompareViewModel.cs @@ -0,0 +1,625 @@ +// Copyright (c) 2025 Siegfried Pammer +// +// 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; +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; + +#nullable enable + +namespace ICSharpCode.ILSpy.ViewModels +{ + using System.Linq; + using System.Threading.Tasks; + using System.Windows.Input; + using System.Windows.Media; + + using ICSharpCode.Decompiler; + using ICSharpCode.Decompiler.TypeSystem; + using ICSharpCode.ILSpy.Commands; + using ICSharpCode.ILSpy.TreeNodes; + using ICSharpCode.ILSpy.Views; + + class CompareViewModel : ObservableObject + { + private readonly TabPageModel tabPage; + private readonly AssemblyTreeModel assemblyTreeModel; + private LoadedAssembly leftAssembly; + private LoadedAssembly rightAssembly; + [AllowNull] + private LoadedAssembly[] assemblies; + private ComparisonEntryTreeNode root; + private bool updating = false; + private bool showIdentical; + + public CompareViewModel(TabPageModel tabPage, AssemblyTreeModel assemblyTreeModel, LoadedAssembly left, LoadedAssembly right) + { + this.tabPage = tabPage; + this.assemblyTreeModel = assemblyTreeModel; + leftAssembly = left; + rightAssembly = right; + + var leftTree = CreateEntityTree(left.GetTypeSystemOrNull()!); + var rightTree = CreateEntityTree(right.GetTypeSystemOrNull()!); + + this.root = new ComparisonEntryTreeNode(MergeTrees(leftTree.Item2, rightTree.Item2), this); + + this.SwapAssembliesCommand = new DelegateCommand(OnSwapAssemblies); + + this.PropertyChanged += CompareViewModel_PropertyChanged; + } + + private void CompareViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(LeftAssembly): + case nameof(RightAssembly): + if (updating) + break; + updating = true; + var view = tabPage.Content; + tabPage.ShowTextView(t => t.RunWithCancellation(token => Task.Run(DoCompare, token), $"Comparing {LeftAssembly.Text} - {RightAssembly.Text}").Then(_ => { + tabPage.Title = $"Compare {LeftAssembly.Text} - {RightAssembly.Text}"; + tabPage.SupportsLanguageSwitching = false; + tabPage.FrozenContent = true; + tabPage.Content = view; + updating = false; + }).HandleExceptions()); + + Task DoCompare() + { + var leftTree = CreateEntityTree(leftAssembly.GetTypeSystemOrNull()!); + var rightTree = CreateEntityTree(rightAssembly.GetTypeSystemOrNull()!); + + this.RootEntry = new ComparisonEntryTreeNode(MergeTrees(leftTree.Item2, rightTree.Item2), this); + return Task.FromResult(true); + } + break; + case nameof(ShowIdentical): + this.RootEntry.EnsureChildrenFiltered(); + break; + } + } + + 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(); + } + } + } + + public bool ShowIdentical { + get => showIdentical; + set { + if (showIdentical != value) + { + showIdentical = value; + OnPropertyChanged(); + } + } + } + + public ICommand SwapAssembliesCommand { get; set; } + + void OnSwapAssemblies() + { + var left = this.leftAssembly; + this.leftAssembly = this.rightAssembly; + this.rightAssembly = left; + OnPropertyChanged(nameof(LeftAssembly)); + OnPropertyChanged(nameof(RightAssembly)); + } + + Entry MergeTrees(Entry a, Entry b) + { + var m = new Entry() { + Entity = a.Entity, + OtherEntity = b.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(ICompilation typeSystem) + { + var module = (MetadataModule)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 = module, 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 = ResolveNamespace(typeDef.Namespace, typeDef.ParentModule)! }; + 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); + + INamespace? ResolveNamespace(string namespaceName, IModule module) + { + INamespace current = module.RootNamespace; + string[] parts = namespaceName.Split('.'); + + for (int i = 0; i < parts.Length; i++) + { + if (i == 0 && string.IsNullOrEmpty(parts[i])) + { + continue; + } + var next = current.GetChildNamespace(parts[i]); + if (next != null) + current = next; + else + return null; + } + + return current; + } + } + + 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) + { + SetKind(item, DiffKind.Remove); + } + } + } + else + { + foreach (var item in items) + { + SetKind(item, 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)); + SetKind(item, DiffKind.Add); + } + } + } + else + { + foreach (var item in items) + { + SetKind(item, DiffKind.Add); + results.Add((null, item)); + } + } + } + + return results; + + static void SetKind(Entry item, DiffKind kind) + { + if (item.Children?.Count > 0) + { + foreach (var child in item.Children) + { + SetKind(child, kind); + } + } + else + { + item.Kind = kind; + } + } + } + + internal static void Show(TabPageModel tabPage, LoadedAssembly left, LoadedAssembly right, AssemblyTreeModel assemblyTreeModel) + { + tabPage.ShowTextView(t => t.RunWithCancellation(token => Task.Run(DoCompare, token), $"Comparing {left.Text} - {right.Text}").Then(vm => { + tabPage.Title = $"Compare {left.Text} - {right.Text}"; + tabPage.SupportsLanguageSwitching = false; + tabPage.FrozenContent = true; + var compareView = new CompareView(); + compareView.DataContext = vm; + tabPage.Content = compareView; + })); + + CompareViewModel DoCompare() + { + return new CompareViewModel(tabPage, assemblyTreeModel, left, right); + } + } + } + + [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] + public class Entry + { + private DiffKind? kind; + + public DiffKind RecursiveKind { + get { + if (kind != null) + return kind.Value; + if (Children == null) + return DiffKind.None; + + int addCount = 0, removeCount = 0, updateCount = 0; + + foreach (var item in Children) + { + switch (item.RecursiveKind) + { + 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; + } + } + + public DiffKind Kind { + get => this.kind ?? DiffKind.None; + set => this.kind = value; + } + public required string Signature { get; init; } + public required ISymbol Entity { get; init; } + public ISymbol? OtherEntity { 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; + private readonly CompareViewModel compareViewModel; + + public ComparisonEntryTreeNode(Entry entry, CompareViewModel compareViewModel) + { + this.entry = entry; + this.LazyLoading = entry.Children != null; + this.compareViewModel = compareViewModel; + } + + protected override void LoadChildren() + { + if (entry.Children == null) + return; + + foreach (var item in entry.Children.OrderBy(e => (-(int)e.RecursiveKind, e.Entity.SymbolKind, e.Signature))) + { + this.Children.Add(new ComparisonEntryTreeNode(item, compareViewModel)); + } + } + + public override object Text { + get { + string? entityText = GetEntityText(entry.Entity); + string? otherText = GetEntityText(entry.OtherEntity); + + return entityText + (otherText != null && entityText != otherText ? " -> " + otherText : ""); + + string? GetEntityText(ISymbol? symbol) => symbol switch { + ITypeDefinition t => this.Language.TypeToString(t, includeNamespace: false) + GetSuffixString(t.MetadataToken), + IMethod m => this.Language.MethodToString(m, false, false, false) + GetSuffixString(m.MetadataToken), + IField f => this.Language.FieldToString(f, false, false, false) + GetSuffixString(f.MetadataToken), + IProperty p => this.Language.PropertyToString(p, false, false, false) + GetSuffixString(p.MetadataToken), + IEvent e => this.Language.EventToString(e, false, false, false) + GetSuffixString(e.MetadataToken), + INamespace n => n.FullName, + IModule m => m.FullAssemblyName, + _ => null, + }; + } + } + + public override object Icon { + get { + switch (entry.Entity) + { + case ITypeDefinition t: + return TypeTreeNode.GetIcon(t); + case IMethod m: + return MethodTreeNode.GetIcon(m); + case IField f: + return FieldTreeNode.GetIcon(f); + case IProperty p: + return PropertyTreeNode.GetIcon(p); + case IEvent e: + return EventTreeNode.GetIcon(e); + case INamespace n: + return Images.Namespace; + case IModule m: + return Images.Assembly; + default: + throw new NotSupportedException(); + } + } + } + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + } + + public override FilterResult Filter(LanguageSettings settings) + { + return compareViewModel.ShowIdentical || entry.RecursiveKind != DiffKind.None ? FilterResult.Match : FilterResult.Hidden; + } + + public Brush Background { + get { + switch (entry.RecursiveKind) + { + case DiffKind.Add: + return Brushes.LightGreen; + case DiffKind.Remove: + return Brushes.LightPink; + case DiffKind.Update: + return Brushes.LightBlue; + } + + return Brushes.Transparent; + } + } + } +} 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..ab9c35005 --- /dev/null +++ b/ILSpy/Views/CompareView.xaml @@ -0,0 +1,56 @@ + + + + + + + +