diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index dece8f493..be5b1d778 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -68,7 +68,10 @@ public class AssemblyTreeTests resources.Text.Should().Be(Resources._Resources); resources.EnsureLazyChildren(); resources.Children.Should().NotBeEmpty(); - resources.Children.Should().AllBeAssignableTo(); + // 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(); } [AvaloniaTest] diff --git a/ILSpy.Tests/Resources/ResourceFactoryTests.cs b/ILSpy.Tests/Resources/ResourceFactoryTests.cs new file mode 100644 index 000000000..15aaa1cf5 --- /dev/null +++ b/ILSpy.Tests/Resources/ResourceFactoryTests.cs @@ -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(); + + [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(""); + 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)); + } +} diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs index 858451ade..4b6441355 100644 --- a/ILSpy/TextView/AvaloniaEditTextOutput.cs +++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs @@ -73,6 +73,14 @@ namespace ILSpy.TextView public string Title { get; set; } = string.Empty; + /// + /// 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. ".xml" so an embedded XML resource still highlights as XML even though + /// the active language is C#. + /// + public string? SyntaxExtensionOverride { get; set; } + public string IndentationString { get; set; } = "\t"; public int TextLength => builder.Length; diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 858556cb0..7d31a2c5b 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -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; diff --git a/ILSpy/TreeNodes/CursorResourceEntryNode.cs b/ILSpy/TreeNodes/CursorResourceEntryNode.cs new file mode 100644 index 000000000..70fb2388c --- /dev/null +++ b/ILSpy/TreeNodes/CursorResourceEntryNode.cs @@ -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 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); + } + } +} diff --git a/ILSpy/TreeNodes/IconResourceEntryNode.cs b/ILSpy/TreeNodes/IconResourceEntryNode.cs new file mode 100644 index 000000000..90d1af7fd --- /dev/null +++ b/ILSpy/TreeNodes/IconResourceEntryNode.cs @@ -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 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); + } + } +} diff --git a/ILSpy/TreeNodes/ImageResourceEntryNode.cs b/ILSpy/TreeNodes/ImageResourceEntryNode.cs new file mode 100644 index 000000000..6790327c4 --- /dev/null +++ b/ILSpy/TreeNodes/ImageResourceEntryNode.cs @@ -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 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); + } + } +} diff --git a/ILSpy/TreeNodes/XamlResourceEntryNode.cs b/ILSpy/TreeNodes/XamlResourceEntryNode.cs new file mode 100644 index 000000000..55f8e092c --- /dev/null +++ b/ILSpy/TreeNodes/XamlResourceEntryNode.cs @@ -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 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"; + } + } +} diff --git a/ILSpy/TreeNodes/XmlResourceEntryNode.cs b/ILSpy/TreeNodes/XmlResourceEntryNode.cs new file mode 100644 index 000000000..40da50d5d --- /dev/null +++ b/ILSpy/TreeNodes/XmlResourceEntryNode.cs @@ -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 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"; + } + } + } +}