From 0b770e3d020acf0d70be63bea83b032acd427212 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Mon, 22 Sep 2025 18:12:26 +1000 Subject: [PATCH] add html razor syntax highlighting --- .globalconfig | 20 + Directory.Packages.props | 4 +- .../Analysis/CustomSemanticTokensVisitor.cs | 649 ++++++++++++++++++ .../Features/Analysis/RoslynAnalysis.cs | 66 +- .../Features/Analysis/TokenTypeProvider.cs | 29 + .../Analysis/TranslatedSemanticRange.cs | 9 + .../SharpIDE.Application.csproj | 8 +- src/SharpIDE.Godot/CustomSyntaxHighlighter.cs | 23 +- src/SharpIDE.Godot/SharpIDE.Godot.csproj | 7 +- src/SharpIDE.Godot/SharpIDE.Godot.sln | 1 + .../SharpIdeRazorClassifiedSpan.cs | 2 +- 11 files changed, 801 insertions(+), 17 deletions(-) create mode 100644 .globalconfig create mode 100644 src/SharpIDE.Application/Features/Analysis/CustomSemanticTokensVisitor.cs create mode 100644 src/SharpIDE.Application/Features/Analysis/TokenTypeProvider.cs create mode 100644 src/SharpIDE.Application/Features/Analysis/TranslatedSemanticRange.cs diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 0000000..b63c9c2 --- /dev/null +++ b/.globalconfig @@ -0,0 +1,20 @@ +# Roslyn.Diagnostics.Analyzers +dotnet_analyzer_diagnostic.category-ApiDesign.severity = none +dotnet_analyzer_diagnostic.category-RoslynDiagnosticsMaintainability.severity = suggestion + +# CA2012: Use ValueTasks correctly +dotnet_diagnostic.CA2012.severity = warning + +# CA2016: Forward the 'CancellationToken' parameter to methods +dotnet_diagnostic.CA2016.severity = warning + +# CA2025: Do not pass 'IDisposable' instances into unawaited tasks +dotnet_diagnostic.CA2025.severity = suggestion + +# VSTHRD200: Use "Async" suffix for async methods +dotnet_diagnostic.VSTHRD200.severity = none + +# VSTHRD003: Avoid awaiting foreign Tasks +dotnet_diagnostic.VSTHRD003.severity = none + +dotnet_diagnostic.VSTHRD*.severity = none diff --git a/Directory.Packages.props b/Directory.Packages.props index a3cd44b..08a8d12 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,6 +2,8 @@ true true + + $(NoWarn);NU1507 @@ -25,7 +27,7 @@ - + diff --git a/src/SharpIDE.Application/Features/Analysis/CustomSemanticTokensVisitor.cs b/src/SharpIDE.Application/Features/Analysis/CustomSemanticTokensVisitor.cs new file mode 100644 index 0000000..3dbdd54 --- /dev/null +++ b/src/SharpIDE.Application/Features/Analysis/CustomSemanticTokensVisitor.cs @@ -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 _semanticRanges; + private readonly RazorCodeDocument _razorCodeDocument; + private readonly TextSpan _range; + private readonly ISemanticTokensLegendService _semanticTokensLegend; + private readonly bool _colorCodeBackground; + + private bool _addRazorCodeModifier; + + private CustomSemanticTokensVisitor(List semanticRanges, RazorCodeDocument razorCodeDocument, TextSpan range, ISemanticTokensLegendService semanticTokensLegend, bool colorCodeBackground) + { + _semanticRanges = semanticRanges; + _razorCodeDocument = razorCodeDocument; + _range = range; + _semanticTokensLegend = semanticTokensLegend; + _colorCodeBackground = colorCodeBackground; + } + + public static void AddSemanticRanges(List 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 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`` + // needs to not be marked as a TagHelper, but `` 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 "

_codeFixProviders = []; private static HashSet _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().FirstOrDefault(); _snapshotManager = snapshotManager; + + _semanticTokensLegendService = container.GetExports().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 relevantTypes = ["razorDirective", "razorTransition", "markupTextLiteral", "markupTagDelimiter", "markupElement", "razorComponentElement", "razorComponentAttribute", "razorComment", "razorCommentTransition", "razorCommentStar", "markupOperator", "markupAttributeQuote"]; + var ranges = new List(); + 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 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> GetDocumentSyntaxHighlighting(SharpIdeFile fileModel) diff --git a/src/SharpIDE.Application/Features/Analysis/TokenTypeProvider.cs b/src/SharpIDE.Application/Features/Analysis/TokenTypeProvider.cs new file mode 100644 index 0000000..45a1c9f --- /dev/null +++ b/src/SharpIDE.Application/Features/Analysis/TokenTypeProvider.cs @@ -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 GetStaticFieldValues(Type type) + { + var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static).Select(s => s.GetValue(null)).OfType().ToImmutableArray(); + return fields; + } +} diff --git a/src/SharpIDE.Application/Features/Analysis/TranslatedSemanticRange.cs b/src/SharpIDE.Application/Features/Analysis/TranslatedSemanticRange.cs new file mode 100644 index 0000000..29a2869 --- /dev/null +++ b/src/SharpIDE.Application/Features/Analysis/TranslatedSemanticRange.cs @@ -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; } +} diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj index 747bf65..b77a2e7 100644 --- a/src/SharpIDE.Application/SharpIDE.Application.csproj +++ b/src/SharpIDE.Application/SharpIDE.Application.csproj @@ -8,10 +8,13 @@ - - + + + + + @@ -32,6 +35,7 @@ + diff --git a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs index 2d03401..c50f4b3 100644 --- a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs +++ b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs @@ -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) diff --git a/src/SharpIDE.Godot/SharpIDE.Godot.csproj b/src/SharpIDE.Godot/SharpIDE.Godot.csproj index 111bb46..149de62 100644 --- a/src/SharpIDE.Godot/SharpIDE.Godot.csproj +++ b/src/SharpIDE.Godot/SharpIDE.Godot.csproj @@ -4,14 +4,13 @@ true enable true - false - - - + + + \ No newline at end of file diff --git a/src/SharpIDE.Godot/SharpIDE.Godot.sln b/src/SharpIDE.Godot/SharpIDE.Godot.sln index 4de37fe..4d5f9bd 100644 --- a/src/SharpIDE.Godot/SharpIDE.Godot.sln +++ b/src/SharpIDE.Godot/SharpIDE.Godot.sln @@ -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}" diff --git a/src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs b/src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs index d864c2c..08f528f 100644 --- a/src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs +++ b/src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs @@ -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 {