add html razor syntax highlighting
This commit is contained in:
@@ -0,0 +1,649 @@
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.AspNetCore.Razor.Language.Syntax;
|
||||
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
||||
|
||||
namespace SharpIDE.Application.Features.Analysis;
|
||||
|
||||
// https://github.com/dotnet/razor/blob/main/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SemanticTokens/SemanticTokensVisitor.cs
|
||||
internal sealed class CustomSemanticTokensVisitor : SyntaxWalker
|
||||
{
|
||||
private readonly List<SemanticRange> _semanticRanges;
|
||||
private readonly RazorCodeDocument _razorCodeDocument;
|
||||
private readonly TextSpan _range;
|
||||
private readonly ISemanticTokensLegendService _semanticTokensLegend;
|
||||
private readonly bool _colorCodeBackground;
|
||||
|
||||
private bool _addRazorCodeModifier;
|
||||
|
||||
private CustomSemanticTokensVisitor(List<SemanticRange> semanticRanges, RazorCodeDocument razorCodeDocument, TextSpan range, ISemanticTokensLegendService semanticTokensLegend, bool colorCodeBackground)
|
||||
{
|
||||
_semanticRanges = semanticRanges;
|
||||
_razorCodeDocument = razorCodeDocument;
|
||||
_range = range;
|
||||
_semanticTokensLegend = semanticTokensLegend;
|
||||
_colorCodeBackground = colorCodeBackground;
|
||||
}
|
||||
|
||||
public static void AddSemanticRanges(List<SemanticRange> ranges, RazorCodeDocument razorCodeDocument, TextSpan textSpan, ISemanticTokensLegendService razorSemanticTokensLegendService, bool colorCodeBackground)
|
||||
{
|
||||
var visitor = new CustomSemanticTokensVisitor(ranges, razorCodeDocument, textSpan, razorSemanticTokensLegendService, colorCodeBackground);
|
||||
|
||||
visitor.Visit(razorCodeDocument.GetRequiredSyntaxRoot());
|
||||
}
|
||||
|
||||
private void Visit(SyntaxList<RazorSyntaxNode> syntaxNodes)
|
||||
{
|
||||
for (var i = 0; i < syntaxNodes.Count; i++)
|
||||
{
|
||||
Visit(syntaxNodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsInRange(TextSpan span)
|
||||
{
|
||||
return _range.OverlapsWith(span);
|
||||
}
|
||||
|
||||
public override void Visit(SyntaxNode? node)
|
||||
{
|
||||
if (node != null && IsInRange(node.Span))
|
||||
{
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
|
||||
#region HTML
|
||||
|
||||
public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
|
||||
{
|
||||
// Don't return anything for MarkupTextLiterals. It translates to "text" on the VS side, which is the default color anyway
|
||||
|
||||
// We want to return something, due to how Godot colorizes text.
|
||||
AddSemanticRange(node, _semanticTokensLegend.TokenTypes.MarkupTextLiteral);
|
||||
}
|
||||
|
||||
public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
|
||||
{
|
||||
AddSemanticRange(node, _semanticTokensLegend.TokenTypes.MarkupAttributeValue);
|
||||
}
|
||||
|
||||
public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
Visit(node.NamePrefix);
|
||||
AddSemanticRange(node.Name, tokenTypes.MarkupAttribute);
|
||||
Visit(node.NameSuffix);
|
||||
AddSemanticRange(node.EqualsToken, tokenTypes.MarkupOperator);
|
||||
|
||||
AddSemanticRange(node.ValuePrefix, tokenTypes.MarkupAttributeQuote);
|
||||
Visit(node.Value);
|
||||
AddSemanticRange(node.ValueSuffix, tokenTypes.MarkupAttributeQuote);
|
||||
}
|
||||
|
||||
public override void VisitMarkupStartTag(MarkupStartTagSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
if (node.IsMarkupTransition)
|
||||
{
|
||||
AddSemanticRange(node, tokenTypes.RazorDirective);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSemanticRange(node.OpenAngle, tokenTypes.MarkupTagDelimiter);
|
||||
|
||||
if (node.Bang.IsValid(out var bang))
|
||||
{
|
||||
AddSemanticRange(bang, tokenTypes.RazorTransition);
|
||||
}
|
||||
|
||||
AddSemanticRange(node.Name, tokenTypes.MarkupElement);
|
||||
|
||||
Visit(node.Attributes);
|
||||
if (node.ForwardSlash.IsValid(out var forwardSlash))
|
||||
{
|
||||
AddSemanticRange(forwardSlash, tokenTypes.MarkupTagDelimiter);
|
||||
}
|
||||
|
||||
AddSemanticRange(node.CloseAngle, tokenTypes.MarkupTagDelimiter);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitMarkupEndTag(MarkupEndTagSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
if (node.IsMarkupTransition)
|
||||
{
|
||||
AddSemanticRange(node, tokenTypes.RazorDirective);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSemanticRange(node.OpenAngle, tokenTypes.MarkupTagDelimiter);
|
||||
|
||||
if (node.Bang.IsValid(out var bang))
|
||||
{
|
||||
AddSemanticRange(bang, tokenTypes.RazorTransition);
|
||||
}
|
||||
|
||||
if (node.ForwardSlash.IsValid(out var forwardSlash))
|
||||
{
|
||||
AddSemanticRange(forwardSlash, tokenTypes.MarkupTagDelimiter);
|
||||
}
|
||||
|
||||
AddSemanticRange(node.Name, tokenTypes.MarkupElement);
|
||||
|
||||
AddSemanticRange(node.CloseAngle, tokenTypes.MarkupTagDelimiter);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
AddSemanticRange(node.Children[0], tokenTypes.MarkupCommentPunctuation);
|
||||
|
||||
for (var i = 1; i < node.Children.Count - 1; i++)
|
||||
{
|
||||
var commentNode = node.Children[i];
|
||||
switch (commentNode.Kind)
|
||||
{
|
||||
case SyntaxKind.MarkupTextLiteral:
|
||||
AddSemanticRange(commentNode, tokenTypes.MarkupComment);
|
||||
break;
|
||||
default:
|
||||
Visit(commentNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AddSemanticRange(node.Children[^1], tokenTypes.MarkupCommentPunctuation);
|
||||
}
|
||||
|
||||
public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node)
|
||||
{
|
||||
Visit(node.NamePrefix);
|
||||
AddSemanticRange(node.Name, _semanticTokensLegend.TokenTypes.MarkupAttribute);
|
||||
}
|
||||
|
||||
#endregion HTML
|
||||
|
||||
#region C#
|
||||
|
||||
public override void VisitCSharpStatementBody(CSharpStatementBodySyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
AddSemanticRange(node.OpenBrace, tokenTypes.RazorTransition);
|
||||
|
||||
Visit(node.CSharpCode);
|
||||
|
||||
AddSemanticRange(node.CloseBrace, tokenTypes.RazorTransition);
|
||||
|
||||
}
|
||||
|
||||
public override void VisitCSharpImplicitExpressionBody(CSharpImplicitExpressionBodySyntax node)
|
||||
{
|
||||
// Generally same as explicit expression, below, but different because the parens might not be there,
|
||||
// and because the compiler isn't nice and doesn't give us OpenParen and CloseParen properties we can
|
||||
// easily use.
|
||||
|
||||
// Matches @(SomeCSharpCode())
|
||||
if (node.CSharpCode.Children is
|
||||
[
|
||||
CSharpExpressionLiteralSyntax { LiteralTokens: [{ Kind: SyntaxKind.LeftParenthesis } openParen] },
|
||||
CSharpExpressionLiteralSyntax body,
|
||||
CSharpExpressionLiteralSyntax { LiteralTokens: [{ Kind: SyntaxKind.RightParenthesis } closeParen] },
|
||||
])
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
AddSemanticRange(openParen, tokenTypes.RazorTransition);
|
||||
|
||||
Visit(body);
|
||||
|
||||
AddSemanticRange(closeParen, tokenTypes.RazorTransition);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Matches @SomeCSharpCode()
|
||||
Visit(node.CSharpCode);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitCSharpExplicitExpressionBody(CSharpExplicitExpressionBodySyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
AddSemanticRange(node.OpenParen, tokenTypes.RazorTransition);
|
||||
|
||||
Visit(node.CSharpCode);
|
||||
|
||||
AddSemanticRange(node.CloseParen, tokenTypes.RazorTransition);
|
||||
|
||||
}
|
||||
|
||||
#endregion C#
|
||||
|
||||
#region Razor
|
||||
|
||||
public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
AddSemanticRange(node.StartCommentTransition, tokenTypes.RazorCommentTransition);
|
||||
AddSemanticRange(node.StartCommentStar, tokenTypes.RazorCommentStar);
|
||||
AddSemanticRange(node.Comment, tokenTypes.RazorComment);
|
||||
AddSemanticRange(node.EndCommentStar, tokenTypes.RazorCommentStar);
|
||||
AddSemanticRange(node.EndCommentTransition, tokenTypes.RazorCommentTransition);
|
||||
}
|
||||
|
||||
public override void VisitRazorMetaCode(RazorMetaCodeSyntax node)
|
||||
{
|
||||
if (node.Kind == SyntaxKind.RazorMetaCode)
|
||||
{
|
||||
AddSemanticRange(node, _semanticTokensLegend.TokenTypes.RazorTransition);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unknown RazorMetaCode");
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitRazorDirectiveBody(RazorDirectiveBodySyntax node)
|
||||
{
|
||||
// We can't provide colors for CSharp because if we both provided them then they would overlap, which violates the LSP spec.
|
||||
if (node.Keyword.Kind != SyntaxKind.CSharpStatementLiteral)
|
||||
{
|
||||
AddSemanticRange(node.Keyword, _semanticTokensLegend.TokenTypes.RazorDirective);
|
||||
}
|
||||
else
|
||||
{
|
||||
Visit(node.Keyword);
|
||||
}
|
||||
|
||||
Visit(node.CSharpCode);
|
||||
}
|
||||
|
||||
public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
AddSemanticRange(node.OpenAngle, tokenTypes.MarkupTagDelimiter);
|
||||
|
||||
if (node.Bang.IsValid(out var bang))
|
||||
{
|
||||
AddSemanticRange(bang, tokenTypes.RazorTransition);
|
||||
}
|
||||
|
||||
if (ClassifyTagName((MarkupTagHelperElementSyntax)node.Parent))
|
||||
{
|
||||
var semanticKind = GetElementSemanticKind(node);
|
||||
AddSemanticRange(node.Name, semanticKind);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSemanticRange(node.Name, tokenTypes.MarkupElement);
|
||||
}
|
||||
|
||||
Visit(node.Attributes);
|
||||
|
||||
if (node.ForwardSlash.IsValid(out var forwardSlash))
|
||||
{
|
||||
AddSemanticRange(forwardSlash, tokenTypes.MarkupTagDelimiter);
|
||||
}
|
||||
|
||||
AddSemanticRange(node.CloseAngle, tokenTypes.MarkupTagDelimiter);
|
||||
}
|
||||
|
||||
public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
AddSemanticRange(node.OpenAngle, tokenTypes.MarkupTagDelimiter);
|
||||
AddSemanticRange(node.ForwardSlash, tokenTypes.MarkupTagDelimiter);
|
||||
|
||||
if (node.Bang.IsValid(out var bang))
|
||||
{
|
||||
AddSemanticRange(bang, tokenTypes.RazorTransition);
|
||||
}
|
||||
|
||||
if (ClassifyTagName((MarkupTagHelperElementSyntax)node.Parent))
|
||||
{
|
||||
var semanticKind = GetElementSemanticKind(node);
|
||||
AddSemanticRange(node.Name, semanticKind);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSemanticRange(node.Name, tokenTypes.MarkupElement);
|
||||
}
|
||||
|
||||
AddSemanticRange(node.CloseAngle, tokenTypes.MarkupTagDelimiter);
|
||||
}
|
||||
|
||||
public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHelperAttributeSyntax node)
|
||||
{
|
||||
Visit(node.NamePrefix);
|
||||
|
||||
if (node.TagHelperAttributeInfo.Bound)
|
||||
{
|
||||
var semanticKind = GetAttributeSemanticKind(node);
|
||||
AddSemanticRange(node.Name, semanticKind);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSemanticRange(node.Name, _semanticTokensLegend.TokenTypes.MarkupAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
Visit(node.NamePrefix);
|
||||
if (node.TagHelperAttributeInfo.Bound)
|
||||
{
|
||||
var semanticKind = GetAttributeSemanticKind(node);
|
||||
AddSemanticRange(node.Name, semanticKind);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSemanticRange(node.Name, tokenTypes.MarkupAttribute);
|
||||
}
|
||||
|
||||
Visit(node.NameSuffix);
|
||||
|
||||
AddSemanticRange(node.EqualsToken, tokenTypes.MarkupOperator);
|
||||
|
||||
AddSemanticRange(node.ValuePrefix, tokenTypes.MarkupAttributeQuote);
|
||||
Visit(node.Value);
|
||||
AddSemanticRange(node.ValueSuffix, tokenTypes.MarkupAttributeQuote);
|
||||
}
|
||||
|
||||
public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node)
|
||||
{
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
if (child.Kind == SyntaxKind.MarkupTextLiteral)
|
||||
{
|
||||
AddSemanticRange(child, _semanticTokensLegend.TokenTypes.MarkupAttributeValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Visit(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirectiveAttributeSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
if (node.TagHelperAttributeInfo.Bound)
|
||||
{
|
||||
Visit(node.Transition);
|
||||
Visit(node.NamePrefix);
|
||||
AddSemanticRange(node.Name, tokenTypes.RazorDirectiveAttribute);
|
||||
Visit(node.NameSuffix);
|
||||
|
||||
if (node.Colon != null)
|
||||
{
|
||||
AddSemanticRange(node.Colon, tokenTypes.RazorDirectiveColon);
|
||||
}
|
||||
|
||||
if (node.ParameterName != null)
|
||||
{
|
||||
AddSemanticRange(node.ParameterName, tokenTypes.RazorDirectiveAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
AddSemanticRange(node.EqualsToken, tokenTypes.MarkupOperator);
|
||||
AddSemanticRange(node.ValuePrefix, tokenTypes.MarkupAttributeQuote);
|
||||
Visit(node.Value);
|
||||
AddSemanticRange(node.ValueSuffix, tokenTypes.MarkupAttributeQuote);
|
||||
}
|
||||
|
||||
public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinimizedTagHelperDirectiveAttributeSyntax node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
if (node.TagHelperAttributeInfo.Bound)
|
||||
{
|
||||
AddSemanticRange(node.Transition, tokenTypes.RazorTransition);
|
||||
Visit(node.NamePrefix);
|
||||
AddSemanticRange(node.Name, tokenTypes.RazorDirectiveAttribute);
|
||||
|
||||
if (node.Colon != null)
|
||||
{
|
||||
AddSemanticRange(node.Colon, tokenTypes.RazorDirectiveColon);
|
||||
}
|
||||
|
||||
if (node.ParameterName != null)
|
||||
{
|
||||
AddSemanticRange(node.ParameterName, tokenTypes.RazorDirectiveAttribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitCSharpTransition(CSharpTransitionSyntax node)
|
||||
{
|
||||
if (node.Parent is not RazorDirectiveSyntax)
|
||||
{
|
||||
AddSemanticRange(node, _semanticTokensLegend.TokenTypes.RazorTransition);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSemanticRange(node, _semanticTokensLegend.TokenTypes.RazorTransition);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitMarkupTransition(MarkupTransitionSyntax node)
|
||||
{
|
||||
AddSemanticRange(node, _semanticTokensLegend.TokenTypes.RazorTransition);
|
||||
}
|
||||
|
||||
#endregion Razor
|
||||
|
||||
private int GetElementSemanticKind(SyntaxNode node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
var semanticKind = IsComponent(node) ? tokenTypes.RazorComponentElement : tokenTypes.RazorTagHelperElement;
|
||||
return semanticKind;
|
||||
}
|
||||
|
||||
private int GetAttributeSemanticKind(SyntaxNode node)
|
||||
{
|
||||
var tokenTypes = _semanticTokensLegend.TokenTypes;
|
||||
|
||||
var semanticKind = IsComponent(node) ? tokenTypes.RazorComponentAttribute : tokenTypes.RazorTagHelperAttribute;
|
||||
return semanticKind;
|
||||
}
|
||||
|
||||
private static bool IsComponent(SyntaxNode node)
|
||||
{
|
||||
if (node is MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding })
|
||||
{
|
||||
var componentDescriptor = binding.Descriptors.FirstOrDefault(static d => d.Kind == TagHelperKind.Component);
|
||||
return componentDescriptor is not null;
|
||||
}
|
||||
else if (node is MarkupTagHelperStartTagSyntax startTag)
|
||||
{
|
||||
return IsComponent(startTag.Parent);
|
||||
}
|
||||
else if (node is MarkupTagHelperEndTagSyntax endTag)
|
||||
{
|
||||
return IsComponent(endTag.Parent);
|
||||
}
|
||||
else if (node is MarkupTagHelperAttributeSyntax attribute)
|
||||
{
|
||||
return IsComponent(attribute.Parent.Parent);
|
||||
}
|
||||
else if (node is MarkupMinimizedTagHelperAttributeSyntax minimizedTagHelperAttribute)
|
||||
{
|
||||
return IsComponent(minimizedTagHelperAttribute.Parent.Parent);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
// We don't want to classify TagNames of well-known HTML
|
||||
// elements as TagHelpers (even if they are). So the 'input' in`<input @onclick='...' />`
|
||||
// needs to not be marked as a TagHelper, but `<Input @onclick='...' />` should be.
|
||||
private static bool ClassifyTagName(MarkupTagHelperElementSyntax node)
|
||||
{
|
||||
if (node is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
if (node.StartTag?.Name != null &&
|
||||
node.TagHelperInfo is { BindingResult: var binding })
|
||||
{
|
||||
return !binding.IsAttributeMatch;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddSemanticRange(SyntaxNode node, int semanticKind)
|
||||
{
|
||||
if (node is null)
|
||||
{
|
||||
// This can happen in situations like "<p class='", where the trailing ' hasn't been typed yet.
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.Width == 0)
|
||||
{
|
||||
// Under no circumstances can we have 0-width spans.
|
||||
// This can happen in situations like "@* comment ", where EndCommentStar and EndCommentTransition are empty.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsInRange(node.Span))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var source = _razorCodeDocument.Source;
|
||||
var range = node.GetLinePositionSpan(source);
|
||||
var tokenModifier = _addRazorCodeModifier ? _semanticTokensLegend.TokenModifiers.RazorCodeModifier : 0;
|
||||
|
||||
if (range.Start.Line != range.End.Line)
|
||||
{
|
||||
// We have to iterate over the individual nodes because this node might consist of multiple lines
|
||||
// ie: "\r\ntext\r\n" would be parsed as one node containing three elements (newline, "text", newline).
|
||||
foreach (var childNodeOrToken in node.ChildNodesAndTokens())
|
||||
{
|
||||
// We skip whitespace to avoid "multiline" ranges for "/r/n", where the /n is interpreted as being on a new line.
|
||||
// This also stops us from returning data for " ", which seems like a nice side-effect as it's not likely to have any colorization anyway.
|
||||
if (!childNodeOrToken.ContainsOnlyWhitespace())
|
||||
{
|
||||
var lineSpan = childNodeOrToken.GetLinePositionSpan(source);
|
||||
AddRange(semanticKind, lineSpan, tokenModifier, fromRazor: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddRange(semanticKind, range, tokenModifier, fromRazor: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSemanticRange(SyntaxToken token, int semanticKind)
|
||||
{
|
||||
if (token.Width == 0 || token.ContainsOnlyWhitespace())
|
||||
{
|
||||
// Under no circumstances can we have 0-width spans.
|
||||
// This can happen in situations like "@* comment ", where EndCommentStar and EndCommentTransition are empty.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsInRange(token.Span))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var source = _razorCodeDocument.Source;
|
||||
var lineSpan = token.GetLinePositionSpan(source);
|
||||
var tokenModifier = _addRazorCodeModifier ? _semanticTokensLegend.TokenModifiers.RazorCodeModifier : 0;
|
||||
|
||||
var charPosition = lineSpan.Start.Character;
|
||||
var lineStartAbsoluteIndex = token.SpanStart - charPosition;
|
||||
|
||||
for (var line = lineSpan.Start.Line; line <= lineSpan.End.Line; line++)
|
||||
{
|
||||
var originalCharPosition = charPosition;
|
||||
// NOTE: We don't report tokens for newlines so need to account for them.
|
||||
var lineLength = source.Text.Lines[line].SpanIncludingLineBreak.Length;
|
||||
|
||||
// For the last line, we end where the syntax tree tells us to. For all other lines, we end at the
|
||||
// last non-newline character
|
||||
var endChar = line == lineSpan.End.Line
|
||||
? lineSpan.End.Character
|
||||
: GetLastNonWhitespaceCharacterOffset(source, lineStartAbsoluteIndex, lineLength);
|
||||
|
||||
// Make sure we move our line start index pointer on, before potentially breaking out of the loop
|
||||
lineStartAbsoluteIndex += lineLength;
|
||||
charPosition = 0;
|
||||
|
||||
// No tokens for blank lines
|
||||
if (endChar == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddRange(new(
|
||||
semanticKind,
|
||||
start: new(line, originalCharPosition),
|
||||
end: new(line, endChar),
|
||||
tokenModifier,
|
||||
fromRazor: true));
|
||||
}
|
||||
|
||||
static int GetLastNonWhitespaceCharacterOffset(RazorSourceDocument source, int lineStartAbsoluteIndex, int lineLength)
|
||||
{
|
||||
// lineStartAbsoluteIndex + lineLength is the first character of the next line, so move back one to get to the end of the line
|
||||
lineLength--;
|
||||
|
||||
var lineEndAbsoluteIndex = lineStartAbsoluteIndex + lineLength;
|
||||
if (lineEndAbsoluteIndex == 0 || lineLength == 0)
|
||||
{
|
||||
return lineLength;
|
||||
}
|
||||
|
||||
return source.Text[lineEndAbsoluteIndex - 1] is '\n' or '\r'
|
||||
? lineLength - 1
|
||||
: lineLength;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRange(int semanticKind, LinePositionSpan range, int tokenModifer, bool fromRazor)
|
||||
{
|
||||
AddRange(new(semanticKind, range, tokenModifer, fromRazor));
|
||||
}
|
||||
|
||||
private void AddRange(SemanticRange semanticRange)
|
||||
{
|
||||
// If the end is before the start, well that's no good!
|
||||
if (semanticRange.EndLine < semanticRange.StartLine)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the end is before the start, that's still no good, but I'm separating out this check
|
||||
// to make it clear that it also checks for equality: No point classifying 0-length ranges.
|
||||
if (semanticRange.EndLine == semanticRange.StartLine &&
|
||||
semanticRange.EndCharacter <= semanticRange.StartCharacter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_semanticRanges.Add(semanticRange);
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,26 @@
|
||||
using System.Composition.Hosting;
|
||||
using System.Diagnostics;
|
||||
using Ardalis.GuardClauses;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Classification;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CodeRefactorings;
|
||||
using Microsoft.CodeAnalysis.Completion;
|
||||
using Microsoft.CodeAnalysis.Host.Mef;
|
||||
using Microsoft.CodeAnalysis.MSBuild;
|
||||
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
||||
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using NuGet.Packaging;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
using SharpIDE.RazorAccess;
|
||||
using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction;
|
||||
using CompletionList = Microsoft.CodeAnalysis.Completion.CompletionList;
|
||||
using Diagnostic = Microsoft.CodeAnalysis.Diagnostic;
|
||||
using DiagnosticSeverity = Microsoft.CodeAnalysis.DiagnosticSeverity;
|
||||
|
||||
namespace SharpIDE.Application.Features.Analysis;
|
||||
|
||||
@@ -23,6 +29,7 @@ public static class RoslynAnalysis
|
||||
{
|
||||
public static MSBuildWorkspace? _workspace;
|
||||
private static RemoteSnapshotManager? _snapshotManager;
|
||||
private static RemoteSemanticTokensLegendService? _semanticTokensLegendService;
|
||||
private static SharpIdeSolutionModel? _sharpIdeSolutionModel;
|
||||
private static HashSet<CodeFixProvider> _codeFixProviders = [];
|
||||
private static HashSet<CodeRefactoringProvider> _codeRefactoringProviders = [];
|
||||
@@ -58,8 +65,12 @@ public static class RoslynAnalysis
|
||||
var host = MefHostServices.Create(container);
|
||||
_workspace = MSBuildWorkspace.Create(host);
|
||||
_workspace.RegisterWorkspaceFailedHandler(o => throw new InvalidOperationException($"Workspace failed: {o.Diagnostic.Message}"));
|
||||
|
||||
var snapshotManager = container.GetExports<RemoteSnapshotManager>().FirstOrDefault();
|
||||
_snapshotManager = snapshotManager;
|
||||
|
||||
_semanticTokensLegendService = container.GetExports<RemoteSemanticTokensLegendService>().FirstOrDefault();
|
||||
_semanticTokensLegendService!.SetLegend(TokenTypeProvider.ConstructTokenTypes(false), TokenTypeProvider.ConstructTokenModifiers());
|
||||
}
|
||||
var solution = await _workspace.OpenSolutionAsync(_sharpIdeSolutionModel.FilePath, new Progress());
|
||||
timer.Stop();
|
||||
@@ -188,7 +199,49 @@ public static class RoslynAnalysis
|
||||
|
||||
var razorText = await razorDocument.GetTextAsync(cancellationToken);
|
||||
|
||||
List<string> relevantTypes = ["razorDirective", "razorTransition", "markupTextLiteral", "markupTagDelimiter", "markupElement", "razorComponentElement", "razorComponentAttribute", "razorComment", "razorCommentTransition", "razorCommentStar", "markupOperator", "markupAttributeQuote"];
|
||||
var ranges = new List<SemanticRange>();
|
||||
CustomSemanticTokensVisitor.AddSemanticRanges(ranges, razorCodeDocument, generatedDocSyntaxRoot!.FullSpan, _semanticTokensLegendService!, false);
|
||||
var relevantRanges = ranges.Select(s =>
|
||||
{
|
||||
var kind = _semanticTokensLegendService!.TokenTypes.All[s.Kind];
|
||||
return new TranslatedSemanticRange { Range = s, Kind = kind };
|
||||
}).Where(s => relevantTypes.Contains(s.Kind)).ToList();
|
||||
|
||||
//var allTypes = ranges.Select(s => _semanticTokensLegendService!.TokenTypes.All[s.Kind]).Distinct().ToList();
|
||||
var semanticRangeRazorSpans = relevantRanges.Select(s =>
|
||||
{
|
||||
var linePositionSpan = s.Range.AsLinePositionSpan();
|
||||
var textSpan = razorText.GetTextSpan(linePositionSpan);
|
||||
var sourceSpan = new SourceSpan(
|
||||
fileModel.Path,
|
||||
textSpan.Start,
|
||||
linePositionSpan.Start.Line,
|
||||
linePositionSpan.Start.Character,
|
||||
textSpan.Length,
|
||||
1,
|
||||
linePositionSpan.End.Character
|
||||
);
|
||||
|
||||
return new SharpIdeRazorClassifiedSpan(sourceSpan.ToSharpIdeSourceSpan(), SharpIdeRazorSpanKind.Markup, null, s.Kind);
|
||||
}).ToList();
|
||||
|
||||
// var debugMappedBackTranslatedSemanticRanges = relevantRanges.Select(s =>
|
||||
// {
|
||||
// var textSpan = razorText.GetTextSpan(s.Range.AsLinePositionSpan());
|
||||
// var text = razorText.GetSubTextString(textSpan);
|
||||
// return new { text, s };
|
||||
// }).ToList();
|
||||
// var semanticRangesAsRazorClassifiedSpans = ranges
|
||||
// .Select(s =>
|
||||
// {
|
||||
// var sourceSpan = new SharpIdeRazorSourceSpan(null, s.)
|
||||
// var span = new SharpIdeRazorClassifiedSpan();
|
||||
// return span;
|
||||
// }).ToList();
|
||||
//var test = _semanticTokensLegendService.TokenTypes.All;
|
||||
var (razorSpans, sourceMappings) = RazorAccessors.GetSpansAndMappingsForRazorCodeDocument(razorCodeDocument, razorCSharpDocument);
|
||||
List<SharpIdeRazorClassifiedSpan> sharpIdeRazorSpans = [];
|
||||
|
||||
var classifiedSpans = await Classifier.GetClassifiedSpansAsync(generatedDocument, generatedDocSyntaxRoot!.FullSpan, cancellationToken);
|
||||
var roslynMappedSpans = classifiedSpans.Select(s =>
|
||||
@@ -220,14 +273,15 @@ public static class RoslynAnalysis
|
||||
return null;
|
||||
}).Where(s => s is not null).ToList();
|
||||
|
||||
razorSpans = [
|
||||
..razorSpans.Where(s => s.Kind is not SharpIdeRazorSpanKind.Code),
|
||||
..roslynMappedSpans.Select(s => new SharpIdeRazorClassifiedSpan(s!.SourceSpanInRazor, SharpIdeRazorSpanKind.Code, s.CsharpClassificationType))
|
||||
sharpIdeRazorSpans = [
|
||||
..sharpIdeRazorSpans.Where(s => s.Kind is not SharpIdeRazorSpanKind.Code),
|
||||
..roslynMappedSpans.Select(s => new SharpIdeRazorClassifiedSpan(s!.SourceSpanInRazor, SharpIdeRazorSpanKind.Code, s.CsharpClassificationType)),
|
||||
..semanticRangeRazorSpans
|
||||
];
|
||||
razorSpans = razorSpans.OrderBy(s => s.Span.AbsoluteIndex).ToImmutableArray();
|
||||
sharpIdeRazorSpans = sharpIdeRazorSpans.OrderBy(s => s.Span.AbsoluteIndex).ToList();
|
||||
timer.Stop();
|
||||
Console.WriteLine($"RoslynAnalysis: Razor syntax highlighting for {fileModel.Name} took {timer.ElapsedMilliseconds}ms");
|
||||
return razorSpans;
|
||||
return sharpIdeRazorSpans;
|
||||
}
|
||||
|
||||
public static async Task<IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)>> GetDocumentSyntaxHighlighting(SharpIdeFile fileModel)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
||||
|
||||
namespace SharpIDE.Application.Features.Analysis;
|
||||
|
||||
public static class TokenTypeProvider
|
||||
{
|
||||
public static string[] ConstructTokenTypes(bool supportsVsExtensions)
|
||||
{
|
||||
string[] types = [.. RazorSemanticTokensAccessor.GetTokenTypes(supportsVsExtensions), .. GetStaticFieldValues(typeof(SemanticTokenTypes))];
|
||||
//return new SemanticTokenTypes(types);
|
||||
return types;
|
||||
}
|
||||
|
||||
public static string[] ConstructTokenModifiers()
|
||||
{
|
||||
string[] types = [ .. RazorSemanticTokensAccessor.GetTokenModifiers(), .. GetStaticFieldValues(typeof(SemanticTokenModifiers))];
|
||||
//return new SemanticTokenModifiers(types);
|
||||
return types;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetStaticFieldValues(Type type)
|
||||
{
|
||||
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static).Select(s => s.GetValue(null)).OfType<string>().ToImmutableArray();
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
||||
|
||||
namespace SharpIDE.Application.Features.Analysis;
|
||||
|
||||
public class TranslatedSemanticRange
|
||||
{
|
||||
public required SemanticRange Range { get; set; }
|
||||
public required string Kind { get; set; }
|
||||
}
|
||||
@@ -8,10 +8,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.Remote.Razor" />
|
||||
<!-- <IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.Razor.Workspaces" />-->
|
||||
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.Razor.Workspaces" />
|
||||
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.Razor.Compiler" />
|
||||
<IgnoresAccessChecksTo Include="Microsoft.VisualStudioCode.RazorExtension" />
|
||||
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.LanguageServer.Protocol" />
|
||||
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.ExternalAccess.Razor.Features" />
|
||||
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.Razor.SemanticTokens" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -32,6 +35,7 @@
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" />
|
||||
<!-- <PackageReference Include="Microsoft.VisualStudioCode.RazorExtension" />--> <!-- Either this or Microsoft.CodeAnalysis.Remote.Razor supplies RemoteSnapshotManager -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Remote.Razor" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.ExternalAccess.Razor.Features" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Features" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" PrivateAssets="all" />
|
||||
|
||||
@@ -57,7 +57,7 @@ public partial class CustomHighlighter : SyntaxHighlighter
|
||||
|
||||
var highlightInfo = new Dictionary
|
||||
{
|
||||
{ ColorStringName, GetColorForRazorSpanKind(razorSpan.Kind, razorSpan.CodeClassificationType) }
|
||||
{ ColorStringName, GetColorForRazorSpanKind(razorSpan.Kind, razorSpan.CodeClassificationType, razorSpan.VsSemanticRangeType) }
|
||||
};
|
||||
|
||||
highlights[columnIndex] = highlightInfo;
|
||||
@@ -66,19 +66,36 @@ public partial class CustomHighlighter : SyntaxHighlighter
|
||||
return highlights;
|
||||
}
|
||||
|
||||
private static Color GetColorForRazorSpanKind(SharpIdeRazorSpanKind kind, string? codeClassificationType)
|
||||
private static Color GetColorForRazorSpanKind(SharpIdeRazorSpanKind kind, string? codeClassificationType, string? vsSemanticRangeType)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
SharpIdeRazorSpanKind.Code => GetColorForClassification(codeClassificationType!),
|
||||
SharpIdeRazorSpanKind.Comment => new Color("57a64a"), // green
|
||||
SharpIdeRazorSpanKind.MetaCode => new Color("a699e6"), // purple
|
||||
SharpIdeRazorSpanKind.Markup => new Color("0b7f7f"), // dark green
|
||||
SharpIdeRazorSpanKind.Markup => GetColorForMarkupSpanKind(vsSemanticRangeType),
|
||||
SharpIdeRazorSpanKind.Transition => new Color("a699e6"), // purple
|
||||
SharpIdeRazorSpanKind.None => new Color("dcdcdc"),
|
||||
_ => new Color("dcdcdc")
|
||||
};
|
||||
}
|
||||
|
||||
private static Color GetColorForMarkupSpanKind(string? vsSemanticRangeType)
|
||||
{
|
||||
return vsSemanticRangeType switch
|
||||
{
|
||||
"razorDirective" or "razorTransition" => new Color("a699e6"), // purple
|
||||
"markupTagDelimiter" => new Color("808080"), // gray
|
||||
"markupTextLiteral" => new Color("dcdcdc"), // white
|
||||
"markupElement" => new Color("569cd6"), // blue
|
||||
"razorComponentElement" => new Color("0b7f7f"), // dark green
|
||||
"razorComponentAttribute" => new Color("dcdcdc"), // white
|
||||
"razorComment" or "razorCommentStar" or "razorCommentTransition" => new Color("57a64a"), // green
|
||||
"markupOperator" =>new Color("dcdcdc"), // white
|
||||
"markupAttributeQuote" => new Color("dcdcdc"), // white
|
||||
_ => new Color("dcdcdc") // default to white
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private Dictionary MapClassifiedSpansToHighlights(int line)
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpIDE.Application\SharpIDE.Application.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ObservableCollections" Version="3.3.4" />
|
||||
<PackageReference Include="ObservableCollections.R3" Version="3.3.4" />
|
||||
<PackageReference Include="R3" Version="1.3.0" />
|
||||
<PackageReference Include="ObservableCollections" />
|
||||
<PackageReference Include="ObservableCollections.R3" />
|
||||
<PackageReference Include="R3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
..\..\nuget.config = ..\..\nuget.config
|
||||
..\..\README.md = ..\..\README.md
|
||||
..\..\Directory.Packages.props = ..\..\Directory.Packages.props
|
||||
..\..\.globalconfig = ..\..\.globalconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpIDE.RazorAccess", "..\SharpIDE.RazorAccess\SharpIDE.RazorAccess.csproj", "{614547C3-6620-4F37-B0A9-AA78A4293EB4}"
|
||||
|
||||
@@ -3,7 +3,7 @@ using RazorCodeDocumentExtensions = WorkspaceAlias::Microsoft.AspNetCore.Razor.L
|
||||
|
||||
namespace SharpIDE.RazorAccess;
|
||||
|
||||
public record struct SharpIdeRazorClassifiedSpan(SharpIdeRazorSourceSpan Span, SharpIdeRazorSpanKind Kind, string? CodeClassificationType = null);
|
||||
public record struct SharpIdeRazorClassifiedSpan(SharpIdeRazorSourceSpan Span, SharpIdeRazorSpanKind Kind, string? CodeClassificationType = null, string? VsSemanticRangeType = null);
|
||||
|
||||
public enum SharpIdeRazorSpanKind
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user