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.
340 lines
13 KiB
340 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.Collections.Generic; |
|
using System.Composition; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Threading.Tasks; |
|
|
|
using global::Avalonia.Controls; |
|
using global::Avalonia.Controls.ApplicationLifetimes; |
|
using global::Avalonia.Platform.Storage; |
|
|
|
using ICSharpCode.Decompiler; |
|
using ICSharpCode.ILSpy.Properties; |
|
|
|
using ICSharpCode.ILSpy.AppEnv; |
|
using ICSharpCode.ILSpy.AssemblyTree; |
|
using ICSharpCode.ILSpy.Docking; |
|
using ICSharpCode.ILSpy.Languages; |
|
using ICSharpCode.ILSpy.NuGetFeeds; |
|
using ICSharpCode.ILSpy.Processes; |
|
using ICSharpCode.ILSpy.TreeNodes; |
|
using ICSharpCode.ILSpy.Views; |
|
|
|
namespace ICSharpCode.ILSpy.Commands |
|
{ |
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._Open), MenuIcon = "Images/Open", MenuCategory = nameof(Resources.Open), MenuOrder = 0, InputGestureText = "Ctrl+O")] |
|
[ExportToolbarCommand(ToolTip = nameof(Resources.Open), ToolbarIcon = "Images/Open", ToolbarCategory = nameof(Resources.Open), ToolbarOrder = 0)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
sealed class OpenCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand |
|
{ |
|
public override async void Execute(object? parameter) |
|
{ |
|
// Tests / scripted callers can pass paths directly and bypass the file picker. |
|
if (parameter is string singlePath) |
|
{ |
|
assemblyTreeModel.OpenFiles(new[] { singlePath }); |
|
return; |
|
} |
|
if (parameter is IEnumerable<string> paths) |
|
{ |
|
assemblyTreeModel.OpenFiles(paths.ToArray()); |
|
return; |
|
} |
|
|
|
var owner = UiContext.MainWindow; |
|
if (owner == null) |
|
return; |
|
|
|
var files = await owner.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { |
|
Title = Resources.Open, |
|
AllowMultiple = true, |
|
FileTypeFilter = new[] { |
|
new FilePickerFileType(".NET assemblies") { |
|
Patterns = new[] { "*.dll", "*.exe", "*.winmd", "*.wasm" }, |
|
}, |
|
new FilePickerFileType("NuGet packages") { Patterns = new[] { "*.nupkg" } }, |
|
new FilePickerFileType("Portable PDB") { Patterns = new[] { "*.pdb" } }, |
|
FilePickerFileTypes.All, |
|
}, |
|
}); |
|
|
|
var picked = files |
|
.Select(f => f.TryGetLocalPath()) |
|
.Where(p => !string.IsNullOrEmpty(p)) |
|
.ToArray()!; |
|
if (picked.Length > 0) |
|
assemblyTreeModel.OpenFiles(picked!); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.OpenFrom_GAC), MenuIcon = "Images/AssemblyListGAC", MenuCategory = nameof(Resources.Open), MenuOrder = 1)] |
|
[Shared] |
|
sealed class OpenFromGacCommand : SimpleCommand |
|
{ |
|
readonly AssemblyTreeModel assemblyTreeModel; |
|
|
|
[ImportingConstructor] |
|
public OpenFromGacCommand(AssemblyTreeModel assemblyTreeModel) |
|
{ |
|
this.assemblyTreeModel = assemblyTreeModel; |
|
} |
|
|
|
// The Windows GAC doesn't exist on Linux/macOS. WPF gates this command on |
|
// `AppEnvironment.IsWindows`; Avalonia uses the BCL runtime check that already |
|
// returns the right answer per OS. |
|
public override bool CanExecute(object? parameter) => System.OperatingSystem.IsWindows(); |
|
|
|
public override void Execute(object? parameter) |
|
{ |
|
if (!CanExecute(parameter)) |
|
return; |
|
var owner = UiContext.MainWindow; |
|
if (owner == null) |
|
return; |
|
ShowAsync(owner).HandleExceptions(); |
|
} |
|
|
|
async System.Threading.Tasks.Task ShowAsync(global::Avalonia.Controls.Window owner) |
|
{ |
|
var dlg = new Views.OpenFromGacDialog(); |
|
var fileNames = await dlg.ShowDialog<string[]>(owner); |
|
if (fileNames == null || fileNames.Length == 0) |
|
return; |
|
foreach (var name in fileNames) |
|
assemblyTreeModel.AssemblyList?.OpenAssembly(name); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.OpenFrom_NuGetFeed), MenuIcon = "Images/NuGet", MenuCategory = nameof(Resources.Open), MenuOrder = 1.5)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
sealed class OpenFromNuGetFeedCommand(AssemblyTreeModel assemblyTreeModel, SettingsService settingsService) : SimpleCommand |
|
{ |
|
// One client for the command's lifetime so the per-feed service-index discovery |
|
// and the NuGet HTTP cache survive across dialog invocations. |
|
readonly NuGetFeedClient feedClient = new(); |
|
// Likewise long-lived so the icon cache survives across dialog invocations. |
|
readonly NuGetIconLoader iconLoader = new(); |
|
|
|
public override void Execute(object? parameter) |
|
{ |
|
var owner = UiContext.MainWindow; |
|
if (owner == null) |
|
return; |
|
ShowAsync(owner).HandleExceptions(); |
|
} |
|
|
|
async Task ShowAsync(global::Avalonia.Controls.Window owner) |
|
{ |
|
var dlg = new Views.OpenFromNuGetFeedDialog(settingsService, feedClient, iconLoader: iconLoader); |
|
// The dialog result is the absolute path of the .nupkg in the global packages |
|
// folder; ILSpy opens .nupkg natively (ArchiveFileLoader/LoadedPackage). |
|
var path = await dlg.ShowDialog<string?>(owner); |
|
if (!string.IsNullOrEmpty(path)) |
|
assemblyTreeModel.OpenFiles(new[] { path }); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.OpenFrom_RunningProcess), MenuIcon = "Images/Process", MenuCategory = nameof(Resources.Open), MenuOrder = 1.7)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
sealed class OpenFromRunningProcessCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand |
|
{ |
|
public override void Execute(object? parameter) |
|
{ |
|
var owner = UiContext.MainWindow; |
|
if (owner == null) |
|
return; |
|
ShowAsync(owner).HandleExceptions(); |
|
} |
|
|
|
async Task ShowAsync(global::Avalonia.Controls.Window owner) |
|
{ |
|
// A fresh explorer per invocation: it holds no cache worth keeping alive, and |
|
// the process list is stale the moment the dialog closes anyway. |
|
var dlg = new Views.OpenFromProcessDialog(new ProcessExplorer()); |
|
var paths = await dlg.ShowDialog<string[]?>(owner); |
|
if (paths is { Length: > 0 }) |
|
assemblyTreeModel.OpenFiles(paths); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.ManageAssembly_Lists), MenuIcon = "Images/AssemblyList", MenuCategory = "AssemblyList", MenuOrder = 10)] |
|
[Shared] |
|
sealed class ManageAssemblyListsCommand : SimpleCommand |
|
{ |
|
readonly SettingsService settingsService; |
|
|
|
[ImportingConstructor] |
|
public ManageAssemblyListsCommand(SettingsService settingsService) |
|
{ |
|
this.settingsService = settingsService; |
|
} |
|
|
|
public override void Execute(object? parameter) |
|
{ |
|
var owner = UiContext.MainWindow; |
|
if (owner == null) |
|
return; |
|
var dlg = new Views.ManageAssemblyListsDialog(settingsService); |
|
_ = dlg.ShowDialog(owner); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._Reload), MenuIcon = "Images/Refresh", MenuCategory = nameof(Resources.Open), MenuOrder = 2, InputGestureText = "F5")] |
|
[ExportToolbarCommand(ToolTip = nameof(Resources.RefreshCommand_ReloadAssemblies), ToolbarIcon = "Images/Refresh", ToolbarCategory = nameof(Resources.Open), ToolbarOrder = 2)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
sealed class RefreshCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand |
|
{ |
|
public override void Execute(object? parameter) => assemblyTreeModel.Refresh(); |
|
} |
|
|
|
// DEBUG-only DecompileAllCommand + Decompile100TimesCommand live in DecompileAllCommand.cs; |
|
// the stubs that lived here were replaced with the real implementations in `<commit>`. |
|
|
|
// DEBUG-only DisassembleAllCommand also moved to DecompileAllCommand.cs to keep all |
|
// three parallel-decompile/disassemble stress-test commands in one place. |
|
|
|
// DEBUG-only Pdb2XmlCommand moved to its own file with `#if DEBUG && WINDOWS` gating. |
|
// On non-Windows or non-Debug builds the entry simply isn't compiled. |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._RemoveAssembliesWithLoadErrors), MenuCategory = "AssemblyList", MenuOrder = 11)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
sealed class RemoveAssembliesWithLoadErrors(AssemblyTreeModel assemblyTreeModel) : SimpleCommand |
|
{ |
|
public override bool CanExecute(object? parameter) |
|
=> assemblyTreeModel.AssemblyList?.GetAssemblies().Any(l => l.HasLoadError) ?? false; |
|
|
|
public override void Execute(object? parameter) |
|
{ |
|
var list = assemblyTreeModel.AssemblyList; |
|
if (list == null) |
|
return; |
|
foreach (var assembly in list.GetAssemblies()) |
|
{ |
|
if (assembly.HasLoadError) |
|
list.Unload(assembly); |
|
} |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.ClearAssemblyList), MenuCategory = "AssemblyList", MenuOrder = 12)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
sealed class ClearAssemblyListCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand |
|
{ |
|
public override bool CanExecute(object? parameter) |
|
=> (assemblyTreeModel.AssemblyList?.Count ?? 0) > 0; |
|
|
|
public override void Execute(object? parameter) => assemblyTreeModel.AssemblyList?.Clear(); |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._SaveCode), MenuIcon = "Images/Save", MenuCategory = nameof(Resources.Save), MenuOrder = 20, InputGestureText = "Ctrl+S")] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
internal sealed class SaveCommand( |
|
AssemblyTreeModel assemblyTreeModel, |
|
LanguageService languageService, |
|
DockWorkspace dockWorkspace) : SimpleCommand |
|
{ |
|
public override bool CanExecute(object? parameter) |
|
=> assemblyTreeModel.SelectedItem is ILSpyTreeNode; |
|
|
|
public override async void Execute(object? parameter) |
|
{ |
|
// Several selected assemblies export a Visual Studio solution (one project each), |
|
// matching the Save Code context-menu entry. |
|
if (ProjectExport.TryGetSolutionAssemblies(assemblyTreeModel.SelectedItems, out var assemblies)) |
|
{ |
|
await ProjectExport.PromptAndExportSolutionAsync(assemblies, languageService.CurrentLanguage, dockWorkspace); |
|
return; |
|
} |
|
|
|
if (assemblyTreeModel.SelectedItem is not ILSpyTreeNode node) |
|
return; |
|
// Shared with the context-menu entry: the node handles its own save (resources, the |
|
// assembly project/single-file picker), else fall through to decompile-to-file. |
|
await SaveCodeHelper.SaveNodeAsync(node, languageService, dockWorkspace).ConfigureAwait(false); |
|
} |
|
|
|
/// <summary> |
|
/// Public for tests + scripted callers: re-decompiles the currently selected node with |
|
/// <see cref="DecompilationOptions.FullDecompilation"/> on and writes the output to |
|
/// <paramref name="path"/> as plain text, bypassing the file picker. |
|
/// </summary> |
|
public Task SaveCodeAsync(string path) |
|
{ |
|
if (assemblyTreeModel.SelectedItem is not ILSpyTreeNode node) |
|
return Task.CompletedTask; |
|
return SaveCodeHelper.WriteNodeToFileAsync(node, languageService.CurrentLanguage, path); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.ExportProjectSolution), MenuIcon = "Images/Save", MenuCategory = nameof(Resources.Save), MenuOrder = 21)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
internal sealed class ExportProjectSolutionCommand( |
|
AssemblyTreeModel assemblyTreeModel, |
|
LanguageService languageService, |
|
DockWorkspace dockWorkspace, |
|
SettingsService settingsService) : SimpleCommand |
|
{ |
|
public override bool CanExecute(object? parameter) |
|
=> ProjectExport.TryGetExportableAssemblies(assemblyTreeModel.SelectedItems, out _, out _); |
|
|
|
public override void Execute(object? parameter) |
|
{ |
|
if (!ProjectExport.TryGetExportableAssemblies(assemblyTreeModel.SelectedItems, out var assemblies, out var solutionMode)) |
|
return; |
|
ProjectExport.PromptAndExportAsync(assemblies, solutionMode, |
|
languageService.CurrentLanguage, dockWorkspace, settingsService).HandleExceptions(); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.GeneratePortable), MenuCategory = nameof(Resources.Save), MenuOrder = 22)] |
|
[Shared] |
|
[method: ImportingConstructor] |
|
sealed class GeneratePdbCommand(AssemblyTreeModel assemblyTreeModel, DockWorkspace dockWorkspace) : SimpleCommand |
|
{ |
|
public override bool CanExecute(object? parameter) |
|
=> PdbGenerator.TryGetAssemblies(assemblyTreeModel.SelectedItems, out _); |
|
|
|
public override void Execute(object? parameter) |
|
{ |
|
if (!PdbGenerator.TryGetAssemblies(assemblyTreeModel.SelectedItems, out var assemblies)) |
|
return; |
|
PdbGenerator.GenerateAsync(assemblies, dockWorkspace).HandleExceptions(); |
|
} |
|
} |
|
|
|
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.E_xit), MenuOrder = 99999, MenuCategory = nameof(Resources.Exit))] |
|
[Shared] |
|
sealed class ExitCommand : SimpleCommand |
|
{ |
|
public override void Execute(object? parameter) |
|
{ |
|
UiContext.Shutdown(); |
|
} |
|
} |
|
}
|
|
|