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 @@ - - - + + +