diff --git a/ILSpy.Tests/Docking/FrozenTabRefreshTests.cs b/ILSpy.Tests/Docking/FrozenTabRefreshTests.cs
new file mode 100644
index 000000000..5387f1810
--- /dev/null
+++ b/ILSpy.Tests/Docking/FrozenTabRefreshTests.cs
@@ -0,0 +1,124 @@
+// 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;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ILSpy;
+using ILSpy.AppEnv;
+using ILSpy.Docking;
+using ILSpy.Languages;
+using ILSpy.TextView;
+using ILSpy.TreeNodes;
+using ILSpy.ViewModels;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+///
+/// Frozen tabs keep their own with cached output.
+/// Output-affecting global changes — the language/version combos and the
+/// Redecompile-class display settings — must re-decompile EVERY decompiler tab, not just
+/// the active preview tab, or frozen tabs silently keep showing stale output.
+///
+[TestFixture]
+public class FrozenTabRefreshTests
+{
+ ///
+ /// Boots the app, decompiles Enumerable.Select into the preview tab, freezes it, then
+ /// routes Enumerable.Where into a fresh preview tab. Returns both tab models settled.
+ ///
+ static async Task<(DecompilerTabPageModel Frozen, DecompilerTabPageModel Preview, MainWindowViewModel Vm)> BootWithFrozenAndPreviewTabsAsync()
+ {
+ var (_, vm) = await TestHarness.BootAsync(3);
+
+ var typeNode = vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ typeNode.IsExpanded = true;
+ var selectNode = typeNode.Children.OfType()
+ .First(m => m.MethodDefinition.Name == "Select" && m.MethodDefinition.Parameters.Count == 2);
+ var whereNode = typeNode.Children.OfType()
+ .First(m => m.MethodDefinition.Name == "Where" && m.MethodDefinition.Parameters.Count == 2);
+
+ vm.AssemblyTreeModel.SelectNode(selectNode);
+ vm.DockWorkspace.SettleSelection();
+ var frozen = await vm.DockWorkspace.WaitForDecompiledTextAsync();
+
+ vm.DockWorkspace.FreezeCurrentTab();
+
+ vm.AssemblyTreeModel.SelectNode(whereNode);
+ vm.DockWorkspace.SettleSelection();
+ var preview = await vm.DockWorkspace.WaitForDecompiledTextAsync();
+
+ preview.Should().NotBeSameAs(frozen,
+ "setup precondition — selecting after freeze must route to a fresh preview tab");
+ return (frozen, preview, vm);
+ }
+
+ [AvaloniaTest]
+ public async Task Switching_The_Language_Redecompiles_Frozen_Tabs_Too()
+ {
+ var (frozen, preview, _) = await BootWithFrozenAndPreviewTabsAsync();
+ var frozenCSharp = frozen.Text;
+ var previewCSharp = preview.Text;
+
+ var languageService = AppComposition.Current.GetExport();
+ languageService.CurrentLanguage = languageService.GetLanguage("IL");
+
+ await Waiters.WaitForAsync(
+ () => !frozen.IsDecompiling && !preview.IsDecompiling
+ && frozen.Text != frozenCSharp && preview.Text != previewCSharp,
+ TimeSpan.FromSeconds(15),
+ "both the frozen tab and the preview tab to re-decompile after the language change");
+
+ frozen.Language.Name.Should().Be("IL",
+ "the frozen tab must adopt the newly selected language");
+ frozen.Text.Should().Contain(".method",
+ "the frozen tab must show IL output after the switch");
+ preview.Text.Should().Contain(".method",
+ "the preview tab must show IL output after the switch");
+ }
+
+ [AvaloniaTest]
+ public async Task Toggling_A_Redecompile_Display_Setting_Refreshes_Frozen_Tabs_Too()
+ {
+ var (frozen, preview, _) = await BootWithFrozenAndPreviewTabsAsync();
+ var frozenBefore = frozen.Text;
+ var previewBefore = preview.Text;
+
+ // IndentationUseTabs is in the Redecompile reaction class (DisplaySettingReactions)
+ // and changes every indented line, so the refresh is observable on any output.
+ var display = AppComposition.Current.GetExport().DisplaySettings;
+ display.IndentationUseTabs = !display.IndentationUseTabs;
+
+ await Waiters.WaitForAsync(
+ () => !frozen.IsDecompiling && !preview.IsDecompiling
+ && frozen.Text != frozenBefore && preview.Text != previewBefore,
+ TimeSpan.FromSeconds(15),
+ "both the frozen tab and the preview tab to re-decompile after the display-setting change");
+
+ frozen.Text.Should().NotBe(frozenBefore,
+ "the frozen tab must re-render with the new indentation setting");
+ }
+}
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index 3aff65801..d3a2b8731 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -440,17 +440,28 @@ namespace ILSpy.Docking
///
internal Task CancelPendingOperationsAsync()
{
- if (Layout is not IDockable root)
- return Task.CompletedTask;
- var pending = FlattenDocumentDocks(root)
- .SelectMany(dock => dock.VisibleDockables?.OfType() ?? Enumerable.Empty())
- .Select(tab => tab.Content)
- .OfType()
+ var pending = AllDecompilerTabs()
.Select(tab => tab.CancelPendingAsync())
.ToArray();
return pending.Length == 0 ? Task.CompletedTask : Task.WhenAll(pending);
}
+ ///
+ /// Every decompiler tab model across all document docks, including docks in floating
+ /// windows: the preview tab, frozen tabs, and static-content pages alike. Callers
+ /// that must skip static content (e.g. output refreshes) filter on
+ /// themselves.
+ ///
+ IEnumerable AllDecompilerTabs()
+ {
+ if (Layout is not IDockable root)
+ return [];
+ return FlattenDocumentDocks(root)
+ .SelectMany(dock => dock.VisibleDockables?.OfType() ?? Enumerable.Empty())
+ .Select(tab => tab.Content)
+ .OfType();
+ }
+
// Set true while syncing the tree's selection FROM the active tab so the
// SelectionChanged handler doesn't bounce back into ShowSelectedNode and overwrite
// MainTab.Content with the carved-out tab's node.
@@ -632,15 +643,16 @@ namespace ILSpy.Docking
{
if (e.PropertyName is nameof(LanguageService.CurrentLanguage) or nameof(LanguageService.CurrentVersion))
{
- if (ActiveDecompilerTab is { } tab)
+ // Every decompiler tab caches its own output, so every one re-decompiles —
+ // frozen tabs included, not just the active preview tab. The language
+ // version is read per-run inside TryGetLiveDecompilerSettings, so assigning
+ // the language and re-running covers both combo boxes.
+ foreach (var tab in AllDecompilerTabs())
{
+ if (tab.IsStaticContent)
+ continue;
tab.Language = languageService.CurrentLanguage;
- // Re-decompile by re-assigning the same node so the tab refreshes for the new
- // language or language version (the version is read inside
- // TryGetLiveDecompilerSettings, which builds the per-run DecompilerSettings).
- var node = tab.CurrentNode;
- tab.CurrentNode = null;
- tab.CurrentNode = node;
+ tab.Redecompile();
}
}
}
@@ -720,14 +732,21 @@ namespace ILSpy.Docking
}
///
- /// Re-decompile the decompiler tab's content in place so an output-affecting display setting
- /// takes effect, WITHOUT activating or navigating to it. Unlike
- /// (which re-projects the tree selection via ShowSelectedNode and so activates the preview
- /// tab), changing an option must not steal the user's current tab. Refreshes the cached
- /// decompiler content directly, so it is up to date even when another tab is showing.
+ /// Re-decompiles every decompiler tab's content in place so an output-affecting display
+ /// setting takes effect, WITHOUT activating or navigating to any of them. Unlike
+ /// (which re-projects the tree selection via
+ /// ShowSelectedNode and so activates the preview tab), changing an option must not
+ /// steal the user's current tab. Covers frozen tabs and floated tabs, whose models each
+ /// cache their own output.
///
public void RefreshDecompilerOutputInPlace()
- => decompilerContent?.Redecompile();
+ {
+ foreach (var tab in AllDecompilerTabs())
+ {
+ if (!tab.IsStaticContent)
+ tab.Redecompile();
+ }
+ }
void ShowSelectedNode()
{