From caae42aa7331ffcc77a1d47699c4416d0882bfad Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 17 Nov 2024 13:58:30 +0100 Subject: [PATCH] Implement 3330: Generate diagram from UI without advanced options --- .../Commands/CreateDiagramContextMenuEntry.cs | 124 ++++++++++++++++++ ILSpy/Docking/DockWorkspace.cs | 5 + ILSpy/Properties/Resources.Designer.cs | 18 +++ ILSpy/Properties/Resources.resx | 6 + ILSpy/TextView/DecompilerTextView.cs | 4 +- 5 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 ILSpy/Commands/CreateDiagramContextMenuEntry.cs diff --git a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs new file mode 100644 index 0000000000..edbbe14a3f --- /dev/null +++ b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs @@ -0,0 +1,124 @@ +// Copyright (c) 2024 Christoph Wille 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 System.Windows; + +using ICSharpCode.Decompiler; +using ICSharpCode.ILSpy.Docking; +using ICSharpCode.ILSpy.Properties; +using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.ILSpyX.MermaidDiagrammer; + +using Microsoft.Win32; + +namespace ICSharpCode.ILSpy.TextView +{ + [ExportContextMenuEntry(Header = nameof(Resources._CreateDiagram), Category = nameof(Resources.Save), Icon = "Images/Save")] + [Shared] + sealed class CreateDiagramContextMenuEntry(DockWorkspace dockWorkspace) : IContextMenuEntry + { + public void Execute(TextViewContext context) + { + var assembly = (context.SelectedTreeNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly; + if (assembly == null) + return; + + var selectedPath = SelectDestinationFolder(); + if (string.IsNullOrEmpty(selectedPath)) + return; + + dockWorkspace.RunWithCancellation(ct => Task.Factory.StartNew(() => { + AvalonEditTextOutput output = new() { + EnableHyperlinks = true + }; + Stopwatch stopwatch = Stopwatch.StartNew(); + try + { + var command = new GenerateHtmlDiagrammer { + Assembly = assembly.FileName, + OutputFolder = selectedPath + }; + + command.Run(); + } + catch (OperationCanceledException) + { + output.WriteLine(); + output.WriteLine(Resources.GenerationWasCancelled); + throw; + } + stopwatch.Stop(); + output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")); + output.WriteLine(); + output.WriteLine("Learn more: " + "https://github.com/icsharpcode/ILSpy/wiki/Diagramming#tips-for-using-the-html-diagrammer"); + output.WriteLine(); + + var diagramHtml = Path.Combine(selectedPath, "index.html"); + output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + diagramHtml + "\""); }); + output.WriteLine(); + return output; + }, ct), Properties.Resources.CreatingDiagram).Then(dockWorkspace.ShowText).HandleExceptions(); + + return; + } + + public bool IsEnabled(TextViewContext context) => true; + + public bool IsVisible(TextViewContext context) + { + return context.SelectedTreeNodes?.Length == 1 + && context.SelectedTreeNodes?.FirstOrDefault() is AssemblyTreeNode tn + && tn.LoadedAssembly.IsLoadedAsValidAssembly; + } + + static string SelectDestinationFolder() + { + OpenFolderDialog dialog = new(); + dialog.Multiselect = false; + dialog.Title = "Select target folder"; + + if (dialog.ShowDialog() != true) + { + return null; + } + + string selectedPath = Path.GetDirectoryName(dialog.FolderName); + bool directoryNotEmpty; + try + { + directoryNotEmpty = Directory.EnumerateFileSystemEntries(selectedPath).Any(); + } + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is System.Security.SecurityException) + { + MessageBox.Show( + "The directory cannot be accessed. Please ensure it exists and you have sufficient rights to access it.", + "Target directory not accessible", + MessageBoxButton.OK, MessageBoxImage.Error); + return null; + } + + return dialog.FolderName; + } + } +} diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index d92b928476..cc69c7bdac 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -226,6 +226,11 @@ public Task RunWithCancellation(Func> taskCreat return ActiveTabPage.ShowTextViewAsync(textView => textView.RunWithCancellation(taskCreation)); } + public Task RunWithCancellation(Func> taskCreation, string progressTitle) + { + return ActiveTabPage.ShowTextViewAsync(textView => textView.RunWithCancellation(taskCreation, progressTitle)); + } + internal void ShowNodes(AvalonEditTextOutput output, TreeNodes.ILSpyTreeNode[] nodes, IHighlightingDefinition highlighting) { ActiveTabPage.ShowTextView(textView => textView.ShowNodes(output, nodes, highlighting)); diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index ca8b551519..01628056b5 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -114,6 +114,15 @@ public static string _CollapseTreeNodes { } } + /// + /// Looks up a localized string similar to Create _Diagram.... + /// + public static string _CreateDiagram { + get { + return ResourceManager.GetString("_CreateDiagram", resourceCulture); + } + } + /// /// Looks up a localized string similar to _File. /// @@ -567,6 +576,15 @@ public static string Create { } } + /// + /// Looks up a localized string similar to Creating diagram.... + /// + public static string CreatingDiagram { + get { + return ResourceManager.GetString("CreatingDiagram", resourceCulture); + } + } + /// /// Looks up a localized string similar to Culture. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index b853b06411..bcb79fae72 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -210,6 +210,9 @@ Are you sure you want to continue? Create + + Creating diagram... + Culture @@ -1036,6 +1039,9 @@ Do you want to continue? _Collapse all tree nodes + + Create _Diagram... + _File diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index 2e65074d1a..4e748fd2ac 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -595,14 +595,14 @@ public void Report(DecompilationProgress value) /// the task. /// If another task is started before the previous task finishes running, the previous task is cancelled. /// - public Task RunWithCancellation(Func> taskCreation) + public Task RunWithCancellation(Func> taskCreation, string? progressTitle = null) { if (waitAdorner.Visibility != Visibility.Visible) { waitAdorner.Visibility = Visibility.Visible; // Work around a WPF bug by setting IsIndeterminate only while the progress bar is visible. // https://github.com/icsharpcode/ILSpy/issues/593 - progressTitle.Text = Properties.Resources.Decompiling; + this.progressTitle.Text = progressTitle == null ? Properties.Resources.Decompiling : progressTitle; progressBar.IsIndeterminate = true; progressText.Text = null; progressText.Visibility = Visibility.Collapsed;