Browse Source

Support batch PDB generation. (#3619)

* Support batch PDB generation.

* Use `FileMode.Create` for output PDB files to ensure existing files are fully overwritten/truncated.

* Localize the string `Generating portable PDB...`.

* Refine `GeneratePdbForAssemblies` implementation.

* 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.

* Batch calls to `ShellHelper.OpenFolderAndSelectItems` instead of looping `OpenFolderAndSelectItem`.

* Localize the string `Open Explorer`.

* Fix `OpenCmdHere` malfunction when ILSpy is running from a different drive than the OS.

* Refine `GeneratePdbForAssemblies` implementation.

* Replace WinForms `FolderBrowserDialog` with WPF `OpenFolderDialog`.

* Add license header

* Exclude duplicate entries entered by the user within `OpenFolderAndSelectItems`.

* Explicitly declare that `ShellHelper.cs` is a module that allows Pinvoke.

* Use `FileMode.Create` for output PDB files to ensure existing files are fully overwritten/truncated.

* Show original filenames when generating PDBs to improve UX during batch processing.
pull/3620/head
sonyps5201314 1 month ago committed by GitHub
parent
commit
193a463766
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs
  2. 2
      ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
  3. 2
      ILSpy/Commands/CreateDiagramContextMenuEntry.cs
  4. 22
      ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs
  5. 167
      ILSpy/Commands/GeneratePdbContextMenuEntry.cs
  6. 35
      ILSpy/Properties/Resources.Designer.cs
  7. 14
      ILSpy/Properties/Resources.resx
  8. 14
      ILSpy/Properties/Resources.zh-Hans.resx
  9. 3
      ILSpy/SolutionWriter.cs
  10. 24
      ILSpy/TextView/DecompilerTextView.cs
  11. 11
      ILSpy/TextView/DecompilerTextView.xaml
  12. 7
      ILSpy/TreeNodes/AssemblyTreeNode.cs
  13. 174
      ILSpy/Util/ShellHelper.cs

5
ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs

@ -72,7 +72,8 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -72,7 +72,8 @@ namespace ICSharpCode.Decompiler.DebugInfo
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;
@ -99,7 +100,7 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -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)

2
ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

@ -484,7 +484,7 @@ Examples: @@ -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);

2
ILSpy/Commands/CreateDiagramContextMenuEntry.cs

@ -75,7 +75,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -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();

22
ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs

@ -124,17 +124,22 @@ namespace ICSharpCode.ILSpy @@ -124,17 +124,22 @@ namespace ICSharpCode.ILSpy
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)
{
@ -145,11 +150,15 @@ namespace ICSharpCode.ILSpy @@ -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 @@ -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();

167
ILSpy/Commands/GeneratePdbContextMenuEntry.cs

@ -17,6 +17,7 @@ @@ -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 @@ -47,19 +48,30 @@ namespace ICSharpCode.ILSpy
{
public void Execute(TextViewContext context)
{
var assembly = (context.SelectedTreeNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly;
var selectedNodes = context.SelectedTreeNodes?.OfType<AssemblyTreeNode>().ToArray();
if (selectedNodes == null || selectedNodes.Length == 0)
return;
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 @@ -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 @@ -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<LoadedAssembly> 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<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);
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<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();
@ -113,17 +248,27 @@ namespace ICSharpCode.ILSpy @@ -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;
var selectedNodes = assemblyTreeModel.SelectedNodes?.OfType<AssemblyTreeNode>().ToArray();
if (selectedNodes == null || selectedNodes.Length == 0)
return;
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
ILSpy/Properties/Resources.Designer.cs generated

@ -1911,6 +1911,14 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1911,6 +1911,14 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Generated PDB: {0}.
/// </summary>
public static string GeneratedPDBFile {
get {
return ResourceManager.GetString("GeneratedPDBFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Generate portable PDB.
/// </summary>
@ -1920,6 +1928,15 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1920,6 +1928,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Generating portable PDB....
/// </summary>
public static string GeneratingPortablePDB {
get {
return ResourceManager.GetString("GeneratingPortablePDB", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Generation complete in {0} seconds..
/// </summary>
@ -1929,6 +1946,15 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1929,6 +1946,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Failed to generate PDB for {0}: {1}.
/// </summary>
public static string GenerationFailedForAssembly {
get {
return ResourceManager.GetString("GenerationFailedForAssembly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Generation was cancelled..
/// </summary>
@ -2620,6 +2646,15 @@ namespace ICSharpCode.ILSpy.Properties { @@ -2620,6 +2646,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Select target folder.
/// </summary>
public static string SelectPDBOutputFolder {
get {
return ResourceManager.GetString("SelectPDBOutputFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select version of language to output (Alt+E).
/// </summary>

14
ILSpy/Properties/Resources.resx

@ -175,7 +175,7 @@ Are you sure you want to continue?</value> @@ -175,7 +175,7 @@ Are you sure you want to continue?</value>
<value>Entity could not be resolved. Cannot analyze entities from missing assembly references. Add the missing reference and try again.</value>
</data>
<data name="CannotCreatePDBFile" xml:space="preserve">
<value>Cannot create PDB file for {0}, because it does not contain a PE Debug Directory Entry of type 'CodeView'.</value>
<value>Cannot create PDB file for {0} because the PE debug directory type 'CodeView' is missing.</value>
</data>
<data name="CheckAgain" xml:space="preserve">
<value>Check again</value>
@ -657,9 +657,18 @@ Are you sure you want to continue?</value> @@ -657,9 +657,18 @@ 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="GeneratingPortablePDB" xml:space="preserve">
<value>Generating portable PDB for {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>
@ -895,6 +904,9 @@ Do you want to continue?</value> @@ -895,6 +904,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>

14
ILSpy/Properties/Resources.zh-Hans.resx

@ -176,7 +176,7 @@ @@ -176,7 +176,7 @@
<value>无法解析实体。可能是由于缺少程序集引用。请添加缺少的程序集并重试。</value>
</data>
<data name="CannotCreatePDBFile" xml:space="preserve">
<value>无法创建为{0}创建PDB文件,因为它不包含PE调试目录类型 'CodeView'.</value>
<value>不能为 {0} 创建PDB文件,因为缺少PE调试目录类型 'CodeView'。</value>
</data>
<data name="CheckAgain" xml:space="preserve">
<value>再次检查</value>
@ -610,9 +610,18 @@ @@ -610,9 +610,18 @@
<data name="GeneratePortable" xml:space="preserve">
<value>生成 Portable PDB</value>
</data>
<data name="GeneratedPDBFile" xml:space="preserve">
<value>生成的 PDB: {0}</value>
</data>
<data name="GeneratingPortablePDB" xml:space="preserve">
<value>正在为 {0} 生成 Portable PDB...</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>
@ -843,6 +852,9 @@ @@ -843,6 +852,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>

3
ILSpy/SolutionWriter.cs

@ -28,6 +28,7 @@ using System.Threading.Tasks; @@ -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 @@ -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;

24
ILSpy/TextView/DecompilerTextView.cs

@ -22,6 +22,7 @@ using System; @@ -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 @@ -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 @@ -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();
}
}
}

11
ILSpy/TextView/DecompilerTextView.xaml

@ -25,6 +25,8 @@ @@ -25,6 +25,8 @@
</Setter.Value>
</Setter>
</Style>
<!-- Converter to compute a fraction of the control width for MaxWidth bindings -->
<local:MultiplyConverter x:Key="MultiplyConverter" />
</UserControl.Resources>
<Grid>
<Border BorderThickness="1,1,0,1" BorderBrush="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}">
@ -96,12 +98,13 @@ @@ -96,12 +98,13 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
<!-- make center column auto-sized to content so it expands with the title -->
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Name="progressTitle" FontSize="14pt" Text="{x:Static properties:Resources.Decompiling}" Margin="3" />
<ProgressBar Name="progressBar" Height="16" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center" MaxWidth="{Binding ActualWidth, ElementName=self, Converter={StaticResource MultiplyConverter}, ConverterParameter=0.75}">
<TextBlock Name="progressTitle" FontSize="14pt" Text="{x:Static properties:Resources.Decompiling}" Margin="3" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center" />
<ProgressBar Name="progressBar" Height="16" Width="{Binding ActualWidth, ElementName=self, Converter={StaticResource MultiplyConverter}, ConverterParameter=0.75}" />
<TextBlock Name="progressText" Visibility="Collapsed" Margin="3" />
<Button Click="CancelButton_Click" HorizontalAlignment="Center" Margin="3" Content="{x:Static properties:Resources.Cancel}" />
</StackPanel>

7
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -690,15 +690,18 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -690,15 +690,18 @@ namespace ICSharpCode.ILSpy.TreeNodes
{
if (context.SelectedTreeNodes == null)
return;
var paths = new List<string>();
foreach (var n in context.SelectedTreeNodes)
{
var node = GetAssemblyTreeNode(n);
var path = node.LoadedAssembly.FileName;
if (File.Exists(path))
{
GlobalUtils.ExecuteCommand("explorer.exe", $"/select,\"{path}\"");
paths.Add(path);
}
}
if (paths.Count > 0)
ShellHelper.OpenFolderAndSelectItems(paths);
}
}
@ -738,7 +741,7 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -738,7 +741,7 @@ namespace ICSharpCode.ILSpy.TreeNodes
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}\"");
}
}
}

174
ILSpy/Util/ShellHelper.cs

@ -0,0 +1,174 @@ @@ -0,0 +1,174 @@
// 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.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
#pragma warning disable CA1060 // Move pinvokes to native methods class
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)
{
nint folderPidl = IntPtr.Zero;
try
{
if (string.IsNullOrEmpty(folderPath))
return;
if (!Directory.Exists(folderPath))
return;
int hr = SHParseDisplayName(folderPath, IntPtr.Zero, out folderPidl, 0, out var attrs);
Marshal.ThrowExceptionForHR(hr);
hr = SHOpenFolderAndSelectItems(folderPidl, 0, null, 0);
Marshal.ThrowExceptionForHR(hr);
}
catch (Exception ex) when (ex is COMException or Win32Exception)
{
// fall back to Process.Start
OpenFolderFallback(folderPath);
}
finally
{
if (folderPidl != IntPtr.Zero)
CoTaskMemFree(folderPidl);
}
}
static void OpenFolderFallback(string path)
{
try
{
Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true });
}
catch (Exception)
{
// Process.Start can throw several errors (not all of them documented),
// just ignore all of them.
}
}
public static void OpenFolderAndSelectItem(string path)
{
// Reuse the multi-item implementation for single item selection to avoid duplication.
if (string.IsNullOrEmpty(path))
return;
if (Directory.Exists(path))
{
OpenFolder(path);
return;
}
if (!File.Exists(path))
return;
OpenFolderAndSelectItems(path);
}
public static void OpenFolderAndSelectItems(params IEnumerable<string> paths)
{
if (paths == null)
return;
// Group by containing folder
var files = paths.Distinct(StringComparer.OrdinalIgnoreCase).Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToList();
if (files.Count == 0)
return;
var itemPidlAllocs = new List<IntPtr>();
var relativePidls = new List<IntPtr>();
foreach (var group in files.GroupBy(Path.GetDirectoryName))
{
string folder = group.Key;
if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
continue;
IntPtr folderPidl = IntPtr.Zero;
try
{
int hrFolder = SHParseDisplayName(folder, IntPtr.Zero, out folderPidl, 0, out uint attrs);
Marshal.ThrowExceptionForHR(hrFolder);
foreach (var file in group)
{
int hrItem = SHParseDisplayName(file, IntPtr.Zero, out var 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)
{
int hr = SHOpenFolderAndSelectItems(folderPidl, (uint)relativePidls.Count, relativePidls.ToArray(), 0);
Marshal.ThrowExceptionForHR(hr);
}
else
{
// nothing to select - open folder
OpenFolder(folder);
}
}
catch (Exception ex) when (ex is COMException or Win32Exception)
{
// fall back to Process.Start
OpenFolderFallback(folder);
}
finally
{
foreach (var p in itemPidlAllocs)
CoTaskMemFree(p);
if (folderPidl != IntPtr.Zero)
CoTaskMemFree(folderPidl);
itemPidlAllocs.Clear();
relativePidls.Clear();
}
}
}
}
}
Loading…
Cancel
Save