Run every Save Code path behind a cancellable progress overlay#3884
Merged
Conversation
siegfriedpammer
force-pushed
the
save-code-export-progress
branch
from
July 16, 2026 06:02
dcfce88 to
760bec4
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR routes all File → Save Code flows for assemblies through existing cancellable/progress UI, aligning Save Code behavior with the Export Project/Solution experience and improving UX for long-running whole-assembly exports.
Changes:
- Routed Save Code → project export (
.csproj) through the existing Export Project frozen, determinate-progress tab. - Routed Save Code → single-file export through the cancellable progress overlay used by normal decompilation, via a shared helper.
- Improved solution export progress reporting (aggregated per-file progress across parallel projects) and added headless UI tests covering the new Save Code paths.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ILSpy/Views/ExportProjectDialog.axaml.cs | Adds solution-name input in solution mode and normalizes .sln naming; watermark shows default derived name. |
| ILSpy/Views/ExportProjectDialog.axaml | Updates dialog sizing/scrolling behavior and adds the solution-name UI. |
| ILSpy/TreeNodes/AssemblyTreeNode.cs | Re-routes Save Code for assemblies to progress-enabled export paths (project vs single-file). |
| ILSpy/SolutionWriter.cs | Changes solution export progress from assembly-granular to aggregated per-file progress across projects. |
| ILSpy/Commands/SolutionExport.cs | Removes the old dedicated solution-export launcher in favor of the unified ProjectExport flow. |
| ILSpy/Commands/SaveCodeHelper.cs | Extracts a shared “save node to file with cancellable progress” helper for Save Code fallback + assembly single-file save. |
| ILSpy/Commands/SaveCodeContextMenuEntry.cs | Switches multi-assembly Save Code solution export to ProjectExport entry points. |
| ILSpy/Commands/ProjectExportOptions.cs | Adds an optional SolutionFileName to support Save Code’s picked .sln name. |
| ILSpy/Commands/ProjectExporter.cs | Skips unloadable assemblies with reporting; wires SolutionFileName into solution export output path. |
| ILSpy/Commands/ProjectExport.cs | Centralizes all whole-assembly-to-disk export launches (Export Project + Save Code variants) and tab titling. |
| ILSpy/Commands/FileCommands.cs | Updates File → Save Code multi-assembly solution export to use ProjectExport. |
| ILSpy.Tests/Languages/ProjectExportRunnerTests.cs | Adds tests for determinate per-file solution progress and skip behavior for unloadable assemblies. |
| ILSpy.Tests/Languages/ExportPreviewRowTests.cs | Adds dialog/headless tests for invalid assemblies, solution-name UI, and sizing behavior. |
| ILSpy.Tests/FixtureAssembly.cs | Adds helper for opening an intentionally broken “assembly” to test skip/report behavior. |
| ILSpy.Tests/Commands/SaveCodeProjectExportTests.cs | Adds end-to-end headless tests for Save Code .csproj / .sln / single-file flows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+86
to
+92
| /// <summary> | ||
| /// True when <paramref name="nodes"/> is the selection shape Save Code maps onto a solution: | ||
| /// several assembly nodes that all loaded as valid assemblies. | ||
| /// </summary> | ||
| public static bool TryGetSolutionAssemblies(IReadOnlyList<SharpTreeNode>? nodes, | ||
| out List<LoadedAssembly> assemblies) | ||
| => TryGetExportableAssemblies(nodes, out assemblies, out var solutionMode) && solutionMode; |
Comment on lines
+74
to
+79
| /// <summary> | ||
| /// Decompiles <paramref name="node"/> into <paramref name="path"/> behind the active tab's | ||
| /// cancellable progress overlay, then shows a "decompilation complete" breadcrumb (with an | ||
| /// Open-folder button) in that tab. Shared by the generic Save Code fallback and the assembly | ||
| /// single-file save so both get the same progress/cancel UX. Silently returns if the user cancels. | ||
| /// </summary> |
Comment on lines
+178
to
+188
| internal static string? NormalizeSolutionFileName(string? typedName) | ||
| { | ||
| var name = typedName?.Trim(); | ||
| if (string.IsNullOrEmpty(name)) | ||
| return null; | ||
| // CleanUpFileName appends the extension itself, so drop one the user already typed rather | ||
| // than ending up with "Solution.sln.sln". | ||
| if (name.EndsWith(".sln", System.StringComparison.OrdinalIgnoreCase)) | ||
| name = name[..^4]; | ||
| return WholeProjectDecompiler.CleanUpFileName(name, ".sln"); | ||
| } |
Comment on lines
+60
to
+69
| // An assembly that failed to load has nothing to decompile. Leave it out and name it in the | ||
| // report: a selection that mixes good and broken assemblies still exports what it can, and the | ||
| // user is told what is missing from the output rather than having to notice it. | ||
| var exportable = assemblies.Where(a => a.IsLoadedAsValidAssembly).ToList(); | ||
| var skipReport = SkipReport(assemblies.Where(a => !a.IsLoadedAsValidAssembly)); | ||
| if (exportable.Count == 0) | ||
| { | ||
| return new SolutionExportResult(false, | ||
| "Nothing to export." + Environment.NewLine + skipReport); | ||
| } |
Comment on lines
+101
to
+108
| // One line per assembly left out of the export. | ||
| static string SkipReport(IEnumerable<LoadedAssembly> skipped) | ||
| { | ||
| var report = new StringBuilder(); | ||
| foreach (var assembly in skipped) | ||
| report.AppendLine($"Skipped '{assembly.ShortName}': the assembly failed to load."); | ||
| return report.ToString(); | ||
| } |
File -> Save Code decompiled a whole assembly on a bare Task.Run: no progress bar, no way to cancel, and (for a .csproj) diverging from both normal decompilation and the dedicated Export Project command, which already report progress. Route the assembly-save paths through the shared UI instead: - The .csproj export reuses the Export Project machinery (ProjectExporter in a frozen, determinate-progress tab), so a large assembly reports per-file progress and can be cancelled while the tree stays browsable. - The single-file save runs behind the same RunWithCancellation overlay that normal decompilation uses. Both entry points into the project export compute the tab title in one place, titling it after the assemblies being exported (their tree-node labels, joined the way a multi-node decompile tab is) so the tab reads the same whether reached via Save Code or Export Project. Assisted-by: Claude:claude-opus-4-8:Claude Code
Save Code on several assemblies had its own copy of the export flow: its own selection matcher, its own frozen-tab runner, and a hard-coded "Exporting solution" tab title -- so the same operation read differently depending on whether it was started from Save Code or Export Project, which titles the tab after the assemblies. Route it through ProjectExport like the single-assembly path already is, leaving one runner and one matcher behind every flow that decompiles whole assemblies to disk. Save Code keeps letting the user name the .sln (the Export Project dialog only asks for a folder and derives the name from it), so the export options now carry an optional solution file name; unset means the old folder-derived name. Assisted-by: Claude:claude-opus-4-8:Claude Code
Two gaps in the export paths, both visible from the same selection. A selection holding an assembly that failed to load was turned away by TryGetExportableAssemblies, so Ctrl+S fell through to the single-node save and quietly wrote just the focused assembly -- the rest of the selection vanished with no report. The predicate now only insists that something in the selection loaded, and the exporter skips what it cannot decompile and names it in the status report. That is also what the dialog always assumed: its "not a valid assembly" row badge was unreachable, because no selection containing one could get that far. The dialog asks for an output folder and derived the .sln name from it, while Save Code lets the user name the file. Now the dialog offers the name too, in solution mode, defaulting (via the placeholder) to the folder- derived name the exporter would pick anyway. Assisted-by: Claude:claude-opus-4-8:Claude Code
Solution export reported once per assembly, when that assembly finished. Nothing was reported before the first one did, so the tab sat on the indeterminate spinner it starts with for most of the run and then jumped straight to the end -- exporting two assemblies showed a spinner, 1 of 2, done. A project that bailed out before decompiling never reported at all, stranding the bar short of the end for the rest of the export. Sum the per-project file counts instead: each parallel worker feeds its own counts into a shared map and the bar reports their total. WholeProjectDecompiler carries its whole file count on every report, so the total is known from a project's first written file rather than its last -- measured on two real assemblies, the bar turns determinate after 245ms instead of 15s, and moves through 978 files rather than 2 assemblies. Each project closes its share out in a finally, so bailing out or cancelling still lets the bar reach the end. The denominator grows over the first second as projects discover their file counts. The alternative -- enumerating every project's types up front -- delays the export itself to make the bar look better, which is the wrong trade. Assisted-by: Claude:claude-opus-4-8:Claude Code
siegfriedpammer
force-pushed
the
save-code-export-progress
branch
from
July 16, 2026 15:52
760bec4 to
d512852
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
File → Save Code on an assembly decompiled the whole assembly on a bare
Task.Run: no progress bar, no way to cancel. For the.csprojcase this also diverged from the dedicated Export Project command, which runs the identical whole-project decompilation but reports progress in a cancellable tab.Change
Every assembly Save Code path now runs behind a cancellable progress overlay, sharing the existing export UI:
.csprojexport reuses the Export Project machinery (ProjectExporterin a frozen, determinate-progress tab), so a large assembly reports per-file progress and can be cancelled while the tree stays browsable.ProjectExport.ExportSingleAssemblyAsyncmirrors the live decompiler settings into the export options, so the output is identical to the previous Save Code behaviour — only the progress/cancel UX is added.RunWithCancellationoverlay normal decompilation uses (extracted intoSaveCodeHelper.SaveNodeToFileWithProgressAsync, shared with the generic node-save fallback).The frozen export tab's title is computed in one place (
RunExportAsync) for both entry points, titling it after the assemblies being exported — their tree-node labels, joined the way a multi-node decompile tab is (DecompilerTabPageModel.ComposeBaseTitle). The tab strip ellipsises it and shows the full text as a tooltip. So the tab reads the same whether reached via Save Code or Export Project.Tests
SaveCodeProjectExportTests(headless Avalonia) covers both paths: the.csprojexport writes the project and surfaces its report + assembly-named title in a frozen tab; the single-file save writes the.csand shows the completion breadcrumb in the cancellable tab. ExistingProjectExportRunnerTests/ProjectExportTests/SaveCodeFallbackTests/RunInNewTabTestsstay green.🤖 Generated with Claude Code