add razor file syntax highlighting

This commit is contained in:
Matt Parker
2025-09-14 19:02:33 +10:00
parent b4291cb7f7
commit cc7e766966
19 changed files with 590 additions and 49 deletions

View File

@@ -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<IEnumerable<SharpIdeRazorClassifiedSpan>> 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<IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)>> 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);
}
}
}

View File

@@ -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<SharpIdeFile> allFiles)

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@@ -8,22 +8,26 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.645901" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.Shared.VSCodeDebugProtocol" Version="18.0.10427.1" />
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.Shared.VSCodeDebugProtocol" />
<!-- If any Microsoft.Build.*.dll (Excluding Locator) ends up in the output, it will be prioritised for loading by MSBuild Nodes -->
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />
<PackageReference Include="AsyncReadProcess" Version="1.0.0-preview15" />
<PackageReference Include="Microsoft.Build" Version="17.15.0-preview-25459-108" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.15.0-preview-25459-108" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.9.1" />
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="17.15.0-preview-25459-108" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.15.0-preview-25459-108" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="5.0.0-2.25459.108" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0-2.25459.108" />
<PackageReference Include="Microsoft.CodeAnalysis.Features" Version="5.0.0-2.25459.108" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0-2.25459.108" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.52" />
<PackageReference Include="NuGet.Protocol" Version="7.0.0-preview.1.46008" />
<PackageReference Include="ObservableCollections" Version="3.3.4" />
<PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="AsyncReadProcess" />
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Framework" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Locator" />
<PackageReference Include="Microsoft.Build.Tasks.Core" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.Features" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
<PackageReference Include="NuGet.Protocol" />
<PackageReference Include="ObservableCollections" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharpIDE.RazorAccess\SharpIDE.RazorAccess.csproj" />
</ItemGroup>
</Project>