mirror of https://github.com/icsharpcode/ILSpy.git
25 changed files with 999 additions and 38 deletions
@ -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 }]; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
<!-- This file was generated by the AiToXaml tool.--> |
||||
<!-- Tool Version: 14.0.22307.0 --> |
||||
<DrawingGroup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" ClipGeometry="M0,0 V16 H16 V0 H0 Z"> |
||||
<DrawingGroup.Children> |
||||
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> |
||||
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M15,2L15,11 11,11C10.836,11.005,10,12,10,12L8,12C8,12,7.164,11.005,7,11L7,13 2,13 2,8 3,8 3,2 7.5,2C8.146,2.004 8.62,2.191 9,2.5 9.381,2.191 9.863,2 10.5,2z" /> |
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M13,9L11,9C10.565,9,10.33,8.795,10,9L10,5C10,4.615,10.41,4,11,4L13,4z M10.5,3C9.876,3 9.516,3.365 9,4 8.492,3.374 8.139,3.004 7.5,3L4,3 4,8 5,8 5,4 7,4C7.566,4,8,4.663,8,5L8,9C7.67,8.795,7.436,9,7,9L7,10C7.549,10 8.778,9.975 9,10.5 9.221,9.975 10.451,10 11,10L14,10 14,3z" /> |
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M3,11L6,11 6,12 3,12z" /> |
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M3,10L6,10 6,9 3,9z" /> |
||||
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M7,4L5,4 5,8 7,8 7,9C7.436,9,7.67,8.795,8,9L8,5C8,4.663,7.566,4,7,4" /> |
||||
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M11,4C10.41,4,10,4.615,10,5L10,9C10.33,8.795,10.565,9,11,9L13,9 13,4z" /> |
||||
</DrawingGroup.Children> |
||||
</DrawingGroup> |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
<!-- This file was generated by the AiToXaml tool.--> |
||||
<!-- Tool Version: 14.0.22307.0 --> |
||||
<DrawingGroup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> |
||||
<DrawingGroup.Children> |
||||
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> |
||||
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M15,10L13,10 13,12 11,12 11,14 2,14 2,5 4,5 4,3 6,3 6,1 15,1z" /> |
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M9,7L4,7 4,12 9,12z M10,13L3,13 3,6 10,6z M5,4L5,5 6,5 11,5 11,10 11,11 12,11 12,4z M14,2L14,9 13,9 13,8 13,3 8,3 7,3 7,2z" /> |
||||
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M8,10L7,10 7,11 6.984,11 6,11 6,10 5,10 5,9 6,9 6,8 7,8 7,9 8,9z M4,12L9,12 9,7 4,7z" /> |
||||
<GeometryDrawing Brush="#FF00539C" Geometry="F1M7,9L8,9 8,10 6.995,10 6.984,11 6,11 6,10 5,10 5,9 6,9 6,8 7,8z" /> |
||||
</DrawingGroup.Children> |
||||
</DrawingGroup> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<!-- This file was generated by the AiToXaml tool.--> |
||||
<!-- Tool Version: 14.0.22307.0 --> |
||||
<DrawingGroup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" ClipGeometry="M0,0 V16 H16 V0 H0 Z"> |
||||
<DrawingGroup.Children> |
||||
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> |
||||
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M9.4141,4L5.4141,0 0.5861,0 2.5861,2 9.99999999997669E-05,2 9.99999999997669E-05,6 2.0001,6 2.0001,6.586 1.5861,7 1.0001,7 1.0001,7.586 0.5861,8 1.0001,8 1.0001,9.638 1.3291,11 2.0001,11 2.0001,12.536C2.0001,13.783 2.4951,14.686 3.1901,15.247 3.8311,15.764 4.8871,16 6.1271,16L7.0001,16 7.0001,12.753C7.0001,12.753 5.9891,12.752 5.9671,12.75 5.9591,12.697 6.0001,12.623 6.0001,12.522L6.0001,11.121C6.0001,10.159 5.7761,9.484 5.4571,9.01 5.7131,8.632 5.9011,8.119 5.9681,7.446z M16.0001,8.38L16.0001,10.638C16.0001,10.638 15.0081,10.637 15.0031,10.635 14.9911,10.687 15.0001,10.775 15.0001,10.916L15.0001,12.495C15.0001,13.766 14.6301,14.68 13.9461,15.241 13.3081,15.763 12.3701,16 11.1241,16L10.0001,16 10.0001,12.753C10.0001,12.753 11.0141,12.752 11.0371,12.75 11.0411,12.704 11.0001,12.633 11.0001,12.536L11.0001,11.08C11.0001,10.137 11.2221,9.474 11.5391,9.008 11.2231,8.527 11.0001,7.843 11.0001,6.869L11.0001,5.468C11.0001,5.38 11.1021,5.182 11.0941,5.143 11.0741,5.141 11.1571,5 11.1241,5L10.0001,5 10.0001,2 11.1241,2C12.3751,2 13.3171,2.265 13.9561,2.81 14.6331,3.387 15.0001,4.3 15.0001,5.522L15.0001,7 15.9191,7z" /> |
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M5.0332,12.5225L5.0332,11.1215C5.0332,10.0045,4.6822,9.3055,3.9802,9.0225L3.9802,8.9955C4.4092,8.8195,4.6902,8.4755,4.8572,8.0005L3.0492,8.0005C2.8762,8.2475,2.6132,8.3795,2.2442,8.3795L2.2442,9.6375C2.9362,9.6375,3.2832,10.0575,3.2832,10.8955L3.2832,12.5365C3.2832,13.4705 3.5092,14.1205 3.9602,14.4845 4.4112,14.8485 5.1342,15.0315 6.1272,15.0315L6.1272,13.7525C5.7392,13.7525 5.4612,13.6595 5.2892,13.4725 5.1192,13.2855 5.0332,12.9695 5.0332,12.5225 M15.0002,8.3795L15.0002,9.6375C14.3032,9.6375,13.9542,10.0645,13.9542,10.9165L13.9542,12.4955C13.9542,13.4565 13.7312,14.1205 13.2882,14.4845 12.8432,14.8485 12.1222,15.0315 11.1242,15.0315L11.1242,13.7525C11.5072,13.7525 11.7852,13.6605 11.9582,13.4765 12.1312,13.2915 12.2182,12.9785 12.2182,12.5365L12.2182,11.0805C12.2182,9.9905,12.5662,9.3095,13.2632,9.0365L13.2632,9.0085C12.5662,8.7215,12.2182,8.0085,12.2182,6.8695L12.2182,5.4675C12.2182,4.6745,11.8532,4.2785,11.1242,4.2785L11.1242,3.0005C12.1172,3.0005 12.8382,3.1905 13.2842,3.5705 13.7302,3.9515 13.9542,4.6025 13.9542,5.5225L13.9542,7.0875C13.9542,7.9495,14.3032,8.3795,15.0002,8.3795" /> |
||||
<GeometryDrawing Brush="#FF00529C" Geometry="F1M8,4L5,7 3,7 5,5 1,5 1,3 5,3 3,1 5,1z" /> |
||||
</DrawingGroup.Children> |
||||
</DrawingGroup> |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<!-- This file was generated by the AiToXaml tool.--> |
||||
<!-- Tool Version: 14.0.22307.0 --> |
||||
<DrawingGroup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" ClipGeometry="M0,0 V16 H16 V0 H0 Z"> |
||||
<DrawingGroup.Children> |
||||
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> |
||||
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M4.5855,1.9996L0.5855,6.0006 2.5855,7.9996 2.9995,7.9996 2.9995,12.0006 8.5865,12.0006 6.5865,14.0006 11.4135,14.0006 15.4145,9.9996 13.4145,7.9996 12.9995,7.9996 12.9995,4.0006 7.4145,4.0006 9.4145,1.9996z" /> |
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M13,9L4,9 4,11 11,11 9,13 11,13 14,10z M12,7L3,7 2,6 5,3 7,3 5,5 12,5z" /> |
||||
</DrawingGroup.Children> |
||||
</DrawingGroup> |
@ -0,0 +1,718 @@
@@ -0,0 +1,718 @@
|
||||
// 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.Linq; |
||||
using System.Reflection.Metadata; |
||||
using System.Text.Json; |
||||
using System.Threading.Tasks; |
||||
using System.Windows; |
||||
using System.Windows.Input; |
||||
using System.Windows.Media; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.CSharp.OutputVisitor; |
||||
using ICSharpCode.Decompiler.Util; |
||||
using ICSharpCode.ILSpy.AssemblyTree; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
using ICSharpCode.ILSpy.Views; |
||||
using ICSharpCode.ILSpyX; |
||||
using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions; |
||||
|
||||
#nullable enable |
||||
|
||||
namespace ICSharpCode.ILSpy.ViewModels |
||||
{ |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
using TomsToolbox.Wpf; |
||||
|
||||
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.ExpandAllCommand = new DelegateCommand(OnExpandAll); |
||||
this.CopyToClipboardAsJSONCommand = new DelegateCommand(OnCopyToClipboardAsJSON); |
||||
|
||||
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; } |
||||
public ICommand ExpandAllCommand { get; set; } |
||||
public ICommand CopyToClipboardAsJSONCommand { get; set; } |
||||
|
||||
void OnSwapAssemblies() |
||||
{ |
||||
var left = this.leftAssembly; |
||||
this.leftAssembly = this.rightAssembly; |
||||
this.rightAssembly = left; |
||||
OnPropertyChanged(nameof(LeftAssembly)); |
||||
OnPropertyChanged(nameof(RightAssembly)); |
||||
} |
||||
|
||||
void OnExpandAll() |
||||
{ |
||||
foreach (var node in RootEntry.DescendantsAndSelf()) |
||||
{ |
||||
node.IsExpanded = true; |
||||
} |
||||
} |
||||
|
||||
void OnCopyToClipboardAsJSON() |
||||
{ |
||||
var options = new JsonSerializerOptions { |
||||
WriteIndented = true |
||||
}; |
||||
|
||||
var jsonEntry = ConvertToJson(this.root.Entry); |
||||
var json = JsonSerializer.Serialize(jsonEntry, options); |
||||
Clipboard.SetText(json); |
||||
} |
||||
|
||||
private object ConvertToJson(Entry entry) |
||||
{ |
||||
List<Entry> changedTypes = new(); |
||||
List<Entry> addedTypes = new(); |
||||
List<Entry> removedTypes = new(); |
||||
|
||||
foreach (var item in TreeTraversal.PreOrder(entry, entry => entry.Children)) |
||||
{ |
||||
if (item.Entity is ITypeDefinition) |
||||
{ |
||||
switch (item.RecursiveKind) |
||||
{ |
||||
case DiffKind.Add: |
||||
addedTypes.Add(item); |
||||
break; |
||||
case DiffKind.Remove: |
||||
removedTypes.Add(item); |
||||
break; |
||||
case DiffKind.Update: |
||||
changedTypes.Add(item); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
var result = new { |
||||
left = LeftAssembly.FileName.Replace('\\', '/'), |
||||
right = RightAssembly.FileName.Replace('\\', '/'), |
||||
changedTypes = changedTypes.SelectArray(t => new { typeName = ((ITypeDefinition)t.Entity).FullName, changes = GetChanges(t.Children) }), |
||||
addedTypes = addedTypes.SelectArray(t => new { typeName = ((ITypeDefinition)t.Entity).FullName, changes = GetChanges(t.Children) }), |
||||
removedTypes = removedTypes.SelectArray(t => new { typeName = ((ITypeDefinition)t.Entity).FullName, changes = GetChanges(t.Children) }) |
||||
}; |
||||
return result; |
||||
|
||||
string? GetEntityText(ISymbol? symbol) => symbol switch { |
||||
ITypeDefinition t => this.assemblyTreeModel.CurrentLanguage.TypeToString(t, includeNamespace: true), |
||||
IMethod m => this.assemblyTreeModel.CurrentLanguage.MethodToString(m, false, false, false), |
||||
IField f => this.assemblyTreeModel.CurrentLanguage.FieldToString(f, false, false, false), |
||||
IProperty p => this.assemblyTreeModel.CurrentLanguage.PropertyToString(p, false, false, false), |
||||
IEvent e => this.assemblyTreeModel.CurrentLanguage.EventToString(e, false, false, false), |
||||
INamespace n => n.FullName, |
||||
IModule m => m.FullAssemblyName, |
||||
_ => null, |
||||
}; |
||||
|
||||
IEnumerable<object> GetChanges(List<Entry>? entries) |
||||
{ |
||||
if (entries == null) |
||||
yield break; |
||||
foreach (var item in entries) |
||||
{ |
||||
if (item.Kind is not (DiffKind.Add or DiffKind.Remove or DiffKind.Update)) |
||||
continue; |
||||
yield return new { |
||||
name = GetEntityText(item.Entity), |
||||
operation = item.Kind switch { DiffKind.Add => "added", DiffKind.Remove => "removed", DiffKind.Update => "changed" } |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
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; |
||||
|
||||
internal Entry Entry => entry; |
||||
|
||||
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 void ActivateItem(IPlatformRoutedEventArgs e) |
||||
{ |
||||
var node = AssemblyTreeModel.FindTreeNode(entry.Entity); |
||||
AssemblyTreeModel.SelectNode(node); |
||||
} |
||||
|
||||
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; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
<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" |
||||
xmlns:controls="clr-namespace:ICSharpCode.ILSpy.Controls" |
||||
xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes" |
||||
mc:Ignorable="d" |
||||
d:DesignHeight="450" d:DesignWidth="800"> |
||||
<UserControl.Resources> |
||||
<!-- Make images transparent if menu command is disabled --> |
||||
<Style TargetType="{x:Type Image}"> |
||||
<Style.Triggers> |
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}, AncestorLevel=1}, Path=IsEnabled}" Value="False"> |
||||
<Setter Property="Opacity" Value="0.30" /> |
||||
</DataTrigger> |
||||
</Style.Triggers> |
||||
</Style> |
||||
<Style TargetType="{x:Type Image}" x:Key="DarkModeAwareImageStyle"> |
||||
<Setter Property="Effect" Value="{DynamicResource {x:Static themes:ResourceKeys.ThemeAwareButtonEffect}}" /> |
||||
<Style.Triggers> |
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}, AncestorLevel=1}, Path=IsEnabled}" Value="False"> |
||||
<Setter Property="Opacity" Value="0.30" /> |
||||
</DataTrigger> |
||||
</Style.Triggers> |
||||
</Style> |
||||
</UserControl.Resources> |
||||
<Grid> |
||||
<Grid.RowDefinitions> |
||||
<RowDefinition Height="Auto" /> |
||||
<RowDefinition Height="*" /> |
||||
</Grid.RowDefinitions> |
||||
<ToolBar> |
||||
<Button Command="{Binding SwapAssembliesCommand}" ToolTip="Swap left and right"> |
||||
<Image Width="16" Height="16" Source="{controls:XamlResource Images/SwitchSourceOrTarget}" |
||||
Style="{StaticResource DarkModeAwareImageStyle}" /> |
||||
</Button> |
||||
<ToggleButton IsChecked="{Binding ShowIdentical}" ToolTip="Show identical entries"> |
||||
<Image Width="16" Height="16" Source="{controls:XamlResource Images/DictionaryContain}" |
||||
Style="{StaticResource DarkModeAwareImageStyle}" /> |
||||
</ToggleButton> |
||||
<Button Command="{Binding ExpandAllCommand}" ToolTip="Expand all"> |
||||
<Image Width="16" Height="16" Source="{controls:XamlResource Images/ExpandAll}" |
||||
Style="{StaticResource DarkModeAwareImageStyle}" /> |
||||
</Button> |
||||
<Button Command="{Binding CopyToClipboardAsJSONCommand}" ToolTip="Copy to clipboard as JSON"> |
||||
<Image Width="16" Height="16" Source="{controls:XamlResource Images/ResultToJSON}" |
||||
Style="{StaticResource DarkModeAwareImageStyle}" /> |
||||
</Button> |
||||
</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> |
@ -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(); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue