Browse Source

Merge pull request #3519 from icsharpcode/feature/api-diff

Add CompareView and FrozenContent flag
pull/3523/merge v10.0-preview1
Siegfried Pammer 7 days ago committed by GitHub
parent
commit
844f5b44c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  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. 1
      ILSpy/Images/DictionaryContain.svg
  8. 13
      ILSpy/Images/DictionaryContain.xaml
  9. 1
      ILSpy/Images/ExpandAll.svg
  10. 11
      ILSpy/Images/ExpandAll.xaml
  11. 23
      ILSpy/Images/Images.cs
  12. 4
      ILSpy/Images/README.md
  13. 1
      ILSpy/Images/ResultToJSON.svg
  14. 10
      ILSpy/Images/ResultToJSON.xaml
  15. 1
      ILSpy/Images/SwitchSourceOrTarget.svg
  16. 9
      ILSpy/Images/SwitchSourceOrTarget.xaml
  17. 2
      ILSpy/TreeNodes/EventTreeNode.cs
  18. 8
      ILSpy/TreeNodes/FieldTreeNode.cs
  19. 31
      ILSpy/TreeNodes/MethodTreeNode.cs
  20. 2
      ILSpy/TreeNodes/PropertyTreeNode.cs
  21. 718
      ILSpy/ViewModels/CompareViewModel.cs
  22. 7
      ILSpy/ViewModels/TabPageModel.cs
  23. 86
      ILSpy/Views/CompareView.xaml
  24. 33
      ILSpy/Views/CompareView.xaml.cs
  25. 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; }

1
ILSpy/Images/DictionaryContain.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M15 2v9h-4c-.164.005-1 1-1 1H8s-.836-.995-1-1v2H2V8h1V2h4.5c.646.004 1.12.191 1.5.5.381-.309.863-.5 1.5-.5H15z"/></g><g id="icon_x5F_bg"><path class="st2" d="M10.5 3c-.624 0-.984.365-1.5 1-.508-.626-.861-.996-1.5-1H4v5h1V4h2c.566 0 1 .663 1 1v4c-.33-.205-.564 0-1 0v1c.549 0 1.778-.025 2 .5.221-.525 1.451-.5 2-.5h3V3h-3.5zM13 9h-2c-.435 0-.67-.205-1 0V5c0-.385.41-1 1-1h2v5zM3 11h3v1H3z"/><path class="st2" d="M3 9h3v1H3z"/></g><g id="icon_x5F_fg"><path class="st3" d="M7 4H5v4h2v1c.436 0 .67-.205 1 0V5c0-.337-.434-1-1-1zM11 4c-.59 0-1 .615-1 1v4c.33-.205.565 0 1 0h2V4h-2z"/></g></svg>

After

Width:  |  Height:  |  Size: 811 B

13
ILSpy/Images/DictionaryContain.xaml

@ -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>

1
ILSpy/Images/ExpandAll.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;} .icon-vs-fg{fill:#F0EFF1;} .icon-vs-action-blue{fill:#00539C;}</style><rect class="icon-canvas-transparent" width="16" height="16" id="canvas"/><path class="icon-vs-out" d="M15 10h-2v2h-2v2h-9v-9h2v-2h2v-2h9v9z" id="outline"/><path class="icon-vs-bg" d="M14 2v7h-1v-6h-6v-1h7zm-9 2v1h6v6h1v-7h-7zm5 2v7h-7v-7h7zm-1 1h-5v5h5v-5z" id="iconBg"/><path class="icon-vs-fg" d="M4 7v5h5v-5h-5zm4 3h-1v1h-1v-1h-1v-1h1v-1h1v1h1v1z" id="iconFg"/><path class="icon-vs-action-blue" d="M7 9h1v1h-1.005l-.01 1h-.985v-1h-1v-1h1v-1h1v1z" id="colorAction"/></svg>

After

Width:  |  Height:  |  Size: 736 B

11
ILSpy/Images/ExpandAll.xaml

@ -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>

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);

4
ILSpy/Images/README.md

@ -15,9 +15,11 @@ Icons used in ILSpy: @@ -15,9 +15,11 @@ Icons used in ILSpy:
| Copy | x | x | VS 2017 Icon Pack (Copy) | |
| Delegate | x | x | VS 2017 Icon Pack (Delegate) | |
| Delete | x | x | VS 2017 Icon Pack (Remove_color) | |
| DictionaryContain | x | x | VS 2017 Icon Pack (DictionaryContain) | |
| Enum | x | x | VS 2017 Icon Pack (Enumerator) | |
| EnumValue | x | x | VS 2017 Icon Pack (EnumItem) | |
| Event | x | x | VS 2017 Icon Pack (Event) | |
| ExpandAll | x | x | VS 2017 Icon Pack (ExpandAll) | |
| ExportOverlay | x | x | slightly modified VS 2017 Icon Pack (Export) | |
| ExtensionMethod | x | x | VS 2017 Icon Pack (ExtensionMethod) | |
| Field | x | x | VS 2017 Icon Pack (Field) | |
@ -62,6 +64,7 @@ Icons used in ILSpy: @@ -62,6 +64,7 @@ Icons used in ILSpy:
| ResourceXml | x | x | VS 2017 Icon Pack (XMLFile) | |
| ResourceXsd | x | x | combined VS 2017 Icon Pack (XMLSchema) with the file symbol in ResourceXslt | |
| ResourceXsl | x | x | VS 2017 Icon Pack (XMLTransformation) | |
| ResultToJSON | x | x | VS 2017 Icon Pack (ResultToJSON) | |
| ResourceXslt | x | x | VS 2017 Icon Pack (XSLTTemplate) | |
| Save | x | x | VS 2017 Icon Pack (Save) | |
| Search | x | x | VS 2017 Icon Pack (Search) | |
@ -73,6 +76,7 @@ Icons used in ILSpy: @@ -73,6 +76,7 @@ Icons used in ILSpy:
| Struct | x | x | VS 2017 Icon Pack (Structure) | |
| SubTypes | x | x | based on VS 2017 Icon Pack (BaseType) rotated +90° | |
| SuperTypes | x | x | based on VS 2017 Icon Pack (BaseType) rotated -90° | |
| SwitchSourceOrTarget | x | x | VS 2017 Icon Pack (SwitchSourceOrTarget) | |
| ViewCode | x | x | VS 2017 Icon Pack (GoToSourceCode) | |
| VirtualMethod | x | x | combined VS 2017 Icon Pack (Method) two times | |
| Warning | x | x | VS 2017 Icon Pack (StatusWarning) | |

1
ILSpy/Images/ResultToJSON.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-action-blue{fill:#00539c}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 8.38v2.258s-.992-.001-.997-.003c-.012.052-.003.14-.003.281v1.579c0 1.271-.37 2.185-1.054 2.746-.638.522-1.576.759-2.822.759H10v-3.247s1.014-.001 1.037-.003c.004-.046-.037-.117-.037-.214V11.08c0-.943.222-1.606.539-2.072C11.223 8.527 11 7.843 11 6.869V5.468c0-.087.102-.286.094-.325-.02-.002.063-.143.03-.143H10V2h1.124c1.251 0 2.193.265 2.832.81C14.633 3.387 15 4.3 15 5.522V7h.919L16 8.38zM9.414 4l-4-4H.586l2 2H0v4h2v.586L1.586 7H1v.586L.586 8H1v1.638L1.329 11H2v1.536c0 1.247.495 2.149 1.19 2.711.641.517 1.697.753 2.937.753H7v-3.247s-1.011-.001-1.033-.003c-.008-.053.033-.127.033-.228v-1.401c0-.962-.224-1.637-.542-2.111.256-.378.444-.89.511-1.564L9.414 4z" id="outline"/><path class="icon-vs-bg" d="M15 8.38v1.258c-.697 0-1.046.426-1.046 1.278v1.579c0 .961-.223 1.625-.666 1.989-.445.364-1.166.547-2.164.547v-1.278c.383 0 .661-.092.834-.277s.26-.498.26-.94V11.08c0-1.089.349-1.771 1.046-2.044v-.027c-.697-.287-1.046-1-1.046-2.14V5.468c0-.793-.364-1.189-1.094-1.189V3c.993 0 1.714.19 2.16.571s.67 1.031.67 1.952v1.565c0 .861.349 1.292 1.046 1.292zm-9.967 4.142v-1.401c0-1.117-.351-1.816-1.053-2.099v-.027c.429-.175.71-.519.877-.995H3.049c-.173.247-.436.38-.805.38v1.258c.692 0 1.039.419 1.039 1.258v1.641c0 .934.226 1.584.677 1.948s1.174.547 2.167.547v-1.278c-.388 0-.666-.093-.838-.28-.17-.188-.256-.505-.256-.952z" id="iconBg"/><path class="icon-vs-action-blue" d="M8 4L5 7H3l2-2H1V3h4L3 1h2l3 3z" id="colorAction"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

10
ILSpy/Images/ResultToJSON.xaml

@ -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>

1
ILSpy/Images/SwitchSourceOrTarget.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M15.414 10l-4 4h-4.828l2-2h-5.586v-4h-.414l-2-2 4-4h4.828l-2 2h5.586v4h.414l2 2z" id="outline"/><path class="icon-vs-bg" d="M12 7h-9l-1-1 3-3h2l-2 2h7v2zm1 2h-9v2h7l-2 2h2l3-3-1-1z" id="iconBg"/></svg>

After

Width:  |  Height:  |  Size: 505 B

9
ILSpy/Images/SwitchSourceOrTarget.xaml

@ -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>

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)

718
ILSpy/ViewModels/CompareViewModel.cs

@ -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;
}
}
}
}

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 {

86
ILSpy/Views/CompareView.xaml

@ -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>

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