From 3ee07e4eb1426392a69a965c62b7790227f2a818 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Thu, 20 Nov 2025 23:16:16 +0800 Subject: [PATCH 01/16] Support batch PDB generation. --- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 148 ++++++++++++++++-- ILSpy/Properties/Resources.Designer.cs | 27 ++++ ILSpy/Properties/Resources.resx | 9 ++ ILSpy/Properties/Resources.zh-Hans.resx | 9 ++ 4 files changed, 181 insertions(+), 12 deletions(-) diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 9f58826b7d..1e4d6768e9 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -47,19 +47,30 @@ class GeneratePdbContextMenuEntry(LanguageService languageService, DockWorkspace { 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) @@ -105,6 +116,109 @@ internal static void GeneratePdbForAssembly(LoadedAssembly assembly, LanguageSer return output; }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); } + + internal static void GeneratePdbForAssemblies(System.Collections.Generic.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 + if (!assemblyArray.Any(a => PortablePdbWriter.HasCodeViewDebugDirectoryEntry(a.GetMetadataFileOrNull() as PEFile))) + { + MessageBox.Show(Resources.CannotCreatePDBFile); + return; + } + + // Ask for target folder + using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) + { + dlg.Description = Resources.SelectPDBOutputFolder; + dlg.RootFolder = Environment.SpecialFolder.MyComputer; + dlg.ShowNewFolderButton = true; + // Show dialog on UI thread + System.Windows.Forms.DialogResult result = dlg.ShowDialog(); + if (result != System.Windows.Forms.DialogResult.OK || string.IsNullOrWhiteSpace(dlg.SelectedPath)) + return; + + string targetFolder = dlg.SelectedPath; + 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) + { + if (ct.IsCancellationRequested) + { + output.WriteLine(); + output.WriteLine(Resources.GenerationWasCancelled); + throw new OperationCanceledException(ct); + } + + var file = assembly.GetMetadataFileOrNull() as PEFile; + if (file == null || !PortablePdbWriter.HasCodeViewDebugDirectoryEntry(file)) + { + output.WriteLine(string.Format(Resources.CannotCreatePDBFile, Path.GetFileName(assembly.FileName))); + processed++; + if (options.Progress != null) + { + options.Progress.Report(new DecompilationProgress { + Title = "Generating portable PDB...", + TotalUnits = total, + UnitsCompleted = processed + }); + } + continue; + } + + string fileName = Path.Combine(targetFolder, WholeProjectDecompiler.CleanUpFileName(assembly.ShortName, ".pdb")); + + try + { + using (FileStream stream = new FileStream(fileName, FileMode.OpenOrCreate, 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); + } + 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 = "Generating portable PDB...", + TotalUnits = total, + UnitsCompleted = processed + }); + } + } + + totalWatch.Stop(); + output.WriteLine(); + output.WriteLine(Resources.GenerationCompleteInSeconds, totalWatch.Elapsed.TotalSeconds.ToString("F1")); + output.WriteLine(); + output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + targetFolder + "\""); }); + output.WriteLine(); + return output; + }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); + } + } } [ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.GeneratePortable), MenuCategory = nameof(Resources.Save))] @@ -113,17 +227,27 @@ class GeneratePdbMainMenuEntry(AssemblyTreeModel assemblyTreeModel, LanguageServ { 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 322b946899..88032f1e69 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1920,6 +1920,15 @@ public static string GeneratePortable { } } + /// + /// 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 Generation complete in {0} seconds.. /// @@ -1938,6 +1947,15 @@ public static string GenerationWasCancelled { } } + /// + /// 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 Go to token. /// @@ -2620,6 +2638,15 @@ public static string SelectPDB { } } + /// + /// 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 29c73bb9c8..b82465de45 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -657,9 +657,15 @@ Are you sure you want to continue? Generate portable PDB + + Generated PDB: {0} + Generation complete in {0} seconds. + + Failed to generate PDB for {0}: {1} + Generation was cancelled. @@ -895,6 +901,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 718e0076f4..c318edad18 100644 --- a/ILSpy/Properties/Resources.zh-Hans.resx +++ b/ILSpy/Properties/Resources.zh-Hans.resx @@ -610,9 +610,15 @@ 生成 Portable PDB + + 生成的 PDB: {0} + 生成完成,耗时 {0} 秒。 + + 为 {0} 生成 PDB 失败: {1} + 已取消生成。 @@ -843,6 +849,9 @@ 选择 PDB... + + 选择目标文件夹 + 选择输出语言的版本 From ef52899b0bd7122f7bc3ec922d2bc46534e1521a Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Fri, 21 Nov 2025 15:37:27 +0800 Subject: [PATCH 02/16] Use `FileMode.Create` for output PDB files to ensure existing files are fully overwritten/truncated. --- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 1e4d6768e9..8732a59ea4 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -93,7 +93,7 @@ internal static void GeneratePdbForAssembly(LoadedAssembly assembly, LanguageSer 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 { @@ -180,7 +180,7 @@ internal static void GeneratePdbForAssemblies(System.Collections.Generic.IEnumer try { - using (FileStream stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write)) + 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; From 71cfce3a486152db9e3139b98ab6c4806459e053 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Fri, 21 Nov 2025 16:54:25 +0800 Subject: [PATCH 03/16] Localize the string `Generating portable PDB...`. --- .../DebugInfo/PortablePdbWriter.cs | 5 ++-- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 8 +++--- ILSpy/Properties/Resources.Designer.cs | 26 ++++++++++++------- ILSpy/Properties/Resources.resx | 3 +++ ILSpy/Properties/Resources.zh-Hans.resx | 3 +++ 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs index 5d2ee3a5d3..f1636dbafc 100644 --- a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs +++ b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs @@ -72,7 +72,8 @@ public static void WritePdb( 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 @@ string BuildFileNameFromTypeName(TypeDefinitionHandle handle) DecompilationProgress currentProgress = new() { TotalUnits = sourceFiles.Count, UnitsCompleted = 0, - Title = "Generating portable PDB..." + Title = currentProgressTitle }; foreach (var sourceFile in sourceFiles) diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 8732a59ea4..7c4a1decb4 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -99,7 +99,7 @@ internal static void GeneratePdbForAssembly(LoadedAssembly assembly, LanguageSer { 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: Resources.GeneratingPortablePDB); } catch (OperationCanceledException) { @@ -168,7 +168,7 @@ internal static void GeneratePdbForAssemblies(System.Collections.Generic.IEnumer if (options.Progress != null) { options.Progress.Report(new DecompilationProgress { - Title = "Generating portable PDB...", + Title = Resources.GeneratingPortablePDB, TotalUnits = total, UnitsCompleted = processed }); @@ -184,7 +184,7 @@ internal static void GeneratePdbForAssemblies(System.Collections.Generic.IEnumer { 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: Resources.GeneratingPortablePDB); } output.WriteLine(string.Format(Resources.GeneratedPDBFile, fileName)); } @@ -202,7 +202,7 @@ internal static void GeneratePdbForAssemblies(System.Collections.Generic.IEnumer if (options.Progress != null) { options.Progress.Report(new DecompilationProgress { - Title = "Generating portable PDB...", + Title = Resources.GeneratingPortablePDB, TotalUnits = total, UnitsCompleted = processed }); diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 88032f1e69..9b6b5b0d56 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1911,6 +1911,14 @@ public static string Forward { } } + /// + /// 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. /// @@ -1921,11 +1929,11 @@ public static string GeneratePortable { } /// - /// Looks up a localized string similar to Generated PDB: {0}. + /// Looks up a localized string similar to Generating portable PDB.... /// - public static string GeneratedPDBFile { + public static string GeneratingPortablePDB { get { - return ResourceManager.GetString("GeneratedPDBFile", resourceCulture); + return ResourceManager.GetString("GeneratingPortablePDB", resourceCulture); } } @@ -1939,20 +1947,20 @@ public static string GenerationCompleteInSeconds { } /// - /// Looks up a localized string similar to Generation was cancelled.. + /// Looks up a localized string similar to Failed to generate PDB for {0}: {1}. /// - public static string GenerationWasCancelled { + public static string GenerationFailedForAssembly { get { - return ResourceManager.GetString("GenerationWasCancelled", resourceCulture); + return ResourceManager.GetString("GenerationFailedForAssembly", resourceCulture); } } /// - /// Looks up a localized string similar to Failed to generate PDB for {0}: {1}. + /// Looks up a localized string similar to Generation was cancelled.. /// - public static string GenerationFailedForAssembly { + public static string GenerationWasCancelled { get { - return ResourceManager.GetString("GenerationFailedForAssembly", resourceCulture); + return ResourceManager.GetString("GenerationWasCancelled", resourceCulture); } } diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index b82465de45..0924816a9b 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -660,6 +660,9 @@ Are you sure you want to continue? Generated PDB: {0} + + Generating portable PDB... + Generation complete in {0} seconds. diff --git a/ILSpy/Properties/Resources.zh-Hans.resx b/ILSpy/Properties/Resources.zh-Hans.resx index c318edad18..2651f17aff 100644 --- a/ILSpy/Properties/Resources.zh-Hans.resx +++ b/ILSpy/Properties/Resources.zh-Hans.resx @@ -613,6 +613,9 @@ 生成的 PDB: {0} + + 正在生成 Portable PDB... + 生成完成,耗时 {0} 秒。 From c5c3505a9851370726add16ce4f48a260ec0d51f Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Fri, 21 Nov 2025 17:09:40 +0800 Subject: [PATCH 04/16] Refine `GeneratePdbForAssemblies` implementation. --- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 7c4a1decb4..c2e8f747f9 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; @@ -117,9 +118,9 @@ internal static void GeneratePdbForAssembly(LoadedAssembly assembly, LanguageSer }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); } - internal static void GeneratePdbForAssemblies(System.Collections.Generic.IEnumerable assemblies, LanguageService languageService, DockWorkspace dockWorkspace) + internal static void GeneratePdbForAssemblies(IEnumerable assemblies, LanguageService languageService, DockWorkspace dockWorkspace) { - var assemblyArray = assemblies?.Where(a => a != null).ToArray(); + var assemblyArray = assemblies?.Where(a => a != null).ToArray() ?? []; if (assemblyArray == null || assemblyArray.Length == 0) return; From 1f13b80a5f90abebd055c4710b89d344802d39a6 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Fri, 21 Nov 2025 21:18:49 +0800 Subject: [PATCH 05/16] Replace direct calls to `explorer.exe` with the Shell API to prevent spawning an `explorer.exe` process that doesn't exit automatically on every call. --- .../Commands/CreateDiagramContextMenuEntry.cs | 2 +- .../ExtractPackageEntryContextMenuEntry.cs | 2 +- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 16 +- ILSpy/SolutionWriter.cs | 2 +- ILSpy/TextView/DecompilerTextView.cs | 2 +- ILSpy/TreeNodes/AssemblyTreeNode.cs | 2 +- ILSpy/Util/ShellHelper.cs | 152 ++++++++++++++++++ 7 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 ILSpy/Util/ShellHelper.cs diff --git a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs index 9f8adaa965..1e2240a0ec 100644 --- a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs +++ b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs @@ -75,7 +75,7 @@ public void Execute(TextViewContext context) 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 292b5aea90..e3870cc986 100644 --- a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs +++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs @@ -161,7 +161,7 @@ internal static void Save(DockWorkspace dockWorkspace, IEnumerable assemb output.WriteLine(); output.WriteLine(Resources.GenerationCompleteInSeconds, totalWatch.Elapsed.TotalSeconds.ToString("F1")); output.WriteLine(); - output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + targetFolder + "\""); }); + // 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(); diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 209cec9efc..1d2aa5d31d 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -187,7 +187,7 @@ await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, project 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, "Open Explorer", delegate { ShellHelper.OpenFolderAndSelectItem(solutionFilePath); }); } return result; diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index f8da82342b..703e403086 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -1204,7 +1204,7 @@ Task SaveToDiskAsync(DecompilationContext context, string } } 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); } diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index f67b862019..194c15ce12 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -696,7 +696,7 @@ public void Execute(TextViewContext context) var path = node.LoadedAssembly.FileName; if (File.Exists(path)) { - GlobalUtils.ExecuteCommand("explorer.exe", $"/select,\"{path}\""); + ShellHelper.OpenFolderAndSelectItem(path); } } } diff --git a/ILSpy/Util/ShellHelper.cs b/ILSpy/Util/ShellHelper.cs new file mode 100644 index 0000000000..f78909b41c --- /dev/null +++ b/ILSpy/Util/ShellHelper.cs @@ -0,0 +1,152 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Linq; +using System.Collections.Generic; + +namespace ICSharpCode.ILSpy.Util +{ + static class ShellHelper + { + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut); + + [DllImport("shell32.dll")] + static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, uint dwFlags); + + [DllImport("shell32.dll")] + static extern IntPtr ILFindLastID(IntPtr pidl); + + [DllImport("ole32.dll")] + static extern void CoTaskMemFree(IntPtr pv); + + public static void OpenFolder(string folderPath) + { + try + { + if (string.IsNullOrEmpty(folderPath)) + return; + if (!Directory.Exists(folderPath)) + return; + + IntPtr folderPidl = IntPtr.Zero; + uint attrs; + int hr = SHParseDisplayName(folderPath, IntPtr.Zero, out folderPidl, 0, out attrs); + if (hr == 0 && folderPidl != IntPtr.Zero) + { + SHOpenFolderAndSelectItems(folderPidl, 0, null, 0); + CoTaskMemFree(folderPidl); + } + else + { + // fallback + Process.Start(new ProcessStartInfo { FileName = folderPath, UseShellExecute = true }); + } + } + catch + { + // ignore + } + } + + public static void OpenFolderAndSelectItem(string path) + { + // Reuse the multi-item implementation for single item selection to avoid duplication. + try + { + if (string.IsNullOrEmpty(path)) + return; + if (Directory.Exists(path)) + { + OpenFolder(path); + return; + } + + if (!File.Exists(path)) + return; + + OpenFolderAndSelectItems(new[] { path }); + } + catch + { + // ignore + } + } + + public static void OpenFolderAndSelectItems(IEnumerable paths) + { + try + { + if (paths == null) + return; + // Group by containing folder + var files = paths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToList(); + if (files.Count == 0) + return; + + var groups = files.GroupBy(p => Path.GetDirectoryName(p)); + foreach (var group in groups) + { + string folder = group.Key; + if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder)) + continue; + + IntPtr folderPidl = IntPtr.Zero; + uint attrs; + int hrFolder = SHParseDisplayName(folder, IntPtr.Zero, out folderPidl, 0, out attrs); + if (hrFolder != 0 || folderPidl == IntPtr.Zero) + { + // fallback: open folder normally + OpenFolder(folder); + continue; + } + + var itemPidlAllocs = new List(); + var relativePidls = new List(); + try + { + foreach (var file in group) + { + IntPtr itemPidl = IntPtr.Zero; + int hrItem = SHParseDisplayName(file, IntPtr.Zero, out itemPidl, 0, out attrs); + if (hrItem == 0 && itemPidl != IntPtr.Zero) + { + IntPtr relative = ILFindLastID(itemPidl); + if (relative != IntPtr.Zero) + { + relativePidls.Add(relative); + itemPidlAllocs.Add(itemPidl); + continue; + } + } + if (itemPidl != IntPtr.Zero) + CoTaskMemFree(itemPidl); + } + + if (relativePidls.Count > 0) + { + SHOpenFolderAndSelectItems(folderPidl, (uint)relativePidls.Count, relativePidls.ToArray(), 0); + } + else + { + // nothing to select - open folder + OpenFolder(folder); + } + } + finally + { + foreach (var p in itemPidlAllocs) + CoTaskMemFree(p); + if (folderPidl != IntPtr.Zero) + CoTaskMemFree(folderPidl); + } + } + } + catch + { + // ignore + } + } + } +} From cae002273cd79ccde6da9fe546ba18c0672d95fc Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Fri, 21 Nov 2025 22:38:43 +0800 Subject: [PATCH 06/16] Batch calls to `ShellHelper.OpenFolderAndSelectItems` instead of looping `OpenFolderAndSelectItem`. --- .../ExtractPackageEntryContextMenuEntry.cs | 22 ++++++++++++++++++- ILSpy/TreeNodes/AssemblyTreeNode.cs | 5 ++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs index e3870cc986..e1209a4afd 100644 --- a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs +++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs @@ -124,17 +124,22 @@ internal static void Save(DockWorkspace dockWorkspace, IEnumerable 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 @@ internal static void Save(DockWorkspace dockWorkspace, IEnumerable 0) + { + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItems(writtenFiles); }); + } + else + { + if (isFile && File.Exists(path)) + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItems(new[] { path }); }); + else + output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolder(path); }); + } output.WriteLine(); return output; }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 194c15ce12..652a230246 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -690,15 +690,18 @@ public void Execute(TextViewContext context) { if (context.SelectedTreeNodes == null) return; + var paths = new List(); foreach (var n in context.SelectedTreeNodes) { var node = GetAssemblyTreeNode(n); var path = node.LoadedAssembly.FileName; if (File.Exists(path)) { - ShellHelper.OpenFolderAndSelectItem(path); + paths.Add(path); } } + if (paths.Count > 0) + ShellHelper.OpenFolderAndSelectItems(paths); } } From cfc5d2f2489365bd76c2f4b44edd4bac702edd11 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Fri, 21 Nov 2025 22:51:24 +0800 Subject: [PATCH 07/16] Localize the string `Open Explorer`. --- ILSpy/SolutionWriter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 1d2aa5d31d..65f8e85c47 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -28,6 +28,7 @@ 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 @@ await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, project result.WriteLine(); result.WriteLine("Elapsed time: " + stopwatch.Elapsed.TotalSeconds.ToString("F1") + " seconds."); result.WriteLine(); - result.AddButton(null, "Open Explorer", delegate { ShellHelper.OpenFolderAndSelectItem(solutionFilePath); }); + result.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItem(solutionFilePath); }); } return result; From e807f1ade2d9d4565c56f298d6d280ef2c2b2480 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Fri, 21 Nov 2025 23:07:12 +0800 Subject: [PATCH 08/16] Fix `OpenCmdHere` malfunction when ILSpy is running from a different drive than the OS. --- ILSpy/TreeNodes/AssemblyTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 652a230246..3d1b8c20d5 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -741,7 +741,7 @@ public void Execute(TextViewContext context) var path = Path.GetDirectoryName(node.LoadedAssembly.FileName); if (Directory.Exists(path)) { - GlobalUtils.ExecuteCommand("cmd.exe", $"/k \"cd {path}\""); + GlobalUtils.ExecuteCommand("cmd.exe", $"/k \"cd /d {path}\""); } } } From 5625db2a96d8f05ba6c916dec26c5cfbe69781dc Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Sat, 22 Nov 2025 00:50:28 +0800 Subject: [PATCH 09/16] Refine `GeneratePdbForAssemblies` implementation. --- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 36 +++++++++++++------ ILSpy/Properties/Resources.resx | 2 +- ILSpy/Properties/Resources.zh-Hans.resx | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index a80f89ff99..e453bb7ae4 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -125,9 +125,30 @@ internal static void GeneratePdbForAssemblies(IEnumerable assemb return; // Ensure at least one assembly supports PDB generation - if (!assemblyArray.Any(a => PortablePdbWriter.HasCodeViewDebugDirectoryEntry(a.GetMetadataFileOrNull() as PEFile))) + var supported = new Dictionary(); + var unsupported = new List(); + foreach (var a in assemblyArray) { - MessageBox.Show(Resources.CannotCreatePDBFile); + 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; } @@ -154,15 +175,8 @@ internal static void GeneratePdbForAssemblies(IEnumerable assemb int processed = 0; foreach (var assembly in assemblyArray) { - if (ct.IsCancellationRequested) - { - output.WriteLine(); - output.WriteLine(Resources.GenerationWasCancelled); - throw new OperationCanceledException(ct); - } - - var file = assembly.GetMetadataFileOrNull() as PEFile; - if (file == null || !PortablePdbWriter.HasCodeViewDebugDirectoryEntry(file)) + // only process supported assemblies + if (!supported.TryGetValue(assembly, out var file)) { output.WriteLine(string.Format(Resources.CannotCreatePDBFile, Path.GetFileName(assembly.FileName))); processed++; diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 0924816a9b..e8b5af6692 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 diff --git a/ILSpy/Properties/Resources.zh-Hans.resx b/ILSpy/Properties/Resources.zh-Hans.resx index 2651f17aff..20bbc1dce5 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'。 再次检查 From a973b2e71d1d606980d3cade0df8ced9a91d4901 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Sat, 22 Nov 2025 01:15:58 +0800 Subject: [PATCH 10/16] Replace WinForms `FolderBrowserDialog` with WPF `OpenFolderDialog`. --- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 140 +++++++++--------- 1 file changed, 67 insertions(+), 73 deletions(-) diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index e453bb7ae4..915f1ba11e 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -153,66 +153,27 @@ internal static void GeneratePdbForAssemblies(IEnumerable assemb } // Ask for target folder - using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) - { - dlg.Description = Resources.SelectPDBOutputFolder; - dlg.RootFolder = Environment.SpecialFolder.MyComputer; - dlg.ShowNewFolderButton = true; - // Show dialog on UI thread - System.Windows.Forms.DialogResult result = dlg.ShowDialog(); - if (result != System.Windows.Forms.DialogResult.OK || string.IsNullOrWhiteSpace(dlg.SelectedPath)) - return; + var dlg = new OpenFolderDialog(); + dlg.Title = Resources.SelectPDBOutputFolder; + if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.FolderName)) + return; - string targetFolder = dlg.SelectedPath; - DecompilationOptions options = dockWorkspace.ActiveTabPage.CreateDecompilationOptions(); + 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; + 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) + int total = assemblyArray.Length; + int processed = 0; + foreach (var assembly in assemblyArray) + { + // only process supported assemblies + if (!supported.TryGetValue(assembly, out var file)) { - // 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 = Resources.GeneratingPortablePDB, - 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: Resources.GeneratingPortablePDB); - } - 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)); - } + output.WriteLine(string.Format(Resources.CannotCreatePDBFile, Path.GetFileName(assembly.FileName))); processed++; if (options.Progress != null) { @@ -222,29 +183,62 @@ internal static void GeneratePdbForAssemblies(IEnumerable assemb UnitsCompleted = processed }); } + continue; } - 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()) + string fileName = Path.Combine(targetFolder, WholeProjectDecompiler.CleanUpFileName(assembly.ShortName, ".pdb")); + + try { - output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolderAndSelectItems(generatedFiles); }); + 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: Resources.GeneratingPortablePDB); + } + output.WriteLine(string.Format(Resources.GeneratedPDBFile, fileName)); } - else + catch (OperationCanceledException) { - output.AddButton(null, Resources.OpenExplorer, delegate { ShellHelper.OpenFolder(targetFolder); }); + output.WriteLine(); + output.WriteLine(Resources.GenerationWasCancelled); + throw; } - output.WriteLine(); - return output; - }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); - } + catch (Exception ex) + { + output.WriteLine(string.Format(Resources.GenerationFailedForAssembly, assembly.FileName, ex.Message)); + } + processed++; + if (options.Progress != null) + { + options.Progress.Report(new DecompilationProgress { + Title = Resources.GeneratingPortablePDB, + 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(); } } From e027a1a38a9a367ab6d8fdc2bb4fe536d637d6fb Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Tue, 25 Nov 2025 15:36:12 +0800 Subject: [PATCH 11/16] Add license header --- ILSpy/Util/ShellHelper.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ILSpy/Util/ShellHelper.cs b/ILSpy/Util/ShellHelper.cs index f78909b41c..24c877373e 100644 --- a/ILSpy/Util/ShellHelper.cs +++ b/ILSpy/Util/ShellHelper.cs @@ -1,3 +1,21 @@ +// Copyright (c) 2025 sonyps5201314 +// +// 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.Diagnostics; using System.IO; From 752e6ed9c049e95923581a3d16d997d5e36e6be5 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Tue, 25 Nov 2025 15:44:33 +0800 Subject: [PATCH 12/16] Exclude duplicate entries entered by the user within `OpenFolderAndSelectItems`. --- ILSpy/Util/ShellHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ILSpy/Util/ShellHelper.cs b/ILSpy/Util/ShellHelper.cs index 24c877373e..7bc26d92aa 100644 --- a/ILSpy/Util/ShellHelper.cs +++ b/ILSpy/Util/ShellHelper.cs @@ -99,7 +99,7 @@ public static void OpenFolderAndSelectItems(IEnumerable paths) if (paths == null) return; // Group by containing folder - var files = paths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToList(); + var files = paths.Distinct(StringComparer.OrdinalIgnoreCase).Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToList(); if (files.Count == 0) return; From b631b55054dc90e2faf2f415c4e4c451202793a4 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Tue, 25 Nov 2025 15:53:01 +0800 Subject: [PATCH 13/16] Explicitly declare that `ShellHelper.cs` is a module that allows Pinvoke. --- ILSpy/Util/ShellHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ILSpy/Util/ShellHelper.cs b/ILSpy/Util/ShellHelper.cs index 7bc26d92aa..be0c32244b 100644 --- a/ILSpy/Util/ShellHelper.cs +++ b/ILSpy/Util/ShellHelper.cs @@ -23,6 +23,8 @@ using System.Linq; using System.Collections.Generic; +#pragma warning disable CA1060 // Move pinvokes to native methods class + namespace ICSharpCode.ILSpy.Util { static class ShellHelper From da6d2576d68176363f8f3eef49569a5dd5614c51 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Tue, 25 Nov 2025 23:11:20 +0800 Subject: [PATCH 14/16] Use `FileMode.Create` for output PDB files to ensure existing files are fully overwritten/truncated. --- ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 01de79d53f..518ae08635 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -484,7 +484,7 @@ int GeneratePdbForAssembly(string assemblyFileName, string pdbFileName, CommandL 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); From 4e5727f25871715e805c569856b4dcd417c8ffd6 Mon Sep 17 00:00:00 2001 From: sonyps5201314 Date: Wed, 26 Nov 2025 00:24:49 +0800 Subject: [PATCH 15/16] Show original filenames when generating PDBs to improve UX during batch processing. --- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 8 +++---- ILSpy/Properties/Resources.resx | 2 +- ILSpy/Properties/Resources.zh-Hans.resx | 2 +- ILSpy/TextView/DecompilerTextView.cs | 22 +++++++++++++++++++ ILSpy/TextView/DecompilerTextView.xaml | 11 ++++++---- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 915f1ba11e..e7b18de410 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -100,7 +100,7 @@ internal static void GeneratePdbForAssembly(LoadedAssembly assembly, LanguageSer { 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: Resources.GeneratingPortablePDB); + PortablePdbWriter.WritePdb(file, decompiler, options.DecompilerSettings, stream, progress: options.Progress, currentProgressTitle: string.Format(Resources.GeneratingPortablePDB, Path.GetFileName(assembly.FileName))); } catch (OperationCanceledException) { @@ -178,7 +178,7 @@ internal static void GeneratePdbForAssemblies(IEnumerable assemb if (options.Progress != null) { options.Progress.Report(new DecompilationProgress { - Title = Resources.GeneratingPortablePDB, + Title = string.Format(Resources.GeneratingPortablePDB, Path.GetFileName(assembly.FileName)), TotalUnits = total, UnitsCompleted = processed }); @@ -194,7 +194,7 @@ internal static void GeneratePdbForAssemblies(IEnumerable assemb { 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: Resources.GeneratingPortablePDB); + 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)); } @@ -212,7 +212,7 @@ internal static void GeneratePdbForAssemblies(IEnumerable assemb if (options.Progress != null) { options.Progress.Report(new DecompilationProgress { - Title = Resources.GeneratingPortablePDB, + Title = string.Format(Resources.GeneratingPortablePDB, Path.GetFileName(assembly.FileName)), TotalUnits = total, UnitsCompleted = processed }); diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index e8b5af6692..3ce2becb8c 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -661,7 +661,7 @@ Are you sure you want to continue? Generated PDB: {0} - Generating portable PDB... + Generating portable PDB for {0}... Generation complete in {0} seconds. diff --git a/ILSpy/Properties/Resources.zh-Hans.resx b/ILSpy/Properties/Resources.zh-Hans.resx index 20bbc1dce5..8eb9fc98da 100644 --- a/ILSpy/Properties/Resources.zh-Hans.resx +++ b/ILSpy/Properties/Resources.zh-Hans.resx @@ -614,7 +614,7 @@ 生成的 PDB: {0} - 正在生成 Portable PDB... + 正在为 {0} 生成 Portable PDB... 生成完成,耗时 {0} 秒。 diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index 703e403086..06169ed3e0 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -22,6 +22,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection.Metadata; @@ -1487,4 +1488,25 @@ public static void RegisterHighlighting( } } } + + // 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 dddc96ae6c..a83e277e03 100644 --- a/ILSpy/TextView/DecompilerTextView.xaml +++ b/ILSpy/TextView/DecompilerTextView.xaml @@ -25,6 +25,8 @@ + + @@ -96,12 +98,13 @@ - + + - - - + + +