From cc7e7669667f5f1eb379201ec34ae6c5d2f4e723 Mon Sep 17 00:00:00 2001
From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com>
Date: Sun, 14 Sep 2025 19:02:33 +1000
Subject: [PATCH] add razor file syntax highlighting
---
.editorconfig | 2 +
Directory.Build.props | 1 +
Directory.Packages.props | 47 ++++++++++
SharpIDE.Photino.sln | 49 +++++++++-
nuget.config | 2 +
.../Features/Analysis/RoslynAnalysis.cs | 94 +++++++++++++++++--
.../SolutionDiscovery/SharpIdeFile.cs | 1 +
.../SharpIDE.Application.csproj | 38 ++++----
src/SharpIDE.Godot/CustomSyntaxHighlighter.cs | 69 +++++++++++++-
src/SharpIDE.Godot/SharpIDE.Godot.csproj | 1 +
src/SharpIDE.Godot/SharpIDE.Godot.sln | 20 ++++
src/SharpIDE.Godot/SharpIdeCodeEdit.cs | 17 ++--
src/SharpIDE.Photino/SharpIDE.Photino.csproj | 16 ++--
src/SharpIDE.RazorAccess/RazorAccessors.cs | 72 ++++++++++++++
.../SharpIDE.RazorAccess.csproj | 40 ++++++++
.../SharpIdeRazorClassifiedSpan.cs | 30 ++++++
.../SharpIdeRazorSourceMapping.cs | 49 ++++++++++
.../SharpIdeRazorSourceSpan.cs | 83 ++++++++++++++++
.../Roslyn.Benchmarks.csproj | 8 +-
19 files changed, 590 insertions(+), 49 deletions(-)
create mode 100644 Directory.Packages.props
create mode 100644 src/SharpIDE.RazorAccess/RazorAccessors.cs
create mode 100644 src/SharpIDE.RazorAccess/SharpIDE.RazorAccess.csproj
create mode 100644 src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs
create mode 100644 src/SharpIDE.RazorAccess/SharpIdeRazorSourceMapping.cs
create mode 100644 src/SharpIDE.RazorAccess/SharpIdeRazorSourceSpan.cs
diff --git a/.editorconfig b/.editorconfig
index eb308d2..aaf1aa3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -16,6 +16,8 @@ csharp_space_after_cast = true
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_place_attribute_on_same_line = false
+dotnet_analyzer_diagnostic.category-ApiDesign.severity = none
+dotnet_analyzer_diagnostic.category-RoslynDiagnosticsMaintainability.severity = suggestion
[*.csproj]
indent_style = space
diff --git a/Directory.Build.props b/Directory.Build.props
index 38b8e44..7b40869 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,5 +2,6 @@
true
+ $(Features);use-roslyn-tokenizer=true
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..f29eafb
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,47 @@
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SharpIDE.Photino.sln b/SharpIDE.Photino.sln
index 10b9985..8cf9dab 100644
--- a/SharpIDE.Photino.sln
+++ b/SharpIDE.Photino.sln
@@ -22,31 +22,74 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B6835010
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roslyn.Benchmarks", "tests\Roslyn.Benchmarks\Roslyn.Benchmarks.csproj", "{252CE098-2F9A-4DA3-A172-EE1167B335BF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpIDE.RazorAccess", "src\SharpIDE.RazorAccess\SharpIDE.RazorAccess.csproj", "{0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Debug|x64.Build.0 = Debug|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Debug|x86.Build.0 = Debug|Any CPU
{E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Release|x64.ActiveCfg = Release|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Release|x64.Build.0 = Release|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Release|x86.ActiveCfg = Release|Any CPU
+ {E35167E1-0FF4-4194-97A8-CC95EDA224CD}.Release|x86.Build.0 = Release|Any CPU
{D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Debug|x64.Build.0 = Debug|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Debug|x86.Build.0 = Debug|Any CPU
{D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Release|x64.ActiveCfg = Release|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Release|x64.Build.0 = Release|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Release|x86.ActiveCfg = Release|Any CPU
+ {D7D5D39E-DA3A-4B10-8F40-B07B769347F4}.Release|x86.Build.0 = Release|Any CPU
{252CE098-2F9A-4DA3-A172-EE1167B335BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{252CE098-2F9A-4DA3-A172-EE1167B335BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Debug|x64.Build.0 = Debug|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Debug|x86.Build.0 = Debug|Any CPU
{252CE098-2F9A-4DA3-A172-EE1167B335BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{252CE098-2F9A-4DA3-A172-EE1167B335BF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Release|x64.ActiveCfg = Release|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Release|x64.Build.0 = Release|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Release|x86.ActiveCfg = Release|Any CPU
+ {252CE098-2F9A-4DA3-A172-EE1167B335BF}.Release|x86.Build.0 = Release|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Debug|x64.Build.0 = Debug|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Debug|x86.Build.0 = Debug|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Release|x64.ActiveCfg = Release|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Release|x64.Build.0 = Release|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Release|x86.ActiveCfg = Release|Any CPU
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E35167E1-0FF4-4194-97A8-CC95EDA224CD} = {F4ED837F-888A-4D01-BCED-C360B9CE0865}
{D7D5D39E-DA3A-4B10-8F40-B07B769347F4} = {F4ED837F-888A-4D01-BCED-C360B9CE0865}
{252CE098-2F9A-4DA3-A172-EE1167B335BF} = {B6835010-35FA-4C74-AB48-009FB923185D}
+ {0DE5B721-4C17-4A93-A94B-5DEA9CAAAE99} = {F4ED837F-888A-4D01-BCED-C360B9CE0865}
EndGlobalSection
EndGlobal
diff --git a/nuget.config b/nuget.config
index f499f07..7232a92 100644
--- a/nuget.config
+++ b/nuget.config
@@ -6,5 +6,7 @@
+
+
diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs
index 53f32a6..ded0fb6 100644
--- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs
+++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs
@@ -1,5 +1,4 @@
using System.Collections.Immutable;
-using System.Collections.ObjectModel;
using System.Diagnostics;
using Ardalis.GuardClauses;
using Microsoft.CodeAnalysis;
@@ -12,9 +11,9 @@ using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Text;
using NuGet.Packaging;
-using ObservableCollections;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
+using SharpIDE.RazorAccess;
namespace SharpIDE.Application.Features.Analysis;
@@ -137,7 +136,8 @@ public static class RoslynAnalysis
await _solutionLoadedTcs.Task;
var cancellationToken = CancellationToken.None;
var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath);
- var document = project.Documents.Single(s => s.FilePath == fileModel.Path);
+ var document = project.Documents.SingleOrDefault(s => s.FilePath == fileModel.Path);
+ if (document is null) return [];
//var document = _workspace!.CurrentSolution.GetDocument(fileModel.Path);
Guard.Against.Null(document, nameof(document));
@@ -150,11 +150,80 @@ public static class RoslynAnalysis
return result;
}
+ public record SharpIdeRazorMappedClassifiedSpan(SharpIdeRazorSourceSpan SourceSpanInRazor, string CsharpClassificationType);
+ public static async Task> GetRazorDocumentSyntaxHighlighting(SharpIdeFile fileModel)
+ {
+ await _solutionLoadedTcs.Task;
+ var cancellationToken = CancellationToken.None;
+ var sharpIdeProjectModel = ((IChildSharpIdeNode) fileModel).GetNearestProjectNode()!;
+ var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == sharpIdeProjectModel!.FilePath);
+ if (!fileModel.Name.EndsWith(".razor", StringComparison.OrdinalIgnoreCase))
+ {
+ return [];
+ //throw new InvalidOperationException("File is not a .razor file");
+ }
+
+ var importsFile = sharpIdeProjectModel.Files.Single(s => s.Name.Equals("_Imports.razor", StringComparison.OrdinalIgnoreCase));
+
+ var razorDocument = project.AdditionalDocuments.Single(s => s.FilePath == fileModel.Path);
+ var importsDocument = project.AdditionalDocuments.Single(s => s.FilePath == importsFile.Path);
+
+ var razorText = await razorDocument.GetTextAsync(cancellationToken);
+ var importsText = await importsDocument.GetTextAsync(cancellationToken);
+ var (razorSpans, razorGeneratedSourceText, sourceMappings) = RazorAccessors.GetClassifiedSpans(razorText, importsText, razorDocument.FilePath!, Path.GetDirectoryName(project.FilePath!)!);
+
+ var razorGeneratedDocument = project.AddDocument(fileModel.Name + ".g.cs", razorGeneratedSourceText);
+ var razorSyntaxTree = await razorGeneratedDocument.GetSyntaxTreeAsync(cancellationToken);
+ var razorSyntaxRoot = await razorSyntaxTree!.GetRootAsync(cancellationToken);
+ var classifiedSpans = await Classifier.GetClassifiedSpansAsync(razorGeneratedDocument, razorSyntaxRoot.FullSpan, cancellationToken);
+ var roslynMappedSpans = classifiedSpans.Select(s =>
+ {
+ var genSpan = s.TextSpan;
+ var mapping = sourceMappings.SingleOrDefault(m => m.GeneratedSpan.AsTextSpan().IntersectsWith(genSpan));
+ if (mapping != null)
+ {
+ // Translate generated span back to Razor span
+ var offset = genSpan.Start - mapping.GeneratedSpan.AbsoluteIndex;
+ var mappedStart = mapping.OriginalSpan.AbsoluteIndex + offset;
+ var mappedSpan = new TextSpan(mappedStart, genSpan.Length);
+ var sharpIdeSpan = new SharpIdeRazorSourceSpan(
+ mapping.OriginalSpan.FilePath,
+ mappedSpan.Start,
+ razorText.Lines.GetLineFromPosition(mappedSpan.Start).LineNumber,
+ mappedSpan.Start - razorText.Lines.GetLineFromPosition(mappedSpan.Start).Start,
+ mappedSpan.Length,
+ 1,
+ mappedSpan.Start - razorText.Lines.GetLineFromPosition(mappedSpan.Start).Start + mappedSpan.Length
+ );
+
+ return new SharpIdeRazorMappedClassifiedSpan(
+ sharpIdeSpan,
+ s.ClassificationType
+ );
+ }
+
+ 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))
+ ];
+ razorSpans = razorSpans.OrderBy(s => s.Span.AbsoluteIndex).ToImmutableArray();
+
+ return razorSpans;
+ }
+
public static async Task> GetDocumentSyntaxHighlighting(SharpIdeFile fileModel)
{
await _solutionLoadedTcs.Task;
var cancellationToken = CancellationToken.None;
var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath);
+ if (fileModel.Name.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) is false)
+ {
+ //throw new InvalidOperationException("File is not a .cs");
+ return [];
+ }
+
var document = project.Documents.Single(s => s.FilePath == fileModel.Path);
Guard.Against.Null(document, nameof(document));
@@ -300,11 +369,18 @@ public static class RoslynAnalysis
Guard.Against.NullOrEmpty(newContent, nameof(newContent));
var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath);
- var document = project.Documents.Single(s => s.FilePath == fileModel.Path);
- Guard.Against.Null(document, nameof(document));
-
- //var updatedDocument = document.WithText(SourceText.From(newContent));
- var newSolution = _workspace.CurrentSolution.WithDocumentText(document.Id, SourceText.From(newContent));
- _workspace.TryApplyChanges(newSolution);
+ if (fileModel.IsRazorFile)
+ {
+ var razorDocument = project.AdditionalDocuments.Single(s => s.FilePath == fileModel.Path);
+ var newSolution = _workspace.CurrentSolution.WithAdditionalDocumentText(razorDocument.Id, SourceText.From(newContent));
+ _workspace.TryApplyChanges(newSolution);
+ }
+ else
+ {
+ var document = project.Documents.Single(s => s.FilePath == fileModel.Path);
+ Guard.Against.Null(document, nameof(document));
+ var newSolution = _workspace.CurrentSolution.WithDocumentText(document.Id, SourceText.From(newContent));
+ _workspace.TryApplyChanges(newSolution);
+ }
}
}
diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs
index ff5083f..7aa66e2 100644
--- a/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs
+++ b/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs
@@ -9,6 +9,7 @@ public class SharpIdeFile : ISharpIdeNode, IChildSharpIdeNode
public required IExpandableSharpIdeNode Parent { get; set; }
public required string Path { get; set; }
public required string Name { get; set; }
+ public bool IsRazorFile => Path.EndsWith(".razor", StringComparison.OrdinalIgnoreCase);
[SetsRequiredMembers]
internal SharpIdeFile(string fullPath, string name, IExpandableSharpIdeNode parent, ConcurrentBag allFiles)
diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj
index 62c6dc3..41ee303 100644
--- a/src/SharpIDE.Application/SharpIDE.Application.csproj
+++ b/src/SharpIDE.Application/SharpIDE.Application.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -8,22 +8,26 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs
index 6a203f7..51440d4 100644
--- a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs
+++ b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs
@@ -2,20 +2,85 @@
using Godot.Collections;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
+using SharpIDE.RazorAccess;
namespace SharpIDE.Godot;
public partial class CustomHighlighter : SyntaxHighlighter
{
- public IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> ClassifiedSpans = [];
+ private readonly Dictionary _emptyDict = new();
+ public HashSet<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> ClassifiedSpans = [];
+ public HashSet RazorClassifiedSpans = [];
public override Dictionary _GetLineSyntaxHighlighting(int line)
{
- var highlights = MapClassifiedSpansToHighlights(line);
+ var highlights = (ClassifiedSpans, RazorClassifiedSpans) switch
+ {
+ ({ Count: 0 }, { Count: 0 }) => _emptyDict,
+ ({ Count: > 0 }, _) => MapClassifiedSpansToHighlights(line),
+ (_, { Count: > 0 }) => MapRazorClassifiedSpansToHighlights(line),
+ _ => throw new NotImplementedException("Both ClassifiedSpans and RazorClassifiedSpans are set. This is not supported yet.")
+ };
return highlights;
}
private static readonly StringName ColorStringName = "color";
+ private Dictionary MapRazorClassifiedSpansToHighlights(int line)
+ {
+ var highlights = new Dictionary();
+
+ // Filter spans on the given line, ignore empty spans
+ var spansForLine = RazorClassifiedSpans
+ .Where(s => s.Span.LineIndex == line && s.Span.Length is not 0)
+ .GroupBy(s => s.Span)
+ .ToList();
+
+ foreach (var razorSpanGrouping in spansForLine)
+ {
+ var spans = razorSpanGrouping.ToList();
+ if (spans.Count > 2) throw new NotImplementedException("More than 2 classified spans is not supported yet.");
+ if (spans.Count is not 1)
+ {
+ if (spans.Any(s => s.Kind is SharpIdeRazorSpanKind.Code))
+ {
+ spans = spans.Where(s => s.Kind is SharpIdeRazorSpanKind.Code).ToList();
+ }
+ if (spans.Count is not 1)
+ {
+ SharpIdeRazorClassifiedSpan? staticClassifiedSpan = spans.FirstOrDefault(s => s.CodeClassificationType == ClassificationTypeNames.StaticSymbol);
+ if (staticClassifiedSpan is not null) spans.Remove(staticClassifiedSpan.Value);
+ }
+ }
+ var razorSpan = spans.Single();
+
+ int columnIndex = razorSpan.Span.CharacterIndex;
+
+ var highlightInfo = new Dictionary
+ {
+ { ColorStringName, GetColorForRazorSpanKind(razorSpan.Kind, razorSpan.CodeClassificationType) }
+ };
+
+ highlights[columnIndex] = highlightInfo;
+ }
+
+ return highlights;
+ }
+
+ private static Color GetColorForRazorSpanKind(SharpIdeRazorSpanKind kind, string? codeClassificationType)
+ {
+ 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.Transition => new Color("a699e6"), // purple
+ SharpIdeRazorSpanKind.None => new Color("dcdcdc"),
+ _ => new Color("dcdcdc")
+ };
+ }
+
+
private Dictionary MapClassifiedSpansToHighlights(int line)
{
var highlights = new Dictionary();
diff --git a/src/SharpIDE.Godot/SharpIDE.Godot.csproj b/src/SharpIDE.Godot/SharpIDE.Godot.csproj
index 70baf8e..b8583c0 100644
--- a/src/SharpIDE.Godot/SharpIDE.Godot.csproj
+++ b/src/SharpIDE.Godot/SharpIDE.Godot.csproj
@@ -4,6 +4,7 @@
true
enable
true
+ false
diff --git a/src/SharpIDE.Godot/SharpIDE.Godot.sln b/src/SharpIDE.Godot/SharpIDE.Godot.sln
index 17c9db7..4de37fe 100644
--- a/src/SharpIDE.Godot/SharpIDE.Godot.sln
+++ b/src/SharpIDE.Godot/SharpIDE.Godot.sln
@@ -6,6 +6,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpIDE.Application", "..\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpIDE.Photino", "..\SharpIDE.Photino\SharpIDE.Photino.csproj", "{DFF170D9-D92E-4DB7-83B5-19640EAF79D2}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E33CF95D-DEAB-4CAC-9931-FC3ADCBA54C0}"
+ ProjectSection(SolutionItems) = preProject
+ ..\..\.editorconfig = ..\..\.editorconfig
+ ..\..\.gitattributes = ..\..\.gitattributes
+ ..\..\.gitignore = ..\..\.gitignore
+ ..\..\Directory.Build.props = ..\..\Directory.Build.props
+ ..\..\global.json = ..\..\global.json
+ ..\..\nuget.config = ..\..\nuget.config
+ ..\..\README.md = ..\..\README.md
+ ..\..\Directory.Packages.props = ..\..\Directory.Packages.props
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpIDE.RazorAccess", "..\SharpIDE.RazorAccess\SharpIDE.RazorAccess.csproj", "{614547C3-6620-4F37-B0A9-AA78A4293EB4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -31,5 +45,11 @@ Global
{DFF170D9-D92E-4DB7-83B5-19640EAF79D2}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
{DFF170D9-D92E-4DB7-83B5-19640EAF79D2}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
{DFF170D9-D92E-4DB7-83B5-19640EAF79D2}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
+ {614547C3-6620-4F37-B0A9-AA78A4293EB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {614547C3-6620-4F37-B0A9-AA78A4293EB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {614547C3-6620-4F37-B0A9-AA78A4293EB4}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {614547C3-6620-4F37-B0A9-AA78A4293EB4}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
+ {614547C3-6620-4F37-B0A9-AA78A4293EB4}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
+ {614547C3-6620-4F37-B0A9-AA78A4293EB4}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
EndGlobal
diff --git a/src/SharpIDE.Godot/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/SharpIdeCodeEdit.cs
index c780bd9..d64cc38 100644
--- a/src/SharpIDE.Godot/SharpIdeCodeEdit.cs
+++ b/src/SharpIDE.Godot/SharpIdeCodeEdit.cs
@@ -10,6 +10,7 @@ using SharpIDE.Application.Features.Debugging;
using SharpIDE.Application.Features.Events;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
+using SharpIDE.RazorAccess;
using Task = System.Threading.Tasks.Task;
namespace SharpIDE.Godot;
@@ -120,12 +121,13 @@ public partial class SharpIdeCodeEdit : CodeEdit
_ = Task.GodotRun(async () =>
{
var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
+ var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
var diagnostics = RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
var slnDiagnostics = RoslynAnalysis.UpdateSolutionDiagnostics();
- await Task.WhenAll(syntaxHighlighting, diagnostics);
+ await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting, diagnostics);
Callable.From(() =>
{
- SetSyntaxHighlightingModel(syntaxHighlighting.Result);
+ SetSyntaxHighlightingModel(syntaxHighlighting.Result, razorSyntaxHighlighting.Result);
SetDiagnosticsModel(diagnostics.Result);
}).CallDeferred();
await slnDiagnostics;
@@ -144,12 +146,13 @@ public partial class SharpIdeCodeEdit : CodeEdit
await RoslynAnalysis.ApplyCodeActionAsync(codeAction);
var fileContents = await File.ReadAllTextAsync(_currentFile.Path);
var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
+ var razorSyntaxHighlighting = await RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
Callable.From(() =>
{
BeginComplexOperation();
SetText(fileContents);
- SetSyntaxHighlightingModel(syntaxHighlighting);
+ SetSyntaxHighlightingModel(syntaxHighlighting, razorSyntaxHighlighting);
SetDiagnosticsModel(diagnostics);
SetCaretLine(currentCaretPosition.line);
SetCaretColumn(currentCaretPosition.col);
@@ -167,7 +170,8 @@ public partial class SharpIdeCodeEdit : CodeEdit
SetText(fileContents);
_fileChangingSuppressBreakpointToggleEvent = false;
var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
- SetSyntaxHighlightingModel(syntaxHighlighting);
+ var razorSyntaxHighlighting = await RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
+ SetSyntaxHighlightingModel(syntaxHighlighting, razorSyntaxHighlighting);
var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
SetDiagnosticsModel(diagnostics);
}
@@ -273,9 +277,10 @@ public partial class SharpIdeCodeEdit : CodeEdit
_diagnostics = diagnostics;
}
- private void SetSyntaxHighlightingModel(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans)
+ private void SetSyntaxHighlightingModel(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans, IEnumerable razorClassifiedSpans)
{
- _syntaxHighlighter.ClassifiedSpans = classifiedSpans;
+ _syntaxHighlighter.ClassifiedSpans = classifiedSpans.ToHashSet();
+ _syntaxHighlighter.RazorClassifiedSpans = razorClassifiedSpans.ToHashSet();
Callable.From(() =>
{
_syntaxHighlighter.ClearHighlightingCache();
diff --git a/src/SharpIDE.Photino/SharpIDE.Photino.csproj b/src/SharpIDE.Photino/SharpIDE.Photino.csproj
index 86cead7..766e770 100644
--- a/src/SharpIDE.Photino/SharpIDE.Photino.csproj
+++ b/src/SharpIDE.Photino/SharpIDE.Photino.csproj
@@ -1,4 +1,4 @@
-
+
WinExe
@@ -18,13 +18,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/SharpIDE.RazorAccess/RazorAccessors.cs b/src/SharpIDE.RazorAccess/RazorAccessors.cs
new file mode 100644
index 0000000..1f00799
--- /dev/null
+++ b/src/SharpIDE.RazorAccess/RazorAccessors.cs
@@ -0,0 +1,72 @@
+extern alias WorkspaceAlias;
+using System.Collections.Immutable;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.CodeAnalysis.Text;
+using RazorCodeDocumentExtensions = WorkspaceAlias::Microsoft.AspNetCore.Razor.Language.RazorCodeDocumentExtensions;
+
+namespace SharpIDE.RazorAccess;
+
+public static class RazorAccessors
+{
+ public static (ImmutableArray, SourceText Text, List) GetClassifiedSpans(SourceText sourceText, SourceText importsSourceText, string razorDocumentFilePath, string projectDirectory)
+ {
+
+ var razorSourceDocument = RazorSourceDocument.Create(sourceText.ToString(), razorDocumentFilePath);
+ var importsRazorSourceDocument = RazorSourceDocument.Create(importsSourceText.ToString(), "_Imports.razor");
+
+ var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, RazorProjectFileSystem.Create(projectDirectory),
+ builder => { /* configure features if needed */ });
+
+ //var razorCodeDocument = projectEngine.Process(razorSourceDocument, RazorFileKind.Component, [], []);
+ var razorCodeDocument = projectEngine.ProcessDesignTime(razorSourceDocument, RazorFileKind.Component, [importsRazorSourceDocument], []);
+ var razorCSharpDocument = razorCodeDocument.GetRequiredCSharpDocument();
+ //var generatedSourceText = razorCSharpDocument.Text;
+
+ //var filePath = razorCodeDocument.Source.FilePath.AssumeNotNull();
+ //var razorSourceText = razorCodeDocument.Source.Text;
+ var razorSpans = RazorCodeDocumentExtensions.GetClassifiedSpans(razorCodeDocument);
+
+ //var sharpIdeSpans = MemoryMarshal.Cast(razorSpans);
+ var sharpIdeSpans = razorSpans.Select(s => new SharpIdeRazorClassifiedSpan(s.Span.ToSharpIdeSourceSpan(), s.Kind.ToSharpIdeSpanKind())).ToList();
+
+ return (sharpIdeSpans.ToImmutableArray(), razorCSharpDocument.Text, razorCSharpDocument.SourceMappings.Select(s => s.ToSharpIdeSourceMapping()).ToList());
+ }
+
+ // public static bool TryGetMappedSpans(
+ // TextSpan span,
+ // SourceText source,
+ // RazorCSharpDocument output,
+ // out LinePositionSpan linePositionSpan,
+ // out TextSpan mappedSpan)
+ // {
+ // foreach (SourceMapping sourceMapping in output.SourceMappings)
+ // {
+ // TextSpan textSpan1 = sourceMapping.OriginalSpan.AsTextSpan();
+ // TextSpan textSpan2 = sourceMapping.GeneratedSpan.AsTextSpan();
+ // if (textSpan2.Contains(span))
+ // {
+ // int num1 = span.Start - textSpan2.Start;
+ // int num2 = span.End - textSpan2.End;
+ // if (num1 >= 0 && num2 <= 0)
+ // {
+ // mappedSpan = new TextSpan(textSpan1.Start + num1, textSpan1.End + num2 - (textSpan1.Start + num1));
+ // linePositionSpan = source.Lines.GetLinePositionSpan(mappedSpan);
+ // return true;
+ // }
+ // }
+ // }
+ // mappedSpan = new TextSpan();
+ // linePositionSpan = new LinePositionSpan();
+ // return false;
+ // }
+
+ // ///
+ // /// Wrapper to avoid s in the caller during JITing
+ // /// even though the method is not actually called.
+ // ///
+ // [MethodImpl(MethodImplOptions.NoInlining)]
+ // private static object GetFileKindFromPath(string filePath)
+ // {
+ // return FileKinds.GetFileKindFromPath(filePath);
+ // }
+}
diff --git a/src/SharpIDE.RazorAccess/SharpIDE.RazorAccess.csproj b/src/SharpIDE.RazorAccess/SharpIDE.RazorAccess.csproj
new file mode 100644
index 0000000..576ea02
--- /dev/null
+++ b/src/SharpIDE.RazorAccess/SharpIDE.RazorAccess.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+ Microsoft.CodeAnalysis.Razor.Test
+ $(PkgMicrosoft_DotNet_Arcade_Sdk)\tools\snk\AspNetCore.snk
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs b/src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs
new file mode 100644
index 0000000..d864c2c
--- /dev/null
+++ b/src/SharpIDE.RazorAccess/SharpIdeRazorClassifiedSpan.cs
@@ -0,0 +1,30 @@
+extern alias WorkspaceAlias;
+using RazorCodeDocumentExtensions = WorkspaceAlias::Microsoft.AspNetCore.Razor.Language.RazorCodeDocumentExtensions;
+
+namespace SharpIDE.RazorAccess;
+
+public record struct SharpIdeRazorClassifiedSpan(SharpIdeRazorSourceSpan Span, SharpIdeRazorSpanKind Kind, string? CodeClassificationType = null);
+
+public enum SharpIdeRazorSpanKind
+{
+ Transition,
+ MetaCode,
+ Comment,
+ Code,
+ Markup,
+ None,
+}
+
+public static class SharpIdeRazorClassifiedSpanExtensions
+{
+ public static SharpIdeRazorSpanKind ToSharpIdeSpanKind(this RazorCodeDocumentExtensions.SpanKind kind) => kind switch
+ {
+ RazorCodeDocumentExtensions.SpanKind.Transition => SharpIdeRazorSpanKind.Transition,
+ RazorCodeDocumentExtensions.SpanKind.MetaCode => SharpIdeRazorSpanKind.MetaCode,
+ RazorCodeDocumentExtensions.SpanKind.Comment => SharpIdeRazorSpanKind.Comment,
+ RazorCodeDocumentExtensions.SpanKind.Code => SharpIdeRazorSpanKind.Code,
+ RazorCodeDocumentExtensions.SpanKind.Markup => SharpIdeRazorSpanKind.Markup,
+ RazorCodeDocumentExtensions.SpanKind.None => SharpIdeRazorSpanKind.None,
+ _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
+ };
+}
diff --git a/src/SharpIDE.RazorAccess/SharpIdeRazorSourceMapping.cs b/src/SharpIDE.RazorAccess/SharpIdeRazorSourceMapping.cs
new file mode 100644
index 0000000..c0ad141
--- /dev/null
+++ b/src/SharpIDE.RazorAccess/SharpIdeRazorSourceMapping.cs
@@ -0,0 +1,49 @@
+using System.Globalization;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.Extensions.Internal;
+
+namespace SharpIDE.RazorAccess;
+
+public sealed class SharpIdeRazorSourceMapping(
+ SharpIdeRazorSourceSpan originalSpan,
+ SharpIdeRazorSourceSpan generatedSpan)
+ : IEquatable
+{
+ public SharpIdeRazorSourceSpan OriginalSpan { get; } = originalSpan;
+
+ public SharpIdeRazorSourceSpan GeneratedSpan { get; } = generatedSpan;
+
+ public override bool Equals(object? obj) => Equals(obj as SourceMapping);
+
+ public bool Equals(SharpIdeRazorSourceMapping? other)
+ {
+ if (other == null)
+ return false;
+ var sourceSpan = OriginalSpan;
+ if (!sourceSpan.Equals(other.OriginalSpan))
+ return false;
+ sourceSpan = GeneratedSpan;
+ return sourceSpan.Equals(other.GeneratedSpan);
+ }
+
+ public override int GetHashCode()
+ {
+ HashCodeCombiner hashCode = HashCodeCombiner.Start();
+ hashCode.Add(OriginalSpan);
+ hashCode.Add(GeneratedSpan);
+ return hashCode;
+ }
+
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.CurrentCulture, "{0} -> {1}", OriginalSpan, GeneratedSpan);
+ }
+}
+
+public static class SharpIdeRazorSourceMappingExtensions
+{
+ public static SharpIdeRazorSourceMapping ToSharpIdeSourceMapping(this SourceMapping mapping)
+ {
+ return new SharpIdeRazorSourceMapping(mapping.OriginalSpan.ToSharpIdeSourceSpan(), mapping.GeneratedSpan.ToSharpIdeSourceSpan());
+ }
+}
diff --git a/src/SharpIDE.RazorAccess/SharpIdeRazorSourceSpan.cs b/src/SharpIDE.RazorAccess/SharpIdeRazorSourceSpan.cs
new file mode 100644
index 0000000..025335e
--- /dev/null
+++ b/src/SharpIDE.RazorAccess/SharpIdeRazorSourceSpan.cs
@@ -0,0 +1,83 @@
+using System.Globalization;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.Extensions.Internal;
+
+namespace SharpIDE.RazorAccess;
+
+public readonly struct SharpIdeRazorSourceSpan(
+ string filePath,
+ int absoluteIndex,
+ int lineIndex,
+ int characterIndex,
+ int length,
+ int lineCount,
+ int endCharacterIndex)
+ : IEquatable
+{
+ public int Length { get; } = length;
+ public int AbsoluteIndex { get; } = absoluteIndex;
+ public int LineIndex { get; } = lineIndex;
+ public int CharacterIndex { get; } = characterIndex;
+ public int LineCount { get; } = lineCount;
+ public int EndCharacterIndex { get; } = endCharacterIndex;
+
+ public string FilePath { get; } = filePath;
+
+ public bool Equals(SharpIdeRazorSourceSpan other)
+ {
+ return string.Equals(FilePath, other.FilePath, StringComparison.Ordinal) && this.AbsoluteIndex == other.AbsoluteIndex && this.LineIndex == other.LineIndex && this.CharacterIndex == other.CharacterIndex && this.Length == other.Length;
+ }
+
+ public override bool Equals(object? obj) => obj is SharpIdeRazorSourceSpan other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ var hashCode = HashCodeCombiner.Start();
+ hashCode.Add(FilePath, StringComparer.Ordinal);
+ hashCode.Add(AbsoluteIndex);
+ hashCode.Add(LineIndex);
+ hashCode.Add(CharacterIndex);
+ hashCode.Add(Length);
+ return hashCode;
+ }
+
+ public override string ToString()
+ {
+ return string.Format(
+ CultureInfo.CurrentCulture,
+ "({0}:{1},{2} [{3}] {4})",
+ this.AbsoluteIndex,
+ this.LineIndex,
+ this.CharacterIndex,
+ this.Length,
+ this.FilePath
+ );
+ }
+ public static bool operator ==(SharpIdeRazorSourceSpan left, SharpIdeRazorSourceSpan right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(SharpIdeRazorSourceSpan left, SharpIdeRazorSourceSpan right)
+ {
+ return !(left == right);
+ }
+}
+
+public static class SharpIdeRazorSourceSpanExtensions
+{
+ public static TextSpan AsTextSpan(this SharpIdeRazorSourceSpan sourceSpan)
+ {
+ return new TextSpan(sourceSpan.AbsoluteIndex, sourceSpan.Length);
+ }
+ public static SharpIdeRazorSourceSpan ToSharpIdeSourceSpan(this SourceSpan span)
+ => new SharpIdeRazorSourceSpan(
+ span.FilePath,
+ span.AbsoluteIndex,
+ span.LineIndex,
+ span.CharacterIndex,
+ span.Length,
+ span.LineCount,
+ span.EndCharacterIndex);
+}
diff --git a/tests/Roslyn.Benchmarks/Roslyn.Benchmarks.csproj b/tests/Roslyn.Benchmarks/Roslyn.Benchmarks.csproj
index 3a08838..9d7ffcc 100644
--- a/tests/Roslyn.Benchmarks/Roslyn.Benchmarks.csproj
+++ b/tests/Roslyn.Benchmarks/Roslyn.Benchmarks.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -8,9 +8,9 @@
-
-
-
+
+
+