From b85d14ac5576f1166cb3e4677d3612d8a8db9dd7 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 21 Jun 2019 14:40:08 +0200 Subject: [PATCH 01/10] Add extension method for DependencyObject --- ILSpy/Controls/ExtensionMethods.cs | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/ILSpy/Controls/ExtensionMethods.cs b/ILSpy/Controls/ExtensionMethods.cs index 095ddc41e..1c5e9f794 100644 --- a/ILSpy/Controls/ExtensionMethods.cs +++ b/ILSpy/Controls/ExtensionMethods.cs @@ -19,6 +19,7 @@ using System; using System.Windows; using System.Windows.Markup; +using System.Windows.Media; namespace ICSharpCode.ILSpy.Controls { @@ -27,6 +28,39 @@ namespace ICSharpCode.ILSpy.Controls /// public static class ExtensionMethods { + /// + /// Checks if the current is contained in the visual tree of the + /// object. + /// + /// The object to check, may be null. + /// The object whose visual tree will be inspected. + /// + /// true if this object is contained in the visual tree of the ; + /// otherwise, false. + /// + /// Thrown when is null. + public static bool IsInVisualTreeOf(this DependencyObject thisObject, DependencyObject dependencyObject) + { + if (dependencyObject == null) { + throw new ArgumentNullException(nameof(dependencyObject)); + } + + if (thisObject is null) { + return false; + } + + var parent = VisualTreeHelper.GetParent(thisObject); + while (parent != null) { + if (parent == dependencyObject) { + return true; + } + + parent = VisualTreeHelper.GetParent(parent); + } + + return false; + } + /// /// Sets the value of a dependency property on using a markup extension. /// From 864672c07cc9d709d7840ec72950bb4000c761e0 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 21 Jun 2019 14:41:18 +0200 Subject: [PATCH 02/10] Enable the save code command only when appropriate --- ILSpy/MainWindow.xaml | 1 + ILSpy/MainWindow.xaml.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ILSpy/MainWindow.xaml b/ILSpy/MainWindow.xaml index a168d2e66..166c8ac80 100644 --- a/ILSpy/MainWindow.xaml +++ b/ILSpy/MainWindow.xaml @@ -28,6 +28,7 @@ Executed="RefreshCommandExecuted" /> n is AssemblyTreeNode); + } + void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e) { if (this.SelectedNodes.Count() == 1) { From 46dfa7295442e3f828d98582a99613c67ec847ce Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 21 Jun 2019 15:16:36 +0200 Subject: [PATCH 03/10] Add 'Save Code' context menu item --- ILSpy/Commands/SaveCodeContextMenuEntry.cs | 40 ++++++++++++++++++++++ ILSpy/ILSpy.csproj | 1 + 2 files changed, 41 insertions(+) create mode 100644 ILSpy/Commands/SaveCodeContextMenuEntry.cs diff --git a/ILSpy/Commands/SaveCodeContextMenuEntry.cs b/ILSpy/Commands/SaveCodeContextMenuEntry.cs new file mode 100644 index 000000000..85e5e301f --- /dev/null +++ b/ILSpy/Commands/SaveCodeContextMenuEntry.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2011 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.Windows.Input; +using ICSharpCode.ILSpy.Properties; + +namespace ICSharpCode.ILSpy.TextView +{ + [ExportContextMenuEntry(Header = nameof(Resources._SaveCode), Category = nameof(Resources.Save), Icon = "Images/SaveFile.png")] + sealed class SaveCodeContextMenuEntry : IContextMenuEntry + { + private readonly ICommand saveCommand; + + public SaveCodeContextMenuEntry() + { + saveCommand = ApplicationCommands.Save; + } + + public void Execute(TextViewContext context) => saveCommand.Execute(parameter: null); + + public bool IsEnabled(TextViewContext context) => true; + + public bool IsVisible(TextViewContext context) => context.TreeView != null && saveCommand.CanExecute(parameter: null); + } +} diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index c7f0658d6..e994dddff 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -207,6 +207,7 @@ + From 4ceb8f62d061ff9d3f1f82111dcb6afa9aeb4a15 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 21 Jun 2019 19:00:25 +0200 Subject: [PATCH 04/10] Implement decompilation of multiple selected assemblies icsharpcode#972 --- ILSpy/ILSpy.csproj | 1 + ILSpy/MainWindow.xaml.cs | 29 +++- ILSpy/SolutionWriter.cs | 198 ++++++++++++++++++++++++++++ ILSpy/TreeNodes/AssemblyTreeNode.cs | 53 +++++--- ILSpy/TreeNodes/ILSpyTreeNode.cs | 9 ++ 5 files changed, 263 insertions(+), 27 deletions(-) create mode 100644 ILSpy/SolutionWriter.cs diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index e994dddff..24f0d0b59 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -204,6 +204,7 @@ + diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 6e6ea8607..238c74307 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -907,19 +907,34 @@ namespace ICSharpCode.ILSpy e.CanExecute = true; return; } - var selectedNodes = SelectedNodes.ToArray(); - e.CanExecute = selectedNodes.Length == 1 || Array.TrueForAll(selectedNodes, n => n is AssemblyTreeNode); + var selectedNodes = SelectedNodes.ToList(); + e.CanExecute = selectedNodes.Count == 1 || selectedNodes.TrueForAll(n => n is AssemblyTreeNode); } void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e) { - if (this.SelectedNodes.Count() == 1) { - if (this.SelectedNodes.Single().Save(this.TextView)) + var selectedNodes = SelectedNodes.ToList(); + if (selectedNodes.Count > 1) { + var assemblyNodes = selectedNodes + .OfType() + .Where(n => n.Language is CSharpLanguage) + .ToList(); + + if (assemblyNodes.Count == selectedNodes.Count) { + var initialPath = Path.GetDirectoryName(assemblyNodes[0].LoadedAssembly.FileName); + var selectedPath = SolutionWriter.SelectSolutionFile(initialPath); + + if (!string.IsNullOrEmpty(selectedPath)) { + SolutionWriter.CreateSolution(TextView, selectedPath, assemblyNodes); + } return; + } + } + + if (selectedNodes.Count != 1 || !selectedNodes[0].Save(TextView)) { + var options = new DecompilationOptions() { FullDecompilation = true }; + TextView.SaveToDisk(CurrentLanguage, selectedNodes, options); } - this.TextView.SaveToDisk(this.CurrentLanguage, - this.SelectedNodes, - new DecompilationOptions() { FullDecompilation = true }); } public void RefreshDecompiledView() diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs new file mode 100644 index 000000000..a4ebf5779 --- /dev/null +++ b/ILSpy/SolutionWriter.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2011 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using ICSharpCode.Decompiler; +using ICSharpCode.ILSpy.TextView; +using ICSharpCode.ILSpy.TreeNodes; +using Microsoft.Win32; + +namespace ICSharpCode.ILSpy +{ + /// + /// An utility class that creates a Visual Studio solution containing projects for the + /// decompiled assemblies. + /// + internal static class SolutionWriter + { + private const string SolutionExtension = ".sln"; + private const string DefaultSolutionName = "Solution"; + + /// + /// Shows a File Selection dialog where the user can select the target file for the solution. + /// + /// The initial path to show in the dialog. If not specified, the 'Documents' directory + /// will be used. + /// + /// The full path of the selected target file, or null if the user canceled. + public static string SelectSolutionFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) { + path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + } + + SaveFileDialog dlg = new SaveFileDialog(); + dlg.InitialDirectory = path; + dlg.FileName = Path.Combine(path, DefaultSolutionName + SolutionExtension); + dlg.Filter = "Visual Studio Solution file|*" + SolutionExtension; + + bool targetInvalid; + do { + if (dlg.ShowDialog() != true) { + return null; + } + + string selectedPath = Path.GetDirectoryName(dlg.FileName); + try { + targetInvalid = Directory.EnumerateFileSystemEntries(selectedPath).Any(); + } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is SecurityException) { + MessageBox.Show( + "The directory cannot be accessed. Please ensure it exists and you have sufficient rights to access it.", + "Solution directory not accessible", + MessageBoxButton.OK, MessageBoxImage.Error); + targetInvalid = true; + continue; + } + + if (targetInvalid) { + MessageBox.Show( + "The directory is not empty. Please select an empty directory.", + "Solution directory not empty", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } while (targetInvalid); + + return dlg.FileName; + } + + /// + /// Creates a Visual Studio solution that contains projects with decompiled code + /// of the specified . The solution file will be saved + /// to the . The directory of this file must either + /// be empty or not exist. + /// + /// A reference to the instance. + /// The target file path of the solution file. + /// The assembly nodes to decompile. + /// + /// Thrown when is null, + /// an empty or a whitespace string. + /// Thrown when > or + /// is null. + public static void CreateSolution(DecompilerTextView textView, string solutionFilePath, IEnumerable assemblyNodes) + { + if (textView == null) { + throw new ArgumentNullException(nameof(textView)); + } + + if (string.IsNullOrWhiteSpace(solutionFilePath)) { + throw new ArgumentException("The solution file path cannot be null or empty.", nameof(solutionFilePath)); + } + + if (assemblyNodes == null) { + throw new ArgumentNullException(nameof(assemblyNodes)); + } + + textView + .RunWithCancellation(ct => CreateSolution(solutionFilePath, assemblyNodes, ct)) + .Then(output => textView.ShowText(output)) + .HandleExceptions(); + } + + private static async Task CreateSolution( + string solutionFilePath, + IEnumerable assemblyNodes, + CancellationToken ct) + { + var solutionDirectory = Path.GetDirectoryName(solutionFilePath); + var statusOutput = new ConcurrentBag(); + var result = new AvalonEditTextOutput(); + + var duplicates = new HashSet(); + if (assemblyNodes.Any(n => !duplicates.Add(n.LoadedAssembly.ShortName))) { + result.WriteLine("Duplicate assembly names selected, cannot generate a solution."); + return result; + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + + await Task.Run(() => Parallel.ForEach(assemblyNodes, n => WriteProject(n, solutionDirectory, statusOutput, ct))) + .ConfigureAwait(false); + + + foreach (var item in statusOutput) { + result.WriteLine(item); + } + + if (statusOutput.Count == 0) { + result.WriteLine("Successfully decompiled the following assemblies to a Visual Studio Solution:"); + foreach (var item in assemblyNodes.Select(n => n.Text.ToString())) { + result.WriteLine(item); + } + + result.WriteLine(); + result.WriteLine("Elapsed time: " + stopwatch.Elapsed.TotalSeconds.ToString("F1") + " seconds."); + result.WriteLine(); + result.AddButton(null, "Open Explorer", delegate { Process.Start("explorer", "/select,\"" + solutionFilePath + "\""); }); + } + + return result; + } + + private static void WriteProject(AssemblyTreeNode assemblyNode, string targetDirectory, ConcurrentBag statusOutput, CancellationToken ct) + { + var loadedAssembly = assemblyNode.LoadedAssembly; + + targetDirectory = Path.Combine(targetDirectory, loadedAssembly.ShortName); + string projectFileName = Path.Combine(targetDirectory, loadedAssembly.ShortName + assemblyNode.Language.ProjectFileExtension); + + if (!Directory.Exists(targetDirectory)) { + try { + Directory.CreateDirectory(targetDirectory); + } catch (Exception e) { + statusOutput.Add($"Failed to create a directory '{targetDirectory}':{Environment.NewLine}{e}"); + return; + } + } + + try { + using (var projectFileWriter = new StreamWriter(projectFileName)) { + var projectFileOutput = new PlainTextOutput(projectFileWriter); + var options = new DecompilationOptions() { + FullDecompilation = true, + CancellationToken = ct, + SaveAsProjectDirectory = targetDirectory }; + + assemblyNode.Decompile(assemblyNode.Language, projectFileOutput, options); + } + } catch (Exception e) { + statusOutput.Add($"Failed to decompile the assembly '{loadedAssembly.FileName}':{Environment.NewLine}{e}"); + return; + } + } + } +} diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 51aa65bdf..60f34e6b2 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -279,31 +279,44 @@ namespace ICSharpCode.ILSpy.TreeNodes public override bool Save(DecompilerTextView textView) { Language language = this.Language; - if (string.IsNullOrEmpty(language.ProjectFileExtension)) + if (string.IsNullOrEmpty(language.ProjectFileExtension)) { return false; + } + SaveFileDialog dlg = new SaveFileDialog(); dlg.FileName = DecompilerTextView.CleanUpName(LoadedAssembly.ShortName) + language.ProjectFileExtension; - dlg.Filter = language.Name + " project|*" + language.ProjectFileExtension + "|" + language.Name + " single file|*" + language.FileExtension + "|All files|*.*"; - if (dlg.ShowDialog() == true) { - DecompilationOptions options = new DecompilationOptions(); - options.FullDecompilation = true; - if (dlg.FilterIndex == 1) { - options.SaveAsProjectDirectory = Path.GetDirectoryName(dlg.FileName); - foreach (string entry in Directory.GetFileSystemEntries(options.SaveAsProjectDirectory)) { - if (!string.Equals(entry, dlg.FileName, StringComparison.OrdinalIgnoreCase)) { - var result = MessageBox.Show( - "The directory is not empty. File will be overwritten." + Environment.NewLine + - "Are you sure you want to continue?", - "Project Directory not empty", - MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); - if (result == MessageBoxResult.No) - return true; // don't save, but mark the Save operation as handled - break; - } - } + dlg.Filter = language.Name + " project|*" + language.ProjectFileExtension; + if (dlg.ShowDialog() != true) { + return true; + } + + var targetDirectory = Path.GetDirectoryName(dlg.FileName); + var existingFiles = Directory.GetFileSystemEntries(targetDirectory); + + if (existingFiles.Any(e => !string.Equals(e, dlg.FileName, StringComparison.OrdinalIgnoreCase))) { + var result = MessageBox.Show( + "The directory is not empty. File will be overwritten." + Environment.NewLine + + "Are you sure you want to continue?", + "Project Directory not empty", + MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (result == MessageBoxResult.No) { + return true; // don't save, but mark the Save operation as handled } - textView.SaveToDisk(language, new[] { this }, options, dlg.FileName); } + + Save(textView, dlg.FileName); + return true; + } + + public override bool Save(DecompilerTextView textView, string fileName) + { + var targetDirectory = Path.GetDirectoryName(fileName); + DecompilationOptions options = new DecompilationOptions { + FullDecompilation = true, + SaveAsProjectDirectory = targetDirectory + }; + + textView.SaveToDisk(Language, new[] { this }, options, fileName); return true; } diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs index fa1e663e4..11fe3b278 100644 --- a/ILSpy/TreeNodes/ILSpyTreeNode.cs +++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs @@ -79,6 +79,15 @@ namespace ICSharpCode.ILSpy.TreeNodes return false; } + /// + /// Saves the content this node represents to the specified . + /// The file will be silently overwritten. + /// + /// A reference to a instance. + /// The target full path to save the content to. + /// true on success; otherwise, false. + public virtual bool Save(TextView.DecompilerTextView textView, string fileName) => Save(textView); + protected override void OnChildrenChanged(NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { From 5e6a261b8607c12af71d48e03754cbba90daaba0 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 21 Jun 2019 23:17:42 +0200 Subject: [PATCH 05/10] Implement Visual Studio solution generation for icsharpcode#972 --- .../CSharp/SolutionCreator.cs | 275 ++++++++++++++++++ .../CSharp/WholeProjectDecompiler.cs | 9 +- .../ICSharpCode.Decompiler.csproj | 1 + ILSpy/Languages/CSharpLanguage.cs | 6 +- ILSpy/Languages/ILLanguage.cs | 4 +- ILSpy/Languages/Language.cs | 6 +- ILSpy/SolutionWriter.cs | 54 +++- ILSpy/TreeNodes/AssemblyListTreeNode.cs | 4 +- ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs | 4 +- ILSpy/TreeNodes/AssemblyTreeNode.cs | 12 +- ILSpy/TreeNodes/BaseTypesEntryNode.cs | 3 +- ILSpy/TreeNodes/BaseTypesTreeNode.cs | 4 +- ILSpy/TreeNodes/DerivedTypesEntryNode.cs | 3 +- ILSpy/TreeNodes/DerivedTypesTreeNode.cs | 3 +- ILSpy/TreeNodes/EventTreeNode.cs | 3 +- ILSpy/TreeNodes/FieldTreeNode.cs | 3 +- ILSpy/TreeNodes/ILSpyTreeNode.cs | 2 +- ILSpy/TreeNodes/MethodTreeNode.cs | 3 +- ILSpy/TreeNodes/ModuleReferenceTreeNode.cs | 3 +- ILSpy/TreeNodes/NamespaceTreeNode.cs | 3 +- ILSpy/TreeNodes/PropertyTreeNode.cs | 3 +- ILSpy/TreeNodes/ReferenceFolderTreeNode.cs | 4 +- ILSpy/TreeNodes/ResourceListTreeNode.cs | 4 +- .../ImageListResourceEntryNode.cs | 3 +- .../ResourceNodes/ResourceEntryNode.cs | 3 +- .../ResourceNodes/ResourceTreeNode.cs | 4 +- .../ResourceNodes/ResourcesFileTreeNode.cs | 4 +- ILSpy/TreeNodes/ThreadingSupport.cs | 6 +- ILSpy/TreeNodes/TypeTreeNode.cs | 3 +- 29 files changed, 394 insertions(+), 45 deletions(-) create mode 100644 ICSharpCode.Decompiler/CSharp/SolutionCreator.cs diff --git a/ICSharpCode.Decompiler/CSharp/SolutionCreator.cs b/ICSharpCode.Decompiler/CSharp/SolutionCreator.cs new file mode 100644 index 000000000..7c3f26e0e --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/SolutionCreator.cs @@ -0,0 +1,275 @@ +// Copyright (c) 2016 Daniel Grunwald +// +// 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.IO; +using System.Linq; +using System.Xml.Linq; + +namespace ICSharpCode.Decompiler.CSharp +{ + /// + /// A container class that holds information about a Visual Studio project. + /// + public sealed class ProjectItem : ProjectId + { + /// + /// Initializes a new instance of the class. + /// + /// The full path of the project file. + /// The project platform. + /// The project GUID. + /// + /// Thrown when + /// or is null or empty. + public ProjectItem(string projectFile, string projectPlatform, Guid projectGuid) + : base(projectPlatform, projectGuid) + { + ProjectName = Path.GetFileNameWithoutExtension(projectFile); + FilePath = projectFile; + } + + /// + /// Gets the name of the project. + /// + public string ProjectName { get; } + + /// + /// Gets the full path to the project file. + /// + public string FilePath { get; } + } + + /// + /// A container class that holds platform and GUID information about a Visual Studio project. + /// + public class ProjectId + { + /// + /// Initializes a new instance of the class. + /// + /// The project platform. + /// The project GUID. + /// + /// Thrown when + /// or is null or empty. + public ProjectId(string projectPlatform, Guid projectGuid) + { + if (string.IsNullOrWhiteSpace(projectPlatform)) { + throw new ArgumentException("The platform cannot be null or empty.", nameof(projectPlatform)); + } + + Guid = projectGuid; + PlatformName = projectPlatform; + } + + /// + /// Gets the GUID of this project. + /// + public Guid Guid { get; } + + /// + /// Gets the platform name of this project. Only single platform per project is supported. + /// + public string PlatformName { get; } + } + + /// + /// A helper class that can write a Visual Studio Solution file for the provided projects. + /// + public static class SolutionCreator + { + private static readonly XNamespace ProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); + + /// + /// Writes a solution file to the specified . + /// + /// The full path of the file to write. + /// The projects contained in this solution. + /// + /// Thrown when is null or empty. + /// Thrown when is null. + /// Thrown when contains no items. + public static void WriteSolutionFile(string targetFile, IEnumerable projects) + { + if (string.IsNullOrWhiteSpace(targetFile)) { + throw new ArgumentException("The target file cannot be null or empty.", nameof(targetFile)); + } + + if (projects == null) { + throw new ArgumentNullException(nameof(projects)); + } + + if (!projects.Any()) { + throw new InvalidOperationException("At least one project is expected."); + } + + using (var writer = new StreamWriter(targetFile)) { + WriteSolutionFile(writer, projects, Path.GetDirectoryName(targetFile)); + } + + FixProjectReferences(projects); + } + + private static void WriteSolutionFile(TextWriter writer, IEnumerable projects, string solutionPath) + { + WriteHeader(writer); + WriteProjects(writer, projects, solutionPath); + + writer.WriteLine("Global"); + + var platforms = WriteSolutionConfigurations(writer, projects); + WriteProjectConfigurations(writer, projects, platforms); + + writer.WriteLine("\tGlobalSection(SolutionProperties) = preSolution"); + writer.WriteLine("\t\tHideSolutionNode = FALSE"); + writer.WriteLine("\tEndGlobalSection"); + + writer.WriteLine("EndGlobal"); + } + + private static void WriteHeader(TextWriter writer) + { + writer.WriteLine("Microsoft Visual Studio Solution File, Format Version 12.00"); + writer.WriteLine("# Visual Studio 14"); + writer.WriteLine("VisualStudioVersion = 14.0.24720.0"); + writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1"); + } + + private static void WriteProjects(TextWriter writer, IEnumerable projects, string solutionPath) + { + var solutionGuid = Guid.NewGuid().ToString("B").ToUpperInvariant(); + + foreach (var project in projects) { + var projectRelativePath = GetRelativePath(solutionPath, project.FilePath); + var projectGuid = project.Guid.ToString("B").ToUpperInvariant(); + + writer.WriteLine($"Project(\"{solutionGuid}\") = \"{project.ProjectName}\", \"{projectRelativePath}\", \"{projectGuid}\""); + writer.WriteLine("EndProject"); + } + } + + private static IEnumerable WriteSolutionConfigurations(TextWriter writer, IEnumerable projects) + { + var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList(); + + platforms.Sort(); + + writer.WriteLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution"); + foreach (var platform in platforms) { + writer.WriteLine($"\t\tDebug|{platform} = Debug|{platform}"); + } + + foreach (var platform in platforms) { + writer.WriteLine($"\t\tRelease|{platform} = Release|{platform}"); + } + + writer.WriteLine("\tEndGlobalSection"); + + return platforms; + } + + private static void WriteProjectConfigurations( + TextWriter writer, + IEnumerable projects, + IEnumerable solutionPlatforms) + { + writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); + + foreach (var project in projects) { + var projectGuid = project.Guid.ToString("B").ToUpperInvariant(); + + foreach (var platform in solutionPlatforms) { + writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.ActiveCfg = Debug|{project.PlatformName}"); + writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.Build.0 = Debug|{project.PlatformName}"); + } + + foreach (var platform in solutionPlatforms) { + writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.ActiveCfg = Release|{project.PlatformName}"); + writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.Build.0 = Release|{project.PlatformName}"); + } + } + + writer.WriteLine("\tEndGlobalSection"); + } + + private static void FixProjectReferences(IEnumerable projects) + { + var projectsMap = projects.ToDictionary(p => p.ProjectName, p => p); + + foreach (var project in projects) { + var projectDirectory = Path.GetDirectoryName(project.FilePath); + XDocument projectDoc = XDocument.Load(project.FilePath); + + var referencesItemGroups = projectDoc.Root + .Elements(ProjectFileNamespace + "ItemGroup") + .Where(e => e.Elements(ProjectFileNamespace + "Reference").Any()); + + foreach (var itemGroup in referencesItemGroups) { + FixProjectReferences(projectDirectory, itemGroup, projectsMap); + } + + projectDoc.Save(project.FilePath); + } + } + + private static void FixProjectReferences(string projectDirectory, XElement itemGroup, IDictionary projects) + { + foreach (var item in itemGroup.Elements(ProjectFileNamespace + "Reference").ToList()) { + var assemblyName = item.Attribute("Include")?.Value; + if (assemblyName != null && projects.TryGetValue(assemblyName, out var referencedProject)) { + item.Remove(); + + var projectReference = new XElement(ProjectFileNamespace + "ProjectReference", + new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToUpperInvariant()), + new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName)); + projectReference.SetAttributeValue("Include", GetRelativePath(projectDirectory, referencedProject.FilePath)); + + itemGroup.Add(projectReference); + } + } + } + + private static string GetRelativePath(string fromPath, string toPath) + { + Uri fromUri = new Uri(AppendDirectorySeparatorChar(fromPath)); + Uri toUri = new Uri(AppendDirectorySeparatorChar(toPath)); + + if (fromUri.Scheme != toUri.Scheme) { + return toPath; + } + + Uri relativeUri = fromUri.MakeRelativeUri(toUri); + string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); + + if (string.Equals(toUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase)) { + relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + + return relativePath; + } + + private static string AppendDirectorySeparatorChar(string path) + { + return Path.HasExtension(path) || path.EndsWith(Path.DirectorySeparatorChar.ToString()) + ? path + : path + Path.DirectorySeparatorChar; + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index d467ba883..6ec5d6802 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -101,7 +101,7 @@ namespace ICSharpCode.Decompiler.CSharp } } - public void DecompileProject(PEFile moduleDefinition, string targetDirectory, TextWriter projectFileWriter, CancellationToken cancellationToken = default(CancellationToken)) + public ProjectId DecompileProject(PEFile moduleDefinition, string targetDirectory, TextWriter projectFileWriter, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(targetDirectory)) { throw new InvalidOperationException("Must set TargetDirectory"); @@ -110,7 +110,7 @@ namespace ICSharpCode.Decompiler.CSharp directories.Clear(); var files = WriteCodeFilesInProject(moduleDefinition, cancellationToken).ToList(); files.AddRange(WriteResourceFilesInProject(moduleDefinition)); - WriteProjectFile(projectFileWriter, files, moduleDefinition); + return WriteProjectFile(projectFileWriter, files, moduleDefinition); } enum LanguageTargets @@ -120,11 +120,12 @@ namespace ICSharpCode.Decompiler.CSharp } #region WriteProjectFile - void WriteProjectFile(TextWriter writer, IEnumerable> files, Metadata.PEFile module) + ProjectId WriteProjectFile(TextWriter writer, IEnumerable> files, Metadata.PEFile module) { const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; string platformName = GetPlatformName(module); Guid guid = this.ProjectGuid ?? Guid.NewGuid(); + using (XmlTextWriter w = new XmlTextWriter(writer)) { w.Formatting = Formatting.Indented; w.WriteStartDocument(); @@ -281,6 +282,8 @@ namespace ICSharpCode.Decompiler.CSharp w.WriteEndDocument(); } + + return new ProjectId(platformName, guid); } protected virtual bool IsGacAssembly(Metadata.IAssemblyReference r, Metadata.PEFile asm) diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 8ab2fbd69..6efca3120 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -61,6 +61,7 @@ + diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 2441e0eb2..57cab0ee8 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -343,12 +343,12 @@ namespace ICSharpCode.ILSpy } } - public override void DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public override object DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { var module = assembly.GetPEFileOrNull(); if (options.FullDecompilation && options.SaveAsProjectDirectory != null) { var decompiler = new ILSpyWholeProjectDecompiler(assembly, options); - decompiler.DecompileProject(module, options.SaveAsProjectDirectory, new TextOutputWriter(output), options.CancellationToken); + return decompiler.DecompileProject(module, options.SaveAsProjectDirectory, new TextOutputWriter(output), options.CancellationToken); } else { AddReferenceAssemblyWarningMessage(module, output); AddReferenceWarningMessage(module, output); @@ -415,6 +415,8 @@ namespace ICSharpCode.ILSpy } WriteCode(output, options.DecompilerSettings, st, decompiler.TypeSystem); } + + return true; } } diff --git a/ILSpy/Languages/ILLanguage.cs b/ILSpy/Languages/ILLanguage.cs index e2ab9d1f3..da26caeb2 100644 --- a/ILSpy/Languages/ILLanguage.cs +++ b/ILSpy/Languages/ILLanguage.cs @@ -150,7 +150,7 @@ namespace ICSharpCode.ILSpy dis.DisassembleNamespace(nameSpace, module, types.Select(t => (TypeDefinitionHandle)t.MetadataToken)); } - public override void DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public override object DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { output.WriteLine("// " + assembly.FileName); output.WriteLine(); @@ -174,6 +174,8 @@ namespace ICSharpCode.ILSpy dis.WriteModuleContents(module); } } + + return true; } } } diff --git a/ILSpy/Languages/Language.cs b/ILSpy/Languages/Language.cs index a1aad8d64..76f435244 100644 --- a/ILSpy/Languages/Language.cs +++ b/ILSpy/Languages/Language.cs @@ -131,11 +131,11 @@ namespace ICSharpCode.ILSpy WriteCommentLine(output, nameSpace); } - public virtual void DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public virtual object DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { WriteCommentLine(output, assembly.FileName); var asm = assembly.GetPEFileOrNull(); - if (asm == null) return; + if (asm == null) return false; var metadata = asm.Metadata; if (metadata.IsAssembly) { var name = metadata.GetAssemblyDefinition(); @@ -147,6 +147,8 @@ namespace ICSharpCode.ILSpy } else { WriteCommentLine(output, metadata.GetString(metadata.GetModuleDefinition().Name)); } + + return true; } public virtual void WriteCommentLine(ITextOutput output, string comment) diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index a4ebf5779..45fd3ce67 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -27,6 +27,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.CSharp; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; using Microsoft.Win32; @@ -130,6 +131,8 @@ namespace ICSharpCode.ILSpy { var solutionDirectory = Path.GetDirectoryName(solutionFilePath); var statusOutput = new ConcurrentBag(); + var projects = new ConcurrentBag(); + var result = new AvalonEditTextOutput(); var duplicates = new HashSet(); @@ -140,20 +143,45 @@ namespace ICSharpCode.ILSpy Stopwatch stopwatch = Stopwatch.StartNew(); - await Task.Run(() => Parallel.ForEach(assemblyNodes, n => WriteProject(n, solutionDirectory, statusOutput, ct))) - .ConfigureAwait(false); + try { + await Task.Run(() => Parallel.ForEach(assemblyNodes, n => WriteProject(n, solutionDirectory, statusOutput, projects, ct))) + .ConfigureAwait(false); + + await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects)) + .ConfigureAwait(false); + } catch (AggregateException ae) { + if (ae.Flatten().InnerExceptions.All(e => e is OperationCanceledException)) { + result.WriteLine(); + result.WriteLine("Generation was cancelled."); + return result; + } + result.WriteLine(); + result.WriteLine("Failed to generate the Visual Studio Solution. Errors:"); + ae.Handle(e => { + result.WriteLine(e.Message); + return true; + }); + + return result; + } foreach (var item in statusOutput) { result.WriteLine(item); } if (statusOutput.Count == 0) { - result.WriteLine("Successfully decompiled the following assemblies to a Visual Studio Solution:"); + result.WriteLine("Successfully decompiled the following assemblies into Visual Studio projects:"); foreach (var item in assemblyNodes.Select(n => n.Text.ToString())) { result.WriteLine(item); } + result.WriteLine(); + + if (assemblyNodes.Count() == projects.Count) { + result.WriteLine("Created the Visual Studio Solution file."); + } + result.WriteLine(); result.WriteLine("Elapsed time: " + stopwatch.Elapsed.TotalSeconds.ToString("F1") + " seconds."); result.WriteLine(); @@ -163,7 +191,12 @@ namespace ICSharpCode.ILSpy return result; } - private static void WriteProject(AssemblyTreeNode assemblyNode, string targetDirectory, ConcurrentBag statusOutput, CancellationToken ct) + private static void WriteProject( + AssemblyTreeNode assemblyNode, + string targetDirectory, + ConcurrentBag statusOutput, + ConcurrentBag targetContainer, + CancellationToken ct) { var loadedAssembly = assemblyNode.LoadedAssembly; @@ -186,12 +219,17 @@ namespace ICSharpCode.ILSpy FullDecompilation = true, CancellationToken = ct, SaveAsProjectDirectory = targetDirectory }; - - assemblyNode.Decompile(assemblyNode.Language, projectFileOutput, options); + + if (assemblyNode.Decompile(assemblyNode.Language, projectFileOutput, options) is ProjectId projectInfo) { + targetContainer.Add(new ProjectItem(projectFileName, projectInfo.PlatformName, projectInfo.Guid)); + } } - } catch (Exception e) { + } + catch (OperationCanceledException) { + throw; + } + catch (Exception e) { statusOutput.Add($"Failed to decompile the assembly '{loadedAssembly.FileName}':{Environment.NewLine}{e}"); - return; } } } diff --git a/ILSpy/TreeNodes/AssemblyListTreeNode.cs b/ILSpy/TreeNodes/AssemblyListTreeNode.cs index e53a3a883..3b8e5a519 100644 --- a/ILSpy/TreeNodes/AssemblyListTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyListTreeNode.cs @@ -141,7 +141,7 @@ namespace ICSharpCode.ILSpy.TreeNodes public Action Select = delegate { }; - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, "List: " + assemblyList.ListName); output.WriteLine(); @@ -150,6 +150,8 @@ namespace ICSharpCode.ILSpy.TreeNodes output.WriteLine(); asm.Decompile(language, output, options); } + + return true; } #region Find*Node diff --git a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs index 8aa8f8c0d..192820246 100644 --- a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs @@ -79,7 +79,7 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { var loaded = parentAssembly.LoadedAssembly.LoadedAssemblyReferencesInfo.TryGetInfo(r.FullName, out var info); if (r.IsWindowsRuntime) { @@ -98,6 +98,8 @@ namespace ICSharpCode.ILSpy.TreeNodes output.Unindent(); output.WriteLine(); } + + return true; } } } diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 60f34e6b2..39669c15f 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -240,7 +240,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Recurse; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { void HandleException(Exception ex, string message) { @@ -259,21 +259,21 @@ namespace ICSharpCode.ILSpy.TreeNodes switch (ex.InnerException) { case BadImageFormatException badImage: HandleException(badImage, "This file does not contain a managed assembly."); - return; + return null; case FileNotFoundException fileNotFound: HandleException(fileNotFound, "The file was not found."); - return; + return null; case DirectoryNotFoundException dirNotFound: HandleException(dirNotFound, "The directory was not found."); - return; + return null; case PEFileNotSupportedException notSupported: HandleException(notSupported, notSupported.Message); - return; + return null; default: throw; } } - language.DecompileAssembly(LoadedAssembly, output, options); + return language.DecompileAssembly(LoadedAssembly, output, options); } public override bool Save(DecompilerTextView textView) diff --git a/ILSpy/TreeNodes/BaseTypesEntryNode.cs b/ILSpy/TreeNodes/BaseTypesEntryNode.cs index 3c96c40dd..41edd0f8c 100644 --- a/ILSpy/TreeNodes/BaseTypesEntryNode.cs +++ b/ILSpy/TreeNodes/BaseTypesEntryNode.cs @@ -97,9 +97,10 @@ namespace ICSharpCode.ILSpy.TreeNodes return false; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, language.TypeToString(type, includeNamespace: true)); + return true; } IEntity IMemberTreeNode.Member { diff --git a/ILSpy/TreeNodes/BaseTypesTreeNode.cs b/ILSpy/TreeNodes/BaseTypesTreeNode.cs index 8cbdb2304..58565a938 100644 --- a/ILSpy/TreeNodes/BaseTypesTreeNode.cs +++ b/ILSpy/TreeNodes/BaseTypesTreeNode.cs @@ -69,12 +69,14 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(EnsureLazyChildren)); foreach (ILSpyTreeNode child in this.Children) { child.Decompile(language, output, options); } + + return true; } } } \ No newline at end of file diff --git a/ILSpy/TreeNodes/DerivedTypesEntryNode.cs b/ILSpy/TreeNodes/DerivedTypesEntryNode.cs index 91da3e033..f3d9fe29f 100644 --- a/ILSpy/TreeNodes/DerivedTypesEntryNode.cs +++ b/ILSpy/TreeNodes/DerivedTypesEntryNode.cs @@ -90,9 +90,10 @@ namespace ICSharpCode.ILSpy.TreeNodes e.Handled = BaseTypesEntryNode.ActivateItem(this, type); } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, language.TypeToString(type, includeNamespace: true)); + return true; } IEntity IMemberTreeNode.Member => type; diff --git a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs index 3b6e50a30..8518e08e3 100644 --- a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs +++ b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs @@ -92,9 +92,10 @@ namespace ICSharpCode.ILSpy.TreeNodes return typeRef.GetFullTypeName(referenceMetadata) == typeDef.GetFullTypeName(definitionMetadata); } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { threading.Decompile(language, output, options, EnsureLazyChildren); + return true; } } } \ No newline at end of file diff --git a/ILSpy/TreeNodes/EventTreeNode.cs b/ILSpy/TreeNodes/EventTreeNode.cs index 1aadff896..c1797f794 100644 --- a/ILSpy/TreeNodes/EventTreeNode.cs +++ b/ILSpy/TreeNodes/EventTreeNode.cs @@ -67,9 +67,10 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileEvent(EventDefinition, output, options); + return true; } public override bool IsPublicAPI { diff --git a/ILSpy/TreeNodes/FieldTreeNode.cs b/ILSpy/TreeNodes/FieldTreeNode.cs index 5d4d2f7b0..5d3323beb 100644 --- a/ILSpy/TreeNodes/FieldTreeNode.cs +++ b/ILSpy/TreeNodes/FieldTreeNode.cs @@ -68,9 +68,10 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileField(FieldDefinition, output, options); + return true; } public override bool IsPublicAPI { diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs index 11fe3b278..515188374 100644 --- a/ILSpy/TreeNodes/ILSpyTreeNode.cs +++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs @@ -57,7 +57,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public abstract void Decompile(Language language, ITextOutput output, DecompilationOptions options); + public abstract object Decompile(Language language, ITextOutput output, DecompilationOptions options); /// /// Used to implement special view logic for some items. diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs index 9e4d95e45..89c12ec10 100644 --- a/ILSpy/TreeNodes/MethodTreeNode.cs +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -83,9 +83,10 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileMethod(MethodDefinition, output, options); + return true; } public override FilterResult Filter(FilterSettings settings) diff --git a/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs b/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs index 4d51f46af..a363c46db 100644 --- a/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs @@ -72,10 +72,11 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, moduleName); language.WriteCommentLine(output, containsMetadata ? "contains metadata" : "contains no metadata"); + return true; } } } diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index d054603dc..3b9ff4845 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -56,9 +56,10 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Recurse; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileNamespace(name, this.Children.OfType().Select(t => t.TypeDefinition), output, options); + return true; } } } diff --git a/ILSpy/TreeNodes/PropertyTreeNode.cs b/ILSpy/TreeNodes/PropertyTreeNode.cs index 14769ea66..3267a1e1a 100644 --- a/ILSpy/TreeNodes/PropertyTreeNode.cs +++ b/ILSpy/TreeNodes/PropertyTreeNode.cs @@ -74,9 +74,10 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileProperty(PropertyDefinition, output, options); + return true; } public override bool IsPublicAPI { diff --git a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs index 331f8da08..9121e213f 100644 --- a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs +++ b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs @@ -62,7 +62,7 @@ namespace ICSharpCode.ILSpy.TreeNodes this.Children.Add(new ModuleReferenceTreeNode(parentAssembly, r, metadata)); } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, $"Detected Target-Framework-Id: {parentAssembly.LoadedAssembly.GetTargetFrameworkIdAsync().Result}"); App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(EnsureLazyChildren)); @@ -86,7 +86,7 @@ namespace ICSharpCode.ILSpy.TreeNodes output.Unindent(); output.WriteLine(); } - + return true; } } } diff --git a/ILSpy/TreeNodes/ResourceListTreeNode.cs b/ILSpy/TreeNodes/ResourceListTreeNode.cs index c67c9d4b1..79cc1df50 100644 --- a/ILSpy/TreeNodes/ResourceListTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceListTreeNode.cs @@ -64,13 +64,15 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Recurse; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(EnsureLazyChildren)); foreach (ILSpyTreeNode child in this.Children) { child.Decompile(language, output, options); output.WriteLine(); } + + return true; } } } diff --git a/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs b/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs index fb8b2199f..3805db739 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs @@ -75,9 +75,10 @@ namespace ICSharpCode.ILSpy.TreeNodes } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { EnsureLazyChildren(); + return true; } } } diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs index ec74ad193..cf5a3d5ac 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs @@ -73,9 +73,10 @@ namespace ICSharpCode.ILSpy.TreeNodes return result; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, string.Format("{0} = {1}", key, data)); + return true; } public override bool Save(DecompilerTextView textView) diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs index 3433f70d7..c9adaa49c 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs @@ -67,7 +67,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, string.Format("{0} ({1}, {2})", r.Name, r.ResourceType, r.Attributes)); @@ -76,6 +76,8 @@ namespace ICSharpCode.ILSpy.TreeNodes smartOutput.AddButton(Images.Save, Resources.Save, delegate { Save(MainWindow.Instance.TextView); }); output.WriteLine(); } + + return true; } public override bool View(DecompilerTextView textView) diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs index c1d908481..a44edeae4 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs @@ -141,7 +141,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return true; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { EnsureLazyChildren(); base.Decompile(language, output, options); @@ -168,6 +168,8 @@ namespace ICSharpCode.ILSpy.TreeNodes } output.WriteLine(); } + + return true; } internal class SerializedObjectRepresentation diff --git a/ILSpy/TreeNodes/ThreadingSupport.cs b/ILSpy/TreeNodes/ThreadingSupport.cs index e95787efc..8df078cad 100644 --- a/ILSpy/TreeNodes/ThreadingSupport.cs +++ b/ILSpy/TreeNodes/ThreadingSupport.cs @@ -128,8 +128,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Match; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { + return false; } } @@ -151,8 +152,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Match; } - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { + return false; } } diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs index 3ef91c411..4464a3521 100644 --- a/ILSpy/TreeNodes/TypeTreeNode.cs +++ b/ILSpy/TreeNodes/TypeTreeNode.cs @@ -103,9 +103,10 @@ namespace ICSharpCode.ILSpy.TreeNodes public override bool CanExpandRecursively => true; - public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileType(TypeDefinition, output, options); + return true; } public override object Icon => GetIcon(TypeDefinition); From a63e94e5b4bc22d0ea3687b6bfc13c983c95c725 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 24 Jul 2019 00:56:54 +0200 Subject: [PATCH 06/10] Refactor Solution decompilation to use Language instead of AssemblyTreeNode. --- .../CSharp/WholeProjectDecompiler.cs | 1 + .../ICSharpCode.Decompiler.csproj | 4 +- ICSharpCode.Decompiler/Solution/ProjectId.cs | 56 +++++++ .../Solution/ProjectItem.cs | 55 +++++++ .../{CSharp => Solution}/SolutionCreator.cs | 70 +-------- ILSpy/Languages/CSharpLanguage.cs | 8 +- ILSpy/Languages/ILLanguage.cs | 7 +- ILSpy/Languages/Language.cs | 8 +- ILSpy/MainWindow.xaml.cs | 85 ++++++++--- ILSpy/SolutionWriter.cs | 140 ++++++------------ ILSpy/TreeNodes/AssemblyListTreeNode.cs | 4 +- ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs | 4 +- ILSpy/TreeNodes/AssemblyTreeNode.cs | 12 +- ILSpy/TreeNodes/BaseTypesEntryNode.cs | 3 +- ILSpy/TreeNodes/BaseTypesTreeNode.cs | 4 +- ILSpy/TreeNodes/DerivedTypesEntryNode.cs | 3 +- ILSpy/TreeNodes/DerivedTypesTreeNode.cs | 3 +- ILSpy/TreeNodes/EventTreeNode.cs | 3 +- ILSpy/TreeNodes/FieldTreeNode.cs | 3 +- ILSpy/TreeNodes/ILSpyTreeNode.cs | 2 +- ILSpy/TreeNodes/MethodTreeNode.cs | 3 +- ILSpy/TreeNodes/ModuleReferenceTreeNode.cs | 3 +- ILSpy/TreeNodes/NamespaceTreeNode.cs | 3 +- ILSpy/TreeNodes/PropertyTreeNode.cs | 3 +- ILSpy/TreeNodes/ReferenceFolderTreeNode.cs | 3 +- ILSpy/TreeNodes/ResourceListTreeNode.cs | 4 +- .../ImageListResourceEntryNode.cs | 3 +- .../ResourceNodes/ResourceEntryNode.cs | 3 +- .../ResourceNodes/ResourceTreeNode.cs | 4 +- .../ResourceNodes/ResourcesFileTreeNode.cs | 4 +- ILSpy/TreeNodes/ThreadingSupport.cs | 6 +- ILSpy/TreeNodes/TypeTreeNode.cs | 3 +- 32 files changed, 268 insertions(+), 249 deletions(-) create mode 100644 ICSharpCode.Decompiler/Solution/ProjectId.cs create mode 100644 ICSharpCode.Decompiler/Solution/ProjectItem.cs rename ICSharpCode.Decompiler/{CSharp => Solution}/SolutionCreator.cs (78%) diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index 6ec5d6802..377a617d5 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -35,6 +35,7 @@ using System.Reflection.Metadata; using static ICSharpCode.Decompiler.Metadata.DotNetCorePathFinderExtensions; using static ICSharpCode.Decompiler.Metadata.MetadataExtensions; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Solution; namespace ICSharpCode.Decompiler.CSharp { diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 6efca3120..afb73dcf4 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -61,7 +61,9 @@ - + + + diff --git a/ICSharpCode.Decompiler/Solution/ProjectId.cs b/ICSharpCode.Decompiler/Solution/ProjectId.cs new file mode 100644 index 000000000..65a4e8d9e --- /dev/null +++ b/ICSharpCode.Decompiler/Solution/ProjectId.cs @@ -0,0 +1,56 @@ +// 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; + +namespace ICSharpCode.Decompiler.Solution +{ + /// + /// A container class that holds platform and GUID information about a Visual Studio project. + /// + public class ProjectId + { + /// + /// Initializes a new instance of the class. + /// + /// The project platform. + /// The project GUID. + /// + /// Thrown when + /// or is null or empty. + public ProjectId(string projectPlatform, Guid projectGuid) + { + if (string.IsNullOrWhiteSpace(projectPlatform)) { + throw new ArgumentException("The platform cannot be null or empty.", nameof(projectPlatform)); + } + + Guid = projectGuid; + PlatformName = projectPlatform; + } + + /// + /// Gets the GUID of this project. + /// + public Guid Guid { get; } + + /// + /// Gets the platform name of this project. Only single platform per project is supported. + /// + public string PlatformName { get; } + } +} diff --git a/ICSharpCode.Decompiler/Solution/ProjectItem.cs b/ICSharpCode.Decompiler/Solution/ProjectItem.cs new file mode 100644 index 000000000..bf1222368 --- /dev/null +++ b/ICSharpCode.Decompiler/Solution/ProjectItem.cs @@ -0,0 +1,55 @@ +// 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.IO; + +namespace ICSharpCode.Decompiler.Solution +{ + /// + /// A container class that holds information about a Visual Studio project. + /// + public sealed class ProjectItem : ProjectId + { + /// + /// Initializes a new instance of the class. + /// + /// The full path of the project file. + /// The project platform. + /// The project GUID. + /// + /// Thrown when + /// or is null or empty. + public ProjectItem(string projectFile, string projectPlatform, Guid projectGuid) + : base(projectPlatform, projectGuid) + { + ProjectName = Path.GetFileNameWithoutExtension(projectFile); + FilePath = projectFile; + } + + /// + /// Gets the name of the project. + /// + public string ProjectName { get; } + + /// + /// Gets the full path to the project file. + /// + public string FilePath { get; } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs similarity index 78% rename from ICSharpCode.Decompiler/CSharp/SolutionCreator.cs rename to ICSharpCode.Decompiler/Solution/SolutionCreator.cs index 7c3f26e0e..fc63beca5 100644 --- a/ICSharpCode.Decompiler/CSharp/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Daniel Grunwald +// 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 @@ -22,74 +22,8 @@ using System.IO; using System.Linq; using System.Xml.Linq; -namespace ICSharpCode.Decompiler.CSharp +namespace ICSharpCode.Decompiler.Solution { - /// - /// A container class that holds information about a Visual Studio project. - /// - public sealed class ProjectItem : ProjectId - { - /// - /// Initializes a new instance of the class. - /// - /// The full path of the project file. - /// The project platform. - /// The project GUID. - /// - /// Thrown when - /// or is null or empty. - public ProjectItem(string projectFile, string projectPlatform, Guid projectGuid) - : base(projectPlatform, projectGuid) - { - ProjectName = Path.GetFileNameWithoutExtension(projectFile); - FilePath = projectFile; - } - - /// - /// Gets the name of the project. - /// - public string ProjectName { get; } - - /// - /// Gets the full path to the project file. - /// - public string FilePath { get; } - } - - /// - /// A container class that holds platform and GUID information about a Visual Studio project. - /// - public class ProjectId - { - /// - /// Initializes a new instance of the class. - /// - /// The project platform. - /// The project GUID. - /// - /// Thrown when - /// or is null or empty. - public ProjectId(string projectPlatform, Guid projectGuid) - { - if (string.IsNullOrWhiteSpace(projectPlatform)) { - throw new ArgumentException("The platform cannot be null or empty.", nameof(projectPlatform)); - } - - Guid = projectGuid; - PlatformName = projectPlatform; - } - - /// - /// Gets the GUID of this project. - /// - public Guid Guid { get; } - - /// - /// Gets the platform name of this project. Only single platform per project is supported. - /// - public string PlatformName { get; } - } - /// /// A helper class that can write a Visual Studio Solution file for the provided projects. /// diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 57cab0ee8..35f614764 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -34,6 +34,7 @@ using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Output; +using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; using ICSharpCode.ILSpy.TreeNodes; @@ -343,7 +344,7 @@ namespace ICSharpCode.ILSpy } } - public override object DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public override ProjectId DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { var module = assembly.GetPEFileOrNull(); if (options.FullDecompilation && options.SaveAsProjectDirectory != null) { @@ -389,7 +390,7 @@ namespace ICSharpCode.ILSpy } if (metadata.IsAssembly) { var asm = metadata.GetAssemblyDefinition(); - if (asm.HashAlgorithm != System.Reflection.AssemblyHashAlgorithm.None) + if (asm.HashAlgorithm != AssemblyHashAlgorithm.None) output.WriteLine("// Hash algorithm: " + asm.HashAlgorithm.ToString().ToUpper()); if (!asm.PublicKey.IsNil) { output.Write("// Public key: "); @@ -415,8 +416,7 @@ namespace ICSharpCode.ILSpy } WriteCode(output, options.DecompilerSettings, st, decompiler.TypeSystem); } - - return true; + return null; } } diff --git a/ILSpy/Languages/ILLanguage.cs b/ILSpy/Languages/ILLanguage.cs index da26caeb2..3dd1ec1ab 100644 --- a/ILSpy/Languages/ILLanguage.cs +++ b/ILSpy/Languages/ILLanguage.cs @@ -27,6 +27,8 @@ using System.Reflection.Metadata.Ecma335; using System.Linq; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; +using ICSharpCode.Decompiler.Solution; namespace ICSharpCode.ILSpy { @@ -150,7 +152,7 @@ namespace ICSharpCode.ILSpy dis.DisassembleNamespace(nameSpace, module, types.Select(t => (TypeDefinitionHandle)t.MetadataToken)); } - public override object DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public override ProjectId DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { output.WriteLine("// " + assembly.FileName); output.WriteLine(); @@ -174,8 +176,7 @@ namespace ICSharpCode.ILSpy dis.WriteModuleContents(module); } } - - return true; + return null; } } } diff --git a/ILSpy/Languages/Language.cs b/ILSpy/Languages/Language.cs index 76f435244..e97696fea 100644 --- a/ILSpy/Languages/Language.cs +++ b/ILSpy/Languages/Language.cs @@ -23,6 +23,7 @@ using System.Reflection.PortableExecutable; using System.Text; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem.Implementation; using ICSharpCode.Decompiler.Util; @@ -131,11 +132,11 @@ namespace ICSharpCode.ILSpy WriteCommentLine(output, nameSpace); } - public virtual object DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public virtual ProjectId DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { WriteCommentLine(output, assembly.FileName); var asm = assembly.GetPEFileOrNull(); - if (asm == null) return false; + if (asm == null) return null; var metadata = asm.Metadata; if (metadata.IsAssembly) { var name = metadata.GetAssemblyDefinition(); @@ -147,8 +148,7 @@ namespace ICSharpCode.ILSpy } else { WriteCommentLine(output, metadata.GetString(metadata.GetModuleDefinition().Name)); } - - return true; + return null; } public virtual void WriteCommentLine(ITextOutput output, string comment) diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 238c74307..fe7c45374 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -914,29 +914,80 @@ namespace ICSharpCode.ILSpy void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e) { var selectedNodes = SelectedNodes.ToList(); - if (selectedNodes.Count > 1) { - var assemblyNodes = selectedNodes - .OfType() - .Where(n => n.Language is CSharpLanguage) - .ToList(); - - if (assemblyNodes.Count == selectedNodes.Count) { - var initialPath = Path.GetDirectoryName(assemblyNodes[0].LoadedAssembly.FileName); - var selectedPath = SolutionWriter.SelectSolutionFile(initialPath); - - if (!string.IsNullOrEmpty(selectedPath)) { - SolutionWriter.CreateSolution(TextView, selectedPath, assemblyNodes); - } + if (selectedNodes.Count == 1) { + // if there's only one treenode selected + // we will invoke the custom Save logic + if (selectedNodes[0].Save(TextView)) return; + } else if (selectedNodes.All(n => n is AssemblyTreeNode)) { + var initialPath = Path.GetDirectoryName(((AssemblyTreeNode)selectedNodes[0]).LoadedAssembly.FileName); + var selectedPath = SelectSolutionFile(initialPath); + + if (!string.IsNullOrEmpty(selectedPath)) { + var assemblies = selectedNodes.OfType() + .Select(n => n.LoadedAssembly) + .Where(a => a != null).ToArray(); + SolutionWriter.CreateSolution(TextView, selectedPath, CurrentLanguage, assemblies); } + return; } - if (selectedNodes.Count != 1 || !selectedNodes[0].Save(TextView)) { - var options = new DecompilationOptions() { FullDecompilation = true }; - TextView.SaveToDisk(CurrentLanguage, selectedNodes, options); + // Fallback: if nobody was able to handle the request, use default behavior. + // try to save all nodes to disk. + var options = new DecompilationOptions() { FullDecompilation = true }; + TextView.SaveToDisk(CurrentLanguage, selectedNodes, options); + } + + /// + /// Shows a File Selection dialog where the user can select the target file for the solution. + /// + /// The initial path to show in the dialog. If not specified, the 'Documents' directory + /// will be used. + /// + /// The full path of the selected target file, or null if the user canceled. + string SelectSolutionFile(string path) + { + const string SolutionExtension = ".sln"; + const string DefaultSolutionName = "Solution"; + + if (string.IsNullOrWhiteSpace(path)) { + path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); } + + SaveFileDialog dlg = new SaveFileDialog(); + dlg.InitialDirectory = path; + dlg.FileName = Path.Combine(path, DefaultSolutionName + SolutionExtension); + dlg.Filter = "Visual Studio Solution file|*" + SolutionExtension; + + bool targetInvalid; + do { + if (dlg.ShowDialog() != true) { + return null; + } + + string selectedPath = Path.GetDirectoryName(dlg.FileName); + try { + targetInvalid = 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.", + "Solution directory not accessible", + MessageBoxButton.OK, MessageBoxImage.Error); + targetInvalid = true; + continue; + } + + if (targetInvalid) { + MessageBox.Show( + "The directory is not empty. Please select an empty directory.", + "Solution directory not empty", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } while (targetInvalid); + + return dlg.FileName; } - + public void RefreshDecompiledView() { try { diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 45fd3ce67..d892e22ec 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -22,15 +22,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Security; using System.Threading; using System.Threading.Tasks; -using System.Windows; using ICSharpCode.Decompiler; -using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.Solution; +using ICSharpCode.Decompiler.Util; using ICSharpCode.ILSpy.TextView; -using ICSharpCode.ILSpy.TreeNodes; -using Microsoft.Win32; namespace ICSharpCode.ILSpy { @@ -38,73 +35,23 @@ namespace ICSharpCode.ILSpy /// An utility class that creates a Visual Studio solution containing projects for the /// decompiled assemblies. /// - internal static class SolutionWriter + internal class SolutionWriter { - private const string SolutionExtension = ".sln"; - private const string DefaultSolutionName = "Solution"; - - /// - /// Shows a File Selection dialog where the user can select the target file for the solution. - /// - /// The initial path to show in the dialog. If not specified, the 'Documents' directory - /// will be used. - /// - /// The full path of the selected target file, or null if the user canceled. - public static string SelectSolutionFile(string path) - { - if (string.IsNullOrWhiteSpace(path)) { - path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - } - - SaveFileDialog dlg = new SaveFileDialog(); - dlg.InitialDirectory = path; - dlg.FileName = Path.Combine(path, DefaultSolutionName + SolutionExtension); - dlg.Filter = "Visual Studio Solution file|*" + SolutionExtension; - - bool targetInvalid; - do { - if (dlg.ShowDialog() != true) { - return null; - } - - string selectedPath = Path.GetDirectoryName(dlg.FileName); - try { - targetInvalid = Directory.EnumerateFileSystemEntries(selectedPath).Any(); - } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is SecurityException) { - MessageBox.Show( - "The directory cannot be accessed. Please ensure it exists and you have sufficient rights to access it.", - "Solution directory not accessible", - MessageBoxButton.OK, MessageBoxImage.Error); - targetInvalid = true; - continue; - } - - if (targetInvalid) { - MessageBox.Show( - "The directory is not empty. Please select an empty directory.", - "Solution directory not empty", - MessageBoxButton.OK, MessageBoxImage.Warning); - } - } while (targetInvalid); - - return dlg.FileName; - } - /// /// Creates a Visual Studio solution that contains projects with decompiled code - /// of the specified . The solution file will be saved + /// of the specified . The solution file will be saved /// to the . The directory of this file must either /// be empty or not exist. /// /// A reference to the instance. /// The target file path of the solution file. - /// The assembly nodes to decompile. + /// The assembly nodes to decompile. /// /// Thrown when is null, /// an empty or a whitespace string. /// Thrown when > or - /// is null. - public static void CreateSolution(DecompilerTextView textView, string solutionFilePath, IEnumerable assemblyNodes) + /// is null. + public static void CreateSolution(DecompilerTextView textView, string solutionFilePath, Language language, IEnumerable assemblies) { if (textView == null) { throw new ArgumentNullException(nameof(textView)); @@ -114,29 +61,37 @@ namespace ICSharpCode.ILSpy throw new ArgumentException("The solution file path cannot be null or empty.", nameof(solutionFilePath)); } - if (assemblyNodes == null) { - throw new ArgumentNullException(nameof(assemblyNodes)); + if (assemblies == null) { + throw new ArgumentNullException(nameof(assemblies)); } + var writer = new SolutionWriter(solutionFilePath); + textView - .RunWithCancellation(ct => CreateSolution(solutionFilePath, assemblyNodes, ct)) + .RunWithCancellation(ct => writer.CreateSolution(assemblies, language, ct)) .Then(output => textView.ShowText(output)) .HandleExceptions(); } - private static async Task CreateSolution( - string solutionFilePath, - IEnumerable assemblyNodes, - CancellationToken ct) + readonly string solutionFilePath; + readonly string solutionDirectory; + readonly ConcurrentBag projects; + readonly ConcurrentBag statusOutput; + + SolutionWriter(string solutionFilePath) { - var solutionDirectory = Path.GetDirectoryName(solutionFilePath); - var statusOutput = new ConcurrentBag(); - var projects = new ConcurrentBag(); + this.solutionFilePath = solutionFilePath; + solutionDirectory = Path.GetDirectoryName(solutionFilePath); + statusOutput = new ConcurrentBag(); + projects = new ConcurrentBag(); + } + async Task CreateSolution(IEnumerable assemblies, Language language, CancellationToken ct) + { var result = new AvalonEditTextOutput(); var duplicates = new HashSet(); - if (assemblyNodes.Any(n => !duplicates.Add(n.LoadedAssembly.ShortName))) { + if (assemblies.Any(asm => !duplicates.Add(asm.ShortName))) { result.WriteLine("Duplicate assembly names selected, cannot generate a solution."); return result; } @@ -144,7 +99,7 @@ namespace ICSharpCode.ILSpy Stopwatch stopwatch = Stopwatch.StartNew(); try { - await Task.Run(() => Parallel.ForEach(assemblyNodes, n => WriteProject(n, solutionDirectory, statusOutput, projects, ct))) + await Task.Run(() => Parallel.ForEach(assemblies, n => WriteProject(n, language, solutionDirectory, ct))) .ConfigureAwait(false); await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects)) @@ -162,7 +117,7 @@ namespace ICSharpCode.ILSpy result.WriteLine(e.Message); return true; }); - + return result; } @@ -172,13 +127,13 @@ namespace ICSharpCode.ILSpy if (statusOutput.Count == 0) { result.WriteLine("Successfully decompiled the following assemblies into Visual Studio projects:"); - foreach (var item in assemblyNodes.Select(n => n.Text.ToString())) { + foreach (var item in assemblies.Select(n => n.Text.ToString())) { result.WriteLine(item); } result.WriteLine(); - if (assemblyNodes.Count() == projects.Count) { + if (assemblies.Count() == projects.Count) { result.WriteLine("Created the Visual Studio Solution file."); } @@ -191,17 +146,10 @@ namespace ICSharpCode.ILSpy return result; } - private static void WriteProject( - AssemblyTreeNode assemblyNode, - string targetDirectory, - ConcurrentBag statusOutput, - ConcurrentBag targetContainer, - CancellationToken ct) + void WriteProject(LoadedAssembly loadedAssembly, Language language, string targetDirectory, CancellationToken ct) { - var loadedAssembly = assemblyNode.LoadedAssembly; - targetDirectory = Path.Combine(targetDirectory, loadedAssembly.ShortName); - string projectFileName = Path.Combine(targetDirectory, loadedAssembly.ShortName + assemblyNode.Language.ProjectFileExtension); + string projectFileName = Path.Combine(targetDirectory, loadedAssembly.ShortName + language.ProjectFileExtension); if (!Directory.Exists(targetDirectory)) { try { @@ -214,21 +162,19 @@ namespace ICSharpCode.ILSpy try { using (var projectFileWriter = new StreamWriter(projectFileName)) { - var projectFileOutput = new PlainTextOutput(projectFileWriter); - var options = new DecompilationOptions() { - FullDecompilation = true, - CancellationToken = ct, - SaveAsProjectDirectory = targetDirectory }; - - if (assemblyNode.Decompile(assemblyNode.Language, projectFileOutput, options) is ProjectId projectInfo) { - targetContainer.Add(new ProjectItem(projectFileName, projectInfo.PlatformName, projectInfo.Guid)); + var projectFileOutput = new PlainTextOutput(projectFileWriter); + var options = new DecompilationOptions() { + FullDecompilation = true, + CancellationToken = ct, + SaveAsProjectDirectory = targetDirectory + }; + + var projectInfo = language.DecompileAssembly(loadedAssembly, projectFileOutput, options); + if (projectInfo != null) { + projects.Add(new ProjectItem(projectFileName, projectInfo.PlatformName, projectInfo.Guid)); } } - } - catch (OperationCanceledException) { - throw; - } - catch (Exception e) { + } catch (Exception e) when (!(e is OperationCanceledException)) { statusOutput.Add($"Failed to decompile the assembly '{loadedAssembly.FileName}':{Environment.NewLine}{e}"); } } diff --git a/ILSpy/TreeNodes/AssemblyListTreeNode.cs b/ILSpy/TreeNodes/AssemblyListTreeNode.cs index 3b8e5a519..e53a3a883 100644 --- a/ILSpy/TreeNodes/AssemblyListTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyListTreeNode.cs @@ -141,7 +141,7 @@ namespace ICSharpCode.ILSpy.TreeNodes public Action Select = delegate { }; - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, "List: " + assemblyList.ListName); output.WriteLine(); @@ -150,8 +150,6 @@ namespace ICSharpCode.ILSpy.TreeNodes output.WriteLine(); asm.Decompile(language, output, options); } - - return true; } #region Find*Node diff --git a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs index 192820246..8aa8f8c0d 100644 --- a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs @@ -79,7 +79,7 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { var loaded = parentAssembly.LoadedAssembly.LoadedAssemblyReferencesInfo.TryGetInfo(r.FullName, out var info); if (r.IsWindowsRuntime) { @@ -98,8 +98,6 @@ namespace ICSharpCode.ILSpy.TreeNodes output.Unindent(); output.WriteLine(); } - - return true; } } } diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 39669c15f..60f34e6b2 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -240,7 +240,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Recurse; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { void HandleException(Exception ex, string message) { @@ -259,21 +259,21 @@ namespace ICSharpCode.ILSpy.TreeNodes switch (ex.InnerException) { case BadImageFormatException badImage: HandleException(badImage, "This file does not contain a managed assembly."); - return null; + return; case FileNotFoundException fileNotFound: HandleException(fileNotFound, "The file was not found."); - return null; + return; case DirectoryNotFoundException dirNotFound: HandleException(dirNotFound, "The directory was not found."); - return null; + return; case PEFileNotSupportedException notSupported: HandleException(notSupported, notSupported.Message); - return null; + return; default: throw; } } - return language.DecompileAssembly(LoadedAssembly, output, options); + language.DecompileAssembly(LoadedAssembly, output, options); } public override bool Save(DecompilerTextView textView) diff --git a/ILSpy/TreeNodes/BaseTypesEntryNode.cs b/ILSpy/TreeNodes/BaseTypesEntryNode.cs index 41edd0f8c..3c96c40dd 100644 --- a/ILSpy/TreeNodes/BaseTypesEntryNode.cs +++ b/ILSpy/TreeNodes/BaseTypesEntryNode.cs @@ -97,10 +97,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return false; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, language.TypeToString(type, includeNamespace: true)); - return true; } IEntity IMemberTreeNode.Member { diff --git a/ILSpy/TreeNodes/BaseTypesTreeNode.cs b/ILSpy/TreeNodes/BaseTypesTreeNode.cs index 58565a938..8cbdb2304 100644 --- a/ILSpy/TreeNodes/BaseTypesTreeNode.cs +++ b/ILSpy/TreeNodes/BaseTypesTreeNode.cs @@ -69,14 +69,12 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(EnsureLazyChildren)); foreach (ILSpyTreeNode child in this.Children) { child.Decompile(language, output, options); } - - return true; } } } \ No newline at end of file diff --git a/ILSpy/TreeNodes/DerivedTypesEntryNode.cs b/ILSpy/TreeNodes/DerivedTypesEntryNode.cs index f3d9fe29f..91da3e033 100644 --- a/ILSpy/TreeNodes/DerivedTypesEntryNode.cs +++ b/ILSpy/TreeNodes/DerivedTypesEntryNode.cs @@ -90,10 +90,9 @@ namespace ICSharpCode.ILSpy.TreeNodes e.Handled = BaseTypesEntryNode.ActivateItem(this, type); } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, language.TypeToString(type, includeNamespace: true)); - return true; } IEntity IMemberTreeNode.Member => type; diff --git a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs index 8518e08e3..3b6e50a30 100644 --- a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs +++ b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs @@ -92,10 +92,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return typeRef.GetFullTypeName(referenceMetadata) == typeDef.GetFullTypeName(definitionMetadata); } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { threading.Decompile(language, output, options, EnsureLazyChildren); - return true; } } } \ No newline at end of file diff --git a/ILSpy/TreeNodes/EventTreeNode.cs b/ILSpy/TreeNodes/EventTreeNode.cs index c1797f794..1aadff896 100644 --- a/ILSpy/TreeNodes/EventTreeNode.cs +++ b/ILSpy/TreeNodes/EventTreeNode.cs @@ -67,10 +67,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileEvent(EventDefinition, output, options); - return true; } public override bool IsPublicAPI { diff --git a/ILSpy/TreeNodes/FieldTreeNode.cs b/ILSpy/TreeNodes/FieldTreeNode.cs index 5d3323beb..5d4d2f7b0 100644 --- a/ILSpy/TreeNodes/FieldTreeNode.cs +++ b/ILSpy/TreeNodes/FieldTreeNode.cs @@ -68,10 +68,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileField(FieldDefinition, output, options); - return true; } public override bool IsPublicAPI { diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs index 515188374..11fe3b278 100644 --- a/ILSpy/TreeNodes/ILSpyTreeNode.cs +++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs @@ -57,7 +57,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public abstract object Decompile(Language language, ITextOutput output, DecompilationOptions options); + public abstract void Decompile(Language language, ITextOutput output, DecompilationOptions options); /// /// Used to implement special view logic for some items. diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs index 89c12ec10..9e4d95e45 100644 --- a/ILSpy/TreeNodes/MethodTreeNode.cs +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -83,10 +83,9 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileMethod(MethodDefinition, output, options); - return true; } public override FilterResult Filter(FilterSettings settings) diff --git a/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs b/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs index a363c46db..4d51f46af 100644 --- a/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs @@ -72,11 +72,10 @@ namespace ICSharpCode.ILSpy.TreeNodes } } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, moduleName); language.WriteCommentLine(output, containsMetadata ? "contains metadata" : "contains no metadata"); - return true; } } } diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index 3b9ff4845..d054603dc 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -56,10 +56,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Recurse; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileNamespace(name, this.Children.OfType().Select(t => t.TypeDefinition), output, options); - return true; } } } diff --git a/ILSpy/TreeNodes/PropertyTreeNode.cs b/ILSpy/TreeNodes/PropertyTreeNode.cs index 3267a1e1a..14769ea66 100644 --- a/ILSpy/TreeNodes/PropertyTreeNode.cs +++ b/ILSpy/TreeNodes/PropertyTreeNode.cs @@ -74,10 +74,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileProperty(PropertyDefinition, output, options); - return true; } public override bool IsPublicAPI { diff --git a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs index 9121e213f..fde4630f1 100644 --- a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs +++ b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs @@ -62,7 +62,7 @@ namespace ICSharpCode.ILSpy.TreeNodes this.Children.Add(new ModuleReferenceTreeNode(parentAssembly, r, metadata)); } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, $"Detected Target-Framework-Id: {parentAssembly.LoadedAssembly.GetTargetFrameworkIdAsync().Result}"); App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(EnsureLazyChildren)); @@ -86,7 +86,6 @@ namespace ICSharpCode.ILSpy.TreeNodes output.Unindent(); output.WriteLine(); } - return true; } } } diff --git a/ILSpy/TreeNodes/ResourceListTreeNode.cs b/ILSpy/TreeNodes/ResourceListTreeNode.cs index 79cc1df50..c67c9d4b1 100644 --- a/ILSpy/TreeNodes/ResourceListTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceListTreeNode.cs @@ -64,15 +64,13 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Recurse; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(EnsureLazyChildren)); foreach (ILSpyTreeNode child in this.Children) { child.Decompile(language, output, options); output.WriteLine(); } - - return true; } } } diff --git a/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs b/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs index 3805db739..fb8b2199f 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ImageListResourceEntryNode.cs @@ -75,10 +75,9 @@ namespace ICSharpCode.ILSpy.TreeNodes } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { EnsureLazyChildren(); - return true; } } } diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs index cf5a3d5ac..ec74ad193 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourceEntryNode.cs @@ -73,10 +73,9 @@ namespace ICSharpCode.ILSpy.TreeNodes return result; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, string.Format("{0} = {1}", key, data)); - return true; } public override bool Save(DecompilerTextView textView) diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs index c9adaa49c..3433f70d7 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourceTreeNode.cs @@ -67,7 +67,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Hidden; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.WriteCommentLine(output, string.Format("{0} ({1}, {2})", r.Name, r.ResourceType, r.Attributes)); @@ -76,8 +76,6 @@ namespace ICSharpCode.ILSpy.TreeNodes smartOutput.AddButton(Images.Save, Resources.Save, delegate { Save(MainWindow.Instance.TextView); }); output.WriteLine(); } - - return true; } public override bool View(DecompilerTextView textView) diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs index a44edeae4..c1d908481 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs @@ -141,7 +141,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return true; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { EnsureLazyChildren(); base.Decompile(language, output, options); @@ -168,8 +168,6 @@ namespace ICSharpCode.ILSpy.TreeNodes } output.WriteLine(); } - - return true; } internal class SerializedObjectRepresentation diff --git a/ILSpy/TreeNodes/ThreadingSupport.cs b/ILSpy/TreeNodes/ThreadingSupport.cs index 8df078cad..e95787efc 100644 --- a/ILSpy/TreeNodes/ThreadingSupport.cs +++ b/ILSpy/TreeNodes/ThreadingSupport.cs @@ -128,9 +128,8 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Match; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { - return false; } } @@ -152,9 +151,8 @@ namespace ICSharpCode.ILSpy.TreeNodes return FilterResult.Match; } - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { - return false; } } diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs index 4464a3521..3ef91c411 100644 --- a/ILSpy/TreeNodes/TypeTreeNode.cs +++ b/ILSpy/TreeNodes/TypeTreeNode.cs @@ -103,10 +103,9 @@ namespace ICSharpCode.ILSpy.TreeNodes public override bool CanExpandRecursively => true; - public override object Decompile(Language language, ITextOutput output, DecompilationOptions options) + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { language.DecompileType(TypeDefinition, output, options); - return true; } public override object Icon => GetIcon(TypeDefinition); From 24e492bfe306bdbd3718f76b5254d684ee2fe566 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 24 Jul 2019 01:10:19 +0200 Subject: [PATCH 07/10] Adjust SaveCommandCanExecute to match SaveCommandExecuted. --- ILSpy/MainWindow.xaml | 2 +- ILSpy/MainWindow.xaml.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ILSpy/MainWindow.xaml b/ILSpy/MainWindow.xaml index 166c8ac80..ed8cc1b1b 100644 --- a/ILSpy/MainWindow.xaml +++ b/ILSpy/MainWindow.xaml @@ -28,7 +28,7 @@ Executed="RefreshCommandExecuted" /> n is AssemblyTreeNode); + e.CanExecute = selectedNodes.Count == 1 || (selectedNodes.Count > 1 && selectedNodes.All(n => n is AssemblyTreeNode)); } void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e) @@ -919,7 +919,7 @@ namespace ICSharpCode.ILSpy // we will invoke the custom Save logic if (selectedNodes[0].Save(TextView)) return; - } else if (selectedNodes.All(n => n is AssemblyTreeNode)) { + } else if (selectedNodes.Count > 1 && selectedNodes.All(n => n is AssemblyTreeNode)) { var initialPath = Path.GetDirectoryName(((AssemblyTreeNode)selectedNodes[0]).LoadedAssembly.FileName); var selectedPath = SelectSolutionFile(initialPath); From 9dd22011d65fcdd91cb4f7a9b062d0fca6034722 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 24 Jul 2019 08:05:37 +0200 Subject: [PATCH 08/10] Remove ILSpyTreeNode.Save accepting a file name, as it is currently not necessary. Remove focused element check in SaveCommandCanExecute, because saving the text view content independently from the tree view selection is currently not supported. --- ILSpy/Controls/ExtensionMethods.cs | 34 ----------------- ILSpy/MainWindow.xaml.cs | 7 +--- ILSpy/Properties/Resources.Designer.cs | 18 +++++++++ ILSpy/Properties/Resources.resx | 6 +++ ILSpy/TreeNodes/AssemblyTreeNode.cs | 52 ++++++++++---------------- ILSpy/TreeNodes/ILSpyTreeNode.cs | 9 ----- 6 files changed, 44 insertions(+), 82 deletions(-) diff --git a/ILSpy/Controls/ExtensionMethods.cs b/ILSpy/Controls/ExtensionMethods.cs index 1c5e9f794..095ddc41e 100644 --- a/ILSpy/Controls/ExtensionMethods.cs +++ b/ILSpy/Controls/ExtensionMethods.cs @@ -19,7 +19,6 @@ using System; using System.Windows; using System.Windows.Markup; -using System.Windows.Media; namespace ICSharpCode.ILSpy.Controls { @@ -28,39 +27,6 @@ namespace ICSharpCode.ILSpy.Controls /// public static class ExtensionMethods { - /// - /// Checks if the current is contained in the visual tree of the - /// object. - /// - /// The object to check, may be null. - /// The object whose visual tree will be inspected. - /// - /// true if this object is contained in the visual tree of the ; - /// otherwise, false. - /// - /// Thrown when is null. - public static bool IsInVisualTreeOf(this DependencyObject thisObject, DependencyObject dependencyObject) - { - if (dependencyObject == null) { - throw new ArgumentNullException(nameof(dependencyObject)); - } - - if (thisObject is null) { - return false; - } - - var parent = VisualTreeHelper.GetParent(thisObject); - while (parent != null) { - if (parent == dependencyObject) { - return true; - } - - parent = VisualTreeHelper.GetParent(parent); - } - - return false; - } - /// /// Sets the value of a dependency property on using a markup extension. /// diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 5bfb8366b..1fabedcd1 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -902,11 +902,6 @@ namespace ICSharpCode.ILSpy void SaveCommandCanExecute(object sender, CanExecuteRoutedEventArgs e) { e.Handled = true; - var focusedElement = FocusManager.GetFocusedElement(this) as DependencyObject; - if (focusedElement.IsInVisualTreeOf(TextView)) { - e.CanExecute = true; - return; - } var selectedNodes = SelectedNodes.ToList(); e.CanExecute = selectedNodes.Count == 1 || (selectedNodes.Count > 1 && selectedNodes.All(n => n is AssemblyTreeNode)); } @@ -926,7 +921,7 @@ namespace ICSharpCode.ILSpy if (!string.IsNullOrEmpty(selectedPath)) { var assemblies = selectedNodes.OfType() .Select(n => n.LoadedAssembly) - .Where(a => a != null).ToArray(); + .Where(a => !a.HasLoadError).ToArray(); SolutionWriter.CreateSolution(TextView, selectedPath, CurrentLanguage, assemblies); } return; diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 5c86e06e2..859948997 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -303,6 +303,24 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to The directory is not empty. File will be overwritten.\r\nAre you sure you want to continue?. + /// + public static string AssemblySaveCodeDirectoryNotEmpty { + get { + return ResourceManager.GetString("AssemblySaveCodeDirectoryNotEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project Directory not empty. + /// + public static string AssemblySaveCodeDirectoryNotEmptyTitle { + get { + return ResourceManager.GetString("AssemblySaveCodeDirectoryNotEmptyTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Automatically check for updates every week. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 0128389d9..899febe76 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -729,4 +729,10 @@ Entity could not be resolved. Cannot analyze entities from missing assembly references. Add the missing reference and try again. + + The directory is not empty. File will be overwritten.\r\nAre you sure you want to continue? + + + Project Directory not empty + \ No newline at end of file diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 60f34e6b2..13e2c1df2 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -279,44 +279,30 @@ namespace ICSharpCode.ILSpy.TreeNodes public override bool Save(DecompilerTextView textView) { Language language = this.Language; - if (string.IsNullOrEmpty(language.ProjectFileExtension)) { + if (string.IsNullOrEmpty(language.ProjectFileExtension)) return false; - } - SaveFileDialog dlg = new SaveFileDialog(); dlg.FileName = DecompilerTextView.CleanUpName(LoadedAssembly.ShortName) + language.ProjectFileExtension; - dlg.Filter = language.Name + " project|*" + language.ProjectFileExtension; - if (dlg.ShowDialog() != true) { - return true; - } - - var targetDirectory = Path.GetDirectoryName(dlg.FileName); - var existingFiles = Directory.GetFileSystemEntries(targetDirectory); - - if (existingFiles.Any(e => !string.Equals(e, dlg.FileName, StringComparison.OrdinalIgnoreCase))) { - var result = MessageBox.Show( - "The directory is not empty. File will be overwritten." + Environment.NewLine + - "Are you sure you want to continue?", - "Project Directory not empty", - MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); - if (result == MessageBoxResult.No) { - return true; // don't save, but mark the Save operation as handled + dlg.Filter = language.Name + " project|*" + language.ProjectFileExtension + "|" + language.Name + " single file|*" + language.FileExtension + "|All files|*.*"; + if (dlg.ShowDialog() == true) { + DecompilationOptions options = new DecompilationOptions(); + options.FullDecompilation = true; + if (dlg.FilterIndex == 1) { + options.SaveAsProjectDirectory = Path.GetDirectoryName(dlg.FileName); + foreach (string entry in Directory.GetFileSystemEntries(options.SaveAsProjectDirectory)) { + if (!string.Equals(entry, dlg.FileName, StringComparison.OrdinalIgnoreCase)) { + var result = MessageBox.Show( + Resources.AssemblySaveCodeDirectoryNotEmpty, + Resources.AssemblySaveCodeDirectoryNotEmptyTitle, + MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (result == MessageBoxResult.No) + return true; // don't save, but mark the Save operation as handled + break; + } + } } + textView.SaveToDisk(language, new[] { this }, options, dlg.FileName); } - - Save(textView, dlg.FileName); - return true; - } - - public override bool Save(DecompilerTextView textView, string fileName) - { - var targetDirectory = Path.GetDirectoryName(fileName); - DecompilationOptions options = new DecompilationOptions { - FullDecompilation = true, - SaveAsProjectDirectory = targetDirectory - }; - - textView.SaveToDisk(Language, new[] { this }, options, fileName); return true; } diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs index 11fe3b278..fa1e663e4 100644 --- a/ILSpy/TreeNodes/ILSpyTreeNode.cs +++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs @@ -79,15 +79,6 @@ namespace ICSharpCode.ILSpy.TreeNodes return false; } - /// - /// Saves the content this node represents to the specified . - /// The file will be silently overwritten. - /// - /// A reference to a instance. - /// The target full path to save the content to. - /// true on success; otherwise, false. - public virtual bool Save(TextView.DecompilerTextView textView, string fileName) => Save(textView); - protected override void OnChildrenChanged(NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { From 5fdeb223dde11dee57fac29e31dca37352af573d Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 24 Jul 2019 08:59:33 +0200 Subject: [PATCH 09/10] "Save Code" should only work with ILSpyTreeNodes. --- ILSpy/Commands/SaveCodeContextMenuEntry.cs | 105 +++++++++++++++++++-- ILSpy/MainWindow.xaml.cs | 77 +-------------- 2 files changed, 101 insertions(+), 81 deletions(-) diff --git a/ILSpy/Commands/SaveCodeContextMenuEntry.cs b/ILSpy/Commands/SaveCodeContextMenuEntry.cs index 85e5e301f..8ee945f0d 100644 --- a/ILSpy/Commands/SaveCodeContextMenuEntry.cs +++ b/ILSpy/Commands/SaveCodeContextMenuEntry.cs @@ -16,25 +16,118 @@ // 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.IO; +using System.Linq; +using System.Windows; using System.Windows.Input; using ICSharpCode.ILSpy.Properties; +using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.TreeView; +using Microsoft.Win32; namespace ICSharpCode.ILSpy.TextView { [ExportContextMenuEntry(Header = nameof(Resources._SaveCode), Category = nameof(Resources.Save), Icon = "Images/SaveFile.png")] sealed class SaveCodeContextMenuEntry : IContextMenuEntry { - private readonly ICommand saveCommand; + public void Execute(TextViewContext context) + { + Execute(context.SelectedTreeNodes); + } + + public bool IsEnabled(TextViewContext context) => true; + + public bool IsVisible(TextViewContext context) + { + return CanExecute(context.SelectedTreeNodes); + } - public SaveCodeContextMenuEntry() + public static bool CanExecute(IReadOnlyList selectedNodes) { - saveCommand = ApplicationCommands.Save; + if (selectedNodes == null || selectedNodes.Any(n => !(n is ILSpyTreeNode))) + return false; + return selectedNodes.Count == 1 + || (selectedNodes.Count > 1 && (selectedNodes.All(n => n is AssemblyTreeNode) || selectedNodes.All(n => n is IMemberTreeNode))); } - public void Execute(TextViewContext context) => saveCommand.Execute(parameter: null); + public static void Execute(IReadOnlyList selectedNodes) + { + var currentLanguage = MainWindow.Instance.CurrentLanguage; + var textView = MainWindow.Instance.TextView; + if (selectedNodes.Count == 1 && selectedNodes[0] is ILSpyTreeNode singleSelection) { + // if there's only one treenode selected + // we will invoke the custom Save logic + if (singleSelection.Save(textView)) + return; + } else if (selectedNodes.Count > 1 && selectedNodes.All(n => n is AssemblyTreeNode)) { + var initialPath = Path.GetDirectoryName(((AssemblyTreeNode)selectedNodes[0]).LoadedAssembly.FileName); + var selectedPath = SelectSolutionFile(initialPath); - public bool IsEnabled(TextViewContext context) => true; + if (!string.IsNullOrEmpty(selectedPath)) { + var assemblies = selectedNodes.OfType() + .Select(n => n.LoadedAssembly) + .Where(a => !a.HasLoadError).ToArray(); + SolutionWriter.CreateSolution(textView, selectedPath, currentLanguage, assemblies); + } + return; + } - public bool IsVisible(TextViewContext context) => context.TreeView != null && saveCommand.CanExecute(parameter: null); + // Fallback: if nobody was able to handle the request, use default behavior. + // try to save all nodes to disk. + var options = new DecompilationOptions() { FullDecompilation = true }; + textView.SaveToDisk(currentLanguage, selectedNodes.OfType(), options); + } + + /// + /// Shows a File Selection dialog where the user can select the target file for the solution. + /// + /// The initial path to show in the dialog. If not specified, the 'Documents' directory + /// will be used. + /// + /// The full path of the selected target file, or null if the user canceled. + static string SelectSolutionFile(string path) + { + const string SolutionExtension = ".sln"; + const string DefaultSolutionName = "Solution"; + + if (string.IsNullOrWhiteSpace(path)) { + path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + } + + SaveFileDialog dlg = new SaveFileDialog(); + dlg.InitialDirectory = path; + dlg.FileName = Path.Combine(path, DefaultSolutionName + SolutionExtension); + dlg.Filter = "Visual Studio Solution file|*" + SolutionExtension; + + bool targetInvalid; + do { + if (dlg.ShowDialog() != true) { + return null; + } + + string selectedPath = Path.GetDirectoryName(dlg.FileName); + try { + targetInvalid = 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.", + "Solution directory not accessible", + MessageBoxButton.OK, MessageBoxImage.Error); + targetInvalid = true; + continue; + } + + if (targetInvalid) { + MessageBox.Show( + "The directory is not empty. Please select an empty directory.", + "Solution directory not empty", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } while (targetInvalid); + + return dlg.FileName; + } } } diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 1fabedcd1..5732b5673 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -902,85 +902,12 @@ namespace ICSharpCode.ILSpy void SaveCommandCanExecute(object sender, CanExecuteRoutedEventArgs e) { e.Handled = true; - var selectedNodes = SelectedNodes.ToList(); - e.CanExecute = selectedNodes.Count == 1 || (selectedNodes.Count > 1 && selectedNodes.All(n => n is AssemblyTreeNode)); + e.CanExecute = SaveCodeContextMenuEntry.CanExecute(SelectedNodes.ToList()); } void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e) { - var selectedNodes = SelectedNodes.ToList(); - if (selectedNodes.Count == 1) { - // if there's only one treenode selected - // we will invoke the custom Save logic - if (selectedNodes[0].Save(TextView)) - return; - } else if (selectedNodes.Count > 1 && selectedNodes.All(n => n is AssemblyTreeNode)) { - var initialPath = Path.GetDirectoryName(((AssemblyTreeNode)selectedNodes[0]).LoadedAssembly.FileName); - var selectedPath = SelectSolutionFile(initialPath); - - if (!string.IsNullOrEmpty(selectedPath)) { - var assemblies = selectedNodes.OfType() - .Select(n => n.LoadedAssembly) - .Where(a => !a.HasLoadError).ToArray(); - SolutionWriter.CreateSolution(TextView, selectedPath, CurrentLanguage, assemblies); - } - return; - } - - // Fallback: if nobody was able to handle the request, use default behavior. - // try to save all nodes to disk. - var options = new DecompilationOptions() { FullDecompilation = true }; - TextView.SaveToDisk(CurrentLanguage, selectedNodes, options); - } - - /// - /// Shows a File Selection dialog where the user can select the target file for the solution. - /// - /// The initial path to show in the dialog. If not specified, the 'Documents' directory - /// will be used. - /// - /// The full path of the selected target file, or null if the user canceled. - string SelectSolutionFile(string path) - { - const string SolutionExtension = ".sln"; - const string DefaultSolutionName = "Solution"; - - if (string.IsNullOrWhiteSpace(path)) { - path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - } - - SaveFileDialog dlg = new SaveFileDialog(); - dlg.InitialDirectory = path; - dlg.FileName = Path.Combine(path, DefaultSolutionName + SolutionExtension); - dlg.Filter = "Visual Studio Solution file|*" + SolutionExtension; - - bool targetInvalid; - do { - if (dlg.ShowDialog() != true) { - return null; - } - - string selectedPath = Path.GetDirectoryName(dlg.FileName); - try { - targetInvalid = 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.", - "Solution directory not accessible", - MessageBoxButton.OK, MessageBoxImage.Error); - targetInvalid = true; - continue; - } - - if (targetInvalid) { - MessageBox.Show( - "The directory is not empty. Please select an empty directory.", - "Solution directory not empty", - MessageBoxButton.OK, MessageBoxImage.Warning); - } - } while (targetInvalid); - - return dlg.FileName; + SaveCodeContextMenuEntry.Execute(SelectedNodes.ToList()); } public void RefreshDecompiledView() From 470cd1ec23802759410d9b7af0f06da6b3635e3a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 26 Jul 2019 17:48:04 +0200 Subject: [PATCH 10/10] Fix SolutionCreator.GetRelativePath so that it properly handles directories with '.' in the name. --- .../Solution/SolutionCreator.cs | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index fc63beca5..824f03a23 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -55,16 +55,16 @@ namespace ICSharpCode.Decompiler.Solution } using (var writer = new StreamWriter(targetFile)) { - WriteSolutionFile(writer, projects, Path.GetDirectoryName(targetFile)); + WriteSolutionFile(writer, projects, targetFile); } FixProjectReferences(projects); } - private static void WriteSolutionFile(TextWriter writer, IEnumerable projects, string solutionPath) + private static void WriteSolutionFile(TextWriter writer, IEnumerable projects, string solutionFilePath) { WriteHeader(writer); - WriteProjects(writer, projects, solutionPath); + WriteProjects(writer, projects, solutionFilePath); writer.WriteLine("Global"); @@ -86,12 +86,12 @@ namespace ICSharpCode.Decompiler.Solution writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1"); } - private static void WriteProjects(TextWriter writer, IEnumerable projects, string solutionPath) + private static void WriteProjects(TextWriter writer, IEnumerable projects, string solutionFilePath) { var solutionGuid = Guid.NewGuid().ToString("B").ToUpperInvariant(); foreach (var project in projects) { - var projectRelativePath = GetRelativePath(solutionPath, project.FilePath); + var projectRelativePath = GetRelativePath(solutionFilePath, project.FilePath); var projectGuid = project.Guid.ToString("B").ToUpperInvariant(); writer.WriteLine($"Project(\"{solutionGuid}\") = \"{project.ProjectName}\", \"{projectRelativePath}\", \"{projectGuid}\""); @@ -148,7 +148,6 @@ namespace ICSharpCode.Decompiler.Solution var projectsMap = projects.ToDictionary(p => p.ProjectName, p => p); foreach (var project in projects) { - var projectDirectory = Path.GetDirectoryName(project.FilePath); XDocument projectDoc = XDocument.Load(project.FilePath); var referencesItemGroups = projectDoc.Root @@ -156,14 +155,14 @@ namespace ICSharpCode.Decompiler.Solution .Where(e => e.Elements(ProjectFileNamespace + "Reference").Any()); foreach (var itemGroup in referencesItemGroups) { - FixProjectReferences(projectDirectory, itemGroup, projectsMap); + FixProjectReferences(project.FilePath, itemGroup, projectsMap); } projectDoc.Save(project.FilePath); } } - private static void FixProjectReferences(string projectDirectory, XElement itemGroup, IDictionary projects) + private static void FixProjectReferences(string projectFilePath, XElement itemGroup, IDictionary projects) { foreach (var item in itemGroup.Elements(ProjectFileNamespace + "Reference").ToList()) { var assemblyName = item.Attribute("Include")?.Value; @@ -173,20 +172,20 @@ namespace ICSharpCode.Decompiler.Solution var projectReference = new XElement(ProjectFileNamespace + "ProjectReference", new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToUpperInvariant()), new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName)); - projectReference.SetAttributeValue("Include", GetRelativePath(projectDirectory, referencedProject.FilePath)); + projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)); itemGroup.Add(projectReference); } } } - private static string GetRelativePath(string fromPath, string toPath) + private static string GetRelativePath(string fromFilePath, string toFilePath) { - Uri fromUri = new Uri(AppendDirectorySeparatorChar(fromPath)); - Uri toUri = new Uri(AppendDirectorySeparatorChar(toPath)); + Uri fromUri = new Uri(fromFilePath); + Uri toUri = new Uri(toFilePath); if (fromUri.Scheme != toUri.Scheme) { - return toPath; + return toFilePath; } Uri relativeUri = fromUri.MakeRelativeUri(toUri); @@ -198,12 +197,5 @@ namespace ICSharpCode.Decompiler.Solution return relativePath; } - - private static string AppendDirectorySeparatorChar(string path) - { - return Path.HasExtension(path) || path.EndsWith(Path.DirectorySeparatorChar.ToString()) - ? path - : path + Path.DirectorySeparatorChar; - } } }