Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 3 additions & 2 deletions ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ public static void WritePdb(
Stream targetStream,
bool noLogo = false,
BlobContentId? pdbId = null,
IProgress<DecompilationProgress> progress = null)
IProgress<DecompilationProgress> progress = null,
string currentProgressTitle = "Generating portable PDB...")
{
MetadataBuilder metadata = new MetadataBuilder();
MetadataReader reader = file.Metadata;
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/CreateDiagramContextMenuEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
22 changes: 21 additions & 1 deletion ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,22 @@ internal static void Save(DockWorkspace dockWorkspace, IEnumerable<SharpTreeNode
dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
AvalonEditTextOutput output = new AvalonEditTextOutput();
Stopwatch stopwatch = Stopwatch.StartNew();
var writtenFiles = new List<string>();
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)
{
Expand All @@ -145,11 +150,15 @@ internal static void Save(DockWorkspace dockWorkspace, IEnumerable<SharpTreeNode
{
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)
{
Expand All @@ -161,7 +170,18 @@ internal static void Save(DockWorkspace dockWorkspace, IEnumerable<SharpTreeNode
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();
Expand Down
175 changes: 160 additions & 15 deletions ILSpy/Commands/GeneratePdbContextMenuEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Composition;
using System.Diagnostics;
using System.IO;
Expand Down Expand Up @@ -47,19 +48,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 All @@ -82,13 +94,13 @@ 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
{
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)
{
Expand All @@ -100,7 +112,130 @@ internal static void GeneratePdbForAssembly(LoadedAssembly assembly, LanguageSer
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<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
var supported = new Dictionary<LoadedAssembly, PEFile>();
var unsupported = new List<LoadedAssembly>();
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);
Comment on lines +148 to +150
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 error message formatting is incorrect. The first parameter to string.Format should be just the assembly filename, not a formatted string with additional content. This will result in a malformed error message like "Cannot create PDB file for :
Assembly1.dll
Assembly2.dll
because the PE debug directory type 'CodeView' is missing."

Consider changing this to properly format the list of unsupported assemblies, for example:

string msg = Resources.CannotCreatePDBFile + Environment.NewLine +
    string.Join(Environment.NewLine, unsupported.Select(u => "  - " + Path.GetFileName(u.FileName)));
MessageBox.Show(msg);
Suggested change
string msg = string.Format(Resources.CannotCreatePDBFile, ":" + Environment.NewLine +
string.Join(Environment.NewLine, unsupported.Select(u => Path.GetFileName(u.FileName)))
+ Environment.NewLine);
string msg = Resources.CannotCreatePDBFile + Environment.NewLine +
string.Join(Environment.NewLine, unsupported.Select(u => " - " + Path.GetFileName(u.FileName)));

Copilot uses AI. Check for mistakes.
MessageBox.Show(msg);
return;
}

// Ask for target folder
var dlg = new OpenFolderDialog();
dlg.Title = Resources.SelectPDBOutputFolder;
if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.FolderName))
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 expression 'A != true' can be simplified to '!A'.

Suggested change
if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.FolderName))
if (!dlg.ShowDialog() || string.IsNullOrWhiteSpace(dlg.FolderName))

Copilot uses AI. Check for mistakes.
return;

string targetFolder = dlg.FolderName;
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)
{
// 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();
Expand All @@ -113,17 +248,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);
}
}
}
}
35 changes: 35 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.

Loading
Loading