From 0a0ca73d79d175ca902fd46ce470b933e52f5614 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 22:29:07 +0800 Subject: [PATCH 1/2] feat(analyzer): syntax-highlight entity signatures in Analyze panel Render analyzer result entries with C# syntax coloring so type and member names are easier to scan in long signature lists. Also fix "assmbly" -> "assembly" in ilspycmd help text. Fixes #2164 --- ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs | 4 ++-- ICSharpCode.ILSpyCmd/README.md | 4 ++-- ILSpy/Analyzers/AnalyzerEntityTreeNode.cs | 20 +++++++++++++++++++ .../TreeNodes/AnalyzedEventTreeNode.cs | 2 +- .../TreeNodes/AnalyzedFieldTreeNode.cs | 2 +- .../TreeNodes/AnalyzedMethodTreeNode.cs | 2 +- .../TreeNodes/AnalyzedPropertyTreeNode.cs | 2 +- .../TreeNodes/AnalyzedTypeTreeNode.cs | 2 +- 8 files changed, 29 insertions(+), 9 deletions(-) diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 36587d730d..4dcc350b3e 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -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; } diff --git a/ICSharpCode.ILSpyCmd/README.md b/ICSharpCode.ILSpyCmd/README.md index e523fa1220..f4924eb3f3 100644 --- a/ICSharpCode.ILSpyCmd/README.md +++ b/ICSharpCode.ILSpyCmd/README.md @@ -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 diff --git a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs index c86dc1e808..25b0493746 100644 --- a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs @@ -19,7 +19,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Windows; +using System.Windows.Controls; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Highlighting; +using ICSharpCode.AvalonEdit.Utils; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpyX; @@ -57,6 +61,22 @@ public override void ActivateItem(IPlatformRoutedEventArgs e) public override object? ToolTip => Member?.ParentModule?.MetadataFile?.FileName; + /// + /// Renders a member signature with C# syntax highlighting for the Analyze tree view. + /// + protected object CreateHighlightedSignatureText(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; + } + public override bool HandleAssemblyListChanged(ICollection removedAssemblies, ICollection addedAssemblies) { if (Member == null) diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs index 9df55a0614..372cfbdd66 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs @@ -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 => CreateHighlightedSignatureText(prefix + Language.EntityToString(analyzedEvent, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs index 64a508c57e..ef48f4cd76 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs @@ -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 => CreateHighlightedSignatureText(Language.EntityToString(analyzedField, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs index 2ce76fe51b..3f320f84fa 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs @@ -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 => CreateHighlightedSignatureText(prefix + Language.EntityToString(analyzedMethod, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs index d6cb1df5e6..767e3ecf18 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs @@ -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 => CreateHighlightedSignatureText(prefix + Language.EntityToString(analyzedProperty, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs index 67f37d6f98..3079172c6d 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs @@ -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 => CreateHighlightedSignatureText(Language.TypeToString(analyzedType)); protected override void LoadChildren() { From 6f4c6c3bf8aaf7fef59df68e4d8f9ee1066862e1 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 23:13:46 +0800 Subject: [PATCH 2/2] feat(analyzer): bold type names in Analyze panel signatures Use semantic C# highlighting for analyzer entity nodes and apply bold font weight to type name spans so signatures are easier to scan. Complements syntax highlighting for #2164. --- ILSpy/Analyzers/AnalyzerEntityTreeNode.cs | 107 +++++++++++++++++- .../TreeNodes/AnalyzedEventTreeNode.cs | 2 +- .../TreeNodes/AnalyzedFieldTreeNode.cs | 2 +- .../TreeNodes/AnalyzedMethodTreeNode.cs | 2 +- .../TreeNodes/AnalyzedPropertyTreeNode.cs | 2 +- .../TreeNodes/AnalyzedTypeTreeNode.cs | 2 +- 6 files changed, 110 insertions(+), 7 deletions(-) diff --git a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs index 25b0493746..78fdfd93f8 100644 --- a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs @@ -16,16 +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; @@ -39,6 +49,14 @@ namespace ICSharpCode.ILSpy.Analyzers /// 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; } @@ -62,9 +80,56 @@ public override void ActivateItem(IPlatformRoutedEventArgs e) public override object? ToolTip => Member?.ParentModule?.MetadataFile?.FileName; /// - /// Renders a member signature with C# syntax highlighting for the Analyze tree view. + /// Renders a member signature with semantic C# highlighting and bold type names for the Analyze tree view. /// - protected object CreateHighlightedSignatureText(string signature) + 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) @@ -77,6 +142,44 @@ protected object CreateHighlightedSignatureText(string signature) 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()) + { + 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 GetTypeHighlightingColors(IHighlightingDefinition highlighting) + { + var typeColors = new HashSet(); + foreach (string colorName in TypeHighlightingColorNames) + { + var color = highlighting.GetNamedColor(colorName); + if (color != null) + typeColors.Add(color); + } + return typeColors; + } + public override bool HandleAssemblyListChanged(ICollection removedAssemblies, ICollection addedAssemblies) { if (Member == null) diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs index 372cfbdd66..9c74765649 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedEventTreeNode.cs @@ -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 => CreateHighlightedSignatureText(prefix + Language.EntityToString(analyzedEvent, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); + public override object Text => CreateHighlightedMemberText(prefix, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs index ef48f4cd76..b6e17a5768 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedFieldTreeNode.cs @@ -40,7 +40,7 @@ public AnalyzedFieldTreeNode(IField analyzedField, IEntity? source) public override object Icon => FieldTreeNode.GetIcon(analyzedField); - public override object Text => CreateHighlightedSignatureText(Language.EntityToString(analyzedField, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); + public override object Text => CreateHighlightedMemberText("", ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs index 3f320f84fa..ba3ac424ee 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedMethodTreeNode.cs @@ -42,7 +42,7 @@ public AnalyzedMethodTreeNode(IMethod analyzedMethod, IEntity? source, string pr public override object Icon => MethodTreeNode.GetIcon(analyzedMethod); - public override object Text => CreateHighlightedSignatureText(prefix + Language.EntityToString(analyzedMethod, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); + public override object Text => CreateHighlightedMemberText(prefix, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs index 767e3ecf18..ab3fa30436 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedPropertyTreeNode.cs @@ -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 => CreateHighlightedSignatureText(prefix + Language.EntityToString(analyzedProperty, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames)); + public override object Text => CreateHighlightedMemberText(prefix, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); protected override void LoadChildren() { diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs index 3079172c6d..c77ba31dc4 100644 --- a/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs +++ b/ILSpy/Analyzers/TreeNodes/AnalyzedTypeTreeNode.cs @@ -39,7 +39,7 @@ public AnalyzedTypeTreeNode(ITypeDefinition analyzedType, IEntity? source) public override object Icon => TypeTreeNode.GetIcon(analyzedType); - public override object Text => CreateHighlightedSignatureText(Language.TypeToString(analyzedType)); + public override object Text => CreateHighlightedMemberText("", ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames); protected override void LoadChildren() {