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;
using ICSharpCode.ILSpy.Metadata; using ICSharpCode.ILSpy.Metadata;
using ICSharpCode.ILSpy.Metadata.DebugTables; using ICSharpCode.ILSpy.Metadata.DebugTables;
using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.ViewModels;
using NUnit.Framework; using NUnit.Framework;
@ -58,23 +59,28 @@ public class CustomDebugInformationRowDetailsTests
{ {
// Synthesize a standalone portable PDB whose CustomDebugInformation table holds one // Synthesize a standalone portable PDB whose CustomDebugInformation table holds one
// row per row-details scenario: a text kind (Source Link), an unrecognized kind // 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 // GUID, the four structured kinds that parse into typed rows, and embedded source in
// kinds that parse into typed rows. Parents use ascending MethodDef tokens so the // both formats (raw and DEFLATE), each parented to a document whose name carries the
// (parent-sorted) table preserves this row order. // 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(); var builder = new MetadataBuilder();
AddRow(1, KnownGuids.SourceLink, Encoding.UTF8.GetBytes(SourceLinkJson)); var csharpDocument = AddDocument("/src/Example.cs", KnownGuids.CSharpLanguageGuid);
AddRow(2, UnknownKindGuid, new byte[] { 0x01, 0x02, 0x03 }); var vbDocument = AddDocument("/src/Module1.vb", KnownGuids.VBLanguageGuid);
AddRow(3, KnownGuids.EmbeddedSource, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x41 });
AddRow(4, KnownGuids.StateMachineHoistedLocalScopes, HoistedScopesBlob((0, 10), (16, 32))); AddRow(MethodRow(1), KnownGuids.SourceLink, Encoding.UTF8.GetBytes(SourceLinkJson));
AddRow(5, KnownGuids.CompilationOptions, NullTerminatedStrings("language", "C#", "version", "2")); AddRow(MethodRow(2), UnknownKindGuid, new byte[] { 0x01, 0x02, 0x03 });
AddRow(6, KnownGuids.CompilationMetadataReferences, MetadataReferenceBlob( 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)); "System.Runtime.dll", "global", flags: 1, timestamp: 0x12345678, fileSize: 1024, ReferenceMvid));
AddRow(7, KnownGuids.TupleElementNames, NullTerminatedStrings("Item1", "Name")); AddRow(MethodRow(6), KnownGuids.TupleElementNames, NullTerminatedStrings("Item1", "Name"));
AddRow(8, KnownGuids.EmbeddedSource, EmbeddedSourceDeflateBlob); AddRow(csharpDocument, KnownGuids.EmbeddedSource, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x41 });
AddRow(vbDocument, KnownGuids.EmbeddedSource, EmbeddedSourceDeflateBlob);
var rowCounts = new int[MetadataTokens.TableCount]; 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 pdbBuilder = new PortablePdbBuilder(builder, rowCounts.ToImmutableArray(), entryPoint: default);
var blob = new BlobBuilder(); var blob = new BlobBuilder();
pdbBuilder.Serialize(blob); pdbBuilder.Serialize(blob);
@ -82,13 +88,25 @@ public class CustomDebugInformationRowDetailsTests
var provider = MetadataReaderProvider.FromPortablePdbImage(blob.ToImmutableArray()); var provider = MetadataReaderProvider.FromPortablePdbImage(blob.ToImmutableArray());
return new MetadataFile(MetadataFile.MetadataFileKind.ProgramDebugDatabase, "synthetic.pdb", provider); 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( builder.AddCustomDebugInformation(
MetadataTokens.EntityHandle(TableIndex.MethodDef, methodRow), parent,
builder.GetOrAddGuid(kind), builder.GetOrAddGuid(kind),
builder.GetOrAddBlob(value)); 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) static byte[] BuildEmbeddedSourceDeflateBlob(string text)
@ -156,6 +174,13 @@ public class CustomDebugInformationRowDetailsTests
.Select(h => new CustomDebugInformationEntry(metadataFile, h)) .Select(h => new CustomDebugInformationEntry(metadataFile, h))
.ToList(); .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] [Test]
public void Offset_And_Split_Kind_Columns_Match_The_Tables_Conventions() public void Offset_And_Split_Kind_Columns_Match_The_Tables_Conventions()
{ {
@ -166,12 +191,10 @@ public class CustomDebugInformationRowDetailsTests
var metadataFile = BuildPdbFixture(); var metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile); var entries = LoadEntries(metadataFile);
entries[0].Kind.Should().BePositive("the Kind column is the 1-based GUID heap offset"); ByKind(entries, KnownGuids.SourceLink).Kind.Should().BePositive("the Kind column is the 1-based GUID heap offset");
entries[0].KindGUID.Should().Be(KnownGuids.SourceLink); ByKind(entries, KnownGuids.SourceLink).KindString.Should().Be("Source Link (C# / VB)");
entries[0].KindString.Should().Be("Source Link (C# / VB)"); ByKind(entries, UnknownKindGuid).KindString.Should().Be("Unknown");
entries[1].KindGUID.Should().Be(UnknownKindGuid); ByKind(entries, KnownGuids.StateMachineHoistedLocalScopes).KindString.Should().Be("State Machine Hoisted Local Scopes (C# / VB)");
entries[1].KindString.Should().Be("Unknown");
entries[3].KindString.Should().Be("State Machine Hoisted Local Scopes (C# / VB)");
int rowSize = metadataFile.Metadata.GetTableRowSize(TableIndex.CustomDebugInformation); int rowSize = metadataFile.Metadata.GetTableRowSize(TableIndex.CustomDebugInformation);
entries[0].Offset.Should().BePositive("the table lives at a real offset inside the PDB metadata"); 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 metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile); var entries = LoadEntries(metadataFile);
entries[3].RowDetails.Should().BeAssignableTo<IEnumerable<HoistedLocalScopeDetail>>() ByKind(entries, KnownGuids.StateMachineHoistedLocalScopes).RowDetails
.Should().BeAssignableTo<IEnumerable<HoistedLocalScopeDetail>>()
.Which.Should().Equal( .Which.Should().Equal(
new HoistedLocalScopeDetail(0, 10), new HoistedLocalScopeDetail(0, 10),
new HoistedLocalScopeDetail(16, 32)); new HoistedLocalScopeDetail(16, 32));
entries[4].RowDetails.Should().BeAssignableTo<IEnumerable<CompilationOptionDetail>>() ByKind(entries, KnownGuids.CompilationOptions).RowDetails
.Should().BeAssignableTo<IEnumerable<CompilationOptionDetail>>()
.Which.Should().Equal( .Which.Should().Equal(
new CompilationOptionDetail("language", "C#"), new CompilationOptionDetail("language", "C#"),
new CompilationOptionDetail("version", "2")); new CompilationOptionDetail("version", "2"));
entries[5].RowDetails.Should().BeAssignableTo<IEnumerable<MetadataReferenceDetail>>() ByKind(entries, KnownGuids.CompilationMetadataReferences).RowDetails
.Should().BeAssignableTo<IEnumerable<MetadataReferenceDetail>>()
.Which.Should().Equal( .Which.Should().Equal(
new MetadataReferenceDetail("System.Runtime.dll", "global", 1, 0x12345678, 1024, ReferenceMvid)); 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( .Which.Should().Equal(
new TupleElementNameDetail("Item1"), new TupleElementNameDetail("Item1"),
new TupleElementNameDetail("Name")); new TupleElementNameDetail("Name"));
@ -212,18 +239,25 @@ public class CustomDebugInformationRowDetailsTests
var metadataFile = BuildPdbFixture(); var metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile); var entries = LoadEntries(metadataFile);
entries[0].RowDetails.Should().Be(SourceLinkJson, "source link blobs are UTF-8 JSON"); ByKind(entries, KnownGuids.SourceLink).RowDetails.Should().Be(new TextBlobDetail(SourceLinkJson, ".json"),
entries[1].RowDetails.Should().Be("01-02-03", "unrecognized kinds degrade to a hex dump"); "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] [Test]
public void RowDetails_Decodes_Embedded_Source_To_The_Document_Text() 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 metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile); var entries = LoadEntries(metadataFile);
entries[2].RowDetails.Should().Be("A", "a zero format header means the document bytes follow uncompressed"); var embedded = EmbeddedSourceRows(entries);
entries[7].RowDetails.Should().Be(EmbeddedSourceText, "a positive format header means the document is DEFLATE-compressed"); 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] [Test]
@ -232,9 +266,11 @@ public class CustomDebugInformationRowDetailsTests
var metadataFile = BuildPdbFixture(); var metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile); var entries = LoadEntries(metadataFile);
entries[0].Info.Should().BeNull("only embedded source carries a format header to summarize"); ByKind(entries, KnownGuids.SourceLink).Info.Should().BeNull(
entries[2].Info.Should().Be("Raw, 5 bytes"); "only embedded source carries a format header to summarize");
entries[7].Info.Should().Be( 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"); $"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() 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 // 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 // syntax-highlighting editor, structured kinds in a sub-grid. Selection drives
// the table with the arrow keys previews each blob. // visibility, so scanning the table with the arrow keys previews each blob.
var metadataFile = BuildPdbFixture(); var metadataFile = BuildPdbFixture();
var node = new CustomDebugInformationTableTreeNode(metadataFile); var node = new CustomDebugInformationTableTreeNode(metadataFile);
var tab = (MetadataTablePageModel)node.CreateTab(); var tab = (MetadataTablePageModel)node.CreateTab();
@ -252,13 +288,16 @@ public class CustomDebugInformationRowDetailsTests
tab.RowDetailsTemplate.Should().NotBeNull(); tab.RowDetailsTemplate.Should().NotBeNull();
var entries = tab.Items.Cast<CustomDebugInformationEntry>().ToList(); 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; .Should().BeOfType<MetadataRowDetailsControl>().Subject;
shell.DataContext = entries[0]; shell.DataContext = sourceLink;
shell.Content.Should().BeOfType<TextBox>().Which.Text.Should().Be(SourceLinkJson); 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; var optionsGrid = shell.Content.Should().BeOfType<DataGrid>().Subject;
((IEnumerable)optionsGrid.ItemsSource!).Cast<CompilationOptionDetail>().Should().HaveCount(2); ((IEnumerable)optionsGrid.ItemsSource!).Cast<CompilationOptionDetail>().Should().HaveCount(2);
} }

29
ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs

@ -29,6 +29,7 @@ using Avalonia.VisualTree;
using AwesomeAssertions; using AwesomeAssertions;
using ICSharpCode.ILSpy.Metadata; using ICSharpCode.ILSpy.Metadata;
using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.ViewModels;
using ICSharpCode.ILSpy.Views; using ICSharpCode.ILSpy.Views;
@ -171,11 +172,37 @@ public class MetadataRowDetailsTests
"the last column takes the leftover width"); "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] [AvaloniaTest]
public async Task Double_Tap_Inside_The_Details_Area_Does_Not_Resolve_To_An_Activatable_Row() 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 // 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, // flags sub-grid) is interacting with the details content, not requesting navigation,
// so the row-resolution walk must reject sources under the details presenter. // so the row-resolution walk must reject sources under the details presenter.
var (window, vm) = await TestHarness.BootAsync(); var (window, vm) = await TestHarness.BootAsync();

38
ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs

@ -76,7 +76,7 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables
static Control? BuildRowDetailsContent(object? item) static Control? BuildRowDetailsContent(object? item)
{ {
return (item as CustomDebugInformationEntry)?.RowDetails switch { 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, IReadOnlyList<HoistedLocalScopeDetail> rows => MetadataRowDetails.BuildDetailsGrid(rows,
("Start Offset", nameof(HoistedLocalScopeDetail.StartOffset)), ("Start Offset", nameof(HoistedLocalScopeDetail.StartOffset)),
("Length", nameof(HoistedLocalScopeDetail.Length))), ("Length", nameof(HoistedLocalScopeDetail.Length))),
@ -197,9 +197,11 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables
/// <summary> /// <summary>
/// Parsed view of the Value blob for the row-details area. Structured kinds become /// 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 /// typed row lists; text payloads become a <see cref="TextBlobDetail"/> — source-link
/// (decompressed) document text, everything else (including malformed blobs) a hex /// blobs the decoded JSON, embedded source the (decompressed) document text tagged
/// dump. Cached — the details area re-requests it on every selection change. /// 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> /// </summary>
public object? RowDetails { public object? RowDetails {
get { get {
@ -215,7 +217,7 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables
} }
catch (Exception ex) when (ex is BadImageFormatException or InvalidDataException) 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; return list;
} }
if (kind == KnownGuids.SourceLink) if (kind == KnownGuids.SourceLink)
return reader.ReadUTF8(reader.RemainingBytes); return new TextBlobDetail(reader.ReadUTF8(reader.RemainingBytes), ".json");
if (kind == KnownGuids.EmbeddedSource) if (kind == KnownGuids.EmbeddedSource)
{ {
var embeddedSourceFormat = reader.ReadInt32(); var embeddedSourceFormat = reader.ReadInt32();
if (embeddedSourceFormat < 0) // unknown format, show raw data as hex if (embeddedSourceFormat < 0) // unknown format, show raw data as hex
return reader.ToHexString(); return new TextBlobDetail(reader.ToHexString());
var embeddedSourceBytes = reader.ReadBytes(reader.RemainingBytes); var embeddedSourceBytes = reader.ReadBytes(reader.RemainingBytes);
Stream embeddedSourceByteStream = new MemoryStream(embeddedSourceBytes); 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 if (embeddedSourceFormat > 0) // positive length means the data is compressed using DEFLATE
embeddedSourceByteStream = new System.IO.Compression.DeflateStream(embeddedSourceByteStream, System.IO.Compression.CompressionMode.Decompress); embeddedSourceByteStream = new System.IO.Compression.DeflateStream(embeddedSourceByteStream, System.IO.Compression.CompressionMode.Decompress);
var textReader = new StreamReader(embeddedSourceByteStream, detectEncodingFromByteOrderMarks: true); // Disposing the reader disposes the whole wrapped chain (DeflateStream's
return textReader.ReadToEnd(); // 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) if (kind == KnownGuids.CompilationOptions)
{ {
@ -281,7 +285,21 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables
list.Add(new TupleElementNameDetail(reader.ReadUTF8StringNullTerminated())); list.Add(new TupleElementNameDetail(reader.ReadUTF8StringNullTerminated()));
return list; 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) public CustomDebugInformationEntry(MetadataFile metadataFile, CustomDebugInformationHandle handle)

30
ILSpy/Metadata/MetadataRowDetails.cs

@ -22,10 +22,12 @@ using System.Collections.Generic;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.ViewModels;
namespace ICSharpCode.ILSpy.Metadata 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> /// <summary>
/// Factories for the row-details area of metadata grids: the DataContext-tracking shell /// 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 /// template plus the content shapes shared across tables (flag-bit breakdown, decoded
@ -109,16 +118,29 @@ namespace ICSharpCode.ILSpy.Metadata
return grid; return grid;
} }
/// <summary>Read-only, word-wrapped view of a decoded text blob (source, JSON, hex).</summary> /// <summary>
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 <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); ArgumentNullException.ThrowIfNull(text);
return new TextBox { var editor = new DecompilerTextEditor {
Text = text, Text = text,
IsReadOnly = true, IsReadOnly = true,
TextWrapping = TextWrapping.Wrap, WordWrap = true,
MaxHeight = 400, 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> /// <summary>

12
ILSpy/TextView/DecompilerTextEditor.cs

@ -36,11 +36,6 @@ namespace ICSharpCode.ILSpy.TextView
/// </summary> /// </summary>
public class DecompilerTextEditor : TextEditor public class DecompilerTextEditor : TextEditor
{ {
public DecompilerTextEditor()
{
ThemeManager.Current.ThemeChanged += OnThemeChanged;
}
// Avalonia resolves the control template via the runtime type; subclasses of a // Avalonia resolves the control template via the runtime type; subclasses of a
// templated control inherit the base template only when StyleKeyOverride is // templated control inherit the base template only when StyleKeyOverride is
// pointed at the base. Without this override AvaloniaEdit's template doesn't // pointed at the base. Without this override AvaloniaEdit's template doesn't
@ -61,6 +56,13 @@ namespace ICSharpCode.ILSpy.TextView
TextArea?.TextView?.Redraw(); 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) protected override void OnDetachedFromVisualTree(global::Avalonia.VisualTreeAttachmentEventArgs e)
{ {
ThemeManager.Current.ThemeChanged -= OnThemeChanged; ThemeManager.Current.ThemeChanged -= OnThemeChanged;

Loading…
Cancel
Save