mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Brings the assembly-comparison feature across. Right-click two valid managed assemblies in the tree → "Compare..." → fresh tab opens with a diff tree: identical entries hidden by default, additions tinted green, removals pink, signature-matched-but-different rows blue. Identical entries surface via the "Show identical entries" toolbar checkbox.pull/3755/head
10 changed files with 1051 additions and 0 deletions
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2026 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.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using ILSpy; |
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Commands; |
||||
using ILSpy.Compare; |
||||
using ILSpy.TreeNodes; |
||||
using ILSpy.ViewModels; |
||||
using ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Compare; |
||||
|
||||
/// <summary>
|
||||
/// Pins the right-click → Compare flow: visibility gate (exactly two valid assembly
|
||||
/// nodes), and that <c>Execute</c> opens a fresh tab containing a
|
||||
/// <see cref="CompareTabPageModel"/> with a populated root.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class CompareContextMenuTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Compare_Is_Visible_When_Two_Assemblies_Are_Selected() |
||||
{ |
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2); |
||||
|
||||
var entry = AppComposition.Current.GetExport<global::ILSpy.ContextMenuEntryRegistry>() |
||||
.Entries.Single(e => e.Metadata.Header == "Compare...").Value; |
||||
var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() |
||||
.Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList(); |
||||
assemblies.Should().HaveCount(2, "the fixture must load at least two valid assemblies"); |
||||
|
||||
var nodes = assemblies.Select(a => |
||||
(SharpTreeNode)vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(a.ShortName)).ToArray(); |
||||
entry.IsVisible(new TextViewContext { SelectedTreeNodes = nodes }).Should().BeTrue(); |
||||
entry.IsEnabled(new TextViewContext { SelectedTreeNodes = nodes }).Should().BeTrue(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Compare_Is_Hidden_For_Single_Selection() |
||||
{ |
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||
|
||||
var entry = AppComposition.Current.GetExport<global::ILSpy.ContextMenuEntryRegistry>() |
||||
.Entries.Single(e => e.Metadata.Header == "Compare...").Value; |
||||
var coreLibName = typeof(object).Assembly.GetName().Name!; |
||||
var node = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName); |
||||
entry.IsVisible(new TextViewContext { |
||||
SelectedTreeNodes = new[] { (SharpTreeNode)node } |
||||
}).Should().BeFalse( |
||||
"Compare needs exactly two assemblies — single selection must hide the entry"); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Execute_Opens_A_Tab_Containing_A_CompareTabPageModel_With_Populated_Tree() |
||||
{ |
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2); |
||||
|
||||
var entry = AppComposition.Current.GetExport<global::ILSpy.ContextMenuEntryRegistry>() |
||||
.Entries.Single(e => e.Metadata.Header == "Compare...").Value; |
||||
var dockWorkspace = AppComposition.Current.GetExport<global::ILSpy.Docking.DockWorkspace>(); |
||||
var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() |
||||
.Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList(); |
||||
var nodes = assemblies.Select(a => |
||||
(SharpTreeNode)vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(a.ShortName)).ToArray(); |
||||
|
||||
entry.Execute(new TextViewContext { SelectedTreeNodes = nodes }); |
||||
|
||||
// The new tab's Content is the CompareTabPageModel. Walk the dock's tabs and find it.
|
||||
var page = dockWorkspace.Documents?.VisibleDockables? |
||||
.OfType<ContentTabPage>() |
||||
.Select(t => t.Content) |
||||
.OfType<CompareTabPageModel>() |
||||
.FirstOrDefault(); |
||||
Assert.That(page, Is.Not.Null, "a ContentTabPage with CompareTabPageModel content must exist"); |
||||
ReferenceEquals(page!.LeftAssembly, assemblies[0]).Should().BeTrue( |
||||
"first-selected node becomes the left-hand assembly of the comparison"); |
||||
ReferenceEquals(page.RightAssembly, assemblies[1]).Should().BeTrue( |
||||
"second-selected node becomes the right-hand assembly"); |
||||
((object?)page.RootEntry).Should().NotBeNull("the merged-tree root must be set at construction"); |
||||
} |
||||
} |
||||
@ -0,0 +1,136 @@
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2026 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.Collections.Generic; |
||||
using System.Linq; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
using ILSpy.Compare; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Compare; |
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the assembly-comparison engine. The diff logic is pure data — synthetic
|
||||
/// <see cref="Entry"/> trees, no decompiler required — so each scenario runs in
|
||||
/// milliseconds.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class CompareEngineTests |
||||
{ |
||||
static Entry Leaf(string signature) => new Entry { Signature = signature, Entity = null!, }; |
||||
|
||||
static Entry Branch(string signature, params Entry[] children) => new Entry { |
||||
Signature = signature, |
||||
Entity = null!, |
||||
Children = children.ToList(), |
||||
}; |
||||
|
||||
[Test] |
||||
public void CalculateDiff_Pairs_Identical_Signatures() |
||||
{ |
||||
var left = new List<Entry> { Leaf("M1"), Leaf("M2") }; |
||||
var right = new List<Entry> { Leaf("M1"), Leaf("M2") }; |
||||
var diff = CompareEngine.CalculateDiff(left, right); |
||||
diff.Should().HaveCount(2); |
||||
diff.Should().OnlyContain(p => p.Left != null && p.Right != null); |
||||
diff.All(p => p.Left!.Kind == DiffKind.None).Should().BeTrue(); |
||||
} |
||||
|
||||
[Test] |
||||
public void CalculateDiff_Marks_Left_Only_As_Remove() |
||||
{ |
||||
var left = new List<Entry> { Leaf("Stays"), Leaf("Gone") }; |
||||
var right = new List<Entry> { Leaf("Stays") }; |
||||
var diff = CompareEngine.CalculateDiff(left, right); |
||||
var gone = diff.Single(p => p.Left?.Signature == "Gone"); |
||||
((object?)gone.Right).Should().BeNull(); |
||||
gone.Left!.Kind.Should().Be(DiffKind.Remove); |
||||
} |
||||
|
||||
[Test] |
||||
public void CalculateDiff_Marks_Right_Only_As_Add() |
||||
{ |
||||
var left = new List<Entry> { Leaf("Stays") }; |
||||
var right = new List<Entry> { Leaf("Stays"), Leaf("New") }; |
||||
var diff = CompareEngine.CalculateDiff(left, right); |
||||
var added = diff.Single(p => p.Right?.Signature == "New"); |
||||
((object?)added.Left).Should().BeNull(); |
||||
added.Right!.Kind.Should().Be(DiffKind.Add); |
||||
} |
||||
|
||||
[Test] |
||||
public void MergeTrees_Aligns_Identical_Subtrees_And_Tags_Diff_On_Children() |
||||
{ |
||||
var left = Branch("Type", |
||||
Leaf("Stays"), |
||||
Leaf("Gone")); |
||||
var right = Branch("Type", |
||||
Leaf("Stays"), |
||||
Leaf("New")); |
||||
var merged = CompareEngine.MergeTrees(left, right); |
||||
merged.Children.Should().NotBeNull(); |
||||
merged.Children!.Should().HaveCount(3, |
||||
"the merge keeps one slot per distinct signature across both sides"); |
||||
merged.Children.Should().Contain(c => c.Signature == "Gone" && c.Kind == DiffKind.Remove); |
||||
merged.Children.Should().Contain(c => c.Signature == "New" && c.Kind == DiffKind.Add); |
||||
merged.Children.Should().Contain(c => c.Signature == "Stays" && c.Kind == DiffKind.None); |
||||
} |
||||
|
||||
[Test] |
||||
public void RecursiveKind_Summarises_All_Removed_Children_As_Remove() |
||||
{ |
||||
// A branch whose every leaf is gone reads as Remove at the branch level —
|
||||
// matches WPF's "if every child is the same change kind, propagate it".
|
||||
var entry = Branch("T", |
||||
new Entry { Signature = "a", Entity = null!, Kind = DiffKind.Remove }, |
||||
new Entry { Signature = "b", Entity = null!, Kind = DiffKind.Remove }); |
||||
entry.RecursiveKind.Should().Be(DiffKind.Remove); |
||||
} |
||||
|
||||
[Test] |
||||
public void RecursiveKind_Mixed_Changes_Becomes_Update() |
||||
{ |
||||
var entry = Branch("T", |
||||
new Entry { Signature = "a", Entity = null!, Kind = DiffKind.Add }, |
||||
new Entry { Signature = "b", Entity = null!, Kind = DiffKind.Remove }); |
||||
entry.RecursiveKind.Should().Be(DiffKind.Update); |
||||
} |
||||
|
||||
[Test] |
||||
public void RecursiveKind_All_Identical_Children_Is_None() |
||||
{ |
||||
var entry = Branch("T", |
||||
new Entry { Signature = "a", Entity = null!, Kind = DiffKind.None }, |
||||
new Entry { Signature = "b", Entity = null!, Kind = DiffKind.None }); |
||||
entry.RecursiveKind.Should().Be(DiffKind.None); |
||||
} |
||||
|
||||
[Test] |
||||
public void EntryComparer_Keys_On_Signature_String() |
||||
{ |
||||
var left = Leaf("public void Foo()"); |
||||
var right = Leaf("public void Foo()"); |
||||
EntryComparer.Instance.Equals(left, right).Should().BeTrue(); |
||||
EntryComparer.Instance.GetHashCode(left).Should().Be(EntryComparer.Instance.GetHashCode(right)); |
||||
} |
||||
} |
||||
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Composition; |
||||
|
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Compare; |
||||
using ILSpy.Docking; |
||||
using ILSpy.TreeNodes; |
||||
|
||||
namespace ILSpy.Commands |
||||
{ |
||||
/// <summary>
|
||||
/// Right-click → "Compare…" on exactly two valid managed assemblies in the tree.
|
||||
/// Opens a fresh tab whose content is a <see cref="CompareTabPageModel"/> driven by
|
||||
/// <see cref="CompareEngine"/>. Order matters: the first-selected node becomes the
|
||||
/// "left" side, the second becomes the "right".
|
||||
/// </summary>
|
||||
[ExportContextMenuEntry(Header = "Compare...", Order = 9999)] |
||||
[Shared] |
||||
public sealed class CompareContextMenuEntry : IContextMenuEntry |
||||
{ |
||||
public bool IsEnabled(TextViewContext context) => IsVisible(context); |
||||
|
||||
public bool IsVisible(TextViewContext context) |
||||
=> context.SelectedTreeNodes is |
||||
[AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true }, |
||||
AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true }]; |
||||
|
||||
public void Execute(TextViewContext context) |
||||
{ |
||||
if (context.SelectedTreeNodes is not |
||||
[AssemblyTreeNode left, AssemblyTreeNode right]) |
||||
return; |
||||
|
||||
var dockWorkspace = TryGetExport<DockWorkspace>(); |
||||
if (dockWorkspace == null) |
||||
return; |
||||
var pane = new CompareTabPageModel(left.LoadedAssembly, right.LoadedAssembly); |
||||
// OpenNewTab wraps the content viewmodel in a ContentTabPage; the DataTemplate
|
||||
// for CompareTabPageModel routes through CompareView. Pass null for sourceNode
|
||||
// so the new tab isn't anchored to either assembly's tree node — the compare
|
||||
// view is a pair, not a single-node decompile result.
|
||||
dockWorkspace.OpenNewTab(pane); |
||||
} |
||||
|
||||
static T? TryGetExport<T>() where T : class |
||||
{ |
||||
try |
||||
{ return AppComposition.Current.GetExport<T>(); } |
||||
catch { return null; } |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,303 @@
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) 2026 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.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Reflection.Metadata; |
||||
|
||||
using ICSharpCode.Decompiler.CSharp.OutputVisitor; |
||||
using ICSharpCode.Decompiler.Output; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
namespace ILSpy.Compare |
||||
{ |
||||
/// <summary>
|
||||
/// Builds a merged <see cref="Entry"/> tree from two assemblies' type systems. The
|
||||
/// process is:
|
||||
/// <list type="number">
|
||||
/// <item>Walk each assembly's public surface (<see cref="CreateEntityTree"/>) into a
|
||||
/// per-side <see cref="Entry"/> tree keyed on C#-formatted signature strings.</item>
|
||||
/// <item>Recursively merge the two trees (<see cref="MergeTrees"/>) — paired entries
|
||||
/// get an <see cref="Entry.OtherEntity"/>; left-only entries are tagged
|
||||
/// <see cref="DiffKind.Remove"/>; right-only are tagged <see cref="DiffKind.Add"/>.</item>
|
||||
/// </list>
|
||||
/// Signatures are the keying mechanism because metadata tokens don't survive
|
||||
/// re-compilation; <see cref="EntryComparer"/> wraps the string-equality check.
|
||||
/// </summary>
|
||||
public static class CompareEngine |
||||
{ |
||||
/// <summary>
|
||||
/// Walks <paramref name="typeSystem"/>'s public types and members and returns
|
||||
/// (flat-list-of-leaves, namespace-rooted-tree). The flat list is for the side
|
||||
/// that drives diff pairing in <see cref="MergeTrees"/>; the tree is the
|
||||
/// hierarchical view the UI consumes.
|
||||
/// </summary>
|
||||
public static (List<Entry> AllEntries, Entry Root) CreateEntityTree(ICompilation typeSystem) |
||||
{ |
||||
var module = (MetadataModule)typeSystem.MainModule!; |
||||
var metadata = module.MetadataFile!.Metadata; |
||||
var ambience = new CSharpAmbience { |
||||
ConversionFlags = ConversionFlags.All & ~ConversionFlags.ShowDeclaringType, |
||||
}; |
||||
|
||||
var results = new List<Entry>(); |
||||
var typeEntries = new Dictionary<TypeDefinitionHandle, Entry>(); |
||||
var namespaceEntries = new Dictionary<string, Entry>(System.StringComparer.Ordinal); |
||||
var root = new Entry { Entity = module, Signature = module.FullAssemblyName }; |
||||
|
||||
Entry? TryCreateEntry(IEntity entity) |
||||
{ |
||||
if (entity.EffectiveAccessibility() != Accessibility.Public) |
||||
return null; |
||||
Entry? parent = null; |
||||
// Nested entities are only included when their declaring type is itself a
|
||||
// recorded entry (i.e. public). Skipping otherwise drops the whole subtree —
|
||||
// matching the WPF behaviour that "compare" shows public-API only.
|
||||
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 List<Entry>(); |
||||
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 List<Entry>(); |
||||
root.Children.Add(nsEntry); |
||||
} |
||||
entry.Parent = nsEntry; |
||||
nsEntry.Children ??= new List<Entry>(); |
||||
nsEntry.Children.Add(entry); |
||||
} |
||||
} |
||||
|
||||
foreach (var fieldHandle in metadata.FieldDefinitions) |
||||
{ |
||||
var entry = TryCreateEntry(module.GetDefinition(fieldHandle)); |
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
foreach (var eventHandle in metadata.EventDefinitions) |
||||
{ |
||||
var entry = TryCreateEntry(module.GetDefinition(eventHandle)); |
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
foreach (var propertyHandle in metadata.PropertyDefinitions) |
||||
{ |
||||
var entry = TryCreateEntry(module.GetDefinition(propertyHandle)); |
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
foreach (var methodHandle in metadata.MethodDefinitions) |
||||
{ |
||||
var methodDef = module.GetDefinition(methodHandle); |
||||
// Accessor methods (get_X / set_X / add_X / remove_X) are folded into their
|
||||
// owning property / event row in the tree; comparing them separately would
|
||||
// double-count every property as four entries.
|
||||
if (methodDef.AccessorOwner != null) |
||||
continue; |
||||
var entry = TryCreateEntry(methodDef); |
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
|
||||
return (results, root); |
||||
} |
||||
|
||||
static INamespace? ResolveNamespace(string namespaceName, IModule module) |
||||
{ |
||||
INamespace current = module.RootNamespace; |
||||
var 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; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Recursively merges two parallel entry trees into a single one. Each interior
|
||||
/// node's children are paired by <see cref="EntryComparer"/>; unmatched children
|
||||
/// keep their leaf <see cref="Entry.Kind"/> tag (Add / Remove) so the recursive
|
||||
/// summary on ancestors reflects the actual change shape.
|
||||
/// </summary>
|
||||
public static 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 List<Entry>(); |
||||
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("CalculateDiff returned a (null, null) pair"); |
||||
m.Children[^1].Parent = m; |
||||
} |
||||
} |
||||
else if (a.Children?.Count > 0) |
||||
{ |
||||
m.Children ??= new List<Entry>(); |
||||
foreach (var child in a.Children) |
||||
{ |
||||
child.Parent = m; |
||||
m.Children.Add(child); |
||||
} |
||||
} |
||||
else if (b.Children?.Count > 0) |
||||
{ |
||||
m.Children ??= new List<Entry>(); |
||||
foreach (var child in b.Children) |
||||
{ |
||||
child.Parent = m; |
||||
m.Children.Add(child); |
||||
} |
||||
} |
||||
return m; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Pairs <paramref name="left"/> and <paramref name="right"/> by signature.
|
||||
/// Returns one <c>(Left, Right)</c> tuple per slot: both non-null when matched;
|
||||
/// only Left → Remove (and tags the leaf <see cref="DiffKind.Remove"/>); only
|
||||
/// Right → Add (and tags the leaf <see cref="DiffKind.Add"/>).
|
||||
/// </summary>
|
||||
public static List<(Entry? Left, Entry? Right)> CalculateDiff(List<Entry> left, List<Entry> right) |
||||
{ |
||||
var leftMap = new Dictionary<string, List<Entry>>(); |
||||
var rightMap = new Dictionary<string, List<Entry>>(); |
||||
foreach (var item in left) |
||||
{ |
||||
if (leftMap.TryGetValue(item.Signature, out var bucket)) |
||||
bucket.Add(item); |
||||
else |
||||
leftMap[item.Signature] = new List<Entry> { item }; |
||||
} |
||||
foreach (var item in right) |
||||
{ |
||||
if (rightMap.TryGetValue(item.Signature, out var bucket)) |
||||
bucket.Add(item); |
||||
else |
||||
rightMap[item.Signature] = new List<Entry> { item }; |
||||
} |
||||
|
||||
var results = new List<(Entry? Left, Entry? Right)>(); |
||||
foreach (var (key, items) in leftMap) |
||||
{ |
||||
if (rightMap.TryGetValue(key, out var rightEntries)) |
||||
{ |
||||
foreach (var item in items) |
||||
{ |
||||
var other = rightEntries.Find(r => EntryComparer.Instance.Equals(r, 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.Exists(l => EntryComparer.Instance.Equals(l, 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; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2026 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.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
namespace ILSpy.Compare |
||||
{ |
||||
/// <summary>
|
||||
/// How an <see cref="Entry"/> compares between the left and right assemblies. Stored on
|
||||
/// leaf entries by <see cref="CompareEngine"/>; interior nodes derive a recursive
|
||||
/// summary via <see cref="Entry.RecursiveKind"/>.
|
||||
/// </summary>
|
||||
public enum DiffKind |
||||
{ |
||||
/// <summary>Identical on both sides.</summary>
|
||||
None = ' ', |
||||
/// <summary>Only present on the right.</summary>
|
||||
Add = '+', |
||||
/// <summary>Only present on the left.</summary>
|
||||
Remove = '-', |
||||
/// <summary>Present on both but the bodies differ.</summary>
|
||||
Update = '~', |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// One node in the merged comparison tree. The <see cref="Entity"/> is the symbol on
|
||||
/// the left assembly; <see cref="OtherEntity"/> is the right-side counterpart when one
|
||||
/// exists. <see cref="Signature"/> is the C#-formatted text the engine keys on for
|
||||
/// matching pairs across sides.
|
||||
/// </summary>
|
||||
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] |
||||
public sealed class Entry |
||||
{ |
||||
DiffKind? kind; |
||||
|
||||
/// <summary>
|
||||
/// Per-node diff kind for leaves; for interior nodes, summarises the children:
|
||||
/// all-Add → Add; all-Remove → Remove; any-non-None → Update; otherwise None.
|
||||
/// </summary>
|
||||
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 => kind ?? DiffKind.None; |
||||
set => 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; } |
||||
|
||||
string GetDebuggerDisplay() => $"Entry{Kind} {Entity?.ToString() ?? Signature}"; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Two entries match when their <see cref="Entry.Signature"/> string is equal — that's
|
||||
/// the only stable key across an assembly version, since metadata tokens are not
|
||||
/// preserved across rebuilds.
|
||||
/// </summary>
|
||||
public sealed class EntryComparer : IEqualityComparer<Entry> |
||||
{ |
||||
public static readonly EntryComparer Instance = new(); |
||||
|
||||
public bool Equals(Entry? x, Entry? y) => x?.Signature == y?.Signature; |
||||
|
||||
public int GetHashCode([DisallowNull] Entry obj) => obj.Signature.GetHashCode(); |
||||
} |
||||
} |
||||
@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2026 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.Linq; |
||||
|
||||
using Avalonia.Media; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.Output; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX; |
||||
|
||||
using ILSpy.Languages; |
||||
using ILSpy.TreeNodes; |
||||
|
||||
namespace ILSpy.Compare |
||||
{ |
||||
/// <summary>
|
||||
/// Tree-node façade over a merged <see cref="Entry"/>. The text combines both sides'
|
||||
/// formatted entity strings (with a <c>" -> "</c> separator when they differ); the
|
||||
/// background brush is a light tint that mirrors the diff kind (green for additions,
|
||||
/// pink for removals, blue for updates). Identical entries render with a transparent
|
||||
/// background and stay hidden unless the toolbar's "Show Identical" toggle is on.
|
||||
/// </summary>
|
||||
public sealed class ComparisonEntryTreeNode : ILSpyTreeNode |
||||
{ |
||||
readonly Entry entry; |
||||
readonly CompareTabPageModel pane; |
||||
|
||||
internal Entry Entry => entry; |
||||
|
||||
public ComparisonEntryTreeNode(Entry entry, CompareTabPageModel pane) |
||||
{ |
||||
this.entry = entry; |
||||
this.pane = pane; |
||||
LazyLoading = entry.Children != null; |
||||
} |
||||
|
||||
protected override void LoadChildren() |
||||
{ |
||||
if (entry.Children == null) |
||||
return; |
||||
// Order: changes first (Add/Remove/Update lexically before None via the negated
|
||||
// enum cast), then by SymbolKind so types group together, then alphabetically
|
||||
// by signature. Matches the WPF view's display order.
|
||||
foreach (var item in entry.Children.OrderBy(e => (-(int)e.RecursiveKind, e.Entity.SymbolKind, e.Signature))) |
||||
Children.Add(new ComparisonEntryTreeNode(item, pane)); |
||||
} |
||||
|
||||
public override object Text { |
||||
get { |
||||
var leftText = FormatEntity(entry.Entity); |
||||
var rightText = FormatEntity(entry.OtherEntity); |
||||
return leftText + (rightText != null && leftText != rightText ? " -> " + rightText : ""); |
||||
} |
||||
} |
||||
|
||||
string? FormatEntity(ISymbol? symbol) => symbol switch { |
||||
ITypeDefinition t => Language.TypeToString(t, ConversionFlags.None), |
||||
IEntity e => Language.EntityToString(e, |
||||
ConversionFlags.All & ~(ConversionFlags.ShowDeclaringType |
||||
| ConversionFlags.UseFullyQualifiedEntityNames |
||||
| ConversionFlags.UseFullyQualifiedTypeNames)), |
||||
INamespace n => n.FullName, |
||||
IModule m => m.FullAssemblyName, |
||||
_ => null, |
||||
}; |
||||
|
||||
public override object Icon => entry.Entity switch { |
||||
ITypeDefinition t => TypeIconForKind(t), |
||||
IMethod => ILSpy.Images.Images.Method, |
||||
IField => ILSpy.Images.Images.Field, |
||||
IProperty => ILSpy.Images.Images.Property, |
||||
IEvent => ILSpy.Images.Images.Event, |
||||
INamespace => ILSpy.Images.Images.Namespace, |
||||
IModule => ILSpy.Images.Images.Assembly, |
||||
_ => ILSpy.Images.Images.Class, |
||||
}; |
||||
|
||||
static object TypeIconForKind(ITypeDefinition t) => t.Kind switch { |
||||
TypeKind.Interface => ILSpy.Images.Images.Interface, |
||||
TypeKind.Struct => ILSpy.Images.Images.Struct, |
||||
TypeKind.Enum => ILSpy.Images.Images.Enum, |
||||
TypeKind.Delegate => ILSpy.Images.Images.Delegate, |
||||
_ => ILSpy.Images.Images.Class, |
||||
}; |
||||
|
||||
public override void Decompile(Language language, ICSharpCode.Decompiler.ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
// Compare nodes aren't decompilable on their own — they're a diff overlay.
|
||||
// Clicking a row jumps to the underlying entity in the assembly tree, which
|
||||
// then drives the normal decompile flow on whichever tab the user picks.
|
||||
} |
||||
|
||||
public override FilterResult Filter(LanguageSettings settings) |
||||
{ |
||||
// Hide identical rows unless the toolbar toggle says otherwise. Recursive
|
||||
// because a parent node whose ALL descendants are unchanged should also hide.
|
||||
return pane.ShowIdentical || entry.RecursiveKind != DiffKind.None |
||||
? FilterResult.Match |
||||
: FilterResult.Hidden; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Pastel-tint background per diff kind: green = added on the right, pink = removed
|
||||
/// from the right (i.e. only on the left), blue = signatures match but contents
|
||||
/// differ. Identical rows fall through to the parent background.
|
||||
/// </summary>
|
||||
public IBrush BackgroundBrush => entry.RecursiveKind switch { |
||||
DiffKind.Add => new SolidColorBrush(Color.FromArgb(0xFF, 0x90, 0xEE, 0x90)), |
||||
DiffKind.Remove => new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0xB6, 0xC1)), |
||||
DiffKind.Update => new SolidColorBrush(Color.FromArgb(0xFF, 0xAD, 0xD8, 0xE6)), |
||||
_ => Brushes.Transparent, |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2026 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 CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
|
||||
using ICSharpCode.ILSpyX; |
||||
|
||||
using ILSpy.ViewModels; |
||||
|
||||
namespace ILSpy.Compare |
||||
{ |
||||
/// <summary>
|
||||
/// Tab content for the assembly-comparison view. Constructed by
|
||||
/// <see cref="Commands.CompareContextMenuEntry"/> when the user right-clicks two
|
||||
/// assemblies and picks "Compare…". Static content (no language switching, no
|
||||
/// re-decompile) — the merge is computed once at construction.
|
||||
/// </summary>
|
||||
public sealed partial class CompareTabPageModel : TabPageModel |
||||
{ |
||||
readonly LoadedAssembly leftAssembly; |
||||
readonly LoadedAssembly rightAssembly; |
||||
|
||||
[ObservableProperty] |
||||
private ComparisonEntryTreeNode rootEntry; |
||||
|
||||
/// <summary>
|
||||
/// When true, identical rows surface in the tree. The default false hides them so
|
||||
/// only the actual differences are visible — the typical use case for "what changed
|
||||
/// between these two versions?"
|
||||
/// </summary>
|
||||
[ObservableProperty] |
||||
private bool showIdentical; |
||||
|
||||
public LoadedAssembly LeftAssembly => leftAssembly; |
||||
public LoadedAssembly RightAssembly => rightAssembly; |
||||
|
||||
public CompareTabPageModel(LoadedAssembly left, LoadedAssembly right) |
||||
{ |
||||
leftAssembly = left; |
||||
rightAssembly = right; |
||||
Title = $"Compare {left.Text} - {right.Text}"; |
||||
|
||||
var leftTree = CompareEngine.CreateEntityTree(left.GetTypeSystemOrNull()!); |
||||
var rightTree = CompareEngine.CreateEntityTree(right.GetTypeSystemOrNull()!); |
||||
var merged = CompareEngine.MergeTrees(leftTree.Root, rightTree.Root); |
||||
rootEntry = new ComparisonEntryTreeNode(merged, this); |
||||
|
||||
ExpandAllCommand = new RelayCommand(OnExpandAll); |
||||
} |
||||
|
||||
public IRelayCommand ExpandAllCommand { get; } |
||||
|
||||
void OnExpandAll() |
||||
{ |
||||
ExpandRecursive(RootEntry); |
||||
} |
||||
|
||||
static void ExpandRecursive(ComparisonEntryTreeNode node) |
||||
{ |
||||
node.IsExpanded = true; |
||||
node.EnsureLazyChildren(); |
||||
foreach (var child in node.Children) |
||||
if (child is ComparisonEntryTreeNode c) |
||||
ExpandRecursive(c); |
||||
} |
||||
|
||||
partial void OnShowIdenticalChanged(bool value) |
||||
{ |
||||
// Re-apply the filter cascade to refresh hidden/visible state — the existing
|
||||
// ApplyFilterToChild path looks at the new ShowIdentical flag through
|
||||
// ComparisonEntryTreeNode.Filter.
|
||||
RootEntry.EnsureChildrenFiltered(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:compare="using:ILSpy.Compare" |
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400" |
||||
x:Class="ILSpy.Compare.CompareView" |
||||
x:DataType="compare:CompareTabPageModel"> |
||||
<DockPanel> |
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="4"> |
||||
<CheckBox Content="Show identical entries" |
||||
IsChecked="{Binding ShowIdentical, Mode=TwoWay}" |
||||
ToolTip.Tip="Reveal rows that are identical on both sides — by default the tree only shows actual differences." /> |
||||
<Button Margin="8,0,0,0" Content="Expand all" |
||||
Command="{Binding ExpandAllCommand}" |
||||
ToolTip.Tip="Expand every namespace and type in the diff tree." /> |
||||
</StackPanel> |
||||
<DataGrid Name="DiffGrid" |
||||
Classes="hierarchical" |
||||
AutoGenerateColumns="False" |
||||
HeadersVisibility="None" |
||||
HierarchicalRowsEnabled="True" |
||||
GridLinesVisibility="None" |
||||
CanUserResizeColumns="False" |
||||
SelectionMode="Single" |
||||
ItemsSource="{Binding RootEntry.Children}"> |
||||
<DataGrid.Columns> |
||||
<DataGridHierarchicalColumn Header="Diff" Width="*" |
||||
IsReadOnly="True" |
||||
x:CompileBindings="False" |
||||
Binding="{Binding Item}"> |
||||
<DataGridHierarchicalColumn.CellTemplate> |
||||
<DataTemplate> |
||||
<Border Background="{Binding BackgroundBrush}" Padding="4,1"> |
||||
<Grid ColumnDefinitions="Auto,7,*" |
||||
VerticalAlignment="Center"> |
||||
<Image Grid.Column="0" Source="{Binding Icon}" |
||||
Width="16" Height="16" |
||||
VerticalAlignment="Center" /> |
||||
<TextBlock Grid.Column="2" Text="{Binding Text}" |
||||
VerticalAlignment="Center" /> |
||||
</Grid> |
||||
</Border> |
||||
</DataTemplate> |
||||
</DataGridHierarchicalColumn.CellTemplate> |
||||
</DataGridHierarchicalColumn> |
||||
</DataGrid.Columns> |
||||
</DataGrid> |
||||
</DockPanel> |
||||
</UserControl> |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2026 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 Avalonia.Controls; |
||||
|
||||
namespace ILSpy.Compare |
||||
{ |
||||
public partial class CompareView : UserControl |
||||
{ |
||||
public CompareView() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue