Browse Source

Add CompareView and FrozenContent flag

pull/3519/head
Siegfried Pammer 8 months ago
parent
commit
0af45643eb
  1. 2
      ICSharpCode.ILSpyX/AssemblyListSnapshot.cs
  2. 5
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  3. 53
      ILSpy/Commands/CompareContextMenuEntry.cs
  4. 1
      ILSpy/Commands/SearchMsdnContextMenuEntry.cs
  5. 2
      ILSpy/Controls/TreeView/SharpTreeView.xaml
  6. 6
      ILSpy/Docking/DockWorkspace.cs
  7. 23
      ILSpy/Images/Images.cs
  8. 2
      ILSpy/TreeNodes/EventTreeNode.cs
  9. 8
      ILSpy/TreeNodes/FieldTreeNode.cs
  10. 31
      ILSpy/TreeNodes/MethodTreeNode.cs
  11. 2
      ILSpy/TreeNodes/PropertyTreeNode.cs
  12. 625
      ILSpy/ViewModels/CompareViewModel.cs
  13. 7
      ILSpy/ViewModels/TabPageModel.cs
  14. 56
      ILSpy/Views/CompareView.xaml
  15. 33
      ILSpy/Views/CompareView.xaml.cs
  16. 7
      ILSpy/Views/DebugSteps.xaml.cs

2
ICSharpCode.ILSpyX/AssemblyListSnapshot.cs

@ -186,7 +186,7 @@ namespace ICSharpCode.ILSpyX @@ -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)

5
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -794,6 +794,11 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -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)

53
ILSpy/Commands/CompareContextMenuEntry.cs

@ -0,0 +1,53 @@ @@ -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 }];
}
}
}

1
ILSpy/Commands/SearchMsdnContextMenuEntry.cs

@ -17,7 +17,6 @@ @@ -17,7 +17,6 @@
// DEALINGS IN THE SOFTWARE.
using System.Linq;
using System.Threading;
using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpy.TreeNodes;

2
ILSpy/Controls/TreeView/SharpTreeView.xaml

@ -246,7 +246,7 @@ @@ -246,7 +246,7 @@
VerticalAlignment="Center" />
</Border>
<StackPanel Orientation="Horizontal"
Background="Transparent"
Background="{Binding Background}"
ToolTip="{Binding ToolTip}">
<ContentPresenter Name="icon"
Content="{Binding Icon}"

6
ILSpy/Docking/DockWorkspace.cs

@ -118,9 +118,11 @@ namespace ICSharpCode.ILSpy.Docking @@ -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>());
TabPageModel item = tabPage ?? exportProvider.GetExportedValue<TabPageModel>();
tabPages.Add(item);
return item;
}
public ReadOnlyObservableCollection<TabPageModel> TabPages { get; }

23
ILSpy/Images/Images.cs

@ -25,6 +25,8 @@ using System.Windows.Media.Imaging; @@ -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 @@ -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);

2
ILSpy/TreeNodes/EventTreeNode.cs

@ -65,7 +65,7 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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)

8
ILSpy/TreeNodes/FieldTreeNode.cs

@ -58,15 +58,15 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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)

31
ILSpy/TreeNodes/MethodTreeNode.cs

@ -58,40 +58,19 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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)

2
ILSpy/TreeNodes/PropertyTreeNode.cs

@ -68,7 +68,7 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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)

625
ILSpy/ViewModels/CompareViewModel.cs

@ -0,0 +1,625 @@ @@ -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<bool> 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>, 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<Entry> results = new();
Dictionary<TypeDefinitionHandle, Entry> typeEntries = new();
Dictionary<string, Entry> 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<Entry> left, List<Entry> right)
{
Dictionary<string, List<Entry>> leftMap = new();
Dictionary<string, List<Entry>> 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<Entry>? Children { get; set; }
private string GetDebuggerDisplay()
{
return $"Entry{Kind}{Entity.ToString() ?? Signature}";
}
}
public class EntryComparer : IEqualityComparer<Entry>
{
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;
}
}
}
}

7
ILSpy/ViewModels/TabPageModel.cs

@ -51,6 +51,13 @@ namespace ICSharpCode.ILSpy.ViewModels @@ -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 {

56
ILSpy/Views/CompareView.xaml

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
<UserControl x:Class="ICSharpCode.ILSpy.Views.CompareView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ICSharpCode.ILSpy.Views"
xmlns:stv="clr-namespace:ICSharpCode.ILSpy.Controls.TreeView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ToolBar>
<Label Content="Left:" />
<TextBlock VerticalAlignment="Center" Text="{Binding LeftAssembly.Text}" />
<Label Content="Right:" />
<TextBlock VerticalAlignment="Center" Text="{Binding RightAssembly.Text}" />
<Button Content="Swap" Command="{Binding SwapAssembliesCommand}" />
<ToggleButton Content="Show identical" IsChecked="{Binding ShowIdentical}" />
</ToolBar>
<stv:SharpTreeView Grid.Row="1" Root="{Binding RootEntry}" ShowRoot="True">
<stv:SharpTreeView.ItemContainerStyle>
<Style TargetType="stv:SharpTreeViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type stv:SharpTreeViewItem}">
<Border Background="Transparent">
<Border Background="{TemplateBinding Background}">
<stv:SharpTreeNodeView x:Name="nodeView" HorizontalAlignment="Left" />
</Border>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsPublicAPI}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</DataTrigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="nodeView" Property="TextBackground"
Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
<Setter TargetName="nodeView" Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="nodeView" Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</stv:SharpTreeView.ItemContainerStyle>
</stv:SharpTreeView>
</Grid>
</UserControl>

33
ILSpy/Views/CompareView.xaml.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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.Windows.Controls;
namespace ICSharpCode.ILSpy.Views
{
/// <summary>
/// Interaction logic for CompareView.xaml
/// </summary>
public partial class CompareView : UserControl
{
public CompareView()
{
InitializeComponent();
}
}
}

7
ILSpy/Views/DebugSteps.xaml.cs

@ -1,5 +1,4 @@ @@ -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 @@ -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) {

Loading…
Cancel
Save