Browse Source

Typed resource nodes for xml/xsd/xslt/xaml + image/icon/cursor

Five new IResourceNodeFactory implementations route .xml/.xsd/.xslt to a shared
XmlResourceEntryNode (text + XML highlighting via SyntaxExtensionOverride on the
output), .xaml to XamlResourceEntryNode (same pattern), and .png/.gif/.bmp/.jpg /
.ico / .cur to image-rendering nodes that inline the bitmap with a Save button.
SyntaxExtensionOverride is read after Decompile to switch the editor's highlighter
without changing the active language. Cursor handling flips byte 2 of the on-disk
header so Skia decodes the .cur as an icon for preview while Save preserves the
original bytes. ResourceFactoryTests pin the dispatch — including .xsd — and the
existing assembly-tree assertion is relaxed since typed entries are no longer all
ResourceTreeNodes.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
17a833f8c0
  1. 5
      ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs
  2. 76
      ILSpy.Tests/Resources/ResourceFactoryTests.cs
  3. 8
      ILSpy/TextView/AvaloniaEditTextOutput.cs
  4. 5
      ILSpy/TextView/DecompilerTabPageModel.cs
  5. 96
      ILSpy/TreeNodes/CursorResourceEntryNode.cs
  6. 91
      ILSpy/TreeNodes/IconResourceEntryNode.cs
  7. 96
      ILSpy/TreeNodes/ImageResourceEntryNode.cs
  8. 65
      ILSpy/TreeNodes/XamlResourceEntryNode.cs
  9. 78
      ILSpy/TreeNodes/XmlResourceEntryNode.cs

5
ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs

@ -68,7 +68,10 @@ public class AssemblyTreeTests @@ -68,7 +68,10 @@ public class AssemblyTreeTests
resources.Text.Should().Be(Resources._Resources);
resources.EnsureLazyChildren();
resources.Children.Should().NotBeEmpty();
resources.Children.Should().AllBeAssignableTo<ResourceTreeNode>();
// The factory dispatcher routes typed resources (.xml/.png/.ico/.cur/.xaml) to
// specialised ResourceEntryNode subclasses; everything else falls back to
// ResourceTreeNode. Both branches are ILSpyTreeNodes.
resources.Children.Should().AllBeAssignableTo<ILSpyTreeNode>();
}
[AvaloniaTest]

76
ILSpy.Tests/Resources/ResourceFactoryTests.cs

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Text;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.Decompiler.Metadata;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
[TestFixture]
public class ResourceFactoryTests
{
// Touch the composition host once so ILSpyTreeNode's static `ResourceNodeFactories`
// initialiser succeeds. AvaloniaTest already boots the app builder; resolving the
// MainWindow ensures App.Initialize has run and AppComposition.Current is wired.
static void EnsureComposition() => AppComposition.Current.GetExport<MainWindow>();
[AvaloniaTest]
[TestCase("schema.xsd")]
[TestCase("config.xml")]
[TestCase("transform.xslt")]
[TestCase("Window.xaml")]
[TestCase("logo.png")]
[TestCase("logo.gif")]
[TestCase("logo.bmp")]
[TestCase("logo.jpg")]
[TestCase("favicon.ico")]
[TestCase("pointer.cur")]
[TestCase("strings.resources")]
public void Typed_Resource_Names_Route_To_Specialised_Node(string name)
{
EnsureComposition();
// Tiny payload — node creation must not depend on stream contents being parseable.
var payload = Encoding.UTF8.GetBytes("<root />");
var node = ResourceEntryNode.Create(new ByteArrayResource(name, payload));
// Anything ending in a known extension must NOT fall back to the generic node — the
// dispatcher's whole job is to pick the right typed handler. Compare by exact type
// (not BeOfType) since SharpTreeNode has a custom .Should() extension in this assembly.
node.GetType().Should().NotBe(typeof(ResourceTreeNode),
$"resource '{name}' should be routed to a specialised node, not the generic fallback");
}
[AvaloniaTest]
public void Unknown_Extension_Falls_Back_To_Generic_Resource_Node()
{
EnsureComposition();
var node = ResourceEntryNode.Create(new ByteArrayResource("data.bin", new byte[] { 1, 2, 3 }));
node.GetType().Should().Be(typeof(ResourceTreeNode));
}
}

8
ILSpy/TextView/AvaloniaEditTextOutput.cs

@ -73,6 +73,14 @@ namespace ILSpy.TextView @@ -73,6 +73,14 @@ namespace ILSpy.TextView
public string Title { get; set; } = string.Empty;
/// <summary>
/// Optional file extension override that decides syntax highlighting for the output.
/// Defaults to null (use the active language's extension). Resource entry nodes set this
/// to e.g. <c>".xml"</c> so an embedded XML resource still highlights as XML even though
/// the active language is C#.
/// </summary>
public string? SyntaxExtensionOverride { get; set; }
public string IndentationString { get; set; } = "\t";
public int TextLength => builder.Length;

5
ILSpy/TextView/DecompilerTabPageModel.cs

@ -248,12 +248,15 @@ namespace ILSpy.TextView @@ -248,12 +248,15 @@ namespace ILSpy.TextView
var collectedReferences = output.References;
var collectedLookup = output.DefinitionLookup;
var collectedUIElements = output.UIElements;
// Resource nodes (XML/XAML/…) override the highlighter so their content reads as
// the embedded format, not as the active language.
var effectiveSyntaxExtension = output.SyntaxExtensionOverride ?? newSyntaxExtension;
await Dispatcher.UIThread.InvokeAsync(() => {
// 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 = ComposeBaseTitle();
SyntaxExtension = newSyntaxExtension;
SyntaxExtension = effectiveSyntaxExtension;
HighlightingModel = model;
Foldings = collectedFoldings;
References = collectedReferences;

96
ILSpy/TreeNodes/CursorResourceEntryNode.cs

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Composition;
using System.IO;
using global::Avalonia.Controls;
using global::Avalonia.Media.Imaging;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX.Abstractions;
using ILSpy.Commands;
using ILSpy.Languages;
using ILSpy.TextView;
namespace ILSpy.TreeNodes
{
[Export(typeof(IResourceNodeFactory))]
[Shared]
sealed class CursorResourceNodeFactory : IResourceNodeFactory
{
public ITreeNode? CreateNode(Resource resource)
{
if (resource.Name.EndsWith(".cur", StringComparison.OrdinalIgnoreCase))
return new CursorResourceEntryNode(resource.Name, resource.TryOpenStream);
return null;
}
}
sealed class CursorResourceEntryNode : ResourceEntryNode
{
public CursorResourceEntryNode(string key, Func<Stream?> openStream)
: base(key, () => openStream() ?? Stream.Null)
{
}
public override object Icon => Images.Images.Resource;
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
if (output is not ISmartTextOutput smart)
return;
byte[] cursor;
using (var src = OpenStream())
{
if (src == Stream.Null)
{
output.WriteLine("ILSpy: Failed opening resource stream.");
return;
}
using var ms = new MemoryStream();
src.CopyTo(ms);
cursor = ms.ToArray();
}
// CUR and ICO share the same on-disk layout; the only difference is byte 2 of the
// header (1=icon, 2=cursor). Skia's image decoder only accepts the icon variant, so
// flip the bit before handing it off — preview only, doesn't affect Save (which
// writes the original snapshot).
byte[] iconView = cursor.Length >= 3 ? (byte[])cursor.Clone() : cursor;
if (iconView.Length >= 3)
iconView[2] = 1;
smart.AddUIElement(() => new Image { Source = new Bitmap(new MemoryStream(iconView)) });
smart.WriteLine();
smart.AddButton(Images.Images.Save, "Save", async (_, _) => await SaveSnapshotAsync(cursor).ConfigureAwait(false));
smart.WriteLine();
}
async System.Threading.Tasks.Task SaveSnapshotAsync(byte[] snapshot)
{
var defaultName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName((string)Text));
var path = await FilePickers.SaveAsync("All files|*.*", defaultName).ConfigureAwait(false);
if (path == null)
return;
await File.WriteAllBytesAsync(path, snapshot).ConfigureAwait(false);
}
}
}

91
ILSpy/TreeNodes/IconResourceEntryNode.cs

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Composition;
using System.IO;
using global::Avalonia.Controls;
using global::Avalonia.Media.Imaging;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX.Abstractions;
using ILSpy.Commands;
using ILSpy.Languages;
using ILSpy.TextView;
namespace ILSpy.TreeNodes
{
[Export(typeof(IResourceNodeFactory))]
[Shared]
sealed class IconResourceNodeFactory : IResourceNodeFactory
{
public ITreeNode? CreateNode(Resource resource)
{
if (resource.Name.EndsWith(".ico", StringComparison.OrdinalIgnoreCase))
return new IconResourceEntryNode(resource.Name, resource.TryOpenStream);
return null;
}
}
sealed class IconResourceEntryNode : ResourceEntryNode
{
public IconResourceEntryNode(string key, Func<Stream?> openStream)
: base(key, () => openStream() ?? Stream.Null)
{
}
public override object Icon => Images.Images.Resource;
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
if (output is not ISmartTextOutput smart)
return;
byte[] snapshot;
using (var src = OpenStream())
{
if (src == Stream.Null)
{
output.WriteLine("ILSpy: Failed opening resource stream.");
return;
}
using var ms = new MemoryStream();
src.CopyTo(ms);
snapshot = ms.ToArray();
}
// Avalonia's Bitmap decodes .ico via Skia and picks the largest frame. Per-frame
// rendering (with size/bpp labels) needs ICONDIR/ICONDIRENTRY parsing — TODO follow-up.
smart.AddUIElement(() => new Image { Source = new Bitmap(new MemoryStream(snapshot)) });
smart.WriteLine();
smart.AddButton(Images.Images.Save, "Save", async (_, _) => await SaveSnapshotAsync(snapshot).ConfigureAwait(false));
smart.WriteLine();
}
async System.Threading.Tasks.Task SaveSnapshotAsync(byte[] snapshot)
{
var defaultName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName((string)Text));
var path = await FilePickers.SaveAsync("All files|*.*", defaultName).ConfigureAwait(false);
if (path == null)
return;
await File.WriteAllBytesAsync(path, snapshot).ConfigureAwait(false);
}
}
}

96
ILSpy/TreeNodes/ImageResourceEntryNode.cs

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Composition;
using System.IO;
using global::Avalonia.Controls;
using global::Avalonia.Media.Imaging;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX.Abstractions;
using ILSpy.Languages;
using ILSpy.TextView;
namespace ILSpy.TreeNodes
{
[Export(typeof(IResourceNodeFactory))]
[Shared]
sealed class ImageResourceNodeFactory : IResourceNodeFactory
{
static readonly string[] imageFileExtensions = { ".png", ".gif", ".bmp", ".jpg" };
public ITreeNode? CreateNode(Resource resource)
{
foreach (var ext in imageFileExtensions)
{
if (resource.Name.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
return new ImageResourceEntryNode(resource.Name, resource.TryOpenStream);
}
return null;
}
}
sealed class ImageResourceEntryNode : ResourceEntryNode
{
public ImageResourceEntryNode(string key, Func<Stream?> openStream)
: base(key, () => openStream() ?? Stream.Null)
{
}
// TODO: ship a ResourceImage.svg asset; the generic resource glyph is used for now.
public override object Icon => Images.Images.Resource;
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
if (output is not ISmartTextOutput smart)
return;
byte[] snapshot;
using (var src = OpenStream())
{
if (src == Stream.Null)
{
output.WriteLine("ILSpy: Failed opening resource stream.");
return;
}
using var ms = new MemoryStream();
src.CopyTo(ms);
snapshot = ms.ToArray();
}
// Bitmap consumes the stream, so capture into a byte[] first — the original may be
// one-shot, and AddUIElement runs lazily on the UI thread well after Decompile returns.
smart.AddUIElement(() => new Image { Source = new Bitmap(new MemoryStream(snapshot)) });
smart.WriteLine();
smart.AddButton(Images.Images.Save, "Save", async (_, _) => await SaveSnapshotAsync(snapshot).ConfigureAwait(false));
smart.WriteLine();
}
async System.Threading.Tasks.Task SaveSnapshotAsync(byte[] snapshot)
{
var defaultName = Path.GetFileName(global::ICSharpCode.Decompiler.CSharp.ProjectDecompiler
.WholeProjectDecompiler.SanitizeFileName((string)Text));
var path = await Commands.FilePickers.SaveAsync("All files|*.*", defaultName).ConfigureAwait(false);
if (path == null)
return;
await File.WriteAllBytesAsync(path, snapshot).ConfigureAwait(false);
}
}
}

65
ILSpy/TreeNodes/XamlResourceEntryNode.cs

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Composition;
using System.IO;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX.Abstractions;
using ILSpy.Languages;
using ILSpy.TextView;
namespace ILSpy.TreeNodes
{
[Export(typeof(IResourceNodeFactory))]
[Shared]
sealed class XamlResourceNodeFactory : IResourceNodeFactory
{
public ITreeNode? CreateNode(Resource resource)
{
if (resource.Name.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase))
return new XamlResourceEntryNode(resource.Name, resource.TryOpenStream);
return null;
}
}
sealed class XamlResourceEntryNode : ResourceEntryNode
{
public XamlResourceEntryNode(string key, Func<Stream?> openStream)
: base(key, () => openStream() ?? Stream.Null)
{
}
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
using var data = OpenStream();
if (data == Stream.Null)
{
output.WriteLine("ILSpy: Failed opening resource stream.");
return;
}
using var reader = new StreamReader(data);
output.Write(reader.ReadToEnd());
if (output is AvaloniaEditTextOutput aeto)
aeto.SyntaxExtensionOverride = ".xml";
}
}
}

78
ILSpy/TreeNodes/XmlResourceEntryNode.cs

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Composition;
using System.IO;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX.Abstractions;
using ILSpy.Languages;
using ILSpy.TextView;
namespace ILSpy.TreeNodes
{
[Export(typeof(IResourceNodeFactory))]
[Shared]
sealed class XmlResourceNodeFactory : IResourceNodeFactory
{
static readonly string[] xmlFileExtensions = { ".xml", ".xsd", ".xslt" };
public ITreeNode? CreateNode(Resource resource)
{
foreach (var ext in xmlFileExtensions)
{
if (resource.Name.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
return new XmlResourceEntryNode(resource.Name, resource.TryOpenStream);
}
return null;
}
}
sealed class XmlResourceEntryNode : ResourceEntryNode
{
public XmlResourceEntryNode(string key, Func<Stream?> openStream)
: base(key, () => openStream() ?? Stream.Null)
{
}
// TODO: ship Resource{Xml,Xsd,Xslt}.svg assets and pick by extension. Until then the
// generic resource glyph is fine — the file name in the tree disambiguates.
public override object Icon => Images.Images.Resource;
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
using var data = OpenStream();
if (data == Stream.Null)
{
output.WriteLine("ILSpy: Failed opening resource stream.");
return;
}
using var reader = new StreamReader(data);
output.Write(reader.ReadToEnd());
if (output is AvaloniaEditTextOutput aeto)
{
// Force XML highlighting regardless of the active language. AvaloniaEdit's bundled
// XmlHighlighting.xshd handles .xml / .xsd / .xslt identically.
aeto.SyntaxExtensionOverride = ".xml";
}
}
}
}
Loading…
Cancel
Save