diff --git a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs index 5d2ee3a5d..f1636dbaf 100644 --- a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs +++ b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs @@ -72,7 +72,8 @@ namespace ICSharpCode.Decompiler.DebugInfo Stream targetStream, bool noLogo = false, BlobContentId? pdbId = null, - IProgress progress = null) + IProgress progress = null, + string currentProgressTitle = "Generating portable PDB...") { MetadataBuilder metadata = new MetadataBuilder(); MetadataReader reader = file.Metadata; @@ -99,7 +100,7 @@ namespace ICSharpCode.Decompiler.DebugInfo DecompilationProgress currentProgress = new() { TotalUnits = sourceFiles.Count, UnitsCompleted = 0, - Title = "Generating portable PDB..." + Title = currentProgressTitle }; foreach (var sourceFile in sourceFiles) diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 01de79d53..518ae0863 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -484,7 +484,7 @@ Examples: return ProgramExitCodes.EX_DATAERR; } - using (FileStream stream = new FileStream(pdbFileName, FileMode.OpenOrCreate, FileAccess.Write)) + using (FileStream stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write)) { var decompiler = GetDecompiler(assemblyFileName); PortablePdbWriter.WritePdb(module, decompiler, GetSettings(module), stream); diff --git a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs index 9f8adaa96..1e2240a0e 100644 --- a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs +++ b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs @@ -75,7 +75,7 @@ namespace ICSharpCode.ILSpy.TextView output.WriteLine(); var diagramHtml = Path.Combine(selectedPath, "index.html"); - output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + diagramHtml + "\""); }); + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItem(diagramHtml); }); output.WriteLine(); return output; }, ct), Properties.Resources.CreatingDiagram).Then(dockWorkspace.ShowText).HandleExceptions(); diff --git a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs index 292b5aea9..ace1c863a 100644 --- a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs +++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs @@ -124,17 +124,22 @@ namespace ICSharpCode.ILSpy dockWorkspace.RunWithCancellation(ct => Task.Factory.StartNew(() => { AvalonEditTextOutput output = new AvalonEditTextOutput(); Stopwatch stopwatch = Stopwatch.StartNew(); + var writtenFiles = new List(); foreach (var node in nodes) { if (node is AssemblyTreeNode { PackageEntry: { } assembly }) { string fileName = GetFileName(path, isFile, node.Parent, assembly); SaveEntry(output, assembly, fileName); + if (File.Exists(fileName)) + writtenFiles.Add(fileName); } else if (node is ResourceTreeNode { Resource: PackageEntry { } resource }) { string fileName = GetFileName(path, isFile, node.Parent, resource); SaveEntry(output, resource, fileName); + if (File.Exists(fileName)) + writtenFiles.Add(fileName); } else if (node is PackageFolderTreeNode) { @@ -145,11 +150,15 @@ namespace ICSharpCode.ILSpy { string fileName = GetFileName(path, isFile, item.Parent, asm); SaveEntry(output, asm, fileName); + if (File.Exists(fileName)) + writtenFiles.Add(fileName); } else if (item is ResourceTreeNode { Resource: PackageEntry { } entry }) { string fileName = GetFileName(path, isFile, item.Parent, entry); SaveEntry(output, entry, fileName); + if (File.Exists(fileName)) + writtenFiles.Add(fileName); } else if (item is PackageFolderTreeNode) { @@ -161,7 +170,18 @@ namespace ICSharpCode.ILSpy stopwatch.Stop(); output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")); output.WriteLine(); - output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", isFile ? $"/select,\"{path}\"" : $"\"{path}\""); }); + // If we have written files, open explorer and select them grouped by folder; otherwise fall back to opening containing folder. + if (writtenFiles.Count > 0) + { + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItems(writtenFiles); }); + } + else + { + if (isFile && File.Exists(path)) + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItem(path); }); + else + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolder(path); }); + } output.WriteLine(); return output; }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 9f58826b7..e7b18de41 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Composition; using System.Diagnostics; using System.IO; @@ -47,19 +48,30 @@ namespace ICSharpCode.ILSpy { public void Execute(TextViewContext context) { - var assembly = (context.SelectedTreeNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly; - if (assembly == null) + var selectedNodes = context.SelectedTreeNodes?.OfType().ToArray(); + if (selectedNodes == null || selectedNodes.Length == 0) return; - GeneratePdbForAssembly(assembly, languageService, dockWorkspace); + + if (selectedNodes.Length == 1) + { + var assembly = selectedNodes.First().LoadedAssembly; + if (assembly == null) + return; + GeneratePdbForAssembly(assembly, languageService, dockWorkspace); + } + else + { + GeneratePdbForAssemblies(selectedNodes.Select(n => n.LoadedAssembly), languageService, dockWorkspace); + } } public bool IsEnabled(TextViewContext context) => true; public bool IsVisible(TextViewContext context) { - return context.SelectedTreeNodes?.Length == 1 - && context.SelectedTreeNodes?.FirstOrDefault() is AssemblyTreeNode tn - && tn.LoadedAssembly.IsLoadedAsValidAssembly; + var selectedNodes = context.SelectedTreeNodes; + return selectedNodes?.Any() == true + && selectedNodes.All(n => n is AssemblyTreeNode asm && asm.LoadedAssembly.IsLoadedAsValidAssembly); } internal static void GeneratePdbForAssembly(LoadedAssembly assembly, LanguageService languageService, DockWorkspace dockWorkspace) @@ -82,13 +94,13 @@ namespace ICSharpCode.ILSpy AvalonEditTextOutput output = new AvalonEditTextOutput(); Stopwatch stopwatch = Stopwatch.StartNew(); options.CancellationToken = ct; - using (FileStream stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write)) + using (FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write)) { try { var decompiler = new CSharpDecompiler(file, assembly.GetAssemblyResolver(options.DecompilerSettings.AutoLoadAssemblyReferences), options.DecompilerSettings); decompiler.CancellationToken = ct; - PortablePdbWriter.WritePdb(file, decompiler, options.DecompilerSettings, stream, progress: options.Progress); + PortablePdbWriter.WritePdb(file, decompiler, options.DecompilerSettings, stream, progress: options.Progress, currentProgressTitle: string.Format(Resources.GeneratingPortablePDB, Path.GetFileName(assembly.FileName))); } catch (OperationCanceledException) { @@ -100,7 +112,130 @@ namespace ICSharpCode.ILSpy stopwatch.Stop(); output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")); output.WriteLine(); - output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); }); + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItem(fileName); }); + output.WriteLine(); + return output; + }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); + } + + internal static void GeneratePdbForAssemblies(IEnumerable assemblies, LanguageService languageService, DockWorkspace dockWorkspace) + { + var assemblyArray = assemblies?.Where(a => a != null).ToArray() ?? []; + if (assemblyArray == null || assemblyArray.Length == 0) + return; + + // Ensure at least one assembly supports PDB generation + var supported = new Dictionary(); + var unsupported = new List(); + foreach (var a in assemblyArray) + { + try + { + var file = a.GetMetadataFileOrNull() as PEFile; + if (PortablePdbWriter.HasCodeViewDebugDirectoryEntry(file)) + supported.Add(a, file); + else + unsupported.Add(a); + } + catch + { + unsupported.Add(a); + } + } + if (supported.Count == 0) + { + // none can be generated + string msg = string.Format(Resources.CannotCreatePDBFile, ":" + Environment.NewLine + + string.Join(Environment.NewLine, unsupported.Select(u => Path.GetFileName(u.FileName))) + + Environment.NewLine); + MessageBox.Show(msg); + return; + } + + // Ask for target folder + var dlg = new OpenFolderDialog(); + dlg.Title = Resources.SelectPDBOutputFolder; + if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.FolderName)) + return; + + string targetFolder = dlg.FolderName; + DecompilationOptions options = dockWorkspace.ActiveTabPage.CreateDecompilationOptions(); + + dockWorkspace.RunWithCancellation(ct => Task.Factory.StartNew(() => { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + Stopwatch totalWatch = Stopwatch.StartNew(); + options.CancellationToken = ct; + + int total = assemblyArray.Length; + int processed = 0; + foreach (var assembly in assemblyArray) + { + // only process supported assemblies + if (!supported.TryGetValue(assembly, out var file)) + { + output.WriteLine(string.Format(Resources.CannotCreatePDBFile, Path.GetFileName(assembly.FileName))); + processed++; + if (options.Progress != null) + { + options.Progress.Report(new DecompilationProgress { + Title = string.Format(Resources.GeneratingPortablePDB, Path.GetFileName(assembly.FileName)), + TotalUnits = total, + UnitsCompleted = processed + }); + } + continue; + } + + string fileName = Path.Combine(targetFolder, WholeProjectDecompiler.CleanUpFileName(assembly.ShortName, ".pdb")); + + try + { + using (FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write)) + { + var decompiler = new CSharpDecompiler(file, assembly.GetAssemblyResolver(options.DecompilerSettings.AutoLoadAssemblyReferences), options.DecompilerSettings); + decompiler.CancellationToken = ct; + PortablePdbWriter.WritePdb(file, decompiler, options.DecompilerSettings, stream, progress: options.Progress, currentProgressTitle: string.Format(Resources.GeneratingPortablePDB, Path.GetFileName(assembly.FileName))); + } + output.WriteLine(string.Format(Resources.GeneratedPDBFile, fileName)); + } + catch (OperationCanceledException) + { + output.WriteLine(); + output.WriteLine(Resources.GenerationWasCancelled); + throw; + } + catch (Exception ex) + { + output.WriteLine(string.Format(Resources.GenerationFailedForAssembly, assembly.FileName, ex.Message)); + } + processed++; + if (options.Progress != null) + { + options.Progress.Report(new DecompilationProgress { + Title = string.Format(Resources.GeneratingPortablePDB, Path.GetFileName(assembly.FileName)), + TotalUnits = total, + UnitsCompleted = processed + }); + } + } + + totalWatch.Stop(); + output.WriteLine(); + output.WriteLine(Resources.GenerationCompleteInSeconds, totalWatch.Elapsed.TotalSeconds.ToString("F1")); + output.WriteLine(); + // Select all generated pdb files in explorer + var generatedFiles = assemblyArray + .Select(a => Path.Combine(targetFolder, WholeProjectDecompiler.CleanUpFileName(a.ShortName, ".pdb"))) + .Where(File.Exists) + .ToList(); + if (generatedFiles.Any()) + { + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItems(generatedFiles); }); + } + else + { + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolder(targetFolder); }); + } output.WriteLine(); return output; }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); @@ -113,17 +248,27 @@ namespace ICSharpCode.ILSpy { public override bool CanExecute(object parameter) { - return assemblyTreeModel.SelectedNodes?.Count() == 1 - && assemblyTreeModel.SelectedNodes?.FirstOrDefault() is AssemblyTreeNode tn - && !tn.LoadedAssembly.HasLoadError; + return assemblyTreeModel.SelectedNodes?.Any() == true + && assemblyTreeModel.SelectedNodes?.All(n => n is AssemblyTreeNode tn && !tn.LoadedAssembly.HasLoadError) == true; } public override void Execute(object parameter) { - var assembly = (assemblyTreeModel.SelectedNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly; - if (assembly == null) + var selectedNodes = assemblyTreeModel.SelectedNodes?.OfType().ToArray(); + if (selectedNodes == null || selectedNodes.Length == 0) return; - GeneratePdbContextMenuEntry.GeneratePdbForAssembly(assembly, languageService, dockWorkspace); + + if (selectedNodes.Length == 1) + { + var assembly = selectedNodes.First().LoadedAssembly; + if (assembly == null) + return; + GeneratePdbContextMenuEntry.GeneratePdbForAssembly(assembly, languageService, dockWorkspace); + } + else + { + GeneratePdbContextMenuEntry.GeneratePdbForAssemblies(selectedNodes.Select(n => n.LoadedAssembly), languageService, dockWorkspace); + } } } } diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 322b94689..9b6b5b0d5 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1911,6 +1911,14 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Generated PDB: {0}. + /// + public static string GeneratedPDBFile { + get { + return ResourceManager.GetString("GeneratedPDBFile", resourceCulture); + } + } /// /// Looks up a localized string similar to Generate portable PDB. /// @@ -1920,6 +1928,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Generating portable PDB.... + /// + public static string GeneratingPortablePDB { + get { + return ResourceManager.GetString("GeneratingPortablePDB", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generation complete in {0} seconds.. /// @@ -1929,6 +1946,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Failed to generate PDB for {0}: {1}. + /// + public static string GenerationFailedForAssembly { + get { + return ResourceManager.GetString("GenerationFailedForAssembly", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generation was cancelled.. /// @@ -2620,6 +2646,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Select target folder. + /// + public static string SelectPDBOutputFolder { + get { + return ResourceManager.GetString("SelectPDBOutputFolder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Select version of language to output (Alt+E). /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 29c73bb9c..3ce2becb8 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -175,7 +175,7 @@ Are you sure you want to continue? Entity could not be resolved. Cannot analyze entities from missing assembly references. Add the missing reference and try again. - Cannot create PDB file for {0}, because it does not contain a PE Debug Directory Entry of type 'CodeView'. + Cannot create PDB file for {0} because the PE debug directory type 'CodeView' is missing. Check again @@ -657,9 +657,18 @@ Are you sure you want to continue? Generate portable PDB + + Generated PDB: {0} + + + Generating portable PDB for {0}... + Generation complete in {0} seconds. + + Failed to generate PDB for {0}: {1} + Generation was cancelled. @@ -895,6 +904,9 @@ Do you want to continue? Select PDB... + + Select target folder + Select version of language to output (Alt+E) diff --git a/ILSpy/Properties/Resources.zh-Hans.resx b/ILSpy/Properties/Resources.zh-Hans.resx index 718e0076f..8eb9fc98d 100644 --- a/ILSpy/Properties/Resources.zh-Hans.resx +++ b/ILSpy/Properties/Resources.zh-Hans.resx @@ -176,7 +176,7 @@ 无法解析实体。可能是由于缺少程序集引用。请添加缺少的程序集并重试。 - 无法创建为{0}创建PDB文件,因为它不包含PE调试目录类型 'CodeView'. + 不能为 {0} 创建PDB文件,因为缺少PE调试目录类型 'CodeView'。 再次检查 @@ -610,9 +610,18 @@ 生成 Portable PDB + + 生成的 PDB: {0} + + + 正在为 {0} 生成 Portable PDB... + 生成完成,耗时 {0} 秒。 + + 为 {0} 生成 PDB 失败: {1} + 已取消生成。 @@ -843,6 +852,9 @@ 选择 PDB... + + 选择目标文件夹 + 选择输出语言的版本 diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 209cec9ef..65f8e85c4 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -28,6 +28,7 @@ using System.Threading.Tasks; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.Util; +using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpyX; @@ -187,7 +188,7 @@ namespace ICSharpCode.ILSpy 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 + "\""); }); + result.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItem(solutionFilePath); }); } return result; diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index f8da82342..06169ed3e 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection.Metadata; @@ -1204,7 +1205,7 @@ namespace ICSharpCode.ILSpy.TextView } } output.WriteLine(); - output.AddButton(null, Properties.Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); }); + output.AddButton(null, Properties.Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItem(fileName); }); output.WriteLine(); tcs.SetResult(output); } @@ -1487,4 +1488,25 @@ namespace ICSharpCode.ILSpy.TextView } } } + + // Converter to multiply a double by a factor provided as ConverterParameter + public class MultiplyConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double d && parameter != null) + { + if (double.TryParse(parameter.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out double factor)) + { + return d * factor; + } + } + return Binding.DoNothing; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } } diff --git a/ILSpy/TextView/DecompilerTextView.xaml b/ILSpy/TextView/DecompilerTextView.xaml index dddc96ae6..a83e277e0 100644 --- a/ILSpy/TextView/DecompilerTextView.xaml +++ b/ILSpy/TextView/DecompilerTextView.xaml @@ -25,6 +25,8 @@ + + @@ -96,12 +98,13 @@ - + + - - - + + +