From f1b40a68b81c3f71155bc91c2fa1e4812dc142e1 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 10 Jun 2026 10:37:10 +0200 Subject: [PATCH] Fold the full exception trace into failure reports Generate-PDB and assembly-node decompile failures reported only the exception Message, which is too thin to act on: the runtime-async PDB-generation crash surfaces as a bare "Object reference not set to an instance of an object" with no indication of where it came from. Append the whole exception (type, message, stack trace, inner exceptions) inside a default-collapsed "Exception details" fold so the report stays scannable while the trace is one click away. The fold span is counted in WriteLine() calls rather than embedded newline characters, so the trace is emitted one line per WriteLine() -- a single Write() of the multi-line string would collapse to one counted line and be dropped as a single-line fold. Also fixes the PDB tab title showing a literal "{0}": the GeneratingPortablePDB resource is a format string whose placeholder was never filled. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy/Commands/PdbGenerator.cs | 10 +++++++++- ILSpy/SmartTextOutputExtensions.cs | 29 +++++++++++++++++++++++++++++ ILSpy/TreeNodes/AssemblyTreeNode.cs | 4 +--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/ILSpy/Commands/PdbGenerator.cs b/ILSpy/Commands/PdbGenerator.cs index 3bdec887b..2a75b5967 100644 --- a/ILSpy/Commands/PdbGenerator.cs +++ b/ILSpy/Commands/PdbGenerator.cs @@ -99,8 +99,15 @@ namespace ILSpy.Commands if (string.IsNullOrEmpty(folder)) return; + // GeneratingPortablePDB is a "...for {0}..." format string; fill the placeholder with the + // single assembly's name, or a count when several were selected, so the tab title isn't a + // literal "{0}". + string title = supported.Count == 1 + ? string.Format(Resources.GeneratingPortablePDB, supported.Keys.First().ShortName) + : string.Format(Resources.GeneratingPortablePDB, supported.Count + " assemblies"); + // Run in a dedicated frozen tab so browsing the tree while PDBs generate can't cancel it. - await dockWorkspace.RunInNewTabAsync(Resources.GeneratingPortablePDB, token => Task.Run(() => { + await dockWorkspace.RunInNewTabAsync(title, token => Task.Run(() => { var output = new AvaloniaEditTextOutput { Title = "Generate Portable PDB" }; var totalWatch = Stopwatch.StartNew(); foreach (var (assembly, file) in supported) @@ -129,6 +136,7 @@ namespace ILSpy.Commands { output.Write(string.Format(Resources.GenerationFailedForAssembly, assembly.FileName, ex.Message)); output.WriteLine(); + output.WriteExceptionDetails(ex); } } totalWatch.Stop(); diff --git a/ILSpy/SmartTextOutputExtensions.cs b/ILSpy/SmartTextOutputExtensions.cs index 8bce49d7e..50ef8f96e 100644 --- a/ILSpy/SmartTextOutputExtensions.cs +++ b/ILSpy/SmartTextOutputExtensions.cs @@ -25,6 +25,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using ICSharpCode.Decompiler; using ICSharpCode.ILSpy.Properties; using ILSpy.TextView; @@ -34,6 +35,34 @@ namespace ILSpy { public static class SmartTextOutputExtensions { + /// + /// Writes the full exception (type, message, stack trace, inner exceptions) inside a + /// default-collapsed "Exception details" fold. Callers write their own header line first; this + /// keeps a failure report scannable while leaving the trace one click away when a failure is not + /// self-explanatory. + /// + public static void WriteExceptionDetails(this ITextOutput output, Exception ex) + { + ArgumentNullException.ThrowIfNull(output); + ArgumentNullException.ThrowIfNull(ex); + // Blank line between the caller's header and the fold so the collapsed "Exception details" + // marker is visually separated from the message above it. + output.WriteLine(); + output.MarkFoldStart("Exception details", true); + // The fold span is counted in WriteLine() calls, not in the '\n' characters embedded in a + // single Write(). Emit the trace one line per WriteLine() so the fold genuinely spans + // multiple lines; otherwise it collapses to a single line and is dropped as noise. + var lines = ex.ToString().Split('\n'); + for (int i = 0; i < lines.Length; i++) + { + if (i > 0) + output.WriteLine(); + output.Write(lines[i].TrimEnd('\r')); + } + output.MarkFoldEnd(); + output.WriteLine(); + } + /// /// Appends the standard "Open Explorer" result button that opens in /// the OS file manager, followed by a blank line. Shared tail of the export / save / PDB outputs. diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 666d9fb2b..d617cfb93 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -468,9 +468,7 @@ namespace ILSpy.TreeNodes { language.WriteCommentLine(output, message); output.WriteLine(); - output.MarkFoldStart("Exception details", true); - output.Write(ex.ToString()); - output.MarkFoldEnd(); + output.WriteExceptionDetails(ex); } try