Browse Source

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
pull/3945/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
48bea149f4
  1. 113
      ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs
  2. 29
      ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs
  3. 38
      ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs
  4. 30
      ILSpy/Metadata/MetadataRowDetails.cs
  5. 12
      ILSpy/TextView/DecompilerTextEditor.cs

113
ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs

@ -37,6 +37,7 @@ using ICSharpCode.Decompiler.Metadata; @@ -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 @@ -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 @@ -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 @@ -156,6 +174,13 @@ public class CustomDebugInformationRowDetailsTests
.Select(h => new CustomDebugInformationEntry(metadataFile, h))
.ToList();
static CustomDebugInformationEntry ByKind(IEnumerable<CustomDebugInformationEntry> entries, Guid kind)
=> entries.Single(e => e.KindGUID == kind);
/// <summary>The embedded-source rows in table order: raw (".cs" document) first, DEFLATE (".vb" document) second.</summary>
static List<CustomDebugInformationEntry> EmbeddedSourceRows(IEnumerable<CustomDebugInformationEntry> 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 @@ -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 @@ -186,21 +209,25 @@ public class CustomDebugInformationRowDetailsTests
var metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile);
entries[3].RowDetails.Should().BeAssignableTo<IEnumerable<HoistedLocalScopeDetail>>()
ByKind(entries, KnownGuids.StateMachineHoistedLocalScopes).RowDetails
.Should().BeAssignableTo<IEnumerable<HoistedLocalScopeDetail>>()
.Which.Should().Equal(
new HoistedLocalScopeDetail(0, 10),
new HoistedLocalScopeDetail(16, 32));
entries[4].RowDetails.Should().BeAssignableTo<IEnumerable<CompilationOptionDetail>>()
ByKind(entries, KnownGuids.CompilationOptions).RowDetails
.Should().BeAssignableTo<IEnumerable<CompilationOptionDetail>>()
.Which.Should().Equal(
new CompilationOptionDetail("language", "C#"),
new CompilationOptionDetail("version", "2"));
entries[5].RowDetails.Should().BeAssignableTo<IEnumerable<MetadataReferenceDetail>>()
ByKind(entries, KnownGuids.CompilationMetadataReferences).RowDetails
.Should().BeAssignableTo<IEnumerable<MetadataReferenceDetail>>()
.Which.Should().Equal(
new MetadataReferenceDetail("System.Runtime.dll", "global", 1, 0x12345678, 1024, ReferenceMvid));
entries[6].RowDetails.Should().BeAssignableTo<IEnumerable<TupleElementNameDetail>>()
ByKind(entries, KnownGuids.TupleElementNames).RowDetails
.Should().BeAssignableTo<IEnumerable<TupleElementNameDetail>>()
.Which.Should().Equal(
new TupleElementNameDetail("Item1"),
new TupleElementNameDetail("Name"));
@ -212,18 +239,25 @@ public class CustomDebugInformationRowDetailsTests @@ -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 @@ -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 @@ -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 @@ -252,13 +288,16 @@ public class CustomDebugInformationRowDetailsTests
tab.RowDetailsTemplate.Should().NotBeNull();
var entries = tab.Items.Cast<CustomDebugInformationEntry>().ToList();
var shell = tab.RowDetailsTemplate!.Build(entries[0])
var sourceLink = ByKind(entries, KnownGuids.SourceLink);
var shell = tab.RowDetailsTemplate!.Build(sourceLink)
.Should().BeOfType<MetadataRowDetailsControl>().Subject;
shell.DataContext = entries[0];
shell.Content.Should().BeOfType<TextBox>().Which.Text.Should().Be(SourceLinkJson);
shell.DataContext = sourceLink;
var editor = shell.Content.Should().BeOfType<DecompilerTextEditor>().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<DataGrid>().Subject;
((IEnumerable)optionsGrid.ItemsSource!).Cast<CompilationOptionDetail>().Should().HaveCount(2);
}

29
ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs

@ -29,6 +29,7 @@ using Avalonia.VisualTree; @@ -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 @@ -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<DecompilerTextEditor>().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();

38
ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs

@ -76,7 +76,7 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables @@ -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<HoistedLocalScopeDetail> rows => MetadataRowDetails.BuildDetailsGrid(rows,
("Start Offset", nameof(HoistedLocalScopeDetail.StartOffset)),
("Length", nameof(HoistedLocalScopeDetail.Length))),
@ -197,9 +197,11 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables @@ -197,9 +197,11 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables
/// <summary>
/// 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 <see cref="TextBlobDetail"/> — 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.
/// </summary>
public object? RowDetails {
get {
@ -215,7 +217,7 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables @@ -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 @@ -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 @@ -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 @@ -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)

30
ILSpy/Metadata/MetadataRowDetails.cs

@ -22,10 +22,12 @@ using System.Collections.Generic; @@ -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 @@ -56,6 +58,13 @@ namespace ICSharpCode.ILSpy.Metadata
}
}
/// <summary>
/// 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 <see langword="null"/> for plain text.
/// </summary>
public sealed record TextBlobDetail(string Text, string? HighlightExtension = null);
/// <summary>
/// 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 @@ -109,16 +118,29 @@ namespace ICSharpCode.ILSpy.Metadata
return grid;
}
/// <summary>Read-only, word-wrapped view of a decoded text blob (source, JSON, hex).</summary>
public static Control BuildTextBlob(string text)
/// <summary>
/// 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 <paramref name="highlightExtension"/>, e.g. ".cs" or
/// ".json"; <see langword="null"/> 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.
/// </summary>
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;
}
/// <summary>

12
ILSpy/TextView/DecompilerTextEditor.cs

@ -36,11 +36,6 @@ namespace ICSharpCode.ILSpy.TextView @@ -36,11 +36,6 @@ namespace ICSharpCode.ILSpy.TextView
/// </summary>
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 @@ -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;

Loading…
Cancel
Save