Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ class ILSpyCmdProgram
public bool ReportExcludedTypes { get; set; }

[Option(generateDiagrammerCmd + "-docs", "The path or file:// URI of the XML file containing the target assembly's documentation comments." +
" You only need to set this if a) you want your diagrams annotated with them and b) the file name differs from that of the assmbly." +
" To enable XML documentation output for your assmbly, see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output",
" You only need to set this if a) you want your diagrams annotated with them and b) the file name differs from that of the assembly." +
" To enable XML documentation output for your assembly, see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output",
CommandOptionType.SingleValue)]
public string XmlDocs { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions ICSharpCode.ILSpyCmd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ Options:
your regular expressions.
--generate-diagrammer-docs The path or file:// URI of the XML file containing the target assembly's
documentation comments. You only need to set this if a) you want your diagrams
annotated with them and b) the file name differs from that of the assmbly. To
enable XML documentation output for your assmbly, see
annotated with them and b) the file name differs from that of the assembly. To
enable XML documentation output for your assembly, see
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output
--generate-diagrammer-strip-namespaces Optional space-separated namespace names that are removed for brevity from XML
documentation comments. Note that the order matters: e.g. replace
Expand Down
123 changes: 123 additions & 0 deletions ILSpy/Analyzers/AnalyzerEntityTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,26 @@
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Utils;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.Output;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.AssemblyTree;
using ICSharpCode.ILSpy.TreeNodes;
using ICSharpCode.ILSpy.Util;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView;
using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
Expand All @@ -35,6 +49,14 @@ namespace ICSharpCode.ILSpy.Analyzers
/// </summary>
public abstract class AnalyzerEntityTreeNode : AnalyzerTreeNode, IMemberTreeNode
{
static readonly string[] TypeHighlightingColorNames = {
"ReferenceTypes",
"ValueTypes",
"InterfaceTypes",
"EnumTypes",
"DelegateTypes",
};

public abstract IEntity? Member { get; }

public IEntity? SourceMember { get; protected set; }
Expand All @@ -57,6 +79,107 @@ public override void ActivateItem(IPlatformRoutedEventArgs e)

public override object? ToolTip => Member?.ParentModule?.MetadataFile?.FileName;

/// <summary>
/// Renders a member signature with semantic C# highlighting and bold type names for the Analyze tree view.
/// </summary>
protected object CreateHighlightedMemberText(string prefix, ConversionFlags conversionFlags)
{
var member = Member;
if (member == null)
return prefix;

var fallbackText = prefix + Language.EntityToString(member, conversionFlags);
IHighlightingDefinition? highlighting = Language.SyntaxHighlighting;
if (highlighting == null)
return fallbackText;

var richText = TryCreateSemanticRichText(member, conversionFlags);
if (richText == null)
return CreateLexicalHighlightedText(fallbackText);

var signature = richText.Text;
var document = new TextDocument(signature);
var textBlock = new TextBlock();
if (prefix.Length > 0)
textBlock.Inlines.Add(new Run(prefix));
textBlock.Inlines.AddRange(richText.CreateRuns(document));
ApplyBoldToTypeNames(textBlock, richText, document, highlighting, prefix.Length);
return textBlock;
}

static RichText? TryCreateSemanticRichText(IEntity member, ConversionFlags conversionFlags)
{
if (Language is not CSharpLanguage)
return null;

var output = new StringWriter();
var decoratedWriter = new TextWriterTokenWriter(output);
var writer = new CSharpHighlightingTokenWriter(TokenWriter.InsertRequiredSpaces(decoratedWriter), locatable: decoratedWriter);
var settings = SettingsService.DecompilerSettings.Clone();
if (!Enum.TryParse(AssemblyTreeModel.CurrentLanguageVersion?.Version, out Decompiler.CSharp.LanguageVersion languageVersion))
languageVersion = Decompiler.CSharp.LanguageVersion.Latest;
settings.SetLanguageVersion(languageVersion);
if (member is IMethod { IsLocalFunction: true })
{
writer.WriteIdentifier(Identifier.Create("(local)"));
}
new CSharpAmbience() {
ConversionFlags = conversionFlags,
}.ConvertSymbol(member, writer, settings.CSharpFormattingOptions);
return new RichText(output.ToString(), writer.HighlightingModel);
}

static object CreateLexicalHighlightedText(string signature)
{
IHighlightingDefinition? highlighting = Language.SyntaxHighlighting;
if (highlighting == null)
return signature;

var document = new TextDocument(signature);
var richText = DocumentPrinter.ConvertTextDocumentToRichText(document, new DocumentHighlighter(document, highlighting)).ToRichTextModel();
var textBlock = new TextBlock();
textBlock.Inlines.AddRange(richText.CreateRuns(document));
return textBlock;
}

static void ApplyBoldToTypeNames(TextBlock textBlock, RichText richText, TextDocument document, IHighlightingDefinition highlighting, int textOffset)
{
var typeColors = GetTypeHighlightingColors(highlighting);
var model = richText.ToRichTextModel();
if (model == null || typeColors.Count == 0)
return;

var boldRanges = model.GetHighlightedSections(0, document.TextLength)
.Where(section => section.Color != null && typeColors.Contains(section.Color))
.Select(section => (Start: textOffset + section.Offset, End: textOffset + section.Offset + section.Length))
.ToList();
if (boldRanges.Count == 0)
return;

int charOffset = 0;
foreach (var run in textBlock.Inlines.OfType<Run>())
{
int runLength = run.Text?.Length ?? 0;
int runStart = charOffset;
int runEnd = charOffset + runLength;
if (boldRanges.Any(range => runEnd > range.Start && runStart < range.End))
run.FontWeight = FontWeights.Bold;
charOffset += runLength;
}
}

static HashSet<HighlightingColor> GetTypeHighlightingColors(IHighlightingDefinition highlighting)
{
var typeColors = new HashSet<HighlightingColor>();
foreach (string colorName in TypeHighlightingColorNames)
{
var color = highlighting.GetNamedColor(colorName);
if (color != null)
typeColors.Add(color);
}
return typeColors;
}

public override bool HandleAssemblyListChanged(ICollection<LoadedAssembly> removedAssemblies, ICollection<LoadedAssembly> addedAssemblies)
{
if (Member == null)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public AnalyzedEventTreeNode(IEvent analyzedEvent, IEntity? source, string prefi
public override object Icon => EventTreeNode.GetIcon(analyzedEvent);

// TODO: This way of formatting is not suitable for events which explicitly implement interfaces.
public override object Text => prefix + Language.EntityToString(analyzedEvent, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Text => CreateHighlightedMemberText(prefix, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);

protected override void LoadChildren()
{
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public AnalyzedFieldTreeNode(IField analyzedField, IEntity? source)

public override object Icon => FieldTreeNode.GetIcon(analyzedField);

public override object Text => Language.EntityToString(analyzedField, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Text => CreateHighlightedMemberText("", ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);

protected override void LoadChildren()
{
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public AnalyzedMethodTreeNode(IMethod analyzedMethod, IEntity? source, string pr

public override object Icon => MethodTreeNode.GetIcon(analyzedMethod);

public override object Text => prefix + Language.EntityToString(analyzedMethod, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Text => CreateHighlightedMemberText(prefix, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);

protected override void LoadChildren()
{
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public AnalyzedPropertyTreeNode(IProperty analyzedProperty, IEntity? source, str
public override object Icon => PropertyTreeNode.GetIcon(analyzedProperty);

// TODO: This way of formatting is not suitable for properties which explicitly implement interfaces.
public override object Text => prefix + Language.EntityToString(analyzedProperty, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Text => CreateHighlightedMemberText(prefix, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);

protected override void LoadChildren()
{
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public AnalyzedTypeTreeNode(ITypeDefinition analyzedType, IEntity? source)

public override object Icon => TypeTreeNode.GetIcon(analyzedType);

public override object Text => Language.TypeToString(analyzedType);
public override object Text => CreateHighlightedMemberText("", ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames);

protected override void LoadChildren()
{
Expand Down