mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
13 KiB
409 lines
13 KiB
// 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; |
|
using System.Collections.Generic; |
|
using System.Collections.ObjectModel; |
|
using System.Collections.Specialized; |
|
using System.Composition; |
|
using System.Linq; |
|
using System.Reflection.Metadata.Ecma335; |
|
using System.Runtime.Serialization; |
|
|
|
using CommunityToolkit.Mvvm.ComponentModel; |
|
|
|
using ICSharpCode.Decompiler; |
|
using ICSharpCode.Decompiler.Metadata; |
|
using ICSharpCode.Decompiler.TypeSystem; |
|
using ICSharpCode.ILSpyX; |
|
using ICSharpCode.ILSpyX.TreeView; |
|
|
|
using ILSpy; |
|
using ILSpy.Languages; |
|
using ILSpy.TreeNodes; |
|
using ILSpy.ViewModels; |
|
|
|
namespace ILSpy.AssemblyTree |
|
{ |
|
[Export] |
|
[Shared] |
|
public partial class AssemblyTreeModel : ToolPaneModel |
|
{ |
|
public const string PaneContentId = "AssemblyTree"; |
|
|
|
readonly SettingsService settingsService; |
|
readonly LanguageService languageService; |
|
AssemblyListManager? listManager; |
|
AssemblyListTreeNode? assemblyListTreeNode; |
|
|
|
[ObservableProperty] |
|
[property: IgnoreDataMember] |
|
private SharpTreeNode? root; |
|
|
|
/// <summary> |
|
/// Multi-selection set. Each entry is kept in sync with its |
|
/// <see cref="SharpTreeNode.IsSelected"/>. <see cref="SelectedItem"/> is a convenience |
|
/// wrapper around the *primary* (last-added) entry — drives decompilation, navigation |
|
/// history, and tree-view-path persistence — but the underlying state is single-sourced |
|
/// here, mirroring the WPF host's MultiSelectorExtensions.SelectionBinding pattern. |
|
/// </summary> |
|
[IgnoreDataMember] |
|
public ObservableCollection<SharpTreeNode> SelectedItems { get; } = []; |
|
|
|
/// <summary> |
|
/// Primary (last) selection. Get returns the most recently selected entry of |
|
/// <see cref="SelectedItems"/>, or <c>null</c>; set replaces the entire selection |
|
/// with the supplied node (clears the collection then adds it). All |
|
/// <c>PropertyChanged(SelectedItem)</c> notifications are fired by |
|
/// <see cref="SelectedItems.CollectionChanged"/>. |
|
/// </summary> |
|
[IgnoreDataMember] |
|
public SharpTreeNode? SelectedItem { |
|
get => SelectedItems.Count > 0 ? SelectedItems[^1] : null; |
|
set { |
|
if (SelectedItem == value) |
|
return; |
|
SelectedItems.Clear(); |
|
if (value != null) |
|
SelectedItems.Add(value); |
|
} |
|
} |
|
|
|
[ObservableProperty] |
|
[property: IgnoreDataMember] |
|
private string? activeListName; |
|
|
|
[IgnoreDataMember] |
|
public AssemblyList? AssemblyList { get; private set; } |
|
|
|
[IgnoreDataMember] |
|
public ObservableCollection<string> AssemblyLists { get; } = []; |
|
|
|
[ImportingConstructor] |
|
public AssemblyTreeModel(SettingsService settingsService, LanguageService languageService) |
|
{ |
|
this.settingsService = settingsService; |
|
this.languageService = languageService; |
|
languageService.PropertyChanged += (_, e) => { |
|
if (e.PropertyName == nameof(LanguageService.CurrentLanguage) && Root != null) |
|
NotifyTextChanged(Root); |
|
}; |
|
SelectedItems.CollectionChanged += OnSelectedItemsChanged; |
|
Id = PaneContentId; |
|
Title = "Assemblies"; |
|
CanClose = false; |
|
} |
|
|
|
void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) |
|
{ |
|
if (e.NewItems != null) |
|
foreach (SharpTreeNode n in e.NewItems) |
|
n.IsSelected = true; |
|
if (e.OldItems != null) |
|
foreach (SharpTreeNode n in e.OldItems) |
|
n.IsSelected = false; |
|
|
|
// SelectedItem is a wrapper over this collection — anyone bound to it must be |
|
// notified, and the saved path must follow the new primary. |
|
OnPropertyChanged(nameof(SelectedItem)); |
|
settingsService.SessionSettings.ActiveTreeViewPath = GetPathForNode(SelectedItem); |
|
} |
|
|
|
// Walks already-materialized children and re-raises Text PropertyChanged so the cell |
|
// templates pick up the new language's formatting -- without collapsing the user's |
|
// expanded state. Lazy-loaded subtrees that haven't been opened yet are skipped (they'll |
|
// format with the active language the next time they get expanded). |
|
static void NotifyTextChanged(SharpTreeNode node) |
|
{ |
|
node.RaisePropertyChanged(nameof(SharpTreeNode.Text)); |
|
if (node.LazyLoading) |
|
return; |
|
foreach (var child in node.Children) |
|
NotifyTextChanged(child); |
|
} |
|
|
|
public void Initialize() |
|
{ |
|
listManager = settingsService.AssemblyListManager; |
|
listManager.CreateDefaultAssemblyLists(); |
|
|
|
SyncListNames(); |
|
listManager.AssemblyLists.CollectionChanged += (_, _) => SyncListNames(); |
|
|
|
var saved = settingsService.SessionSettings.ActiveAssemblyList; |
|
ActiveListName = !string.IsNullOrEmpty(saved) && AssemblyLists.Contains(saved) |
|
? saved |
|
: AssemblyListManager.DefaultListName; |
|
} |
|
|
|
void SyncListNames() |
|
{ |
|
if (listManager == null) |
|
return; |
|
AssemblyLists.Clear(); |
|
foreach (var name in listManager.AssemblyLists) |
|
AssemblyLists.Add(name); |
|
} |
|
|
|
partial void OnActiveListNameChanged(string? value) |
|
{ |
|
if (listManager == null || string.IsNullOrEmpty(value)) |
|
return; |
|
|
|
settingsService.SessionSettings.ActiveAssemblyList = value; |
|
ShowAssemblyList(value); |
|
|
|
// Restore the previously-selected tree node, if any. Walks the tree by ToString() |
|
// matching, materialising lazy children as it goes. |
|
var savedPath = settingsService.SessionSettings.ActiveTreeViewPath; |
|
if (savedPath is { Length: > 0 }) |
|
{ |
|
var restored = FindNodeByPath(savedPath, returnBestMatch: true); |
|
if (restored != null && restored != Root) |
|
SelectedItem = restored; |
|
} |
|
} |
|
|
|
void ShowAssemblyList(string name) |
|
{ |
|
if (listManager == null) |
|
return; |
|
var list = listManager.LoadList(name); |
|
if (AssemblyList == null || list.ListName != AssemblyList.ListName) |
|
ShowAssemblyList(list); |
|
} |
|
|
|
void ShowAssemblyList(AssemblyList list) |
|
{ |
|
AssemblyList = list; |
|
if (list.GetAssemblies().Length == 0 && list.ListName == AssemblyListManager.DefaultListName) |
|
LoadInitialAssemblies(list); |
|
assemblyListTreeNode = new AssemblyListTreeNode(list); |
|
Root = assemblyListTreeNode; |
|
} |
|
|
|
/// <summary> |
|
/// Walks down from <see cref="Root"/> matching each path segment against |
|
/// <see cref="object.ToString"/>, expanding lazy children along the way. |
|
/// </summary> |
|
public SharpTreeNode? FindNodeByPath(string[]? path, bool returnBestMatch) |
|
{ |
|
if (path == null || Root == null) |
|
return null; |
|
SharpTreeNode? node = Root; |
|
SharpTreeNode? bestMatch = node; |
|
foreach (var element in path) |
|
{ |
|
if (node == null) |
|
break; |
|
bestMatch = node; |
|
node.EnsureLazyChildren(); |
|
node = node.Children.FirstOrDefault(c => c.ToString() == element); |
|
} |
|
return returnBestMatch ? node ?? bestMatch : node; |
|
} |
|
|
|
/// <summary> |
|
/// The path of <paramref name="node"/>'s ancestors (root excluded), in root-first order. |
|
/// </summary> |
|
public static string[]? GetPathForNode(SharpTreeNode? node) |
|
{ |
|
if (node == null) |
|
return null; |
|
var path = new List<string>(); |
|
while (node.Parent != null) |
|
{ |
|
path.Add(node.ToString()!); |
|
node = node.Parent; |
|
} |
|
path.Reverse(); |
|
return path.ToArray(); |
|
} |
|
|
|
internal AssemblyTreeNode? FindAssemblyNode(LoadedAssembly asm) |
|
=> assemblyListTreeNode?.FindAssemblyNode(asm); |
|
|
|
/// <summary> |
|
/// Finds the tree node corresponding to <paramref name="reference"/> — used by |
|
/// hyperlink clicks in the decompiler view to route to the right entity. |
|
/// Mirrors <c>ICSharpCode.ILSpy.AssemblyTree.AssemblyTreeModel.FindTreeNode</c> but only |
|
/// covers the references the Avalonia tree currently knows how to model. |
|
/// </summary> |
|
public ILSpyTreeNode? FindTreeNode(object? reference) |
|
{ |
|
if (assemblyListTreeNode == null) |
|
return null; |
|
|
|
switch (reference) |
|
{ |
|
case EntityReference unresolved: |
|
var module = unresolved.ResolveAssembly(AssemblyList!); |
|
if (module == null) |
|
return null; |
|
var token = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolved.Handle)); |
|
if (token == null) |
|
return null; |
|
var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached); |
|
return FindTreeNode(typeSystem.MainModule.ResolveEntity(token.Value)); |
|
|
|
case ITypeDefinition type: |
|
return FindTypeNode(assemblyListTreeNode, type); |
|
|
|
case IMember member: |
|
return FindMemberNode(assemblyListTreeNode, member); |
|
|
|
default: |
|
return null; |
|
} |
|
} |
|
|
|
static TypeTreeNode? FindTypeNode(AssemblyListTreeNode root, ITypeDefinition type) |
|
{ |
|
var module = type.ParentModule?.MetadataFile; |
|
if (module == null) |
|
return null; |
|
var assembly = root.Children.OfType<AssemblyTreeNode>() |
|
.FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module); |
|
if (assembly == null) |
|
return null; |
|
assembly.EnsureLazyChildren(); |
|
|
|
var nesting = new Stack<ITypeDefinition>(); |
|
for (var current = type; current != null; current = current.DeclaringTypeDefinition) |
|
nesting.Push(current); |
|
|
|
var top = nesting.Pop(); |
|
var ns = assembly.Children.OfType<NamespaceTreeNode>() |
|
.FirstOrDefault(n => n.Name == (top.Namespace ?? string.Empty)); |
|
if (ns == null) |
|
return null; |
|
ns.EnsureLazyChildren(); |
|
var typeNode = ns.Children.OfType<TypeTreeNode>() |
|
.FirstOrDefault(t => t.Handle == top.MetadataToken); |
|
while (typeNode != null && nesting.Count > 0) |
|
{ |
|
typeNode.EnsureLazyChildren(); |
|
var nested = nesting.Pop(); |
|
typeNode = typeNode.Children.OfType<TypeTreeNode>() |
|
.FirstOrDefault(t => t.Handle == nested.MetadataToken); |
|
} |
|
return typeNode; |
|
} |
|
|
|
static ILSpyTreeNode? FindMemberNode(AssemblyListTreeNode root, IMember member) |
|
{ |
|
var typeNode = member.DeclaringTypeDefinition is { } declaring ? FindTypeNode(root, declaring) : null; |
|
if (typeNode == null) |
|
return null; |
|
typeNode.EnsureLazyChildren(); |
|
return member switch { |
|
IField f => typeNode.Children.OfType<FieldTreeNode>().FirstOrDefault(n => n.FieldDefinition.MetadataToken == f.MetadataToken), |
|
IMethod m => typeNode.Children.OfType<MethodTreeNode>().FirstOrDefault(n => n.MethodDefinition.MetadataToken == m.MetadataToken), |
|
IProperty p => typeNode.Children.OfType<PropertyTreeNode>().FirstOrDefault(n => n.PropertyDefinition.MetadataToken == p.MetadataToken), |
|
IEvent e => typeNode.Children.OfType<EventTreeNode>().FirstOrDefault(n => n.EventDefinition.MetadataToken == e.MetadataToken), |
|
_ => null, |
|
}; |
|
} |
|
|
|
static void LoadInitialAssemblies(AssemblyList assemblyList) |
|
{ |
|
System.Reflection.Assembly[] initialAssemblies = { |
|
typeof(object).Assembly, |
|
typeof(Uri).Assembly, |
|
typeof(System.Linq.Enumerable).Assembly, |
|
}; |
|
foreach (var asm in initialAssemblies) |
|
{ |
|
if (!string.IsNullOrEmpty(asm.Location)) |
|
assemblyList.OpenAssembly(asm.Location); |
|
} |
|
} |
|
|
|
public void SelectNode(SharpTreeNode? node) |
|
{ |
|
if (node == null) |
|
return; |
|
SelectedItem = node; |
|
} |
|
|
|
public void OpenFiles(string[] fileNames, bool focusNode = true) |
|
{ |
|
ArgumentNullException.ThrowIfNull(fileNames); |
|
LoadAssemblies(fileNames, focusNode: focusNode); |
|
} |
|
|
|
void LoadAssemblies(IEnumerable<string> fileNames, List<LoadedAssembly>? loadedAssemblies = null, bool focusNode = true) |
|
{ |
|
if (AssemblyList == null) |
|
return; |
|
|
|
AssemblyTreeNode? lastNode = null; |
|
foreach (var file in fileNames) |
|
{ |
|
var assembly = AssemblyList.OpenAssembly(file); |
|
if (loadedAssemblies != null) |
|
{ |
|
loadedAssemblies.Add(assembly); |
|
continue; |
|
} |
|
var node = assemblyListTreeNode?.FindAssemblyNode(assembly); |
|
if (node != null && focusNode) |
|
lastNode = node; |
|
} |
|
|
|
if (focusNode && lastNode != null) |
|
SelectNode(lastNode); |
|
} |
|
|
|
public void SortAssemblyList() |
|
=> AssemblyList?.Sort(AssemblyComparer.Instance); |
|
|
|
sealed class AssemblyComparer : IComparer<LoadedAssembly> |
|
{ |
|
public static readonly AssemblyComparer Instance = new(); |
|
public int Compare(LoadedAssembly? x, LoadedAssembly? y) |
|
=> string.Compare(x?.ShortName, y?.ShortName, StringComparison.CurrentCulture); |
|
} |
|
|
|
public void CollapseAll() => CollapseChildren(Root); |
|
|
|
static void CollapseChildren(SharpTreeNode? node) |
|
{ |
|
if (node is null) |
|
return; |
|
foreach (var child in node.Children) |
|
{ |
|
if (!child.IsExpanded) |
|
continue; |
|
CollapseChildren(child); |
|
child.IsExpanded = false; |
|
} |
|
} |
|
|
|
public void Refresh() => RefreshInternal(); |
|
|
|
void RefreshInternal() |
|
{ |
|
if (AssemblyList == null || listManager == null) |
|
return; |
|
var path = GetPathForNode(SelectedItem); |
|
ShowAssemblyList(listManager.LoadList(AssemblyList.ListName)); |
|
SelectNode(FindNodeByPath(path, returnBestMatch: true)); |
|
} |
|
} |
|
}
|
|
|