mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Right-click an assembly tree node → Create Diagram. Prompts for an output folder via the new FilePickers.PickFolderAsync helper, then runs the shared ICSharpCode.ILSpyX.MermaidDiagrammer.GenerateHtmlDiagrammer engine on a background thread under DockWorkspace.RunWithCancellation (the new wait UI with a custom "Creating diagram…" title). On completion, pushes a report into the active tab via ShowText — elapsed time, learn-more link, and an "Open Explorer" button that selects the generated index.html in the OS file manager (xdg-open / open -R / explorer.exe per platform). Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
3 changed files with 257 additions and 0 deletions
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
// 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.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using ILSpy; |
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Commands; |
||||
using ILSpy.TreeNodes; |
||||
using ILSpy.ViewModels; |
||||
using ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
[TestFixture] |
||||
public class CreateDiagramContextMenuTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Entry_Is_Visible_For_A_Single_Loaded_Assembly() |
||||
{ |
||||
// The "Create Diagram" context-menu entry must surface when the user right-clicks
|
||||
// exactly one assembly tree node whose underlying file loaded as a valid assembly.
|
||||
// Multi-select, child-node selection, and failed-load nodes should all hide it —
|
||||
// the diagrammer needs a single concrete assembly path to feed to GenerateHtmlDiagrammer.
|
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||
|
||||
var entry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>().Entries |
||||
.Select(e => e.Value).OfType<CreateDiagramContextMenuEntry>().Single(); |
||||
|
||||
var assemblyNode = vm.AssemblyTreeModel.Root!.Children.OfType<AssemblyTreeNode>().First(); |
||||
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { assemblyNode } }).Should().BeTrue(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Entry_Is_Hidden_For_Non_Assembly_Selections() |
||||
{ |
||||
// Anything that isn't an AssemblyTreeNode (e.g. a TypeTreeNode under it) doesn't
|
||||
// expose the entry — the diagrammer operates on whole-assembly granularity.
|
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||
|
||||
var entry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>().Entries |
||||
.Select(e => e.Value).OfType<CreateDiagramContextMenuEntry>().Single(); |
||||
|
||||
var coreLibName = typeof(object).Assembly.GetName().Name!; |
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.Object"); |
||||
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { typeNode } }).Should().BeFalse(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Entry_Is_Hidden_For_Multi_Select() |
||||
{ |
||||
// Multi-select must also hide the entry — the diagrammer runs on one assembly at
|
||||
// a time. (The WPF version uses the same `Length == 1` guard.)
|
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||
|
||||
var entry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>().Entries |
||||
.Select(e => e.Value).OfType<CreateDiagramContextMenuEntry>().Single(); |
||||
|
||||
var assemblyNode = vm.AssemblyTreeModel.Root!.Children.OfType<AssemblyTreeNode>().First(); |
||||
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { assemblyNode, assemblyNode } }).Should().BeFalse(); |
||||
} |
||||
} |
||||
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
// 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.Composition; |
||||
using System.Diagnostics; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using ICSharpCode.ILSpy.Properties; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer; |
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using ILSpy.Docking; |
||||
using ILSpy.TextView; |
||||
using ILSpy.TreeNodes; |
||||
|
||||
namespace ILSpy.Commands |
||||
{ |
||||
/// <summary>
|
||||
/// Right-click an assembly → "Create Diagram". Asks for an output folder, invokes the
|
||||
/// shared <see cref="GenerateHtmlDiagrammer"/> engine on a background thread, then
|
||||
/// pushes a completion report (elapsed time, learn-more link, Open-Explorer button)
|
||||
/// into the active decompiler tab via <see cref="DockWorkspace.ShowText"/>. Visible
|
||||
/// only when exactly one valid loaded assembly is selected.
|
||||
/// </summary>
|
||||
[ExportContextMenuEntry(Header = nameof(Resources._CreateDiagram), Category = nameof(Resources.Save), Icon = "Images/Save")] |
||||
[Shared] |
||||
public sealed class CreateDiagramContextMenuEntry : IContextMenuEntry |
||||
{ |
||||
readonly DockWorkspace dockWorkspace; |
||||
|
||||
[ImportingConstructor] |
||||
public CreateDiagramContextMenuEntry(DockWorkspace dockWorkspace) |
||||
{ |
||||
this.dockWorkspace = dockWorkspace; |
||||
} |
||||
|
||||
public bool IsVisible(TextViewContext context) |
||||
{ |
||||
return context.SelectedTreeNodes?.Length == 1 |
||||
&& context.SelectedTreeNodes[0] is AssemblyTreeNode tn |
||||
&& tn.LoadedAssembly.IsLoadedAsValidAssembly; |
||||
} |
||||
|
||||
public bool IsEnabled(TextViewContext context) => true; |
||||
|
||||
public void Execute(TextViewContext context) |
||||
{ |
||||
var assembly = (context.SelectedTreeNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly; |
||||
if (assembly == null) |
||||
return; |
||||
_ = ExecuteAsync(assembly.FileName); |
||||
} |
||||
|
||||
async Task ExecuteAsync(string assemblyFile) |
||||
{ |
||||
var outputFolder = await FilePickers.PickFolderAsync("Select target folder"); |
||||
if (string.IsNullOrEmpty(outputFolder)) |
||||
return; |
||||
|
||||
try |
||||
{ |
||||
var output = await dockWorkspace.RunWithCancellation( |
||||
token => Task.Run(() => RunGenerator(assemblyFile, outputFolder), token), |
||||
Resources.CreatingDiagram); |
||||
dockWorkspace.ShowText(output); |
||||
} |
||||
catch (OperationCanceledException) |
||||
{ |
||||
// User cancelled — leave the previous tab content visible.
|
||||
} |
||||
} |
||||
|
||||
static AvaloniaEditTextOutput RunGenerator(string assemblyFile, string outputFolder) |
||||
{ |
||||
var output = new AvaloniaEditTextOutput(); |
||||
var stopwatch = Stopwatch.StartNew(); |
||||
var command = new GenerateHtmlDiagrammer { |
||||
Assembly = assemblyFile, |
||||
OutputFolder = outputFolder, |
||||
}; |
||||
command.Run(); |
||||
stopwatch.Stop(); |
||||
output.Title = "Create Diagram"; |
||||
output.Write(string.Format(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1"))); |
||||
output.WriteLine(); |
||||
output.WriteLine(); |
||||
output.Write("Learn more: https://github.com/icsharpcode/ILSpy/wiki/Diagramming#tips-for-using-the-html-diagrammer"); |
||||
output.WriteLine(); |
||||
output.WriteLine(); |
||||
var diagramHtml = Path.Combine(outputFolder, "index.html"); |
||||
output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenInShell(diagramHtml)); |
||||
output.WriteLine(); |
||||
return output; |
||||
} |
||||
|
||||
static void OpenInShell(string path) |
||||
{ |
||||
try |
||||
{ |
||||
if (OperatingSystem.IsWindows()) |
||||
{ |
||||
Process.Start(new ProcessStartInfo("explorer.exe", $"/select,\"{path}\"") { UseShellExecute = false }); |
||||
} |
||||
else if (OperatingSystem.IsMacOS()) |
||||
{ |
||||
Process.Start(new ProcessStartInfo("open", $"-R \"{path}\"") { UseShellExecute = false }); |
||||
} |
||||
else |
||||
{ |
||||
// Linux: no universal "select item" command; open the parent directory instead.
|
||||
var parent = Path.GetDirectoryName(path); |
||||
if (!string.IsNullOrEmpty(parent)) |
||||
Process.Start(new ProcessStartInfo("xdg-open", parent) { UseShellExecute = false }); |
||||
} |
||||
} |
||||
catch |
||||
{ |
||||
// Best-effort: the user can navigate to the folder manually if the shell call fails.
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue