From 0cb6968ce4d8d00fcc10132039adf2a067335195 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 1 May 2026 18:14:23 +0200 Subject: [PATCH] Keep decompiler tab title in sync with selected tree node text Assisted-by: Claude:claude-opus-4-7:Claude Code Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy.Tests/Editor/DecompilerViewTests.cs | 35 +++++++++++++++++++++++ ILSpy/TextView/DecompilerTabPageModel.cs | 20 +++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/ILSpy.Tests/Editor/DecompilerViewTests.cs b/ILSpy.Tests/Editor/DecompilerViewTests.cs index a41254c11..9fbef51f8 100644 --- a/ILSpy.Tests/Editor/DecompilerViewTests.cs +++ b/ILSpy.Tests/Editor/DecompilerViewTests.cs @@ -23,7 +23,10 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; +using ICSharpCode.ILSpy.Properties; + using ILSpy.AppEnv; +using ILSpy.Commands; using ILSpy.TreeNodes; using ILSpy.ViewModels; using ILSpy.Views; @@ -185,6 +188,38 @@ public class DecompilerViewTests tab.Text.Should().Contain("SupportedOSPlatformAttribute"); } + [AvaloniaTest] + public async Task Decompiler_Tab_Title_Tracks_Tree_Node_Text_When_Node_Loads_Late() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + // Open a fresh assembly via the command and immediately let the OpenCommand select it, + // before LoadedAssembly.Text has the rich "(version, tfm)" suffix wired up. This is the + // timing window where the tab title used to lock in the bare ShortName. + var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; + var registry = AppComposition.Current.GetExport(); + var openCommand = registry.Commands + .Single(c => c.Metadata.Header == nameof(Resources._Open)) + .CreateExport().Value; + openCommand.Execute(newAsmPath); + + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.SelectedItem is AssemblyTreeNode n + && string.Equals(n.LoadedAssembly.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); + + var node = (AssemblyTreeNode)vm.AssemblyTreeModel.SelectedItem!; + await node.LoadedAssembly.GetLoadResultAsync(); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + await Waiters.WaitForAsync(() => string.Equals(tab.Title, node.Text?.ToString(), System.StringComparison.Ordinal)); + node.Text!.ToString().Should().NotBe(node.LoadedAssembly.ShortName, + "the rich form (with version + tfm) must be available, otherwise the test isn't exercising the late-update path"); + tab.Title.Should().Be(node.Text!.ToString()); + } + [AvaloniaTest] public async Task Selecting_Assembly_Node_Emits_Header_And_Assembly_Attributes() { diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 44388ffca..1747652b0 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -120,11 +121,24 @@ namespace ILSpy.TextView set { if (currentNode == value) return; + if (currentNode != null) + currentNode.PropertyChanged -= OnCurrentNodePropertyChanged; currentNode = value; + if (currentNode != null) + currentNode.PropertyChanged += OnCurrentNodePropertyChanged; _ = DecompileAsync(); } } + void OnCurrentNodePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + // Tree-node Text can change after we capture the title (e.g. AssemblyTreeNode swaps + // from ShortName to "ShortName (version, tfm)" once the assembly finishes loading). + // Re-read it whenever the node fires Text so the tab header stays in sync. + if (e.PropertyName == nameof(ILSpyTreeNode.Text) && sender is ILSpyTreeNode node) + Title = node.Text?.ToString() ?? "(unnamed)"; + } + public DecompilerTabPageModel() { Title = "Empty"; @@ -145,7 +159,6 @@ namespace ILSpy.TextView return; } - var nodeTitle = node.Text?.ToString() ?? "(unnamed)"; var newSyntaxExtension = language.FileExtension; IsDecompiling = true; @@ -182,7 +195,10 @@ namespace ILSpy.TextView var collectedLookup = output.DefinitionLookup; var collectedUIElements = output.UIElements; await Dispatcher.UIThread.InvokeAsync(() => { - Title = nodeTitle; + // Re-read Text now (instead of capturing it before decompile started) — for + // freshly-opened assemblies, Text only has the rich "(version, tfm)" form + // after the load completes during decompile. + Title = currentNode?.Text?.ToString() ?? "(unnamed)"; SyntaxExtension = newSyntaxExtension; HighlightingModel = model; Foldings = collectedFoldings;