diff --git a/ILSpy.Tests/Docking/RunInNewTabTests.cs b/ILSpy.Tests/Docking/RunInNewTabTests.cs
new file mode 100644
index 000000000..ba24db236
--- /dev/null
+++ b/ILSpy.Tests/Docking/RunInNewTabTests.cs
@@ -0,0 +1,94 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// 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.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Avalonia.Threading;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ILSpy.TextView;
+using ILSpy.TreeNodes;
+using ILSpy.ViewModels;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Docking;
+
+///
+/// A long-running operation started via DockWorkspace.RunInNewTabAsync runs in its own
+/// frozen tab, so selecting tree nodes (which re-decompiles the preview tab) does not cancel it —
+/// unlike RunWithCancellation, which runs on the preview tab navigation cancels.
+///
+[TestFixture]
+public class RunInNewTabTests
+{
+ [AvaloniaTest]
+ public async Task A_Long_Op_In_A_New_Tab_Is_Not_Cancelled_By_Tree_Navigation()
+ {
+ var (_, vm) = await TestHarness.BootAsync(3);
+ var dock = vm.DockWorkspace;
+
+ var started = new TaskCompletionSource();
+ var release = new TaskCompletionSource();
+ CancellationToken capturedToken = default;
+
+ int tabsBefore = dock.Documents!.VisibleDockables!.OfType().Count();
+
+ var runTask = dock.RunInNewTabAsync("Long op", async token => {
+ capturedToken = token;
+ started.SetResult();
+ await release.Task;
+ var o = new AvaloniaEditTextOutput { Title = "Long op report" };
+ o.Write("complete");
+ o.WriteLine();
+ return o;
+ });
+
+ await started.Task;
+
+ dock.Documents!.VisibleDockables!.OfType().Count()
+ .Should().BeGreaterThan(tabsBefore, "the long op opens its own tab");
+
+ // Navigate while the op is running: select a type -> the preview tab decompiles.
+ var typeNode = vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ vm.AssemblyTreeModel.SelectNode(typeNode);
+ await dock.WaitForDecompiledTextAsync();
+ for (int i = 0; i < 6; i++)
+ {
+ Dispatcher.UIThread.RunJobs();
+ await Task.Delay(20);
+ }
+
+ capturedToken.IsCancellationRequested.Should().BeFalse(
+ "navigating the tree must NOT cancel a long op running in its own frozen tab");
+
+ // Let it finish; its report lands in its tab.
+ release.SetResult();
+ await runTask;
+
+ dock.Documents!.VisibleDockables!.OfType()
+ .Any(t => t.Content is DecompilerTabPageModel d && d.Text != null && d.Text.Contains("complete"))
+ .Should().BeTrue("the finished op shows its report in its tab");
+ }
+}
diff --git a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs
index aa9af3995..90de4e868 100644
--- a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs
+++ b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs
@@ -75,17 +75,9 @@ namespace ILSpy.Commands
if (string.IsNullOrEmpty(outputFolder))
return;
- try
- {
- var output = await dockWorkspace.RunWithCancellation(
- token => Task.Run(() => RunGenerator(assemblyFile, outputFolder), token),
- Resources.CreatingDiagram);
- dockWorkspace.ShowText(output);
- }
- catch (OperationCanceledException)
- {
- // User cancelled — leave the previous tab content visible.
- }
+ // Run in a dedicated frozen tab so browsing the tree while the diagram generates can't cancel it.
+ await dockWorkspace.RunInNewTabAsync(Resources.CreatingDiagram,
+ token => Task.Run(() => RunGenerator(assemblyFile, outputFolder), token)).ConfigureAwait(true);
}
static AvaloniaEditTextOutput RunGenerator(string assemblyFile, string outputFolder)
diff --git a/ILSpy/Commands/DecompileAllCommand.cs b/ILSpy/Commands/DecompileAllCommand.cs
index 2c37225ef..45be73de7 100644
--- a/ILSpy/Commands/DecompileAllCommand.cs
+++ b/ILSpy/Commands/DecompileAllCommand.cs
@@ -65,46 +65,42 @@ namespace ILSpy.Commands
async Task ExecuteAsync()
{
- try
- {
- var report = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
- var output = new AvaloniaEditTextOutput { Title = "Decompile All" };
- var bag = new ConcurrentBag();
- Parallel.ForEach(
- Partitioner.Create(assemblyTreeModel.AssemblyList!.GetAssemblies(), loadBalance: true),
- new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = token },
- asm => {
- if (asm.HasLoadError)
- return;
- var watch = Stopwatch.StartNew();
- Exception? ex = null;
- var path = Path.Combine(OutputDir, asm.ShortName + ".cs");
- try
- {
- using var writer = new StreamWriter(path);
- var options = new DecompilationOptions {
- CancellationToken = token,
- FullDecompilation = true,
- };
- new CSharpLanguage().DecompileAssembly(asm, new PlainTextOutput(writer), options);
- }
- catch (Exception caught)
- {
- ex = caught;
- }
- watch.Stop();
- bag.Add(asm.ShortName + " - " + watch.Elapsed + (ex != null ? " - " + ex.GetType().Name : ""));
- });
- foreach (var line in bag.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
- {
- output.Write(line);
- output.WriteLine();
- }
- return output;
- }, token), "Decompiling all assemblies…");
- dockWorkspace.ShowText(report);
- }
- catch (OperationCanceledException) { }
+ // Run in a dedicated frozen tab so navigation cannot cancel this long run.
+ await dockWorkspace.RunInNewTabAsync("Decompiling all assemblies…", token => Task.Run(() => {
+ var output = new AvaloniaEditTextOutput { Title = "Decompile All" };
+ var bag = new ConcurrentBag();
+ Parallel.ForEach(
+ Partitioner.Create(assemblyTreeModel.AssemblyList!.GetAssemblies(), loadBalance: true),
+ new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = token },
+ asm => {
+ if (asm.HasLoadError)
+ return;
+ var watch = Stopwatch.StartNew();
+ Exception? ex = null;
+ var path = Path.Combine(OutputDir, asm.ShortName + ".cs");
+ try
+ {
+ using var writer = new StreamWriter(path);
+ var options = new DecompilationOptions {
+ CancellationToken = token,
+ FullDecompilation = true,
+ };
+ new CSharpLanguage().DecompileAssembly(asm, new PlainTextOutput(writer), options);
+ }
+ catch (Exception caught)
+ {
+ ex = caught;
+ }
+ watch.Stop();
+ bag.Add(asm.ShortName + " - " + watch.Elapsed + (ex != null ? " - " + ex.GetType().Name : ""));
+ });
+ foreach (var line in bag.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
+ {
+ output.Write(line);
+ output.WriteLine();
+ }
+ return output;
+ }, token));
}
}
@@ -136,47 +132,43 @@ namespace ILSpy.Commands
async Task ExecuteAsync()
{
- try
- {
- var report = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
- var output = new AvaloniaEditTextOutput { Title = "Disassemble All" };
- var bag = new ConcurrentBag();
- Parallel.ForEach(
- Partitioner.Create(assemblyTreeModel.AssemblyList!.GetAssemblies(), loadBalance: true),
- new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = token },
- asm => {
- if (asm.HasLoadError)
- return;
- var watch = Stopwatch.StartNew();
- Exception? ex = null;
- var safeName = (asm.Text?.ToString() ?? asm.ShortName).Replace("(", "").Replace(")", "").Replace(' ', '_');
- var path = Path.Combine(OutputDir, safeName + ".il");
- try
- {
- using var writer = new StreamWriter(path);
- var options = new DecompilationOptions {
- CancellationToken = token,
- FullDecompilation = true,
- };
- new ILLanguage().DecompileAssembly(asm, new PlainTextOutput(writer), options);
- }
- catch (Exception caught)
- {
- ex = caught;
- }
- watch.Stop();
- bag.Add(asm.ShortName + " - " + watch.Elapsed + (ex != null ? " - " + ex.GetType().Name : ""));
- });
- foreach (var line in bag.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
- {
- output.Write(line);
- output.WriteLine();
- }
- return output;
- }, token), "Disassembling all assemblies…");
- dockWorkspace.ShowText(report);
- }
- catch (OperationCanceledException) { }
+ // Run in a dedicated frozen tab so navigation cannot cancel this long run.
+ await dockWorkspace.RunInNewTabAsync("Disassembling all assemblies…", token => Task.Run(() => {
+ var output = new AvaloniaEditTextOutput { Title = "Disassemble All" };
+ var bag = new ConcurrentBag();
+ Parallel.ForEach(
+ Partitioner.Create(assemblyTreeModel.AssemblyList!.GetAssemblies(), loadBalance: true),
+ new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = token },
+ asm => {
+ if (asm.HasLoadError)
+ return;
+ var watch = Stopwatch.StartNew();
+ Exception? ex = null;
+ var safeName = (asm.Text?.ToString() ?? asm.ShortName).Replace("(", "").Replace(")", "").Replace(' ', '_');
+ var path = Path.Combine(OutputDir, safeName + ".il");
+ try
+ {
+ using var writer = new StreamWriter(path);
+ var options = new DecompilationOptions {
+ CancellationToken = token,
+ FullDecompilation = true,
+ };
+ new ILLanguage().DecompileAssembly(asm, new PlainTextOutput(writer), options);
+ }
+ catch (Exception caught)
+ {
+ ex = caught;
+ }
+ watch.Stop();
+ bag.Add(asm.ShortName + " - " + watch.Elapsed + (ex != null ? " - " + ex.GetType().Name : ""));
+ });
+ foreach (var line in bag.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
+ {
+ output.Write(line);
+ output.WriteLine();
+ }
+ return output;
+ }, token));
}
}
@@ -211,26 +203,22 @@ namespace ILSpy.Commands
var nodes = assemblyTreeModel.SelectedItems.OfType().ToArray();
if (nodes.Length == 0)
return;
- try
- {
- var report = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
- var watch = Stopwatch.StartNew();
- var options = new DecompilationOptions { CancellationToken = token };
- for (int i = 0; i < NumRuns; i++)
- {
- foreach (var node in nodes)
- node.Decompile(language, new PlainTextOutput(), options);
- }
- watch.Stop();
- var output = new AvaloniaEditTextOutput { Title = "Decompile 100×" };
- var msPerRun = watch.Elapsed.TotalMilliseconds / NumRuns;
- output.Write($"Average time: {msPerRun:f1}ms");
- output.WriteLine();
- return output;
- }, token), "Decompiling 100×…");
- dockWorkspace.ShowText(report);
- }
- catch (OperationCanceledException) { }
+ // Run in a dedicated frozen tab so navigation cannot cancel this long run.
+ await dockWorkspace.RunInNewTabAsync("Decompiling 100×…", token => Task.Run(() => {
+ var watch = Stopwatch.StartNew();
+ var options = new DecompilationOptions { CancellationToken = token };
+ for (int i = 0; i < NumRuns; i++)
+ {
+ foreach (var node in nodes)
+ node.Decompile(language, new PlainTextOutput(), options);
+ }
+ watch.Stop();
+ var output = new AvaloniaEditTextOutput { Title = "Decompile 100×" };
+ var msPerRun = watch.Elapsed.TotalMilliseconds / NumRuns;
+ output.Write($"Average time: {msPerRun:f1}ms");
+ output.WriteLine();
+ return output;
+ }, token));
}
}
}
diff --git a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs
index 01dd01b3b..7e47b1846 100644
--- a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs
+++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs
@@ -110,40 +110,33 @@ namespace ILSpy.Commands
internal static async Task SaveAsync(DockWorkspace dockWorkspace, IReadOnlyList nodes, string path, bool isFile)
{
- try
- {
- var output = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
- var output = new AvaloniaEditTextOutput { Title = "Extract package entry" };
- var stopwatch = Stopwatch.StartNew();
- var fileNameCounts = new Dictionary(Platform.FileNameComparer);
- foreach (var (entry, fileName) in CollectAllFiles(nodes))
+ // Run in a dedicated frozen tab so browsing the tree while extraction runs can't cancel it.
+ await dockWorkspace.RunInNewTabAsync("Extracting…", token => Task.Run(() => {
+ var output = new AvaloniaEditTextOutput { Title = "Extract package entry" };
+ var stopwatch = Stopwatch.StartNew();
+ var fileNameCounts = new Dictionary(Platform.FileNameComparer);
+ foreach (var (entry, fileName) in CollectAllFiles(nodes))
+ {
+ var actualFileName = WholeProjectDecompiler.SanitizeFileName(fileName);
+ while (fileNameCounts.TryGetValue(actualFileName, out var index))
{
- var actualFileName = WholeProjectDecompiler.SanitizeFileName(fileName);
- while (fileNameCounts.TryGetValue(actualFileName, out var index))
- {
- index++;
- fileNameCounts[actualFileName] = index;
- actualFileName = Path.ChangeExtension(actualFileName, index + Path.GetExtension(actualFileName));
- }
- if (!fileNameCounts.ContainsKey(actualFileName))
- fileNameCounts[actualFileName] = 1;
- SaveEntry(output, entry, isFile ? path : Path.Combine(path, actualFileName));
+ index++;
+ fileNameCounts[actualFileName] = index;
+ actualFileName = Path.ChangeExtension(actualFileName, index + Path.GetExtension(actualFileName));
}
- stopwatch.Stop();
- output.Write(string.Format(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")));
- output.WriteLine();
- output.WriteLine();
- var openTarget = isFile ? path : path;
- output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenInShell(openTarget, isFile));
- output.WriteLine();
- return output;
- }, token), "Extracting…");
- dockWorkspace.ShowText(output);
- }
- catch (OperationCanceledException)
- {
- // User cancelled — leave the previous tab content visible.
- }
+ if (!fileNameCounts.ContainsKey(actualFileName))
+ fileNameCounts[actualFileName] = 1;
+ SaveEntry(output, entry, isFile ? path : Path.Combine(path, actualFileName));
+ }
+ stopwatch.Stop();
+ output.Write(string.Format(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")));
+ output.WriteLine();
+ output.WriteLine();
+ var openTarget = isFile ? path : path;
+ output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenInShell(openTarget, isFile));
+ output.WriteLine();
+ return output;
+ }, token)).ConfigureAwait(true);
}
static IEnumerable<(PackageEntry Entry, string TargetFileName)> CollectAllFiles(IReadOnlyList nodes)
diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs
index a8d1ac6fa..652e08ab7 100644
--- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs
+++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs
@@ -109,54 +109,47 @@ namespace ILSpy.Commands
if (string.IsNullOrEmpty(folder))
return;
- try
- {
- var output = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
- var output = new AvaloniaEditTextOutput { Title = "Generate Portable PDB" };
- var totalWatch = Stopwatch.StartNew();
- foreach (var (assembly, file) in supported)
+ // 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(() => {
+ var output = new AvaloniaEditTextOutput { Title = "Generate Portable PDB" };
+ var totalWatch = Stopwatch.StartNew();
+ foreach (var (assembly, file) in supported)
+ {
+ var pdbFileName = Path.Combine(folder, WholeProjectDecompiler.CleanUpFileName(assembly.ShortName, ".pdb"));
+ try
{
- var pdbFileName = Path.Combine(folder, WholeProjectDecompiler.CleanUpFileName(assembly.ShortName, ".pdb"));
- try
- {
- using var stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write);
- var resolver = assembly.GetAssemblyResolver();
- var settings = new ICSharpCode.Decompiler.DecompilerSettings();
- var decompiler = new CSharpDecompiler(file, resolver, settings) {
- CancellationToken = token,
- };
- new PortablePdbWriter().WritePdb(file, decompiler, settings, stream);
- output.Write(string.Format(Resources.GeneratedPDBFile, pdbFileName));
- output.WriteLine();
- }
- catch (OperationCanceledException)
- {
- output.WriteLine();
- output.Write(Resources.GenerationWasCancelled);
- output.WriteLine();
- throw;
- }
- catch (Exception ex)
- {
- output.Write(string.Format(Resources.GenerationFailedForAssembly, assembly.FileName, ex.Message));
- output.WriteLine();
- }
+ using var stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write);
+ var resolver = assembly.GetAssemblyResolver();
+ var settings = new ICSharpCode.Decompiler.DecompilerSettings();
+ var decompiler = new CSharpDecompiler(file, resolver, settings) {
+ CancellationToken = token,
+ };
+ new PortablePdbWriter().WritePdb(file, decompiler, settings, stream);
+ output.Write(string.Format(Resources.GeneratedPDBFile, pdbFileName));
+ output.WriteLine();
}
- totalWatch.Stop();
- output.WriteLine();
- output.Write(string.Format(Resources.GenerationCompleteInSeconds, totalWatch.Elapsed.TotalSeconds.ToString("F1")));
- output.WriteLine();
- output.WriteLine();
- output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenFolder(folder));
- output.WriteLine();
- return output;
- }, token), Resources.GeneratingPortablePDB);
- dockWorkspace.ShowText(output);
- }
- catch (OperationCanceledException)
- {
- // User cancelled — leave previous tab content visible.
- }
+ catch (OperationCanceledException)
+ {
+ output.WriteLine();
+ output.Write(Resources.GenerationWasCancelled);
+ output.WriteLine();
+ throw;
+ }
+ catch (Exception ex)
+ {
+ output.Write(string.Format(Resources.GenerationFailedForAssembly, assembly.FileName, ex.Message));
+ output.WriteLine();
+ }
+ }
+ totalWatch.Stop();
+ output.WriteLine();
+ output.Write(string.Format(Resources.GenerationCompleteInSeconds, totalWatch.Elapsed.TotalSeconds.ToString("F1")));
+ output.WriteLine();
+ output.WriteLine();
+ output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenFolder(folder));
+ output.WriteLine();
+ return output;
+ }, token)).ConfigureAwait(true);
}
static void OpenFolder(string path)
diff --git a/ILSpy/Commands/Pdb2XmlCommand.cs b/ILSpy/Commands/Pdb2XmlCommand.cs
index 785196f55..4a941365b 100644
--- a/ILSpy/Commands/Pdb2XmlCommand.cs
+++ b/ILSpy/Commands/Pdb2XmlCommand.cs
@@ -71,28 +71,24 @@ namespace ILSpy.Commands
async Task ExecuteAsync(AssemblyTreeNode[] nodes)
{
- try
- {
- var options = PdbToXmlOptions.IncludeEmbeddedSources
- | PdbToXmlOptions.IncludeMethodSpans
- | PdbToXmlOptions.IncludeTokens;
- var output = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
- var output = new AvaloniaEditTextOutput { Title = "PDB as XML", SyntaxExtensionOverride = ".xml" };
- var writer = new TextOutputWriter(output);
- foreach (var node in nodes)
- {
- var pdbFileName = Path.ChangeExtension(node.LoadedAssembly.FileName, ".pdb");
- if (!File.Exists(pdbFileName))
- continue;
- using var pdbStream = File.OpenRead(pdbFileName);
- using var peStream = File.OpenRead(node.LoadedAssembly.FileName);
- PdbToXmlConverter.ToXml(writer, pdbStream, peStream, options);
- }
- return output;
- }, token), "Dumping PDB as XML…");
- dockWorkspace.ShowText(output);
- }
- catch (System.OperationCanceledException) { }
+ var options = PdbToXmlOptions.IncludeEmbeddedSources
+ | PdbToXmlOptions.IncludeMethodSpans
+ | PdbToXmlOptions.IncludeTokens;
+ // Run in a dedicated frozen tab so navigation cannot cancel this long run.
+ await dockWorkspace.RunInNewTabAsync("Dumping PDB as XML…", token => Task.Run(() => {
+ var output = new AvaloniaEditTextOutput { Title = "PDB as XML", SyntaxExtensionOverride = ".xml" };
+ var writer = new TextOutputWriter(output);
+ foreach (var node in nodes)
+ {
+ var pdbFileName = Path.ChangeExtension(node.LoadedAssembly.FileName, ".pdb");
+ if (!File.Exists(pdbFileName))
+ continue;
+ using var pdbStream = File.OpenRead(pdbFileName);
+ using var peStream = File.OpenRead(node.LoadedAssembly.FileName);
+ PdbToXmlConverter.ToXml(writer, pdbStream, peStream, options);
+ }
+ return output;
+ }, token));
}
}
diff --git a/ILSpy/Commands/ProjectExport.cs b/ILSpy/Commands/ProjectExport.cs
index ff7b45e3c..f208ff01c 100644
--- a/ILSpy/Commands/ProjectExport.cs
+++ b/ILSpy/Commands/ProjectExport.cs
@@ -76,28 +76,21 @@ namespace ILSpy.Commands
return;
var settingsClone = settingsService.DecompilerSettings.Clone();
- try
- {
- var output = await dockWorkspace.RunWithCancellation(async token => {
- var result = await ProjectExporter.ExportAsync(assemblies, solutionMode, options, settingsClone, language, token)
- .ConfigureAwait(false);
- var o = new AvaloniaEditTextOutput { Title = ICSharpCode.ILSpy.Properties.Resources.ExportProjectSolution };
- o.Write(result.StatusText);
+ // Run in a dedicated frozen tab so browsing the tree while the export runs can't cancel it.
+ await dockWorkspace.RunInNewTabAsync(ICSharpCode.ILSpy.Properties.Resources.ExportProjectSolution, async token => {
+ var result = await ProjectExporter.ExportAsync(assemblies, solutionMode, options, settingsClone, language, token)
+ .ConfigureAwait(false);
+ var o = new AvaloniaEditTextOutput { Title = ICSharpCode.ILSpy.Properties.Resources.ExportProjectSolution };
+ o.Write(result.StatusText);
+ o.WriteLine();
+ if (result.Success && Directory.Exists(options.OutputDirectory))
+ {
+ o.AddButton(null, ICSharpCode.ILSpy.Properties.Resources.OpenExplorer,
+ (_, _) => OpenFolder(options.OutputDirectory));
o.WriteLine();
- if (result.Success && Directory.Exists(options.OutputDirectory))
- {
- o.AddButton(null, ICSharpCode.ILSpy.Properties.Resources.OpenExplorer,
- (_, _) => OpenFolder(options.OutputDirectory));
- o.WriteLine();
- }
- return o;
- }, ICSharpCode.ILSpy.Properties.Resources.ExportProjectSolution);
- dockWorkspace.ShowText(output);
- }
- catch (OperationCanceledException)
- {
- // User cancelled — leave the previous tab content visible.
- }
+ }
+ return o;
+ });
}
static void OpenFolder(string path)
diff --git a/ILSpy/Commands/SolutionExport.cs b/ILSpy/Commands/SolutionExport.cs
index 049554068..8c0f73149 100644
--- a/ILSpy/Commands/SolutionExport.cs
+++ b/ILSpy/Commands/SolutionExport.cs
@@ -60,8 +60,8 @@ namespace ILSpy.Commands
///
/// Prompts for a target .sln file and exports into it,
- /// one decompiled project each. The status report lands in the active decompiler tab, where
- /// the user is already looking. Does nothing if the user cancels the picker.
+ /// one decompiled project each. The export runs in its own frozen tab so browsing the tree
+ /// can't cancel it, and the status report lands there. Does nothing if the user cancels the picker.
///
public static async Task PromptAndExportAsync(IReadOnlyList assemblies,
Language language, DockWorkspace dockWorkspace)
@@ -72,27 +72,20 @@ namespace ILSpy.Commands
if (string.IsNullOrEmpty(path))
return;
- try
- {
- var output = await dockWorkspace.RunWithCancellation(async token => {
- var result = await SolutionWriter.CreateSolutionAsync(path, language, assemblies, token)
- .ConfigureAwait(false);
- var o = new AvaloniaEditTextOutput { Title = Resources._SaveCode };
- o.Write(result.StatusText);
+ // Run in a dedicated frozen tab so browsing the tree while the export runs can't cancel it.
+ await dockWorkspace.RunInNewTabAsync("Exporting solution", async token => {
+ var result = await SolutionWriter.CreateSolutionAsync(path, language, assemblies, token)
+ .ConfigureAwait(false);
+ var o = new AvaloniaEditTextOutput { Title = Resources._SaveCode };
+ o.Write(result.StatusText);
+ o.WriteLine();
+ if (result.Success && Path.GetDirectoryName(path) is { Length: > 0 } directory)
+ {
+ o.AddButton(null, Resources.OpenExplorer, (_, _) => OpenFolder(directory));
o.WriteLine();
- if (result.Success && Path.GetDirectoryName(path) is { Length: > 0 } directory)
- {
- o.AddButton(null, Resources.OpenExplorer, (_, _) => OpenFolder(directory));
- o.WriteLine();
- }
- return o;
- }, "Exporting solution");
- dockWorkspace.ShowText(output);
- }
- catch (OperationCanceledException)
- {
- // User cancelled — leave the previous tab content visible.
- }
+ }
+ return o;
+ }).ConfigureAwait(true);
}
static void OpenFolder(string path)
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index 566b2411c..e100ffaf4 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -1063,6 +1063,35 @@ namespace ILSpy.Docking
ActiveDecompilerTab?.ShowText(output);
}
+ ///
+ /// Runs a long-running operation in a NEW, frozen document tab with its own
+ /// decompile/cancellation scope, then shows the operation's report in that tab. Unlike
+ /// (which runs on the active preview tab, whose
+ /// cancellation token tree-node navigation cancels), a frozen tab is never the navigation
+ /// target — so selecting another node while the operation runs cannot cancel it. Use this
+ /// for project/solution export, PDB generation, and similar work the user may want to keep
+ /// running while they browse. Cancellation (the tab's Cancel overlay) is honoured.
+ ///
+ public async Task RunInNewTabAsync(string title,
+ Func> work)
+ {
+ ArgumentNullException.ThrowIfNull(work);
+ var content = new TextView.DecompilerTabPageModel {
+ Language = languageService.CurrentLanguage,
+ Title = title,
+ };
+ OpenNewTab(content);
+ try
+ {
+ var output = await content.RunWithCancellation(work, title).ConfigureAwait(true);
+ content.ShowText(output);
+ }
+ catch (OperationCanceledException)
+ {
+ // User cancelled — leave the (empty) report tab; they can close it.
+ }
+ }
+
///
/// Opens a fresh sibling tab whose is the
/// supplied viewmodel. Used by explicit "Open in new tab" gestures and by static