From 53121d7d14f12b903ffc55b821c4e422751c2f34 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 24 Jun 2026 07:13:08 +0200 Subject: [PATCH] Rebuild analyzer rich text on theme and language change The analyzer signature colours depend on the active theme, and the formatting on the active language, but the highlighted RichText was built once and cached. Toggling the theme or switching the decompiler language therefore left existing analyzer results showing the old colours / syntax. Key the node's RichText cache on the (theme, language) pair so it rebuilds when either changes, and have the rich-text cell re-render rich rows on both ThemeManager.ThemeChanged and the language settings' PropertyChanged (cleaned up on Node change and on detach, like the existing subscriptions). Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy/Analyzers/AnalyzerEntityTreeNode.cs | 14 ++-- ILSpy/Controls/TreeView/RichNodeText.cs | 79 +++++++++++++++++++---- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs index 7e1bf5fdf..49786563a 100644 --- a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs @@ -29,6 +29,7 @@ using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AssemblyTree; using ICSharpCode.ILSpy.Controls.TreeView; +using ICSharpCode.ILSpy.Themes; using ICSharpCode.ILSpy.Util; namespace ICSharpCode.ILSpy.Analyzers @@ -60,18 +61,21 @@ namespace ICSharpCode.ILSpy.Analyzers | ConversionFlags.ShowTypeParameterList; RichText? richText; - bool richTextComputed; + // The build key: the signature's colours depend on the active theme and its formatting on the + // active language, so the cache is rebuilt when either changes (not just once). + (string? theme, object language)? richTextKey; /// - /// The highlighted, type-name-emboldened signature for this row, computed once and cached. - /// Falls back to (plain Text) for non-entity rows. + /// The highlighted, type-name-emboldened signature for this row, cached until the theme or + /// language changes. Falls back to (plain Text) for non-entity rows. /// public RichText? CreateRichText() { - if (!richTextComputed) + var key = (ThemeManager.Current.Theme, (object)Language); + if (richTextKey != key) { richText = BuildRichText(); - richTextComputed = true; + richTextKey = key; } return richText; } diff --git a/ILSpy/Controls/TreeView/RichNodeText.cs b/ILSpy/Controls/TreeView/RichNodeText.cs index 3cb2d3bcc..83bfd75d7 100644 --- a/ILSpy/Controls/TreeView/RichNodeText.cs +++ b/ILSpy/Controls/TreeView/RichNodeText.cs @@ -16,13 +16,16 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; using System.ComponentModel; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.TextView; +using ICSharpCode.ILSpy.Themes; using ICSharpCode.ILSpyX.TreeView; namespace ICSharpCode.ILSpy.Controls.TreeView @@ -39,9 +42,25 @@ namespace ICSharpCode.ILSpy.Controls.TreeView public static readonly AttachedProperty NodeProperty = AvaloniaProperty.RegisterAttached("Node", typeof(RichNodeText)); - // Holds the live PropertyChanged subscription so a recycled TextBlock detaches cleanly. - static readonly AttachedProperty HandlerProperty = - AvaloniaProperty.RegisterAttached("Handler", typeof(RichNodeText)); + // Live PropertyChanged subscription on the bound node, so a recycled TextBlock detaches cleanly. + static readonly AttachedProperty TextHandlerProperty = + AvaloniaProperty.RegisterAttached("TextHandler", typeof(RichNodeText)); + + // Live ThemeChanged / language-settings subscriptions (rich nodes only): the signature colours + // and per-language formatting are baked into the node's cached RichText, so the inlines must be + // rebuilt when the theme or the active language changes. + static readonly AttachedProperty ThemeHandlerProperty = + AvaloniaProperty.RegisterAttached("ThemeHandler", typeof(RichNodeText)); + + static readonly AttachedProperty LanguageHandlerProperty = + AvaloniaProperty.RegisterAttached("LanguageHandler", typeof(RichNodeText)); + + static readonly AttachedProperty CleanupHookedProperty = + AvaloniaProperty.RegisterAttached("CleanupHooked", typeof(RichNodeText)); + + static LanguageSettings? languageSettings; + static LanguageSettings? GetLanguageSettings() + => languageSettings ??= AppComposition.TryGetExport()?.SessionSettings.LanguageSettings; static RichNodeText() { @@ -53,31 +72,65 @@ namespace ICSharpCode.ILSpy.Controls.TreeView static void OnNodeChanged(TextBlock textBlock, AvaloniaPropertyChangedEventArgs e) { - if (e.OldValue is INotifyPropertyChanged oldNode - && textBlock.GetValue(HandlerProperty) is { } oldHandler) - { - oldNode.PropertyChanged -= oldHandler; - textBlock.SetValue(HandlerProperty, null); - } + Detach(textBlock, e.OldValue as SharpTreeNode); var node = e.NewValue as SharpTreeNode; - if (node is INotifyPropertyChanged newNode) + if (node is INotifyPropertyChanged notify) { // Mirror the {Binding Text} the cell used to have: refresh when Text changes // (e.g. a lazily-loaded node swapping its placeholder for the real label). - void Handler(object? sender, PropertyChangedEventArgs args) + void TextHandler(object? sender, PropertyChangedEventArgs args) { if (args.PropertyName is nameof(SharpTreeNode.Text) or null or "") Render(textBlock, node); } - newNode.PropertyChanged += Handler; - textBlock.SetValue(HandlerProperty, Handler); + notify.PropertyChanged += TextHandler; + textBlock.SetValue(TextHandlerProperty, (PropertyChangedEventHandler)TextHandler); + } + + if (node is IRichTextNode) + { + // Rebuild on theme change (palette) and on language change (signature syntax/colours). + void ThemeHandler(object? sender, EventArgs args) => Render(textBlock, GetNode(textBlock)); + + ThemeManager.Current.ThemeChanged += ThemeHandler; + textBlock.SetValue(ThemeHandlerProperty, (EventHandler)ThemeHandler); + + if (GetLanguageSettings() is { } settings) + { + void LanguageHandler(object? sender, PropertyChangedEventArgs args) => Render(textBlock, GetNode(textBlock)); + + settings.PropertyChanged += LanguageHandler; + textBlock.SetValue(LanguageHandlerProperty, (PropertyChangedEventHandler)LanguageHandler); + } + } + + if (!textBlock.GetValue(CleanupHookedProperty)) + { + textBlock.SetValue(CleanupHookedProperty, true); + textBlock.DetachedFromVisualTree += (sender, args) => Detach(textBlock, GetNode(textBlock)); } Render(textBlock, node); } + // Drops the live subscriptions for ; safe to call repeatedly. + static void Detach(TextBlock textBlock, SharpTreeNode? node) + { + if (node is INotifyPropertyChanged notify && textBlock.GetValue(TextHandlerProperty) is { } textHandler) + notify.PropertyChanged -= textHandler; + textBlock.SetValue(TextHandlerProperty, null); + + if (textBlock.GetValue(ThemeHandlerProperty) is { } themeHandler) + ThemeManager.Current.ThemeChanged -= themeHandler; + textBlock.SetValue(ThemeHandlerProperty, null); + + if (textBlock.GetValue(LanguageHandlerProperty) is { } languageHandler && GetLanguageSettings() is { } settings) + settings.PropertyChanged -= languageHandler; + textBlock.SetValue(LanguageHandlerProperty, null); + } + static void Render(TextBlock textBlock, SharpTreeNode? node) { if (node is IRichTextNode richNode && richNode.CreateRichText() is { } rich)