mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 00:42:51 +08:00

* Adding a dark mode * Adding reference for types to summary pages * Adding API Reference * Adding modifiers to methods/fields/etc * Minimizing files input * Caching a lot of the output pages * Cache only for each execution * Adding API references to existing docs
234 lines
8.9 KiB
C#
234 lines
8.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.Classification;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
namespace Docs.Utilities;
|
|
|
|
internal static class HighlightService
|
|
{
|
|
internal enum HighlightOption
|
|
{
|
|
All,
|
|
Body
|
|
}
|
|
|
|
private static readonly AdhocWorkspace _emptyWorkspace = new();
|
|
|
|
public static async Task<string> Highlight(Compilation compilation, ISymbol symbol, HighlightOption option = HighlightOption.All)
|
|
{
|
|
var syntaxReference = symbol.DeclaringSyntaxReferences.FirstOrDefault();
|
|
if (syntaxReference == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var syntax = await syntaxReference.GetSyntaxAsync();
|
|
var indent = GetIndent(syntax.GetLeadingTrivia());
|
|
var model = compilation.GetSemanticModel(syntaxReference.SyntaxTree);
|
|
|
|
var methodWithBodySyntax = syntax as BaseMethodDeclarationSyntax;
|
|
|
|
TextSpan textSpan;
|
|
switch (option)
|
|
{
|
|
case HighlightOption.Body when methodWithBodySyntax is { Body: { } }:
|
|
{
|
|
syntax = methodWithBodySyntax.Body;
|
|
indent = GetIndent(methodWithBodySyntax.Body.Statements.First().GetLeadingTrivia());
|
|
textSpan = TextSpan.FromBounds(syntax.Span.Start + 1, syntax.Span.End - 1);
|
|
break;
|
|
}
|
|
case HighlightOption.Body when methodWithBodySyntax is { ExpressionBody: { } }:
|
|
{
|
|
syntax = methodWithBodySyntax.ExpressionBody;
|
|
textSpan = syntax.Span;
|
|
break;
|
|
}
|
|
case HighlightOption.All:
|
|
default:
|
|
textSpan = syntax.Span;
|
|
break;
|
|
}
|
|
|
|
var text = await syntaxReference.SyntaxTree.GetTextAsync();
|
|
// we need a workspace, but it seems it is only used to resolve a few services and nothing else so an empty one will suffice
|
|
return HighlightElement(_emptyWorkspace, model, text, textSpan, indent);
|
|
}
|
|
|
|
private static int GetIndent(SyntaxTriviaList leadingTrivia)
|
|
{
|
|
var whitespace = leadingTrivia.FirstOrDefault(i => i.Kind() == SyntaxKind.WhitespaceTrivia);
|
|
return whitespace == default ? 0 : whitespace.Span.Length;
|
|
}
|
|
|
|
private static string HighlightElement(Workspace workspace, SemanticModel semanticModel, SourceText fullSourceText,
|
|
TextSpan textSpan, int indent)
|
|
{
|
|
|
|
var classifiedSpans = Classifier.GetClassifiedSpans(semanticModel, textSpan, workspace);
|
|
return HighlightElement(classifiedSpans, fullSourceText, indent);
|
|
}
|
|
|
|
private static string HighlightElement(IEnumerable<ClassifiedSpan> classifiedSpans, SourceText fullSourceText, int indent)
|
|
{
|
|
|
|
var ranges = classifiedSpans.Select(classifiedSpan =>
|
|
new Range(classifiedSpan.ClassificationType, classifiedSpan.TextSpan, fullSourceText)).ToList();
|
|
|
|
// the classified text won't include the whitespace so we need to add to fill in those gaps.
|
|
ranges = FillGaps(fullSourceText, ranges).ToList();
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
foreach (var range in ranges)
|
|
{
|
|
var cssClass = ClassificationTypeToPrismClass(range.ClassificationType);
|
|
if (string.IsNullOrWhiteSpace(cssClass))
|
|
{
|
|
sb.Append(range.Text);
|
|
}
|
|
else
|
|
{
|
|
// include the prism css class but also include the roslyn classification.
|
|
sb.Append(
|
|
$"<span class=\"token {cssClass} roslyn-{range.ClassificationType.Replace(" ", "-")}\">{range.Text}</span>");
|
|
}
|
|
}
|
|
|
|
// there might be a way to do this with roslyn, but for now we'll just normalize everything off of the length of the
|
|
// leading trivia of the element we are looking at.
|
|
var indentString = new string(' ', indent);
|
|
var allLines = sb.ToString()
|
|
.ReplaceLineEndings()
|
|
.Split(Environment.NewLine)
|
|
.Select(i => i.StartsWith(indentString) == false ? i : i[indent..]);
|
|
|
|
return string.Join(Environment.NewLine, allLines);
|
|
}
|
|
|
|
private static string ClassificationTypeToPrismClass(string rangeClassificationType)
|
|
{
|
|
if (rangeClassificationType == null)
|
|
return string.Empty;
|
|
|
|
switch (rangeClassificationType)
|
|
{
|
|
case ClassificationTypeNames.Identifier:
|
|
return "symbol";
|
|
case ClassificationTypeNames.LocalName:
|
|
return "variable";
|
|
case ClassificationTypeNames.ParameterName:
|
|
case ClassificationTypeNames.PropertyName:
|
|
case ClassificationTypeNames.EnumMemberName:
|
|
case ClassificationTypeNames.FieldName:
|
|
return "property";
|
|
case ClassificationTypeNames.ClassName:
|
|
case ClassificationTypeNames.StructName:
|
|
case ClassificationTypeNames.RecordClassName:
|
|
case ClassificationTypeNames.RecordStructName:
|
|
case ClassificationTypeNames.InterfaceName:
|
|
case ClassificationTypeNames.DelegateName:
|
|
case ClassificationTypeNames.EnumName:
|
|
case ClassificationTypeNames.ModuleName:
|
|
case ClassificationTypeNames.TypeParameterName:
|
|
return "title.class";
|
|
case ClassificationTypeNames.MethodName:
|
|
case ClassificationTypeNames.ExtensionMethodName:
|
|
return "title.function";
|
|
case ClassificationTypeNames.Comment:
|
|
return "comment";
|
|
case ClassificationTypeNames.Keyword:
|
|
case ClassificationTypeNames.ControlKeyword:
|
|
case ClassificationTypeNames.PreprocessorKeyword:
|
|
return "keyword";
|
|
case ClassificationTypeNames.StringLiteral:
|
|
case ClassificationTypeNames.VerbatimStringLiteral:
|
|
return "string";
|
|
case ClassificationTypeNames.NumericLiteral:
|
|
return "number";
|
|
case ClassificationTypeNames.Operator:
|
|
case ClassificationTypeNames.StringEscapeCharacter:
|
|
return "operator";
|
|
case ClassificationTypeNames.Punctuation:
|
|
return "punctuation";
|
|
case ClassificationTypeNames.StaticSymbol:
|
|
return string.Empty;
|
|
case ClassificationTypeNames.XmlDocCommentComment:
|
|
case ClassificationTypeNames.XmlDocCommentDelimiter:
|
|
case ClassificationTypeNames.XmlDocCommentName:
|
|
case ClassificationTypeNames.XmlDocCommentText:
|
|
case ClassificationTypeNames.XmlDocCommentAttributeName:
|
|
case ClassificationTypeNames.XmlDocCommentAttributeQuotes:
|
|
case ClassificationTypeNames.XmlDocCommentAttributeValue:
|
|
case ClassificationTypeNames.XmlDocCommentEntityReference:
|
|
case ClassificationTypeNames.XmlDocCommentProcessingInstruction:
|
|
case ClassificationTypeNames.XmlDocCommentCDataSection:
|
|
return "comment";
|
|
default:
|
|
return rangeClassificationType.Replace(" ", "-");
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<Range> FillGaps(SourceText text, IList<Range> ranges)
|
|
{
|
|
const string WhitespaceClassification = null;
|
|
var current = ranges.First().TextSpan.Start;
|
|
var end = ranges.Last().TextSpan.End;
|
|
Range previous = null;
|
|
|
|
foreach (var range in ranges)
|
|
{
|
|
var start = range.TextSpan.Start;
|
|
if (start > current)
|
|
{
|
|
yield return new Range(WhitespaceClassification, TextSpan.FromBounds(current, start), text);
|
|
}
|
|
|
|
if (previous == null || range.TextSpan != previous.TextSpan)
|
|
{
|
|
yield return range;
|
|
}
|
|
|
|
previous = range;
|
|
current = range.TextSpan.End;
|
|
}
|
|
|
|
if (current < end)
|
|
{
|
|
yield return new Range(WhitespaceClassification, TextSpan.FromBounds(current, end), text);
|
|
}
|
|
}
|
|
|
|
private class Range
|
|
{
|
|
private ClassifiedSpan ClassifiedSpan { get; }
|
|
public string Text { get; }
|
|
|
|
public Range(string classification, TextSpan span, SourceText text) :
|
|
this(classification, span, text.GetSubText(span).ToString())
|
|
{
|
|
}
|
|
|
|
private Range(string classification, TextSpan span, string text) :
|
|
this(new ClassifiedSpan(classification, span), text)
|
|
{
|
|
}
|
|
|
|
private Range(ClassifiedSpan classifiedSpan, string text)
|
|
{
|
|
ClassifiedSpan = classifiedSpan;
|
|
Text = text;
|
|
}
|
|
|
|
public string ClassificationType => ClassifiedSpan.ClassificationType;
|
|
|
|
public TextSpan TextSpan => ClassifiedSpan.TextSpan;
|
|
}
|
|
} |