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.
1100 lines
32 KiB
1100 lines
32 KiB
// Copyright (c) 2019 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.Specialized; |
|
using System.ComponentModel; |
|
using System.Composition; |
|
using System.Diagnostics; |
|
using System.Diagnostics.CodeAnalysis; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Reflection.Metadata; |
|
using System.Reflection.Metadata.Ecma335; |
|
using System.Threading.Tasks; |
|
using System.Windows; |
|
using System.Windows.Documents; |
|
using System.Windows.Input; |
|
using System.Windows.Navigation; |
|
using System.Windows.Threading; |
|
|
|
using ICSharpCode.Decompiler.Documentation; |
|
using ICSharpCode.Decompiler.Metadata; |
|
using ICSharpCode.Decompiler.TypeSystem; |
|
using ICSharpCode.Decompiler.TypeSystem.Implementation; |
|
using ICSharpCode.ILSpy.AppEnv; |
|
using ICSharpCode.ILSpy.Properties; |
|
using ICSharpCode.ILSpy.TextView; |
|
using ICSharpCode.ILSpy.TreeNodes; |
|
using ICSharpCode.ILSpy.Updates; |
|
using ICSharpCode.ILSpy.ViewModels; |
|
using ICSharpCode.ILSpyX; |
|
using ICSharpCode.ILSpyX.TreeView; |
|
|
|
using TomsToolbox.Composition; |
|
using TomsToolbox.Essentials; |
|
using TomsToolbox.Wpf; |
|
|
|
#nullable enable |
|
|
|
namespace ICSharpCode.ILSpy.AssemblyTree |
|
{ |
|
[ExportToolPane] |
|
[Shared] |
|
public class AssemblyTreeModel : ToolPaneModel |
|
{ |
|
public const string PaneContentId = "assemblyListPane"; |
|
|
|
private AssemblyListPane? activeView; |
|
private AssemblyListTreeNode? assemblyListTreeNode; |
|
private readonly DispatcherThrottle refreshThrottle; |
|
|
|
private readonly NavigationHistory<NavigationState> history = new(); |
|
private NavigationState? navigatingToState; |
|
private object? sourceOfReference; |
|
private readonly SettingsService settingsService; |
|
private readonly LanguageService languageService; |
|
private readonly IExportProvider exportProvider; |
|
|
|
public AssemblyTreeModel(SettingsService settingsService, LanguageService languageService, IExportProvider exportProvider) |
|
{ |
|
this.settingsService = settingsService; |
|
this.languageService = languageService; |
|
this.exportProvider = exportProvider; |
|
|
|
Title = Resources.Assemblies; |
|
ContentId = PaneContentId; |
|
IsCloseable = false; |
|
ShortcutKey = new KeyGesture(Key.F6); |
|
|
|
MessageBus<NavigateToReferenceEventArgs>.Subscribers += JumpToReference; |
|
MessageBus<SettingsChangedEventArgs>.Subscribers += (sender, e) => Settings_PropertyChanged(sender, e); |
|
MessageBus<ApplySessionSettingsEventArgs>.Subscribers += ApplySessionSettings; |
|
MessageBus<ActiveTabPageChangedEventArgs>.Subscribers += ActiveTabPageChanged; |
|
MessageBus<TabPagesCollectionChangedEventArgs>.Subscribers += (_, e) => history.RemoveAll(s => !DockWorkspace.TabPages.Contains(s.TabPage)); |
|
MessageBus<ResetLayoutEventArgs>.Subscribers += ResetLayout; |
|
MessageBus<NavigateToEventArgs>.Subscribers += (_, e) => NavigateTo(e.Request, e.InNewTabPage); |
|
MessageBus<MainWindowLoadedEventArgs>.Subscribers += (_, _) => { |
|
Initialize(); |
|
Show(); |
|
}; |
|
|
|
EventManager.RegisterClassHandler(typeof(Window), Hyperlink.RequestNavigateEvent, new RequestNavigateEventHandler((_, e) => NavigateTo(e))); |
|
|
|
refreshThrottle = new(DispatcherPriority.Background, RefreshInternal); |
|
|
|
AssemblyList = settingsService.CreateEmptyAssemblyList(); |
|
} |
|
|
|
private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e) |
|
{ |
|
if (sender is SessionSettings sessionSettings) |
|
{ |
|
switch (e.PropertyName) |
|
{ |
|
case nameof(SessionSettings.ActiveAssemblyList): |
|
ShowAssemblyList(sessionSettings.ActiveAssemblyList); |
|
RefreshDecompiledView(); |
|
break; |
|
case nameof(SessionSettings.Theme): |
|
// update syntax highlighting and force reload (AvalonEdit does not automatically refresh on highlighting change) |
|
DecompilerTextView.RegisterHighlighting(); |
|
RefreshDecompiledView(); |
|
break; |
|
case nameof(SessionSettings.CurrentCulture): |
|
MessageBox.Show(Resources.SettingsChangeRestartRequired, "ILSpy"); |
|
break; |
|
} |
|
} |
|
else if (sender is LanguageSettings) |
|
{ |
|
switch (e.PropertyName) |
|
{ |
|
case nameof(LanguageSettings.LanguageId) or nameof(LanguageSettings.LanguageVersionId): |
|
RefreshDecompiledView(); |
|
break; |
|
default: |
|
Refresh(); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
public AssemblyList AssemblyList { get; private set; } |
|
|
|
private SharpTreeNode? root; |
|
public SharpTreeNode? Root { |
|
get => root; |
|
set => SetProperty(ref root, value); |
|
} |
|
|
|
public SharpTreeNode? SelectedItem { |
|
get => SelectedItems.FirstOrDefault(); |
|
set => SelectedItems = value is null ? [] : [value]; |
|
} |
|
|
|
private SharpTreeNode[] selectedItems = []; |
|
public SharpTreeNode[] SelectedItems { |
|
get => selectedItems; |
|
set { |
|
if (selectedItems.SequenceEqual(value)) |
|
return; |
|
|
|
var oldSelection = selectedItems; |
|
selectedItems = value; |
|
OnPropertyChanged(); |
|
TreeView_SelectionChanged(oldSelection, selectedItems); |
|
} |
|
} |
|
|
|
public string[]? SelectedPath => GetPathForNode(SelectedItem); |
|
|
|
private readonly List<LoadedAssembly> commandLineLoadedAssemblies = []; |
|
|
|
private bool HandleCommandLineArguments(CommandLineArguments args) |
|
{ |
|
LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false); |
|
if (args.Language != null) |
|
languageService.Language = languageService.GetLanguage(args.Language); |
|
return true; |
|
} |
|
|
|
/// <summary> |
|
/// Called on startup or when passed arguments via WndProc from a second instance. |
|
/// In the format case, updateSettings is non-null; in the latter it is null. |
|
/// </summary> |
|
private async Task HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, UpdateSettings? updateSettings = null) |
|
{ |
|
var sessionSettings = settingsService.SessionSettings; |
|
|
|
var relevantAssemblies = commandLineLoadedAssemblies.ToList(); |
|
commandLineLoadedAssemblies.Clear(); // clear references once we don't need them anymore |
|
|
|
await NavigateOnLaunch(args.NavigateTo, sessionSettings.ActiveTreeViewPath, updateSettings, relevantAssemblies); |
|
|
|
if (args.Search != null) |
|
{ |
|
MessageBus.Send(this, new ShowSearchPageEventArgs(args.Search)); |
|
} |
|
} |
|
|
|
public async Task HandleSingleInstanceCommandLineArguments(string[] args) |
|
{ |
|
var cmdArgs = CommandLineArguments.Create(args); |
|
|
|
await Dispatcher.InvokeAsync(async () => { |
|
|
|
if (!HandleCommandLineArguments(cmdArgs)) |
|
return; |
|
|
|
var window = Application.Current.MainWindow; |
|
|
|
if (!cmdArgs.NoActivate && window is { WindowState: WindowState.Minimized }) |
|
{ |
|
window.WindowState = WindowState.Normal; |
|
} |
|
|
|
await HandleCommandLineArgumentsAfterShowList(cmdArgs); |
|
}); |
|
} |
|
|
|
private async Task NavigateOnLaunch(string? navigateTo, string[]? activeTreeViewPath, UpdateSettings? updateSettings, List<LoadedAssembly> relevantAssemblies) |
|
{ |
|
var initialSelection = SelectedItem; |
|
if (navigateTo != null) |
|
{ |
|
bool found = false; |
|
if (navigateTo.StartsWith("N:", StringComparison.Ordinal)) |
|
{ |
|
string namespaceName = navigateTo.Substring(2); |
|
foreach (LoadedAssembly asm in relevantAssemblies) |
|
{ |
|
var asmNode = assemblyListTreeNode?.FindAssemblyNode(asm); |
|
if (asmNode != null) |
|
{ |
|
// FindNamespaceNode() blocks the UI if the assembly is not yet loaded, |
|
// so use an async wait instead. |
|
await asm.GetMetadataFileAsync().Catch<Exception>(_ => { }); |
|
NamespaceTreeNode nsNode = asmNode.FindNamespaceNode(namespaceName); |
|
if (nsNode != null) |
|
{ |
|
found = true; |
|
if (SelectedItem == initialSelection) |
|
{ |
|
SelectNode(nsNode); |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
else if (navigateTo == "none") |
|
{ |
|
// Don't navigate anywhere; start empty. |
|
// Used by ILSpy VS addin, it'll send us the real location to navigate to via IPC. |
|
found = true; |
|
} |
|
else |
|
{ |
|
IEntity? mr = await Task.Run(() => FindEntityInRelevantAssemblies(navigateTo, relevantAssemblies)); |
|
|
|
// Make sure we wait for assemblies being loaded... |
|
// BeginInvoke in LoadedAssembly.LookupReferencedAssemblyInternal |
|
await Dispatcher.InvokeAsync(delegate { }, DispatcherPriority.Normal); |
|
|
|
if (mr is { ParentModule.MetadataFile: not null }) |
|
{ |
|
found = true; |
|
if (SelectedItem == initialSelection) |
|
{ |
|
await JumpToReferenceAsync(mr, null); |
|
} |
|
} |
|
} |
|
if (!found && SelectedItem == initialSelection) |
|
{ |
|
AvalonEditTextOutput output = new AvalonEditTextOutput(); |
|
output.Write($"Cannot find '{navigateTo}' in command line specified assemblies."); |
|
DockWorkspace.ShowText(output); |
|
} |
|
} |
|
else if (relevantAssemblies.Count == 1) |
|
{ |
|
// NavigateTo == null and an assembly was given on the command-line: |
|
// Select the newly loaded assembly |
|
var asmNode = assemblyListTreeNode?.FindAssemblyNode(relevantAssemblies[0]); |
|
if (asmNode != null && SelectedItem == initialSelection) |
|
{ |
|
SelectNode(asmNode); |
|
} |
|
} |
|
else if (updateSettings != null) |
|
{ |
|
SharpTreeNode? node = null; |
|
if (activeTreeViewPath?.Length > 0) |
|
{ |
|
foreach (var asm in AssemblyList.GetAssemblies()) |
|
{ |
|
if (asm.FileName == activeTreeViewPath[0]) |
|
{ |
|
// FindNodeByPath() blocks the UI if the assembly is not yet loaded, |
|
// so use an async wait instead. |
|
await asm.GetMetadataFileAsync().Catch<Exception>(_ => { }); |
|
} |
|
} |
|
node = FindNodeByPath(activeTreeViewPath, true); |
|
} |
|
if (SelectedItem == initialSelection) |
|
{ |
|
if (node != null) |
|
{ |
|
SelectNode(node); |
|
|
|
// only if not showing the about page, perform the update check: |
|
MessageBus.Send(this, new CheckIfUpdateAvailableEventArgs()); |
|
} |
|
else |
|
{ |
|
MessageBus.Send(this, new ShowAboutPageEventArgs(DockWorkspace.ActiveTabPage)); |
|
} |
|
} |
|
} |
|
} |
|
|
|
public 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 (LoadedAssembly asm in relevantAssemblies.ToList()) |
|
{ |
|
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; |
|
} |
|
|
|
private static bool CanResolveTypeInPEFile(MetadataFile module, ITypeReference typeRef, out EntityHandle typeHandle) |
|
{ |
|
// We intentionally ignore reference assemblies, so that the loop continues looking for another assembly that might have a usable 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; |
|
} |
|
} |
|
|
|
public void Initialize() |
|
{ |
|
AssemblyList = settingsService.LoadInitialAssemblyList(); |
|
|
|
HandleCommandLineArguments(App.CommandLineArguments); |
|
|
|
var loadPreviousAssemblies = settingsService.MiscSettings.LoadPreviousAssemblies; |
|
if (AssemblyList.GetAssemblies().Length == 0 |
|
&& AssemblyList.ListName == AssemblyListManager.DefaultListName |
|
&& loadPreviousAssemblies) |
|
{ |
|
LoadInitialAssemblies(AssemblyList); |
|
} |
|
|
|
ShowAssemblyList(AssemblyList); |
|
|
|
var sessionSettings = settingsService.SessionSettings; |
|
if (sessionSettings.ActiveAutoLoadedAssembly != null |
|
&& File.Exists(sessionSettings.ActiveAutoLoadedAssembly)) |
|
{ |
|
AssemblyList.Open(sessionSettings.ActiveAutoLoadedAssembly, true); |
|
} |
|
|
|
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, OpenAssemblies); |
|
} |
|
|
|
private async Task OpenAssemblies() |
|
{ |
|
await HandleCommandLineArgumentsAfterShowList(App.CommandLineArguments, settingsService.GetSettings<UpdateSettings>()); |
|
|
|
if (FormatExceptions(App.StartupExceptions.ToArray(), out var output)) |
|
{ |
|
output.Title = "Startup errors"; |
|
|
|
DockWorkspace.AddTabPage(); |
|
DockWorkspace.ShowText(output); |
|
} |
|
} |
|
|
|
private static bool FormatExceptions(App.ExceptionData[] exceptions, [NotNullWhen(true)] out AvalonEditTextOutput? output) |
|
{ |
|
output = null; |
|
|
|
var result = exceptions.FormatExceptions(); |
|
if (result.IsNullOrEmpty()) |
|
return false; |
|
|
|
output = new(); |
|
output.Write(result); |
|
return true; |
|
|
|
} |
|
|
|
private void ShowAssemblyList(string name) |
|
{ |
|
AssemblyList list = settingsService.AssemblyListManager.LoadList(name); |
|
//Only load a new list when it is a different one |
|
if (list.ListName != AssemblyList.ListName) |
|
{ |
|
ShowAssemblyList(list); |
|
SelectNode(Root?.Children.FirstOrDefault()); |
|
} |
|
} |
|
|
|
private void ShowAssemblyList(AssemblyList assemblyList) |
|
{ |
|
history.Clear(); |
|
|
|
AssemblyList.CollectionChanged -= assemblyList_CollectionChanged; |
|
AssemblyList = assemblyList; |
|
assemblyList.CollectionChanged += assemblyList_CollectionChanged; |
|
|
|
assemblyListTreeNode = new(assemblyList) { |
|
Select = x => SelectNode(x) |
|
}; |
|
|
|
Root = assemblyListTreeNode; |
|
|
|
var mainWindow = Application.Current?.MainWindow; |
|
|
|
if (mainWindow == null) |
|
return; |
|
|
|
if (assemblyList.ListName == AssemblyListManager.DefaultListName) |
|
#if DEBUG |
|
mainWindow.Title = $"ILSpy {DecompilerVersionInfo.FullVersion}"; |
|
#else |
|
mainWindow.Title = "ILSpy"; |
|
#endif |
|
else |
|
#if DEBUG |
|
mainWindow.Title = $"ILSpy {DecompilerVersionInfo.FullVersion} - " + assemblyList.ListName; |
|
#else |
|
mainWindow.Title = "ILSpy - " + assemblyList.ListName; |
|
#endif |
|
} |
|
|
|
private void assemblyList_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) |
|
{ |
|
if (e.Action == NotifyCollectionChangedAction.Reset) |
|
{ |
|
history.RemoveAll(_ => true); |
|
} |
|
if (e.OldItems != null) |
|
{ |
|
var oldAssemblies = new HashSet<LoadedAssembly>(e.OldItems.Cast<LoadedAssembly>()); |
|
history.RemoveAll(n => n.TreeNodes.Any( |
|
nd => nd.AncestorsAndSelf().OfType<AssemblyTreeNode>().Any( |
|
a => oldAssemblies.Contains(a.LoadedAssembly)))); |
|
} |
|
|
|
MessageBus.Send(this, new CurrentAssemblyListChangedEventArgs(e)); |
|
} |
|
|
|
private static void LoadInitialAssemblies(AssemblyList assemblyList) |
|
{ |
|
// Called when loading an empty assembly list; so that |
|
// the user can see something initially. |
|
System.Reflection.Assembly[] initialAssemblies = { |
|
typeof(object).Assembly, |
|
typeof(Uri).Assembly, |
|
typeof(System.Linq.Enumerable).Assembly, |
|
typeof(System.Xml.XmlDocument).Assembly, |
|
typeof(System.Windows.Markup.MarkupExtension).Assembly, |
|
typeof(System.Windows.Rect).Assembly, |
|
typeof(System.Windows.UIElement).Assembly, |
|
typeof(System.Windows.FrameworkElement).Assembly |
|
}; |
|
foreach (System.Reflection.Assembly asm in initialAssemblies) |
|
assemblyList.OpenAssembly(asm.Location); |
|
} |
|
|
|
public AssemblyTreeNode? FindAssemblyNode(LoadedAssembly asm) |
|
{ |
|
return assemblyListTreeNode?.FindAssemblyNode(asm); |
|
} |
|
|
|
#region Node Selection |
|
|
|
public void SelectNode(SharpTreeNode? node, bool inNewTabPage = false) |
|
{ |
|
if (node == null) |
|
return; |
|
|
|
if (node.AncestorsAndSelf().Any(item => item.IsHidden)) |
|
{ |
|
MessageBox.Show(Resources.NavigationFailed, "ILSpy", MessageBoxButton.OK, MessageBoxImage.Exclamation); |
|
return; |
|
} |
|
|
|
if (inNewTabPage) |
|
{ |
|
DockWorkspace.AddTabPage(); |
|
SelectedItem = null; |
|
} |
|
|
|
if (SelectedItem == node) |
|
{ |
|
Dispatcher.BeginInvoke(RefreshDecompiledView); |
|
} |
|
else |
|
{ |
|
activeView?.ScrollIntoView(node); |
|
SelectedItem = node; |
|
|
|
Dispatcher.BeginInvoke(DispatcherPriority.Background, () => { |
|
activeView?.ScrollIntoView(node); |
|
}); |
|
} |
|
} |
|
|
|
public void SelectNodes(IEnumerable<SharpTreeNode> nodes) |
|
{ |
|
// Ensure nodes exist |
|
var nodesList = nodes.Select(n => FindNodeByPath(GetPathForNode(n), true)) |
|
.ExceptNullItems() |
|
.ToArray(); |
|
|
|
if (!nodesList.Any() || nodesList.Any(n => n.AncestorsAndSelf().Any(a => a.IsHidden))) |
|
{ |
|
return; |
|
} |
|
|
|
foreach (var node in nodesList) |
|
{ |
|
activeView?.ScrollIntoView(node); |
|
} |
|
|
|
SelectedItems = nodesList.ToArray(); |
|
} |
|
|
|
/// <summary> |
|
/// Retrieves a node using the .ToString() representations of its ancestors. |
|
/// </summary> |
|
public SharpTreeNode? FindNodeByPath(string[]? path, bool returnBestMatch) |
|
{ |
|
if (path == null) |
|
return null; |
|
var node = Root; |
|
var bestMatch = node; |
|
foreach (var element in path) |
|
{ |
|
if (node == null) |
|
break; |
|
bestMatch = node; |
|
node.EnsureLazyChildren(); |
|
if (node is ILSpyTreeNode ilSpyTreeNode) |
|
ilSpyTreeNode.EnsureChildrenFiltered(); |
|
node = node.Children.FirstOrDefault(c => c.ToString() == element); |
|
} |
|
|
|
return returnBestMatch ? node ?? bestMatch : node; |
|
} |
|
|
|
/// <summary> |
|
/// Gets the .ToString() representation of the node's ancestors. |
|
/// </summary> |
|
public static string[]? GetPathForNode(SharpTreeNode? node) |
|
{ |
|
if (node == null) |
|
return null; |
|
List<string> path = new List<string>(); |
|
while (node.Parent != null) |
|
{ |
|
path.Add(node.ToString()); |
|
node = node.Parent; |
|
} |
|
path.Reverse(); |
|
return path.ToArray(); |
|
} |
|
|
|
public ILSpyTreeNode? FindTreeNode(object? reference) |
|
{ |
|
if (assemblyListTreeNode == null) |
|
return null; |
|
|
|
switch (reference) |
|
{ |
|
case LoadedAssembly lasm: |
|
return assemblyListTreeNode.FindAssemblyNode(lasm); |
|
case MetadataFile asm: |
|
return assemblyListTreeNode.FindAssemblyNode(asm); |
|
case Resource res: |
|
return assemblyListTreeNode.FindResourceNode(res); |
|
case ValueTuple<Resource, string> resName: |
|
return assemblyListTreeNode.FindResourceNode(resName.Item1, resName.Item2); |
|
case ITypeDefinition type: |
|
return assemblyListTreeNode.FindTypeNode(type); |
|
case IField fd: |
|
return assemblyListTreeNode.FindFieldNode(fd); |
|
case IMethod md: |
|
return assemblyListTreeNode.FindMethodNode(md); |
|
case IProperty pd: |
|
return assemblyListTreeNode.FindPropertyNode(pd); |
|
case IEvent ed: |
|
return assemblyListTreeNode.FindEventNode(ed); |
|
case INamespace nd: |
|
return assemblyListTreeNode.FindNamespaceNode(nd); |
|
default: |
|
return null; |
|
} |
|
} |
|
|
|
private void JumpToReference(object? sender, NavigateToReferenceEventArgs e) |
|
{ |
|
JumpToReferenceAsync(e.Reference, e.Source, e.InNewTabPage).HandleExceptions(); |
|
IsActive = true; |
|
} |
|
|
|
/// <summary> |
|
/// Jumps to the specified reference. |
|
/// </summary> |
|
/// <returns> |
|
/// Returns a task that will signal completion when the decompilation of the jump target has finished. |
|
/// The task will be marked as canceled if the decompilation is canceled. |
|
/// </returns> |
|
private Task JumpToReferenceAsync(object? reference, object? source, bool inNewTabPage = false) |
|
{ |
|
this.sourceOfReference = source; |
|
var decompilationTask = Task.CompletedTask; |
|
|
|
switch (reference) |
|
{ |
|
case Decompiler.Disassembler.OpCodeInfo opCode: |
|
GlobalUtils.OpenLink(opCode.Link); |
|
break; |
|
case EntityReference unresolvedEntity: |
|
string protocol = unresolvedEntity.Protocol; |
|
var file = unresolvedEntity.ResolveAssembly(AssemblyList); |
|
if (file == null) |
|
{ |
|
break; |
|
} |
|
if (protocol != "decompile") |
|
{ |
|
foreach (var handler in exportProvider.GetExportedValues<IProtocolHandler>()) |
|
{ |
|
var node = handler.Resolve(protocol, file, unresolvedEntity.Handle, out bool newTabPage); |
|
if (node != null) |
|
{ |
|
SelectNode(node, newTabPage || inNewTabPage); |
|
return decompilationTask; |
|
} |
|
} |
|
} |
|
var possibleToken = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolvedEntity.Handle)); |
|
if (possibleToken != null) |
|
{ |
|
var typeSystem = new DecompilerTypeSystem(file, file.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached); |
|
reference = typeSystem.MainModule.ResolveEntity(possibleToken.Value); |
|
goto default; |
|
} |
|
break; |
|
default: |
|
var treeNode = FindTreeNode(reference); |
|
if (treeNode != null) |
|
SelectNode(treeNode, inNewTabPage); |
|
break; |
|
} |
|
return decompilationTask; |
|
} |
|
|
|
#endregion |
|
|
|
private void LoadAssemblies(IEnumerable<string> fileNames, List<LoadedAssembly>? loadedAssemblies = null, bool focusNode = true) |
|
{ |
|
using (Keyboard.FocusedElement.PreserveFocus(!focusNode)) |
|
{ |
|
AssemblyTreeNode? lastNode = null; |
|
|
|
var assemblyList = AssemblyList; |
|
|
|
foreach (string file in fileNames) |
|
{ |
|
var assembly = assemblyList.OpenAssembly(file); |
|
|
|
if (loadedAssemblies != null) |
|
{ |
|
loadedAssemblies.Add(assembly); |
|
} |
|
else |
|
{ |
|
var node = assemblyListTreeNode?.FindAssemblyNode(assembly); |
|
if (node != null && focusNode) |
|
{ |
|
lastNode = node; |
|
activeView?.ScrollIntoView(node); |
|
SelectedItems = [.. SelectedItems, node]; |
|
} |
|
} |
|
} |
|
if (focusNode && lastNode != null) |
|
{ |
|
activeView?.FocusNode(lastNode); |
|
} |
|
} |
|
} |
|
|
|
#region Decompile (TreeView_SelectionChanged) |
|
|
|
private void TreeView_SelectionChanged(SharpTreeNode[] oldSelection, SharpTreeNode[] newSelection) |
|
{ |
|
var activeTabPage = DockWorkspace.ActiveTabPage; |
|
ViewState? oldState = activeTabPage.GetState(); |
|
ViewState? newState; |
|
|
|
if (navigatingToState == null) |
|
{ |
|
if (oldState != null) |
|
{ |
|
history.UpdateCurrent(new NavigationState(activeTabPage, oldState)); |
|
} |
|
|
|
newState = new ViewState { DecompiledNodes = [.. newSelection.Cast<ILSpyTreeNode>()] }; |
|
} |
|
else |
|
{ |
|
newState = navigatingToState.ViewState; |
|
} |
|
|
|
if (newSelection.Length == 0) |
|
{ |
|
// To cancel any pending decompilation requests and show an empty tab |
|
DecompileSelectedNodes(newState); |
|
} |
|
else |
|
{ |
|
var delayDecompilationRequestDueToContextMenu = Mouse.RightButton == MouseButtonState.Pressed; |
|
|
|
if (!delayDecompilationRequestDueToContextMenu) |
|
{ |
|
var previousNodes = oldState?.DecompiledNodes |
|
?.Select(n => FindNodeByPath(GetPathForNode(n), true)) |
|
.ExceptNullItems() |
|
.ToArray() ?? []; |
|
|
|
if (!previousNodes.SequenceEqual(SelectedItems)) |
|
{ |
|
DecompileSelectedNodes(newState); |
|
} |
|
} |
|
else |
|
{ |
|
// ensure that we are only connected once to the event, else we might get multiple notifications |
|
ContextMenuProvider.ContextMenuClosed -= ContextMenuClosed; |
|
ContextMenuProvider.ContextMenuClosed += ContextMenuClosed; |
|
} |
|
} |
|
|
|
MessageBus.Send(this, new AssemblyTreeSelectionChangedEventArgs()); |
|
|
|
return; |
|
|
|
void ContextMenuClosed(object? sender, EventArgs e) |
|
{ |
|
ContextMenuProvider.ContextMenuClosed -= ContextMenuClosed; |
|
|
|
Dispatcher.BeginInvoke(DispatcherPriority.Background, () => { |
|
if (Mouse.RightButton != MouseButtonState.Pressed) |
|
{ |
|
RefreshDecompiledView(); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
public void DecompileSelectedNodes(ViewState? newState = null) |
|
{ |
|
object? source = this.sourceOfReference; |
|
this.sourceOfReference = null; |
|
var activeTabPage = DockWorkspace.ActiveTabPage; |
|
|
|
if (activeTabPage.FrozenContent) |
|
{ |
|
activeTabPage = DockWorkspace.AddTabPage(); |
|
} |
|
|
|
activeTabPage.SupportsLanguageSwitching = true; |
|
|
|
if (newState != null && navigatingToState == null) |
|
{ |
|
history.Record(new NavigationState(activeTabPage, newState)); |
|
} |
|
|
|
if (SelectedItems.Length == 1) |
|
{ |
|
if (SelectedItem is ILSpyTreeNode node && node.View(activeTabPage)) |
|
return; |
|
} |
|
if (newState?.ViewedUri != null) |
|
{ |
|
NavigateTo(new(newState.ViewedUri, null)); |
|
return; |
|
} |
|
|
|
var options = activeTabPage.CreateDecompilationOptions(); |
|
options.TextViewState = newState as DecompilerTextViewState; |
|
activeTabPage.ShowTextViewAsync(textView => { |
|
return textView.DecompileAsync(this.CurrentLanguage, this.SelectedNodes, source, options); |
|
}); |
|
} |
|
|
|
public void RefreshDecompiledView() |
|
{ |
|
DecompileSelectedNodes(DockWorkspace.ActiveTabPage.GetState() as DecompilerTextViewState); |
|
} |
|
|
|
public Language CurrentLanguage => languageService.Language; |
|
|
|
public LanguageVersion? CurrentLanguageVersion => languageService.LanguageVersion; |
|
|
|
public IEnumerable<ILSpyTreeNode> SelectedNodes => GetTopLevelSelection().OfType<ILSpyTreeNode>(); |
|
|
|
#endregion |
|
|
|
public void NavigateHistory(bool forward) |
|
{ |
|
try |
|
{ |
|
TabPageModel tabPage = DockWorkspace.ActiveTabPage; |
|
var state = tabPage.GetState(); |
|
if (state != null) |
|
history.UpdateCurrent(new NavigationState(tabPage, state)); |
|
var newState = forward ? history.GoForward() : history.GoBack(); |
|
navigatingToState = newState; |
|
|
|
TabPageModel activeTabPage = newState.TabPage; |
|
|
|
Debug.Assert(DockWorkspace.TabPages.Contains(activeTabPage)); |
|
DockWorkspace.ActiveTabPage = activeTabPage; |
|
|
|
if (newState.TreeNodes.Any()) |
|
{ |
|
SelectNodes(newState.TreeNodes); |
|
} |
|
else if (newState.ViewState.ViewedUri != null) |
|
{ |
|
NavigateTo(new(newState.ViewState.ViewedUri, null)); |
|
} |
|
} |
|
finally |
|
{ |
|
navigatingToState = null; |
|
} |
|
} |
|
|
|
public bool CanNavigateBack => history.CanNavigateBack; |
|
|
|
public bool CanNavigateForward => history.CanNavigateForward; |
|
|
|
private void NavigateTo(RequestNavigateEventArgs e, bool inNewTabPage = false) |
|
{ |
|
if (e.Uri.Scheme != "resource") |
|
{ |
|
return; |
|
} |
|
|
|
TabPageModel tabPage = DockWorkspace.ActiveTabPage; |
|
ViewState? oldState = tabPage.GetState(); |
|
ViewState? newState; |
|
|
|
if (navigatingToState == null) |
|
{ |
|
if (oldState != null) |
|
{ |
|
history.UpdateCurrent(new NavigationState(tabPage, oldState)); |
|
} |
|
|
|
newState = new ViewState { ViewedUri = e.Uri }; |
|
|
|
if (inNewTabPage) |
|
{ |
|
tabPage = DockWorkspace.AddTabPage(); |
|
} |
|
} |
|
else |
|
{ |
|
newState = navigatingToState.ViewState; |
|
tabPage = DockWorkspace.ActiveTabPage = navigatingToState.TabPage; |
|
} |
|
|
|
bool needsNewNavigationEntry = !inNewTabPage && selectedItems?.Length == 0; |
|
|
|
UnselectAll(); |
|
|
|
if (e.Uri.Host == "aboutpage") |
|
{ |
|
MessageBus.Send(this, new ShowAboutPageEventArgs(DockWorkspace.ActiveTabPage)); |
|
e.Handled = true; |
|
} |
|
else |
|
{ |
|
AvalonEditTextOutput output = new AvalonEditTextOutput { |
|
Address = e.Uri, |
|
Title = e.Uri.AbsolutePath, |
|
EnableHyperlinks = true |
|
}; |
|
using (Stream? s = typeof(App).Assembly.GetManifestResourceStream(typeof(App), e.Uri.AbsolutePath)) |
|
{ |
|
if (s != null) |
|
{ |
|
using StreamReader r = new StreamReader(s); |
|
string? line; |
|
while ((line = r.ReadLine()) != null) |
|
{ |
|
output.Write(line); |
|
output.WriteLine(); |
|
} |
|
} |
|
} |
|
DockWorkspace.ShowText(output); |
|
e.Handled = true; |
|
} |
|
|
|
if (navigatingToState == null) |
|
{ |
|
// the call to UnselectAll() above already creates a new navigation entry, |
|
// we just need to make sure it contains something useful. |
|
if (!needsNewNavigationEntry) |
|
{ |
|
history.UpdateCurrent(new NavigationState(tabPage, tabPage.GetState())); |
|
} |
|
else |
|
{ |
|
history.Record(new NavigationState(tabPage, tabPage.GetState())); |
|
} |
|
} |
|
} |
|
|
|
public void Refresh() |
|
{ |
|
refreshThrottle.Tick(); |
|
} |
|
|
|
private void RefreshInternal() |
|
{ |
|
using (Keyboard.FocusedElement.PreserveFocus()) |
|
{ |
|
var path = GetPathForNode(SelectedItem); |
|
|
|
ShowAssemblyList(settingsService.AssemblyListManager.LoadList(AssemblyList.ListName)); |
|
SelectNode(FindNodeByPath(path, true), inNewTabPage: false); |
|
|
|
RefreshDecompiledView(); |
|
} |
|
} |
|
|
|
private void UnselectAll() |
|
{ |
|
SelectedItems = []; |
|
} |
|
|
|
private IEnumerable<SharpTreeNode> GetTopLevelSelection() |
|
{ |
|
var selection = this.SelectedItems; |
|
var selectionHash = new HashSet<SharpTreeNode>(selection); |
|
|
|
return selection.Where(item => item.Ancestors().All(a => !selectionHash.Contains(a))); |
|
} |
|
|
|
public void SetActiveView(AssemblyListPane activeView) |
|
{ |
|
this.activeView = activeView; |
|
} |
|
|
|
public void SortAssemblyList() |
|
{ |
|
using (activeView?.LockUpdates()) |
|
{ |
|
AssemblyList.Sort(AssemblyComparer.Instance); |
|
} |
|
} |
|
|
|
private class AssemblyComparer : IComparer<LoadedAssembly> |
|
{ |
|
public static readonly AssemblyComparer Instance = new(); |
|
int IComparer<LoadedAssembly>.Compare(LoadedAssembly? x, LoadedAssembly? y) |
|
{ |
|
return string.Compare(x?.ShortName, y?.ShortName, StringComparison.CurrentCulture); |
|
} |
|
} |
|
|
|
public void CollapseAll() |
|
{ |
|
using (activeView?.LockUpdates()) |
|
{ |
|
CollapseChildren(Root); |
|
} |
|
} |
|
|
|
private 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 OpenFiles(string[] fileNames, bool focusNode = true) |
|
{ |
|
if (fileNames == null) |
|
throw new ArgumentNullException(nameof(fileNames)); |
|
|
|
if (focusNode) |
|
UnselectAll(); |
|
|
|
LoadAssemblies(fileNames, focusNode: focusNode); |
|
} |
|
|
|
private void ApplySessionSettings(object? sender, ApplySessionSettingsEventArgs e) |
|
{ |
|
var settings = e.SessionSettings; |
|
|
|
settings.ActiveAssemblyList = AssemblyList.ListName; |
|
settings.ActiveTreeViewPath = SelectedPath; |
|
settings.ActiveAutoLoadedAssembly = GetAutoLoadedAssemblyNode(SelectedItem); |
|
} |
|
|
|
private static string? GetAutoLoadedAssemblyNode(SharpTreeNode? node) |
|
{ |
|
var assemblyTreeNode = node? |
|
.AncestorsAndSelf() |
|
.OfType<AssemblyTreeNode>() |
|
.FirstOrDefault(); |
|
|
|
var loadedAssembly = assemblyTreeNode?.LoadedAssembly; |
|
|
|
return loadedAssembly is not { IsLoaded: true, IsAutoLoaded: true } |
|
? null |
|
: loadedAssembly.FileName; |
|
} |
|
|
|
private void ActiveTabPageChanged(object? sender, ActiveTabPageChangedEventArgs e) |
|
{ |
|
if (e.ViewState is not { } state) |
|
return; |
|
|
|
if (state.DecompiledNodes != null) |
|
{ |
|
SelectNodes(state.DecompiledNodes); |
|
} |
|
else |
|
{ |
|
NavigateTo(new(state.ViewedUri, null)); |
|
} |
|
} |
|
|
|
private void ResetLayout(object? sender, ResetLayoutEventArgs e) |
|
{ |
|
RefreshDecompiledView(); |
|
} |
|
} |
|
}
|
|
|