Browse Source

Add CompareView and FrozenContent flag

feature/api-diff
Siegfried Pammer 2 months ago
parent
commit
7ad4ccc025
  1. 5
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  2. 58
      ILSpy/Commands/CompareContextMenuEntry.cs
  3. 1
      ILSpy/Commands/SearchMsdnContextMenuEntry.cs
  4. 6
      ILSpy/Docking/DockWorkspace.cs
  5. 23
      ILSpy/Images/Images.cs
  6. 21
      ILSpy/TreeNodes/MethodTreeNode.cs
  7. 434
      ILSpy/ViewModels/CompareViewModel.cs
  8. 7
      ILSpy/ViewModels/TabPageModel.cs
  9. 63
      ILSpy/Views/CompareView.xaml
  10. 26
      ILSpy/Views/CompareView.xaml.cs
  11. 7
      ILSpy/Views/DebugSteps.xaml.cs

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)

58
ILSpy/Commands/CompareContextMenuEntry.cs

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
// 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 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();
tabPage.Title = $"Compare {left.Text} - {right.Text}";
tabPage.SupportsLanguageSwitching = false;
tabPage.FrozenContent = true;
var compareView = new CompareView();
compareView.DataContext = new CompareViewModel(assemblyTreeModel, left, right);
tabPage.Content = compareView;
}
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;

6
ILSpy/Docking/DockWorkspace.cs

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

23
ILSpy/Images/Images.cs

@ -25,6 +25,8 @@ using System.Windows.Media.Imaging; @@ -25,6 +25,8 @@ using System.Windows.Media.Imaging;
namespace ICSharpCode.ILSpy
{
using ICSharpCode.Decompiler.TypeSystem;
static class Images
{
private static readonly Rect iconRect = new Rect(0, 0, 16, 16);
@ -212,6 +214,27 @@ namespace ICSharpCode.ILSpy @@ -212,6 +214,27 @@ namespace ICSharpCode.ILSpy
return memberIconCache.GetIcon(icon, overlay, isStatic);
}
public static AccessOverlayIcon GetOverlayIcon(Accessibility accessibility)
{
switch (accessibility)
{
case Accessibility.Public:
return AccessOverlayIcon.Public;
case Accessibility.Internal:
return AccessOverlayIcon.Internal;
case Accessibility.ProtectedAndInternal:
return AccessOverlayIcon.PrivateProtected;
case Accessibility.Protected:
return AccessOverlayIcon.Protected;
case Accessibility.ProtectedOrInternal:
return AccessOverlayIcon.ProtectedInternal;
case Accessibility.Private:
return AccessOverlayIcon.Private;
default:
return AccessOverlayIcon.CompilerControlled;
}
}
private static ImageSource GetIcon(string baseImage, string overlay = null, bool isStatic = false)
{
ImageSource baseImageSource = Load(baseImage);

21
ILSpy/TreeNodes/MethodTreeNode.cs

@ -73,27 +73,6 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -73,27 +73,6 @@ namespace ICSharpCode.ILSpy.TreeNodes
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;
}
}
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
language.DecompileMethod(MethodDefinition, output, options);

434
ILSpy/ViewModels/CompareViewModel.cs

@ -0,0 +1,434 @@ @@ -0,0 +1,434 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.ILSpy.AssemblyTree;
using ICSharpCode.ILSpyX;
using TomsToolbox.Wpf;
namespace ICSharpCode.ILSpy.ViewModels
{
using System.Linq;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.TreeNodes;
class CompareViewModel : ObservableObject
{
private readonly AssemblyTreeModel assemblyTreeModel;
private LoadedAssembly leftAssembly;
private LoadedAssembly rightAssembly;
private LoadedAssembly[] assemblies;
private ComparisonEntryTreeNode root;
public CompareViewModel(AssemblyTreeModel assemblyTreeModel, LoadedAssembly left, LoadedAssembly right)
{
MessageBus<CurrentAssemblyListChangedEventArgs>.Subscribers += (sender, e) => this.Assemblies = assemblyTreeModel.AssemblyList.GetAssemblies();
leftAssembly = left;
rightAssembly = right;
assemblies = assemblyTreeModel.AssemblyList.GetAssemblies();
var leftTree = CreateEntityTree(new DecompilerTypeSystem(leftAssembly.GetMetadataFileOrNull(), leftAssembly.GetAssemblyResolver()));
var rightTree = CreateEntityTree(new DecompilerTypeSystem(rightAssembly.GetMetadataFileOrNull(), rightAssembly.GetAssemblyResolver()));
this.root = new ComparisonEntryTreeNode(MergeTrees(leftTree.Item2, rightTree.Item2));
}
public LoadedAssembly[] Assemblies {
get => assemblies;
set {
if (assemblies != value)
{
assemblies = value;
OnPropertyChanged();
}
}
}
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();
}
}
}
Entry MergeTrees(Entry a, Entry b)
{
var m = new Entry() {
Entity = a.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(DecompilerTypeSystem typeSystem)
{
var module = 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 = null!, 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 = null! };
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);
}
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)
{
item.Kind = DiffKind.Remove;
}
}
}
else
{
foreach (var item in items)
{
item.Kind = 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));
item.Kind = DiffKind.Add;
}
}
}
else
{
foreach (var item in items)
{
item.Kind = DiffKind.Add;
results.Add((null, item));
}
}
}
return results;
}
}
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public class Entry
{
private DiffKind kind = DiffKind.None;
public DiffKind Kind {
get {
if (Children == null || Children.Count == 0)
{
return kind;
}
int addCount = 0, removeCount = 0, updateCount = 0;
foreach (var item in Children)
{
switch (item.Kind)
{
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;
}
set {
if (Children == null || Children.Count == 0)
{
kind = value;
}
}
}
public required string Signature { get; init; }
public required IEntity Entity { 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;
public ComparisonEntryTreeNode(Entry entry)
{
this.entry = entry;
this.LazyLoading = entry.Children != null;
}
protected override void LoadChildren()
{
if (entry.Children == null)
return;
foreach (var item in entry.Children)
{
this.Children.Add(new ComparisonEntryTreeNode(item));
}
}
public override object Text => entry.Signature;
public override object Icon => Images.GetIcon(;
public DiffKind Difference => entry.Kind;
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
}
}
}

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 {

63
ILSpy/Views/CompareView.xaml

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
<UserControl x:Class="ICSharpCode.ILSpy.Views.CompareView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ICSharpCode.ILSpy.Views"
xmlns:stv="clr-namespace:ICSharpCode.ILSpy.Controls.TreeView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="5">
<Label Content="Left:" />
<ComboBox SelectedValue="{Binding LeftAssembly}" ItemsSource="{Binding Assemblies}" DisplayMemberPath="Text" />
<Label Content="Right:" />
<ComboBox SelectedValue="{Binding RightAssembly}" ItemsSource="{Binding Assemblies}" DisplayMemberPath="Text" />
</StackPanel>
<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 Difference}" Value="Update">
<Setter Property="Background" Value="LightBlue" />
</DataTrigger>
<DataTrigger Binding="{Binding Difference}" Value="Add">
<Setter Property="Background" Value="LightGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding Difference}" Value="Remove">
<Setter Property="Background" Value="LightPink" />
</DataTrigger>
<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>

26
ILSpy/Views/CompareView.xaml.cs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
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