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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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