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.
686 lines
25 KiB
686 lines
25 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; |
|
using System.Reflection.Metadata.Ecma335; |
|
using System.Runtime.Serialization; |
|
using System.Threading.Tasks; |
|
|
|
using CommunityToolkit.Mvvm.ComponentModel; |
|
|
|
using ICSharpCode.Decompiler; |
|
using ICSharpCode.Decompiler.Documentation; |
|
using ICSharpCode.Decompiler.Metadata; |
|
using ICSharpCode.Decompiler.TypeSystem; |
|
using ICSharpCode.Decompiler.TypeSystem.Implementation; |
|
using ICSharpCode.ILSpyX; |
|
using ICSharpCode.ILSpyX.TreeView; |
|
|
|
using ILSpy; |
|
using ILSpy.Commands; |
|
using ILSpy.Languages; |
|
using ILSpy.TreeNodes; |
|
using ILSpy.ViewModels; |
|
|
|
namespace ILSpy.AssemblyTree |
|
{ |
|
[Export] |
|
[ExportToolPane(ContentId = PaneContentId, Alignment = ToolPaneAlignment.Left, Order = 0)] |
|
[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. |
|
/// </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) |
|
{ |
|
AppEnv.StartupLog.Mark("AssemblyTreeModel ctor entered"); |
|
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; |
|
AppEnv.StartupLog.Mark("AssemblyTreeModel ctor exited"); |
|
} |
|
|
|
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); |
|
} |
|
|
|
readonly TaskCompletionSource<bool> treeReadyTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); |
|
|
|
/// <summary> |
|
/// Completes when the assembly-tree view (<c>AssemblyListPane</c>) has fired its |
|
/// <c>Loaded</c> event for the first time. <c>RestoreSelectedPathAsync</c> awaits |
|
/// this before assigning <see cref="SelectedItem"/> so the saved-selection path |
|
/// (which kicks off a decompilation through the dock workspace) doesn't paint |
|
/// the document area before the tree itself is on screen. |
|
/// </summary> |
|
public Task TreeReady => treeReadyTcs.Task; |
|
|
|
/// <summary> |
|
/// Called by <c>AssemblyListPane</c> from its <c>Loaded</c> handler to resolve |
|
/// <see cref="TreeReady"/>. Idempotent — only the first call wins. |
|
/// </summary> |
|
internal void MarkTreeReady() |
|
{ |
|
if (treeReadyTcs.TrySetResult(true)) |
|
AppEnv.StartupLog.Mark("AssemblyTreeModel.TreeReady completed"); |
|
} |
|
|
|
public void Initialize() |
|
{ |
|
using var _ = AppEnv.StartupLog.Phase("AssemblyTreeModel.Initialize body"); |
|
listManager = settingsService.AssemblyListManager; |
|
using (AppEnv.StartupLog.Phase("CreateDefaultAssemblyLists")) |
|
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 off the UI critical path. The walk |
|
// crosses an AssemblyTreeNode whose EnsureLazyChildren synchronously blocks on |
|
// GetLoadResultAsync — by going async here and awaiting the load, we let the |
|
// initial paint happen first and the UI stays responsive while metadata loads. |
|
var savedPath = settingsService.SessionSettings.ActiveTreeViewPath; |
|
if (savedPath is { Length: > 0 }) |
|
_ = RestoreSelectedPathAsync(savedPath); |
|
} |
|
|
|
async Task RestoreSelectedPathAsync(string[] path) |
|
{ |
|
using var _ = AppEnv.StartupLog.Phase($"RestoreSelectedPathAsync ({path.Length} segments)"); |
|
try |
|
{ |
|
if (Root == null) |
|
return; |
|
// Snapshot — if the user selects something else before the restore completes, |
|
// don't yank their selection out from under them. |
|
var initialSelection = SelectedItem; |
|
SharpTreeNode? node = Root; |
|
foreach (var element in path) |
|
{ |
|
if (node == null) |
|
break; |
|
// Awaiting GetLoadResultAsync keeps EnsureLazyChildren — which itself does |
|
// GetAwaiter().GetResult() on the same task — from blocking the UI thread. |
|
// Once the load completes the .GetAwaiter().GetResult() returns instantly. |
|
if (node is AssemblyTreeNode asm) |
|
{ |
|
using (AppEnv.StartupLog.Phase($"await GetLoadResultAsync ({asm.LoadedAssembly.ShortName})")) |
|
await asm.LoadedAssembly.GetLoadResultAsync().ConfigureAwait(true); |
|
} |
|
using (AppEnv.StartupLog.Phase($"EnsureLazyChildren ({node.GetType().Name} \"{element}\")")) |
|
node.EnsureLazyChildren(); |
|
node = node.Children.FirstOrDefault(c => c.ToString() == element); |
|
} |
|
// Wait for the tree view to be Loaded before assigning SelectedItem. Without |
|
// this, the SelectedItem assignment runs ShowSelectedNode → CurrentNodes → |
|
// async decompilation, and the user can see the decompiled output appear |
|
// BEFORE the assembly tree has rendered — a confusing reverse order. |
|
// The 5-second timeout is a safety net for environments where the pane |
|
// never loads (headless tests, design-time previews). |
|
using (AppEnv.StartupLog.Phase("await TreeReady before SelectedItem assignment")) |
|
{ |
|
await Task.WhenAny(TreeReady, Task.Delay(TimeSpan.FromSeconds(5))) |
|
.ConfigureAwait(true); |
|
} |
|
if (node != null && node != Root && ReferenceEquals(SelectedItem, initialSelection)) |
|
SelectedItem = node; |
|
} |
|
catch (Exception ex) |
|
{ |
|
System.Diagnostics.Debug.WriteLine($"[AssemblyTreeModel] saved-path restore failed: {ex}"); |
|
} |
|
} |
|
|
|
void ShowAssemblyList(string name) |
|
{ |
|
if (listManager == null) |
|
return; |
|
AssemblyList list; |
|
using (AppEnv.StartupLog.Phase($"LoadList({name})")) |
|
list = listManager.LoadList(name); |
|
if (AssemblyList == null || list.ListName != AssemblyList.ListName) |
|
ShowAssemblyList(list); |
|
} |
|
|
|
void ShowAssemblyList(AssemblyList list) |
|
{ |
|
using var _ = AppEnv.StartupLog.Phase("ShowAssemblyList(list)"); |
|
AssemblyList = list; |
|
if (list.GetAssemblies().Length == 0 && list.ListName == AssemblyListManager.DefaultListName) |
|
{ |
|
using (AppEnv.StartupLog.Phase("LoadInitialAssemblies")) |
|
LoadInitialAssemblies(list); |
|
} |
|
AppEnv.StartupLog.Mark($"AssemblyList contains {list.GetAssemblies().Length} assemblies"); |
|
using (AppEnv.StartupLog.Phase("new AssemblyListTreeNode")) |
|
assemblyListTreeNode = new AssemblyListTreeNode(list); |
|
Root = assemblyListTreeNode; |
|
AppEnv.StartupLog.Mark("Root assigned"); |
|
ScheduleBackgroundLoadSweep(list); |
|
} |
|
|
|
/// <summary> |
|
/// LoadedAssembly entries are now lazy — their <c>Task.Run(LoadAsync)</c> only kicks |
|
/// off when something asks for the metadata. The active assembly's load is awaited |
|
/// by <see cref="RestoreSelectedPathAsync"/>, so it gets a clean run. Everything else |
|
/// only loads on-demand (tree expansion, hyperlink follow, …) which can stretch |
|
/// quietly into "the user never sees a populated icon for assemblies they don't |
|
/// touch". |
|
/// |
|
/// To strike a middle ground, schedule a one-shot sweep a couple of seconds after |
|
/// the list appears that nudges every <see cref="LoadedAssembly"/> to start loading |
|
/// in the background. By that point the active assembly's metadata is usually ready |
|
/// and the user has had a frame or two to interact with the tree. |
|
/// </summary> |
|
void ScheduleBackgroundLoadSweep(AssemblyList list) |
|
{ |
|
_ = Task.Run(async () => { |
|
try |
|
{ |
|
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false); |
|
AppEnv.StartupLog.Mark("Background-load sweep starting"); |
|
foreach (var assembly in list.GetAssemblies()) |
|
{ |
|
// Calling GetLoadResultAsync triggers the Lazy<Task> creation. We don't |
|
// await — fire-and-forget so all 122/200/whatever loads run in parallel |
|
// on the thread pool, just delayed past the active-assembly window. |
|
_ = assembly.GetLoadResultAsync(); |
|
} |
|
AppEnv.StartupLog.Mark("Background-load sweep dispatched"); |
|
} |
|
catch (Exception ex) |
|
{ |
|
System.Diagnostics.Debug.WriteLine($"[AssemblyTreeModel] background load sweep failed: {ex}"); |
|
} |
|
}); |
|
} |
|
|
|
/// <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. Currently |
|
/// only covers the reference kinds the tree 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 => FindMethodNode(typeNode, m), |
|
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 ILSpyTreeNode? FindMethodNode(TypeTreeNode typeNode, IMethod method) |
|
{ |
|
// Accessor methods (get_X / set_X / add_X / remove_X / invoke_X) live as children |
|
// of their owning PropertyTreeNode / EventTreeNode, not directly under the type. |
|
// Route through the owner so MMB on a metadata-grid accessor row finds its node. |
|
if (method.AccessorOwner is IProperty owningProperty) |
|
{ |
|
var propNode = typeNode.Children.OfType<PropertyTreeNode>() |
|
.FirstOrDefault(n => n.PropertyDefinition.MetadataToken == owningProperty.MetadataToken); |
|
if (propNode != null) |
|
return propNode.Children.OfType<MethodTreeNode>() |
|
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken); |
|
} |
|
if (method.AccessorOwner is IEvent owningEvent) |
|
{ |
|
var eventNode = typeNode.Children.OfType<EventTreeNode>() |
|
.FirstOrDefault(n => n.EventDefinition.MetadataToken == owningEvent.MetadataToken); |
|
if (eventNode != null) |
|
return eventNode.Children.OfType<MethodTreeNode>() |
|
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken); |
|
} |
|
return typeNode.Children.OfType<MethodTreeNode>() |
|
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken); |
|
} |
|
|
|
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); |
|
} |
|
|
|
/// <summary> |
|
/// Applies parsed startup arguments: switches the active language, loads any assemblies |
|
/// passed positionally, then navigates to the requested entity / namespace if any. Safe |
|
/// to call before assemblies finish loading — awaits each one's metadata before resolving |
|
/// the navigation target. Search-string handling is deferred until the search pane lands. |
|
/// </summary> |
|
public async Task HandleCommandLineArgumentsAsync(AppEnv.CommandLineArguments args) |
|
{ |
|
ArgumentNullException.ThrowIfNull(args); |
|
if (args.Language is { Length: > 0 } languageName) |
|
languageService.CurrentLanguage = languageService.GetLanguage(languageName); |
|
|
|
var newlyLoaded = new List<LoadedAssembly>(); |
|
if (args.AssembliesToLoad is { Count: > 0 }) |
|
LoadAssemblies(args.AssembliesToLoad, newlyLoaded, focusNode: false); |
|
|
|
// "all currently-loaded entries that the navigation target may live in" — newly |
|
// loaded ones first (matches WPF's "command-line files take precedence") plus the |
|
// existing list as fallback. |
|
var relevant = newlyLoaded.Count > 0 |
|
? new List<LoadedAssembly>(newlyLoaded) |
|
: AssemblyList?.GetAssemblies().ToList() ?? new List<LoadedAssembly>(); |
|
|
|
if (args.NavigateTo is { Length: > 0 } navigateTo) |
|
await NavigateOnLaunchAsync(navigateTo, relevant); |
|
else if (newlyLoaded.Count == 1 && FindAssemblyNode(newlyLoaded[0]) is { } singleNode) |
|
SelectNode(singleNode); |
|
|
|
// Search-pane wiring lands with task 6. Until then the arg parses but is a no-op |
|
// rather than crashing. |
|
} |
|
|
|
async Task NavigateOnLaunchAsync(string navigateTo, IList<LoadedAssembly> relevant) |
|
{ |
|
// "none" is a sentinel used by the WPF VS add-in to suppress initial navigation — |
|
// the real target arrives later via IPC. |
|
if (navigateTo == "none") |
|
return; |
|
|
|
if (navigateTo.StartsWith("N:", StringComparison.Ordinal)) |
|
{ |
|
var namespaceName = navigateTo.Substring(2); |
|
foreach (var asm in relevant) |
|
{ |
|
var assemblyNode = FindAssemblyNode(asm); |
|
if (assemblyNode == null) |
|
continue; |
|
await asm.GetMetadataFileAsync().ConfigureAwait(true); |
|
var nsNode = assemblyNode.FindNamespaceNode(namespaceName); |
|
if (nsNode != null) |
|
{ |
|
SelectNode(nsNode); |
|
return; |
|
} |
|
} |
|
return; |
|
} |
|
|
|
foreach (var asm in relevant) |
|
await asm.GetMetadataFileAsync().ConfigureAwait(true); |
|
|
|
var entity = await Task.Run(() => FindEntityInRelevantAssemblies(navigateTo, relevant)); |
|
if (entity != null) |
|
{ |
|
var node = FindTreeNode(entity); |
|
if (node != null) |
|
SelectNode(node); |
|
} |
|
} |
|
|
|
static IEntity? FindEntityInRelevantAssemblies(string navigateTo, IEnumerable<LoadedAssembly> relevantAssemblies) |
|
{ |
|
ITypeReference typeRef; |
|
IMemberReference? memberRef = null; |
|
if (navigateTo.StartsWith("T:", StringComparison.Ordinal)) |
|
{ |
|
typeRef = IdStringProvider.ParseTypeName(navigateTo); |
|
} |
|
else |
|
{ |
|
memberRef = IdStringProvider.ParseMemberIdString(navigateTo); |
|
typeRef = memberRef.DeclaringTypeReference; |
|
} |
|
foreach (var asm in relevantAssemblies) |
|
{ |
|
var module = asm.GetMetadataFileOrNull(); |
|
if (module != null && CanResolveTypeInPEFile(module, typeRef, out var typeHandle)) |
|
{ |
|
ICompilation compilation = typeHandle.Kind == HandleKind.ExportedType |
|
? new DecompilerTypeSystem(module, module.GetAssemblyResolver()) |
|
: new SimpleCompilation((PEFile)module, MinimalCorlib.Instance); |
|
return memberRef == null |
|
? typeRef.Resolve(new SimpleTypeResolveContext(compilation)) as ITypeDefinition |
|
: memberRef.Resolve(new SimpleTypeResolveContext(compilation)); |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
static bool CanResolveTypeInPEFile(MetadataFile module, ITypeReference typeRef, out EntityHandle typeHandle) |
|
{ |
|
// Reference assemblies are skipped so the loop keeps looking for an actual definition. |
|
if (module.IsReferenceAssembly()) |
|
{ |
|
typeHandle = default; |
|
return false; |
|
} |
|
|
|
switch (typeRef) |
|
{ |
|
case GetPotentiallyNestedClassTypeReference topLevelType: |
|
typeHandle = topLevelType.ResolveInPEFile(module); |
|
return !typeHandle.IsNil; |
|
case NestedTypeReference nestedType: |
|
if (!CanResolveTypeInPEFile(module, nestedType.DeclaringTypeReference, out typeHandle)) |
|
return false; |
|
if (typeHandle.Kind == HandleKind.ExportedType) |
|
return true; |
|
var typeDef = module.Metadata.GetTypeDefinition((TypeDefinitionHandle)typeHandle); |
|
typeHandle = typeDef.GetNestedTypes().FirstOrDefault(t => { |
|
var td = module.Metadata.GetTypeDefinition(t); |
|
var typeName = ReflectionHelper.SplitTypeParameterCountFromReflectionName(module.Metadata.GetString(td.Name), out int typeParameterCount); |
|
return nestedType.AdditionalTypeParameterCount == typeParameterCount && nestedType.Name == typeName; |
|
}); |
|
return !typeHandle.IsNil; |
|
default: |
|
typeHandle = default; |
|
return false; |
|
} |
|
} |
|
|
|
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)); |
|
} |
|
} |
|
}
|
|
|