Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 136 additions & 12 deletions ILSpy/Commands/GeneratePdbContextMenuEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssemblyTreeNode>().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)
Expand Down Expand Up @@ -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<LoadedAssembly> assemblies, LanguageService languageService, DockWorkspace dockWorkspace)
{
var assemblyArray = assemblies?.Where(a => a != null).ToArray();
if (assemblyArray == null || assemblyArray.Length == 0)
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The redundant null check can be simplified. Line 123 already filters out null values with assemblies?.Where(a => a != null).ToArray() ?? [], so the subsequent check if (assemblyArray == null || assemblyArray.Length == 0) will never have a null assemblyArray.

Suggested change
if (assemblyArray == null || assemblyArray.Length == 0)
if (assemblyArray.Length == 0)

Copilot uses AI. Check for mistakes.
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<AvalonEditTextOutput>.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...",
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progress title "Generating portable PDB..." is hardcoded and not localized. Consider adding this string to the resource files for proper internationalization, similar to other user-facing messages in this file.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sonyps5201314 if you want, you can add this. There are other locations where this same string is used, that might need updating as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

71cfce3 resolved.

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 + "\""); });
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using /select with a folder path (targetFolder) will not work as intended. The /select parameter expects a file path to select in Explorer. To open the folder directly, use:

Process.Start("explorer", "\"" + targetFolder + "\"");

This will open the target folder itself, which is more appropriate for a batch operation where multiple files were generated.

Suggested change
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + targetFolder + "\""); });
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "\"" + targetFolder + "\""); });

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sonyps5201314 Yes, this a legitimate bug: explorer C:\path\to\folder is the syntax for folders.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1f13b80 resolved.
This also incidentally resolves another small 'unmodern' issue in ILSpy: every time 'Open or Select File/Folder' was executed, a new explorer.exe process was launched, and these processes would not automatically exit even after closing the opened windows.
image

output.WriteLine();
return output;
}, ct)).Then(dockWorkspace.ShowText).HandleExceptions();
}
}
}

[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.GeneratePortable), MenuCategory = nameof(Resources.Save))]
Expand All @@ -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<AssemblyTreeNode>().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);
}
}
}
}
27 changes: 27 additions & 0 deletions ILSpy/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions ILSpy/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -657,9 +657,15 @@ Are you sure you want to continue?</value>
<data name="GeneratePortable" xml:space="preserve">
<value>Generate portable PDB</value>
</data>
<data name="GeneratedPDBFile" xml:space="preserve">
<value>Generated PDB: {0}</value>
</data>
<data name="GenerationCompleteInSeconds" xml:space="preserve">
<value>Generation complete in {0} seconds.</value>
</data>
<data name="GenerationFailedForAssembly" xml:space="preserve">
<value>Failed to generate PDB for {0}: {1}</value>
</data>
<data name="GenerationWasCancelled" xml:space="preserve">
<value>Generation was cancelled.</value>
</data>
Expand Down Expand Up @@ -895,6 +901,9 @@ Do you want to continue?</value>
<data name="SelectPDB" xml:space="preserve">
<value>Select PDB...</value>
</data>
<data name="SelectPDBOutputFolder" xml:space="preserve">
<value>Select target folder</value>
</data>
<data name="SelectVersionDropdownTooltip" xml:space="preserve">
<value>Select version of language to output (Alt+E)</value>
</data>
Expand Down
9 changes: 9 additions & 0 deletions ILSpy/Properties/Resources.zh-Hans.resx
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,15 @@
<data name="GeneratePortable" xml:space="preserve">
<value>生成 Portable PDB</value>
</data>
<data name="GeneratedPDBFile" xml:space="preserve">
<value>生成的 PDB: {0}</value>
</data>
<data name="GenerationCompleteInSeconds" xml:space="preserve">
<value>生成完成,耗时 {0} 秒。</value>
</data>
<data name="GenerationFailedForAssembly" xml:space="preserve">
<value>为 {0} 生成 PDB 失败: {1}</value>
</data>
<data name="GenerationWasCancelled" xml:space="preserve">
<value>已取消生成。</value>
</data>
Expand Down Expand Up @@ -843,6 +849,9 @@
<data name="SelectPDB" xml:space="preserve">
<value>选择 PDB...</value>
</data>
<data name="SelectPDBOutputFolder" xml:space="preserve">
<value>选择目标文件夹</value>
</data>
<data name="SelectVersionDropdownTooltip" xml:space="preserve">
<value>选择输出语言的版本</value>
</data>
Expand Down
Loading