.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

356 lines
13 KiB

// 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.Collections.Generic;
using System.Reflection.Metadata;
using System.Text;
using Avalonia.Controls;
using AvaloniaEdit.Document;
using AvaloniaEdit.Folding;
using AvaloniaEdit.Highlighting;
using AvaloniaEdit.Rendering;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.ILSpy.TextView
{
/// <summary>
/// Avalonia-side <see cref="ITextOutput"/> + <see cref="ISmartTextOutput"/>: accumulates text
/// into a <see cref="StringBuilder"/> and tracks <c>BeginSpan</c>/<c>EndSpan</c> pairs into a
/// <see cref="RichTextModel"/> that the text view renders via <c>RichTextColorizer</c>.
/// Reference markers, fold ranges, and inline UI elements are intentionally dropped here —
/// those land when we add hyperlinks and folding support.
/// </summary>
public sealed class AvaloniaEditTextOutput : ISmartTextOutput, INodeTrackingOutput
{
readonly StringBuilder builder = new();
/// <summary>
/// When the accumulated text exceeds this many characters, the next write throws
/// <see cref="OutputLengthExceededException"/> so a runaway decompile is stopped before it
/// hangs/OOMs the UI thread. Defaults to unlimited; the decompiler tab sets a real limit.
/// </summary>
public int LengthLimit { get; set; } = int.MaxValue;
readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new();
// Keyed by node rather than a stack: node writes are strictly nested today, but keying by
// identity means a stray or out-of-order MarkNodeEnd is a clean no-op that can't desync the
// remaining open nodes. Each node is written exactly once, so there is no self-nesting.
readonly Dictionary<object, int> openNodeStarts = new(ReferenceEqualityComparer.Instance);
readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new();
readonly List<NewFolding> foldings = new();
int indent;
bool needsIndent;
int lineNumber = 1;
/// <summary>The 1-based line the next written text lands on; used to map captured
/// sequence-point lines (relative to a code block) to absolute document lines.</summary>
public int CurrentLine => lineNumber;
public RichTextModel HighlightingModel { get; } = new RichTextModel();
// The same spans that build HighlightingModel, but holding the SHARED named
// HighlightingColor instances (RichTextModel.SetHighlighting clones them, so the model
// itself freezes the colours at decompile time). Kept so the view can rebuild the model
// on a theme switch -- the shared instances are re-coloured in place by ThemeManager.
readonly List<(int Start, int Length, HighlightingColor Color)> highlightingSpans = new();
/// <summary>The highlighting spans referencing the live named colours; see the field note.</summary>
public IReadOnlyList<(int Start, int Length, HighlightingColor Color)> HighlightingSpans => highlightingSpans;
/// <summary>
/// Enables syntax-node range collection for debug-step highlighting. Normal decompiles leave this
/// off because node ranges are only consumed by step-limited replay output.
/// </summary>
public bool EnableNodeTracking { get; set; }
/// <summary>Foldings collected during writing — only ones spanning more than one line.</summary>
public IReadOnlyList<NewFolding> Foldings => foldings;
/// <summary>
/// Hyperlink ranges and their reference targets. Sorted by start offset; safe to query
/// with <see cref="TextSegmentCollection{T}.FindSegmentsContaining"/> after construction.
/// </summary>
public TextSegmentCollection<ReferenceSegment> References { get; } = new();
/// <summary>Maps reference targets to their definition offsets in the rendered text.</summary>
public DefinitionLookup DefinitionLookup { get; } = new();
internal NodeLookup NodeLookup { get; } = new();
internal TextRange? DebugStepHighlight { get; set; }
readonly List<Bookmarks.MethodDebugInfo> methodDebugInfos = new();
/// <summary>
/// Per-method IL-offset &lt;-&gt; line maps captured during a C# decompile (see
/// <see cref="Bookmarks.MethodDebugInfo"/>). Empty for non-C# output; used to anchor
/// in-method bookmarks by IL offset instead of a fragile line number.
/// </summary>
public IReadOnlyList<Bookmarks.MethodDebugInfo> MethodDebugInfos => methodDebugInfos;
/// <summary>Appends a captured method map; called by the C# language after writing the code.</summary>
public void AddMethodDebugInfo(Bookmarks.MethodDebugInfo info) => methodDebugInfos.Add(info);
readonly List<KeyValuePair<int, Func<Control>>> uiElements = new();
/// <summary>Inline UI elements collected during writing, in offset order. Fed to
/// <see cref="UIElementGenerator"/> by the text view.</summary>
public IReadOnlyList<KeyValuePair<int, Func<Control>>> UIElements => uiElements;
readonly List<VisualLineElementGenerator> elementGenerators = new();
/// <summary>
/// Custom <see cref="VisualLineElementGenerator"/>s contributed by the writer (e.g. a
/// regex-based hyperlink generator). Installed on the text view alongside the
/// document and torn down again when the document is replaced.
/// </summary>
public IReadOnlyList<VisualLineElementGenerator> ElementGenerators => elementGenerators;
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";
/// <summary>
/// When set, newlines are written as single spaces and indentation is suppressed, so
/// multi-line writer output (e.g. a disassembled member header) collapses to one
/// line. Used for single-line surfaces like the hover tooltip.
/// </summary>
public bool IgnoreNewLineAndIndent { get; set; }
public int TextLength => builder.Length;
public string GetText() => builder.ToString();
public void Indent()
{
if (!IgnoreNewLineAndIndent)
indent++;
}
public void Unindent()
{
if (!IgnoreNewLineAndIndent && indent > 0)
indent--;
}
void WriteIndentIfNeeded()
{
if (IgnoreNewLineAndIndent || !needsIndent)
return;
needsIndent = false;
for (int i = 0; i < indent; i++)
builder.Append(IndentationString);
}
public void Write(char ch)
{
WriteIndentIfNeeded();
builder.Append(ch);
CheckLength();
}
public void Write(string text)
{
WriteIndentIfNeeded();
builder.Append(text);
CheckLength();
}
public void WriteLine()
{
if (IgnoreNewLineAndIndent)
{
builder.Append(' ');
CheckLength();
return;
}
builder.Append('\n');
lineNumber++;
needsIndent = true;
CheckLength();
}
void CheckLength()
{
if (builder.Length > LengthLimit)
throw new OutputLengthExceededException();
}
public void WriteReference(OpCodeInfo opCode, bool omitSuffix = false)
{
WriteIndentIfNeeded();
int start = builder.Length;
string name = opCode.Name;
if (omitSuffix)
{
int lastDot = name.LastIndexOf('.');
if (lastDot > 0)
name = name.Remove(lastDot + 1);
}
builder.Append(name);
References.Add(new ReferenceSegment {
StartOffset = start,
EndOffset = builder.Length,
Reference = opCode,
});
}
public void WriteReference(MetadataFile metadata, Handle handle, string text, string protocol = "decompile", bool isDefinition = false)
=> AddReference(text, new EntityReference(metadata, handle, protocol), ReferenceMode.Link, isDefinition);
public void WriteReference(IType type, string text, bool isDefinition = false)
=> AddReference(text, type, ReferenceMode.Link, isDefinition);
public void WriteReference(IMember member, string text, bool isDefinition = false)
=> AddReference(text, member, ReferenceMode.Link, isDefinition);
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
=> AddReference(text, reference, isHoverOnly ? ReferenceMode.HoverOnly : ReferenceMode.LocalHighlight, isDefinition);
void AddReference(string text, object reference, ReferenceMode kind, bool isDefinition)
{
WriteIndentIfNeeded();
int start = builder.Length;
builder.Append(text);
int end = builder.Length;
if (isDefinition)
DefinitionLookup.AddDefinition(reference, end);
References.Add(new ReferenceSegment {
StartOffset = start,
EndOffset = end,
Reference = reference,
Kind = kind,
IsDefinition = isDefinition,
});
}
int pendingDefinitionStart = -1;
public void MarkDefinitionStart()
{
pendingDefinitionStart = builder.Length;
}
public void MarkFoldStart(string collapsedText = "...", bool defaultCollapsed = false, bool isDefinition = false)
{
WriteIndentIfNeeded();
NewFolding folding;
if (isDefinition && pendingDefinitionStart >= 0)
{
// The definition's logical region reaches back to the entity's first output
// character, so leading documentation folds count as part of the group.
folding = new DefinitionNewFolding {
StartOffset = builder.Length,
DefinitionStartOffset = pendingDefinitionStart,
};
pendingDefinitionStart = -1;
}
else
{
folding = new NewFolding { StartOffset = builder.Length };
}
folding.Name = collapsedText;
folding.DefaultClosed = defaultCollapsed;
folding.IsDefinition = isDefinition;
openFoldings.Push((folding, lineNumber));
}
public void MarkFoldEnd()
{
if (openFoldings.Count == 0)
return;
var (folding, startLine) = openFoldings.Pop();
folding.EndOffset = builder.Length;
// Skip single-line foldings — they only add visual noise.
if (startLine != lineNumber)
foldings.Add(folding);
}
public void AddUIElement(Func<Control> element)
{
if (element == null)
return;
if (uiElements.Count > 0 && uiElements[uiElements.Count - 1].Key == builder.Length)
throw new InvalidOperationException("Only one UIElement is allowed for each position in the document.");
// Store the factory itself (not a cached Lazy): each editor's UIElementGenerator
// builds and caches its own control instance, so the same control is never shared
// across two editors -- which would make AvaloniaEdit throw "already has a visual
// parent" when a reused/reopened tab renders into a different TextView.
uiElements.Add(new KeyValuePair<int, Func<Control>>(builder.Length, element));
}
public void AddVisualLineElementGenerator(VisualLineElementGenerator generator)
{
ArgumentNullException.ThrowIfNull(generator);
elementGenerators.Add(generator);
}
public void BeginSpan(HighlightingColor highlightingColor)
{
WriteIndentIfNeeded();
openSpans.Push((builder.Length, highlightingColor));
}
public void EndSpan()
{
if (openSpans.Count == 0)
return;
var (start, color) = openSpans.Pop();
var length = builder.Length - start;
if (length > 0)
{
HighlightingModel.SetHighlighting(start, length, color);
highlightingSpans.Add((start, length, color));
}
}
public void MarkNodeStart(object node)
{
if (!EnableNodeTracking)
return;
// Flush a pending indent before capturing the offset so a node opened at the start of a
// line records its range from the first real character, not from column 0 across the
// leading indentation.
WriteIndentIfNeeded();
openNodeStarts[node] = builder.Length;
}
public void MarkNodeEnd(object node)
{
if (!EnableNodeTracking)
return;
if (openNodeStarts.Remove(node, out var start))
NodeLookup.AddNode(node, start, builder.Length - start);
}
}
}