From 48bea149f4be410206f5672fef8da87401664346 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 29 Jul 2026 10:13:49 +0200 Subject: [PATCH] Show metadata text-blob row details in a syntax-highlighting editor The CustomDebugInformation details area rendered decoded text payloads (embedded source, source-link JSON, hex dumps) in a plain TextBox, which shows code without any highlighting and materializes the whole formatted text up front, so large embedded-source documents were expensive. Text payloads now travel as a TextBlobDetail tagged with a file extension -- ".json" for source link, the parent document's extension for embedded source, none for hex -- and render in the theme-aware AvaloniaEdit editor, which colours them via the existing highlighting registry and virtualizes long documents. The editor's ThemeChanged subscription moves from the constructor to OnAttachedToVisualTree so it stays paired with the detach-time unsubscribe now that editors can leave and re-enter the visual tree inside recycled row-details containers. Assisted-by: Claude:claude-fable-5:Claude Code --- .../CustomDebugInformationRowDetailsTests.cs | 113 ++++++++++++------ .../Metadata/MetadataRowDetailsTests.cs | 29 ++++- .../CustomDebugInformationTableTreeNode.cs | 38 ++++-- ILSpy/Metadata/MetadataRowDetails.cs | 30 ++++- ILSpy/TextView/DecompilerTextEditor.cs | 12 +- 5 files changed, 165 insertions(+), 57 deletions(-) diff --git a/ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs b/ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs index 22bad80fd..56d28692e 100644 --- a/ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs +++ b/ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs @@ -37,6 +37,7 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.ILSpy.Metadata; using ICSharpCode.ILSpy.Metadata.DebugTables; +using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; using NUnit.Framework; @@ -58,23 +59,28 @@ public class CustomDebugInformationRowDetailsTests { // Synthesize a standalone portable PDB whose CustomDebugInformation table holds one // row per row-details scenario: a text kind (Source Link), an unrecognized kind - // GUID, embedded source in both formats (raw and DEFLATE), and the four structured - // kinds that parse into typed rows. Parents use ascending MethodDef tokens so the - // (parent-sorted) table preserves this row order. + // GUID, the four structured kinds that parse into typed rows, and embedded source in + // both formats (raw and DEFLATE), each parented to a document whose name carries the + // source-language extension. MetadataBuilder sorts the table by its + // HasCustomDebugInformation coded index on serialize, interleaving method- and + // document-parented rows, so tests locate rows by kind GUID rather than by position. var builder = new MetadataBuilder(); - AddRow(1, KnownGuids.SourceLink, Encoding.UTF8.GetBytes(SourceLinkJson)); - AddRow(2, UnknownKindGuid, new byte[] { 0x01, 0x02, 0x03 }); - AddRow(3, KnownGuids.EmbeddedSource, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x41 }); - AddRow(4, KnownGuids.StateMachineHoistedLocalScopes, HoistedScopesBlob((0, 10), (16, 32))); - AddRow(5, KnownGuids.CompilationOptions, NullTerminatedStrings("language", "C#", "version", "2")); - AddRow(6, KnownGuids.CompilationMetadataReferences, MetadataReferenceBlob( + var csharpDocument = AddDocument("/src/Example.cs", KnownGuids.CSharpLanguageGuid); + var vbDocument = AddDocument("/src/Module1.vb", KnownGuids.VBLanguageGuid); + + AddRow(MethodRow(1), KnownGuids.SourceLink, Encoding.UTF8.GetBytes(SourceLinkJson)); + AddRow(MethodRow(2), UnknownKindGuid, new byte[] { 0x01, 0x02, 0x03 }); + AddRow(MethodRow(3), KnownGuids.StateMachineHoistedLocalScopes, HoistedScopesBlob((0, 10), (16, 32))); + AddRow(MethodRow(4), KnownGuids.CompilationOptions, NullTerminatedStrings("language", "C#", "version", "2")); + AddRow(MethodRow(5), KnownGuids.CompilationMetadataReferences, MetadataReferenceBlob( "System.Runtime.dll", "global", flags: 1, timestamp: 0x12345678, fileSize: 1024, ReferenceMvid)); - AddRow(7, KnownGuids.TupleElementNames, NullTerminatedStrings("Item1", "Name")); - AddRow(8, KnownGuids.EmbeddedSource, EmbeddedSourceDeflateBlob); + AddRow(MethodRow(6), KnownGuids.TupleElementNames, NullTerminatedStrings("Item1", "Name")); + AddRow(csharpDocument, KnownGuids.EmbeddedSource, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x41 }); + AddRow(vbDocument, KnownGuids.EmbeddedSource, EmbeddedSourceDeflateBlob); var rowCounts = new int[MetadataTokens.TableCount]; - rowCounts[(int)TableIndex.MethodDef] = 8; + rowCounts[(int)TableIndex.MethodDef] = 6; var pdbBuilder = new PortablePdbBuilder(builder, rowCounts.ToImmutableArray(), entryPoint: default); var blob = new BlobBuilder(); pdbBuilder.Serialize(blob); @@ -82,13 +88,25 @@ public class CustomDebugInformationRowDetailsTests var provider = MetadataReaderProvider.FromPortablePdbImage(blob.ToImmutableArray()); return new MetadataFile(MetadataFile.MetadataFileKind.ProgramDebugDatabase, "synthetic.pdb", provider); - void AddRow(int methodRow, Guid kind, byte[] value) + static EntityHandle MethodRow(int methodRow) + => MetadataTokens.EntityHandle(TableIndex.MethodDef, methodRow); + + void AddRow(EntityHandle parent, Guid kind, byte[] value) { builder.AddCustomDebugInformation( - MetadataTokens.EntityHandle(TableIndex.MethodDef, methodRow), + parent, builder.GetOrAddGuid(kind), builder.GetOrAddBlob(value)); } + + DocumentHandle AddDocument(string name, Guid language) + { + return builder.AddDocument( + builder.GetOrAddDocumentName(name), + builder.GetOrAddGuid(KnownGuids.HashAlgorithmSHA256), + builder.GetOrAddBlob(new byte[32]), + builder.GetOrAddGuid(language)); + } } static byte[] BuildEmbeddedSourceDeflateBlob(string text) @@ -156,6 +174,13 @@ public class CustomDebugInformationRowDetailsTests .Select(h => new CustomDebugInformationEntry(metadataFile, h)) .ToList(); + static CustomDebugInformationEntry ByKind(IEnumerable entries, Guid kind) + => entries.Single(e => e.KindGUID == kind); + + /// The embedded-source rows in table order: raw (".cs" document) first, DEFLATE (".vb" document) second. + static List EmbeddedSourceRows(IEnumerable entries) + => entries.Where(e => e.KindGUID == KnownGuids.EmbeddedSource).ToList(); + [Test] public void Offset_And_Split_Kind_Columns_Match_The_Tables_Conventions() { @@ -166,12 +191,10 @@ public class CustomDebugInformationRowDetailsTests var metadataFile = BuildPdbFixture(); var entries = LoadEntries(metadataFile); - entries[0].Kind.Should().BePositive("the Kind column is the 1-based GUID heap offset"); - entries[0].KindGUID.Should().Be(KnownGuids.SourceLink); - entries[0].KindString.Should().Be("Source Link (C# / VB)"); - entries[1].KindGUID.Should().Be(UnknownKindGuid); - entries[1].KindString.Should().Be("Unknown"); - entries[3].KindString.Should().Be("State Machine Hoisted Local Scopes (C# / VB)"); + ByKind(entries, KnownGuids.SourceLink).Kind.Should().BePositive("the Kind column is the 1-based GUID heap offset"); + ByKind(entries, KnownGuids.SourceLink).KindString.Should().Be("Source Link (C# / VB)"); + ByKind(entries, UnknownKindGuid).KindString.Should().Be("Unknown"); + ByKind(entries, KnownGuids.StateMachineHoistedLocalScopes).KindString.Should().Be("State Machine Hoisted Local Scopes (C# / VB)"); int rowSize = metadataFile.Metadata.GetTableRowSize(TableIndex.CustomDebugInformation); entries[0].Offset.Should().BePositive("the table lives at a real offset inside the PDB metadata"); @@ -186,21 +209,25 @@ public class CustomDebugInformationRowDetailsTests var metadataFile = BuildPdbFixture(); var entries = LoadEntries(metadataFile); - entries[3].RowDetails.Should().BeAssignableTo>() + ByKind(entries, KnownGuids.StateMachineHoistedLocalScopes).RowDetails + .Should().BeAssignableTo>() .Which.Should().Equal( new HoistedLocalScopeDetail(0, 10), new HoistedLocalScopeDetail(16, 32)); - entries[4].RowDetails.Should().BeAssignableTo>() + ByKind(entries, KnownGuids.CompilationOptions).RowDetails + .Should().BeAssignableTo>() .Which.Should().Equal( new CompilationOptionDetail("language", "C#"), new CompilationOptionDetail("version", "2")); - entries[5].RowDetails.Should().BeAssignableTo>() + ByKind(entries, KnownGuids.CompilationMetadataReferences).RowDetails + .Should().BeAssignableTo>() .Which.Should().Equal( new MetadataReferenceDetail("System.Runtime.dll", "global", 1, 0x12345678, 1024, ReferenceMvid)); - entries[6].RowDetails.Should().BeAssignableTo>() + ByKind(entries, KnownGuids.TupleElementNames).RowDetails + .Should().BeAssignableTo>() .Which.Should().Equal( new TupleElementNameDetail("Item1"), new TupleElementNameDetail("Name")); @@ -212,18 +239,25 @@ public class CustomDebugInformationRowDetailsTests var metadataFile = BuildPdbFixture(); var entries = LoadEntries(metadataFile); - entries[0].RowDetails.Should().Be(SourceLinkJson, "source link blobs are UTF-8 JSON"); - entries[1].RowDetails.Should().Be("01-02-03", "unrecognized kinds degrade to a hex dump"); + ByKind(entries, KnownGuids.SourceLink).RowDetails.Should().Be(new TextBlobDetail(SourceLinkJson, ".json"), + "source link blobs are UTF-8 JSON"); + ByKind(entries, UnknownKindGuid).RowDetails.Should().Be(new TextBlobDetail("01-02-03"), + "unrecognized kinds degrade to a plain hex dump"); } [Test] public void RowDetails_Decodes_Embedded_Source_To_The_Document_Text() { + // The parent document's file extension travels with the decoded text so the details + // area can highlight the source in its own language. var metadataFile = BuildPdbFixture(); var entries = LoadEntries(metadataFile); - entries[2].RowDetails.Should().Be("A", "a zero format header means the document bytes follow uncompressed"); - entries[7].RowDetails.Should().Be(EmbeddedSourceText, "a positive format header means the document is DEFLATE-compressed"); + var embedded = EmbeddedSourceRows(entries); + embedded[0].RowDetails.Should().Be(new TextBlobDetail("A", ".cs"), + "a zero format header means the document bytes follow uncompressed"); + embedded[1].RowDetails.Should().Be(new TextBlobDetail(EmbeddedSourceText, ".vb"), + "a positive format header means the document is DEFLATE-compressed"); } [Test] @@ -232,9 +266,11 @@ public class CustomDebugInformationRowDetailsTests var metadataFile = BuildPdbFixture(); var entries = LoadEntries(metadataFile); - entries[0].Info.Should().BeNull("only embedded source carries a format header to summarize"); - entries[2].Info.Should().Be("Raw, 5 bytes"); - entries[7].Info.Should().Be( + ByKind(entries, KnownGuids.SourceLink).Info.Should().BeNull( + "only embedded source carries a format header to summarize"); + var embedded = EmbeddedSourceRows(entries); + embedded[0].Info.Should().Be("Raw, 5 bytes"); + embedded[1].Info.Should().Be( $"DEFLATE, {EmbeddedSourceDeflateBlob.Length} bytes, {Encoding.UTF8.GetByteCount(EmbeddedSourceText)} uncompressed"); } @@ -242,8 +278,8 @@ public class CustomDebugInformationRowDetailsTests public void Tab_Configures_Selection_Driven_Row_Details_That_Route_By_Blob_Shape() { // The details area swaps presentation per row: text blobs land in a read-only - // TextBox, structured kinds in a sub-grid. Selection drives visibility, so scanning - // the table with the arrow keys previews each blob. + // syntax-highlighting editor, structured kinds in a sub-grid. Selection drives + // visibility, so scanning the table with the arrow keys previews each blob. var metadataFile = BuildPdbFixture(); var node = new CustomDebugInformationTableTreeNode(metadataFile); var tab = (MetadataTablePageModel)node.CreateTab(); @@ -252,13 +288,16 @@ public class CustomDebugInformationRowDetailsTests tab.RowDetailsTemplate.Should().NotBeNull(); var entries = tab.Items.Cast().ToList(); - var shell = tab.RowDetailsTemplate!.Build(entries[0]) + var sourceLink = ByKind(entries, KnownGuids.SourceLink); + var shell = tab.RowDetailsTemplate!.Build(sourceLink) .Should().BeOfType().Subject; - shell.DataContext = entries[0]; - shell.Content.Should().BeOfType().Which.Text.Should().Be(SourceLinkJson); + shell.DataContext = sourceLink; + var editor = shell.Content.Should().BeOfType().Subject; + editor.Text.Should().Be(SourceLinkJson); + editor.SyntaxHighlighting.Should().NotBeNull("source-link JSON gets JSON highlighting"); - shell.DataContext = entries[4]; + shell.DataContext = ByKind(entries, KnownGuids.CompilationOptions); var optionsGrid = shell.Content.Should().BeOfType().Subject; ((IEnumerable)optionsGrid.ItemsSource!).Cast().Should().HaveCount(2); } diff --git a/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs b/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs index 61523803b..6228bf626 100644 --- a/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs +++ b/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs @@ -29,6 +29,7 @@ using Avalonia.VisualTree; using AwesomeAssertions; using ICSharpCode.ILSpy.Metadata; +using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.Views; @@ -171,11 +172,37 @@ public class MetadataRowDetailsTests "the last column takes the leftover width"); } + [AvaloniaTest] + public void Text_Blob_Is_A_Read_Only_Editor_With_Extension_Driven_Highlighting() + { + // Text payloads are code (embedded source, source-link JSON): they render in the + // theme-aware AvaloniaEdit editor, which adds syntax colours and virtualizes long + // documents, while staying read-only but selectable. The optional extension picks + // the highlighting; without one (hex dumps) the text stays plain. + var editor = MetadataRowDetails.BuildTextBlob("class C { }", ".cs") + .Should().BeOfType().Subject; + editor.Text.Should().Be("class C { }"); + editor.IsReadOnly.Should().BeTrue(); + editor.WordWrap.Should().BeTrue(); + editor.MaxHeight.Should().Be(400, "the host row must stay bounded; the editor scrolls internally"); + editor.SyntaxHighlighting.Should().NotBeNull(); + editor.SyntaxHighlighting!.Name.Should().Be("C#"); + + var json = (DecompilerTextEditor)MetadataRowDetails.BuildTextBlob("{ }", ".json"); + json.SyntaxHighlighting.Should().NotBeNull("AvaloniaEdit ships a built-in JSON definition"); + + var plain = (DecompilerTextEditor)MetadataRowDetails.BuildTextBlob("01-02-03"); + plain.SyntaxHighlighting.Should().BeNull(); + + var unknown = (DecompilerTextEditor)MetadataRowDetails.BuildTextBlob("text", ".xyz"); + unknown.SyntaxHighlighting.Should().BeNull("an unrecognized extension degrades to plain text"); + } + [AvaloniaTest] public async Task Double_Tap_Inside_The_Details_Area_Does_Not_Resolve_To_An_Activatable_Row() { // Row activation navigates away from the metadata view. A double-click inside the - // details area (e.g. word-selection in an embedded-source TextBox, or a click in the + // details area (e.g. word-selection in an embedded-source editor, or a click in the // flags sub-grid) is interacting with the details content, not requesting navigation, // so the row-resolution walk must reject sources under the details presenter. var (window, vm) = await TestHarness.BootAsync(); diff --git a/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs b/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs index e7cc6cff2..fed0a7d7f 100644 --- a/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs +++ b/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs @@ -76,7 +76,7 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables static Control? BuildRowDetailsContent(object? item) { return (item as CustomDebugInformationEntry)?.RowDetails switch { - string text => MetadataRowDetails.BuildTextBlob(text), + TextBlobDetail blob => MetadataRowDetails.BuildTextBlob(blob.Text, blob.HighlightExtension), IReadOnlyList rows => MetadataRowDetails.BuildDetailsGrid(rows, ("Start Offset", nameof(HoistedLocalScopeDetail.StartOffset)), ("Length", nameof(HoistedLocalScopeDetail.Length))), @@ -197,9 +197,11 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables /// /// Parsed view of the Value blob for the row-details area. Structured kinds become - /// typed row lists, source-link blobs the decoded JSON text, embedded source the - /// (decompressed) document text, everything else (including malformed blobs) a hex - /// dump. Cached — the details area re-requests it on every selection change. + /// typed row lists; text payloads become a — source-link + /// blobs the decoded JSON, embedded source the (decompressed) document text tagged + /// with its document's file extension, everything else (including malformed blobs) + /// a plain hex dump. Cached — the details area re-requests it on every selection + /// change. /// public object? RowDetails { get { @@ -215,7 +217,7 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables } catch (Exception ex) when (ex is BadImageFormatException or InvalidDataException) { - return rowDetails = metadataFile.Metadata.GetBlobReader(debugInfo.Value).ToHexString(); + return rowDetails = new TextBlobDetail(metadataFile.Metadata.GetBlobReader(debugInfo.Value).ToHexString()); } } } @@ -231,13 +233,13 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables return list; } if (kind == KnownGuids.SourceLink) - return reader.ReadUTF8(reader.RemainingBytes); + return new TextBlobDetail(reader.ReadUTF8(reader.RemainingBytes), ".json"); if (kind == KnownGuids.EmbeddedSource) { var embeddedSourceFormat = reader.ReadInt32(); if (embeddedSourceFormat < 0) // unknown format, show raw data as hex - return reader.ToHexString(); + return new TextBlobDetail(reader.ToHexString()); var embeddedSourceBytes = reader.ReadBytes(reader.RemainingBytes); Stream embeddedSourceByteStream = new MemoryStream(embeddedSourceBytes); @@ -245,8 +247,10 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables if (embeddedSourceFormat > 0) // positive length means the data is compressed using DEFLATE embeddedSourceByteStream = new System.IO.Compression.DeflateStream(embeddedSourceByteStream, System.IO.Compression.CompressionMode.Decompress); - var textReader = new StreamReader(embeddedSourceByteStream, detectEncodingFromByteOrderMarks: true); - return textReader.ReadToEnd(); + // Disposing the reader disposes the whole wrapped chain (DeflateStream's + // inflater would otherwise wait for its finalizer). + using var textReader = new StreamReader(embeddedSourceByteStream, detectEncodingFromByteOrderMarks: true); + return new TextBlobDetail(textReader.ReadToEnd(), GetEmbeddedSourceExtension()); } if (kind == KnownGuids.CompilationOptions) { @@ -281,7 +285,21 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables list.Add(new TupleElementNameDetail(reader.ReadUTF8StringNullTerminated())); return list; } - return reader.ToHexString(); + return new TextBlobDetail(reader.ToHexString()); + } + + string? GetEmbeddedSourceExtension() + { + // The parent of an embedded-source row is the Document whose text the blob + // holds; the document name's file extension selects the syntax highlighting + // for the decoded source. + if (debugInfo.Parent.Kind != HandleKind.Document) + return null; + var document = metadataFile.Metadata.GetDocument((DocumentHandle)debugInfo.Parent); + if (document.Name.IsNil) + return null; + string extension = Path.GetExtension(metadataFile.Metadata.GetString(document.Name)); + return string.IsNullOrEmpty(extension) ? null : extension; } public CustomDebugInformationEntry(MetadataFile metadataFile, CustomDebugInformationHandle handle) diff --git a/ILSpy/Metadata/MetadataRowDetails.cs b/ILSpy/Metadata/MetadataRowDetails.cs index a9a42c318..2d50d8e11 100644 --- a/ILSpy/Metadata/MetadataRowDetails.cs +++ b/ILSpy/Metadata/MetadataRowDetails.cs @@ -22,10 +22,12 @@ using System.Collections.Generic; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Media; +using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; namespace ICSharpCode.ILSpy.Metadata @@ -56,6 +58,13 @@ namespace ICSharpCode.ILSpy.Metadata } } + /// + /// Decoded text payload of a row (embedded source, source-link JSON, hex dump), tagged + /// with the file extension (".cs", ".json") that selects the syntax highlighting for its + /// display, or for plain text. + /// + public sealed record TextBlobDetail(string Text, string? HighlightExtension = null); + /// /// Factories for the row-details area of metadata grids: the DataContext-tracking shell /// template plus the content shapes shared across tables (flag-bit breakdown, decoded @@ -109,16 +118,29 @@ namespace ICSharpCode.ILSpy.Metadata return grid; } - /// Read-only, word-wrapped view of a decoded text blob (source, JSON, hex). - public static Control BuildTextBlob(string text) + /// + /// Read-only, word-wrapped view of a decoded text blob (source, JSON, hex), rendered + /// in the theme-aware AvaloniaEdit editor: text payloads are mostly code, so they get + /// syntax colours (selected by , e.g. ".cs" or + /// ".json"; stays plain) and line virtualization keeps large + /// embedded-source documents cheap. Height is bounded so the host row cannot grow + /// unbounded; the editor scrolls internally. + /// + public static Control BuildTextBlob(string text, string? highlightExtension = null) { ArgumentNullException.ThrowIfNull(text); - return new TextBox { + var editor = new DecompilerTextEditor { Text = text, IsReadOnly = true, - TextWrapping = TextWrapping.Wrap, + WordWrap = true, MaxHeight = 400, + FontFamily = new FontFamily("Consolas, Menlo, Monospace"), + FontSize = 13, }; + if (highlightExtension != null) + editor.SyntaxHighlighting = HighlightingService.GetByExtension(highlightExtension); + editor.Bind(TemplatedControl.BackgroundProperty, editor.GetResourceObservable("ILSpy.EditorBackground")); + return editor; } /// diff --git a/ILSpy/TextView/DecompilerTextEditor.cs b/ILSpy/TextView/DecompilerTextEditor.cs index 8dac10e4c..b0e5a89f8 100644 --- a/ILSpy/TextView/DecompilerTextEditor.cs +++ b/ILSpy/TextView/DecompilerTextEditor.cs @@ -36,11 +36,6 @@ namespace ICSharpCode.ILSpy.TextView /// public class DecompilerTextEditor : TextEditor { - public DecompilerTextEditor() - { - ThemeManager.Current.ThemeChanged += OnThemeChanged; - } - // Avalonia resolves the control template via the runtime type; subclasses of a // templated control inherit the base template only when StyleKeyOverride is // pointed at the base. Without this override AvaloniaEdit's template doesn't @@ -61,6 +56,13 @@ namespace ICSharpCode.ILSpy.TextView TextArea?.TextView?.Redraw(); } + protected override void OnAttachedToVisualTree(global::Avalonia.VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + ThemeManager.Current.ThemeChanged += OnThemeChanged; + TextArea?.TextView?.Redraw(); + } + protected override void OnDetachedFromVisualTree(global::Avalonia.VisualTreeAttachmentEventArgs e) { ThemeManager.Current.ThemeChanged -= OnThemeChanged;