From f446ef16558bd4389ccdfb7340be1fec42c834ee Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:54:50 +1000 Subject: [PATCH] get razor syntax highlighting from workspace --- Directory.Packages.props | 38 ++++++----- .../Features/Analysis/RoslynAnalysis.cs | 51 +++++++++----- .../SharpIDE.Application.csproj | 11 ++++ src/SharpIDE.Godot/CustomSyntaxHighlighter.cs | 3 +- src/SharpIDE.Godot/IdeRoot.cs | 2 +- src/SharpIDE.Godot/SharpIdeCodeEdit.cs | 12 ++-- src/SharpIDE.RazorAccess/RazorAccessors.cs | 66 +++++++++++-------- .../SharpIDE.RazorAccess.csproj | 4 +- 8 files changed, 115 insertions(+), 72 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f29eafb..a3cd44b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,24 +13,27 @@ - - + + - - - - - - + + + + + + - - - - - + + + + + + + + - + @@ -41,7 +44,8 @@ - - - + + + + diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index ded0fb6..53a530c 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Composition.Hosting; using System.Diagnostics; using Ardalis.GuardClauses; using Microsoft.CodeAnalysis; @@ -9,6 +10,7 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using NuGet.Packaging; using SharpIDE.Application.Features.SolutionDiscovery; @@ -20,6 +22,7 @@ namespace SharpIDE.Application.Features.Analysis; public static class RoslynAnalysis { public static MSBuildWorkspace? _workspace; + private static RemoteSnapshotManager? _snapshotManager; private static SharpIdeSolutionModel? _sharpIdeSolutionModel; private static HashSet _codeFixProviders = []; private static HashSet _codeRefactoringProviders = []; @@ -45,10 +48,18 @@ public static class RoslynAnalysis var timer = Stopwatch.StartNew(); if (_workspace is null) { - // is this hostServices necessary? test without it - just getting providers from assemblies instead - var host = MefHostServices.Create(MefHostServices.DefaultAssemblies); - _workspace ??= MSBuildWorkspace.Create(host); + var configuration = new ContainerConfiguration() + .WithAssemblies(MefHostServices.DefaultAssemblies) + .WithAssembly(typeof(RemoteSnapshotManager).Assembly); + + // TODO: dispose container at some point? + var container = configuration.CreateContainer(); + + 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; } var solution = await _workspace.OpenSolutionAsync(_sharpIdeSolutionModel.FilePath, new Progress()); timer.Stop(); @@ -57,7 +68,6 @@ public static class RoslynAnalysis foreach (var assembly in MefHostServices.DefaultAssemblies) { - //var assembly = analyzer.GetAssembly(); var fixers = CodeFixProviderLoader.LoadCodeFixProviders([assembly], LanguageNames.CSharp); _codeFixProviders.AddRange(fixers); var refactoringProviders = CodeRefactoringProviderLoader.LoadCodeRefactoringProviders([assembly], LanguageNames.CSharp); @@ -137,6 +147,7 @@ public static class RoslynAnalysis var cancellationToken = CancellationToken.None; var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath); 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)); @@ -155,6 +166,7 @@ public static class RoslynAnalysis { await _solutionLoadedTcs.Task; var cancellationToken = CancellationToken.None; + var timer = Stopwatch.StartNew(); var sharpIdeProjectModel = ((IChildSharpIdeNode) fileModel).GetNearestProjectNode()!; var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == sharpIdeProjectModel!.FilePath); if (!fileModel.Name.EndsWith(".razor", StringComparison.OrdinalIgnoreCase)) @@ -162,20 +174,23 @@ public static class RoslynAnalysis 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 razorProjectSnapshot = _snapshotManager!.GetSnapshot(project); + var documentSnapshot = razorProjectSnapshot.GetDocument(razorDocument); + + var razorCodeDocument = await razorProjectSnapshot.GetRequiredCodeDocumentAsync(documentSnapshot, cancellationToken); + var razorCSharpDocument = razorCodeDocument.GetRequiredCSharpDocument(); + var generatedDocument = await razorProjectSnapshot.GetRequiredGeneratedDocumentAsync(documentSnapshot, cancellationToken); + var generatedDocSyntaxRoot = await generatedDocument.GetSyntaxRootAsync(cancellationToken); + //var razorCsharpText = razorCSharpDocument.Text.ToString(); + //var razorSyntaxRoot = razorCodeDocument.GetRequiredSyntaxRoot(); 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 (razorSpans, sourceMappings) = RazorAccessors.GetSpansAndMappingsForRazorCodeDocument(razorCodeDocument, razorCSharpDocument); + + var classifiedSpans = await Classifier.GetClassifiedSpansAsync(generatedDocument, generatedDocSyntaxRoot!.FullSpan, cancellationToken); var roslynMappedSpans = classifiedSpans.Select(s => { var genSpan = s.TextSpan; @@ -205,11 +220,13 @@ 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)) + 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(); - + timer.Stop(); + Console.WriteLine($"RoslynAnalysis: Razor syntax highlighting for {fileModel.Name} took {timer.ElapsedMilliseconds}ms"); return razorSpans; } diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj index 41ee303..2a89e16 100644 --- a/src/SharpIDE.Application/SharpIDE.Application.csproj +++ b/src/SharpIDE.Application/SharpIDE.Application.csproj @@ -7,6 +7,14 @@ enable + + + + + + + + @@ -16,9 +24,12 @@ + + + diff --git a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs index 51440d4..2d03401 100644 --- a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs +++ b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs @@ -139,6 +139,7 @@ public partial class CustomHighlighter : SyntaxHighlighter "method name" => new Color("dcdcaa"), "extension method name" => new Color("dcdcaa"), "property name" => new Color("dcdcdc"), + "field name" => new Color("dcdcdc"), "static symbol" => new Color("dcdcaa"), "parameter name" => new Color("9cdcfe"), "local name" => new Color("9cdcfe"), @@ -150,7 +151,7 @@ public partial class CustomHighlighter : SyntaxHighlighter // Misc "excluded code" => new Color("a9a9a9"), - _ => new Color("dcdcdc") + _ => new Color("f27718") // orange, warning color for unhandled classifications }; } } diff --git a/src/SharpIDE.Godot/IdeRoot.cs b/src/SharpIDE.Godot/IdeRoot.cs index febdda1..344d64c 100644 --- a/src/SharpIDE.Godot/IdeRoot.cs +++ b/src/SharpIDE.Godot/IdeRoot.cs @@ -96,7 +96,7 @@ public partial class IdeRoot : Control var infraProject = solutionModel.AllProjects.Single(s => s.Name == "Infrastructure"); var diFile = infraProject.Files.Single(s => s.Name == "DependencyInjection.cs"); - await this.InvokeAsync(async () => await _sharpIdeCodeEdit.SetSharpIdeFile(diFile)); + await this.InvokeDeferredAsync(async () => await _sharpIdeCodeEdit.SetSharpIdeFile(diFile)); //var runnableProject = solutionModel.AllProjects.First(s => s.IsRunnable); //await this.InvokeAsync(() => _runPanel.NewRunStarted(runnableProject)); diff --git a/src/SharpIDE.Godot/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/SharpIdeCodeEdit.cs index d64cc38..844c279 100644 --- a/src/SharpIDE.Godot/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/SharpIdeCodeEdit.cs @@ -162,6 +162,7 @@ public partial class SharpIdeCodeEdit : CodeEdit }); } + // TODO: Ensure not running on UI thread public async Task SetSharpIdeFile(SharpIdeFile file) { _currentFile = file; @@ -169,11 +170,12 @@ public partial class SharpIdeCodeEdit : CodeEdit _fileChangingSuppressBreakpointToggleEvent = true; SetText(fileContents); _fileChangingSuppressBreakpointToggleEvent = false; - var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile); - var razorSyntaxHighlighting = await RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile); - SetSyntaxHighlightingModel(syntaxHighlighting, razorSyntaxHighlighting); - var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile); - SetDiagnosticsModel(diagnostics); + var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile); + var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile); + var diagnostics = RoslynAnalysis.GetDocumentDiagnostics(_currentFile); + await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting); + SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting); + SetDiagnosticsModel(await diagnostics); } public void UnderlineRange(int line, int caretStartCol, int caretEndCol, Color color, float thickness = 1.5f) diff --git a/src/SharpIDE.RazorAccess/RazorAccessors.cs b/src/SharpIDE.RazorAccess/RazorAccessors.cs index 1f00799..00bf702 100644 --- a/src/SharpIDE.RazorAccess/RazorAccessors.cs +++ b/src/SharpIDE.RazorAccess/RazorAccessors.cs @@ -1,37 +1,55 @@ 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) + //private static RazorProjectEngine? _razorProjectEngine; + + public static (ImmutableArray, List) GetSpansAndMappingsForRazorCodeDocument(RazorCodeDocument razorCodeDocument, RazorCSharpDocument razorCSharpDocument) { - - 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()); + var result = (sharpIdeSpans.ToImmutableArray(), razorCSharpDocument.SourceMappings.Select(s => s.ToSharpIdeSourceMapping()).ToList()); + return result; } + public static ImmutableArray GetClassifiedSpansForRazorCodeDocument(RazorCodeDocument razorCodeDocument) + { + var razorSpans = RazorCodeDocumentExtensions.GetClassifiedSpans(razorCodeDocument); + return razorSpans; + } + + // 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 razorProjectFileSystem = RazorProjectFileSystem.Create(projectDirectory); + // _razorProjectEngine ??= RazorProjectEngine.Create(RazorConfiguration.Default, razorProjectFileSystem, + // builder => { /* configure features if needed */ }); + // //var projectItem = razorProjectFileSystem.GetItem(razorDocumentFilePath, RazorFileKind.Component); + // + // //var razorCodeDocument = projectEngine.Process(razorSourceDocument, RazorFileKind.Component, [], []); + // var razorCodeDocument = _razorProjectEngine.Process(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(); + // + // var result = (sharpIdeSpans.ToImmutableArray(), razorCSharpDocument.Text, razorCSharpDocument.SourceMappings.Select(s => s.ToSharpIdeSourceMapping()).ToList()); + // return result; + // } + // public static bool TryGetMappedSpans( // TextSpan span, // SourceText source, @@ -59,14 +77,4 @@ public static class RazorAccessors // 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 index 576ea02..41f3f9f 100644 --- a/src/SharpIDE.RazorAccess/SharpIDE.RazorAccess.csproj +++ b/src/SharpIDE.RazorAccess/SharpIDE.RazorAccess.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -28,7 +28,7 @@ - +