Skip to content

Commit b0227fb

Browse files
authored
Merge pull request #60 from Mythetech/feat/razor-highlighting
feat: add razor highlighting support
2 parents c95b42d + 0b04fa1 commit b0227fb

19 files changed

Lines changed: 1654 additions & 94 deletions

File tree

Apollo.Analysis.Worker/Program.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,25 @@
206206
loggerBridge.LogTrace($"Error updating user assembly: {ex.Message}");
207207
}
208208
break;
209+
210+
case "get_semantic_tokens":
211+
try
212+
{
213+
loggerBridge.LogDebug("Received semantic tokens request");
214+
var semanticTokensResult = await monacoService.GetSemanticTokensAsync(message.Payload);
215+
var semanticTokensResponse = new WorkerMessage
216+
{
217+
Action = "semantic_tokens_response",
218+
Payload = Convert.ToBase64String(semanticTokensResult)
219+
};
220+
Imports.PostMessage(semanticTokensResponse.ToSerialized());
221+
loggerBridge.LogDebug("Semantic tokens response sent");
222+
}
223+
catch (Exception ex)
224+
{
225+
loggerBridge.LogTrace($"Error getting semantic tokens: {ex.Message}");
226+
}
227+
break;
209228
}
210229
}
211230
catch (Exception ex)

Apollo.Analysis/Apollo.Analysis.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<PackageReference Include="Microsoft.DiaSymReader" />
3030
<PackageReference Include="OmniSharp.Roslyn" />
3131
<PackageReference Include="OmniSharp.Roslyn.CSharp" />
32+
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" />
3233
</ItemGroup>
3334

3435
</Project>

Apollo.Analysis/MonacoService.cs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ public class MonacoService
2525
private readonly IMetadataReferenceResolver _resolver;
2626
private readonly ILoggerProxy _workerLogger;
2727
private readonly RoslynProjectService _projectService;
28-
28+
2929
private RoslynProject? _legacyCompletionProject;
3030
private OmniSharpCompletionService? _legacyCompletionService;
3131
private OmniSharpSignatureHelpService? _signatureService;
3232
private OmniSharpQuickInfoProvider? _quickInfoProvider;
3333

34+
private RazorCodeExtractor? _razorExtractor;
35+
private RazorSemanticTokenService? _razorSemanticTokenService;
36+
3437
private readonly JsonSerializerOptions _jsonOptions = new()
3538
{
3639
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@@ -76,7 +79,11 @@ public async Task Init(string uri)
7679

7780
_signatureService = new OmniSharpSignatureHelpService(_projectService.Workspace);
7881
_quickInfoProvider = new OmniSharpQuickInfoProvider(_projectService.Workspace, formattingOptions, loggerFactory);
79-
82+
83+
// Initialize Razor services
84+
_razorExtractor = new RazorCodeExtractor(_workerLogger);
85+
_razorSemanticTokenService = new RazorSemanticTokenService(_razorExtractor, _projectService, _workerLogger);
86+
8087
_workerLogger.Trace("RoslynProjectService initialized successfully");
8188
}
8289

@@ -365,6 +372,67 @@ public async Task<byte[]> HandleUserAssemblyUpdateAsync(string requestJson)
365372
}
366373
}
367374

375+
public async Task<byte[]> GetSemanticTokensAsync(string requestJson)
376+
{
377+
try
378+
{
379+
var request = JsonSerializer.Deserialize<SemanticTokensRequest>(requestJson, _jsonOptions);
380+
if (request == null)
381+
{
382+
_workerLogger.LogError("Failed to deserialize semantic tokens request");
383+
return [];
384+
}
385+
386+
_workerLogger.LogTrace($"Semantic tokens request for {request.DocumentUri}");
387+
388+
// Check if this is a Razor file
389+
var isRazorFile = request.DocumentUri.EndsWith(".razor", StringComparison.OrdinalIgnoreCase) ||
390+
request.DocumentUri.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase);
391+
392+
if (!isRazorFile)
393+
{
394+
_workerLogger.LogTrace($"Not a Razor file: {request.DocumentUri}");
395+
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
396+
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
397+
_jsonOptions));
398+
}
399+
400+
if (_razorSemanticTokenService == null)
401+
{
402+
_workerLogger.LogError("Razor semantic token service not initialized");
403+
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
404+
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
405+
_jsonOptions));
406+
}
407+
408+
if (string.IsNullOrEmpty(request.RazorContent))
409+
{
410+
_workerLogger.LogTrace("No Razor content provided");
411+
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
412+
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
413+
_jsonOptions));
414+
}
415+
416+
var result = await _razorSemanticTokenService.GetSemanticTokensAsync(
417+
request.RazorContent,
418+
request.DocumentUri);
419+
420+
_workerLogger.LogTrace($"Returning {result.Data.Length / 5} semantic tokens");
421+
422+
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
423+
new ResponsePayload(result, "GetSemanticTokensAsync"),
424+
_jsonOptions));
425+
}
426+
catch (Exception ex)
427+
{
428+
_workerLogger.LogError($"Error getting semantic tokens: {ex.Message}");
429+
_workerLogger.LogTrace(ex.StackTrace ?? string.Empty);
430+
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
431+
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
432+
_jsonOptions));
433+
}
434+
}
435+
368436
private byte[] CreateSuccessResponse(string message)
369437
{
370438
var response = new { Success = true, Message = message };
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using System.Collections.Immutable;
2+
using Apollo.Infrastructure.Workers;
3+
using Microsoft.AspNetCore.Razor.Language;
4+
5+
namespace Apollo.Analysis;
6+
7+
/// <summary>
8+
/// Extracts C# code from Razor files using the Razor compiler.
9+
/// Provides source mappings to translate positions between generated C# and original Razor.
10+
/// </summary>
11+
public class RazorCodeExtractor
12+
{
13+
private readonly ILoggerProxy _logger;
14+
15+
public RazorCodeExtractor(ILoggerProxy logger)
16+
{
17+
_logger = logger;
18+
}
19+
20+
/// <summary>
21+
/// Parse a Razor file and return the extraction result containing generated C# and source mappings.
22+
/// </summary>
23+
public RazorExtractionResult Extract(string razorContent, string filePath)
24+
{
25+
try
26+
{
27+
// Create a minimal file system for the Razor engine
28+
var fileSystem = new VirtualRazorProjectFileSystem();
29+
30+
// Determine file kind based on extension
31+
var fileKind = filePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)
32+
? FileKinds.Legacy
33+
: FileKinds.Component;
34+
35+
// Create the Razor project engine with component configuration
36+
var projectEngine = RazorProjectEngine.Create(
37+
RazorConfiguration.Default,
38+
fileSystem,
39+
builder =>
40+
{
41+
// Configure for Blazor component compilation
42+
builder.SetRootNamespace("Apollo.Generated");
43+
});
44+
45+
// Create a source document from the Razor content
46+
var sourceDocument = RazorSourceDocument.Create(razorContent, filePath);
47+
48+
// Create and process the code document
49+
var codeDocument = projectEngine.Process(
50+
sourceDocument,
51+
fileKind,
52+
ImmutableArray<RazorSourceDocument>.Empty,
53+
tagHelpers: null);
54+
55+
// Get the generated C# document
56+
var csharpDocument = codeDocument.GetCSharpDocument();
57+
if (csharpDocument == null)
58+
{
59+
_logger.LogTrace($"Failed to generate C# from Razor file: {filePath}");
60+
return RazorExtractionResult.Empty;
61+
}
62+
63+
// Log any diagnostics from Razor compilation
64+
foreach (var diagnostic in csharpDocument.Diagnostics)
65+
{
66+
_logger.LogTrace($"Razor diagnostic in {filePath}: {diagnostic.GetMessage()}");
67+
}
68+
69+
// Get syntax tree
70+
var syntaxTree = codeDocument.GetSyntaxTree();
71+
72+
return new RazorExtractionResult
73+
{
74+
GeneratedCode = csharpDocument.GeneratedCode,
75+
SourceMappings = csharpDocument.SourceMappings.ToList(),
76+
SyntaxTree = syntaxTree
77+
};
78+
}
79+
catch (Exception ex)
80+
{
81+
_logger.LogTrace($"Error extracting C# from Razor file {filePath}: {ex.Message}");
82+
return RazorExtractionResult.Empty;
83+
}
84+
}
85+
}
86+
87+
/// <summary>
88+
/// Result of Razor extraction containing generated C# code and source mappings.
89+
/// </summary>
90+
public class RazorExtractionResult
91+
{
92+
/// <summary>
93+
/// The generated C# code that can be analyzed by Roslyn.
94+
/// </summary>
95+
public string GeneratedCode { get; init; } = "";
96+
97+
/// <summary>
98+
/// Source mappings that map spans in generated C# back to original Razor positions.
99+
/// </summary>
100+
public List<SourceMapping> SourceMappings { get; init; } = [];
101+
102+
/// <summary>
103+
/// The Razor syntax tree for component detection.
104+
/// </summary>
105+
public RazorSyntaxTree? SyntaxTree { get; init; }
106+
107+
/// <summary>
108+
/// An empty extraction result.
109+
/// </summary>
110+
public static RazorExtractionResult Empty => new();
111+
112+
/// <summary>
113+
/// Whether this result has valid generated code.
114+
/// </summary>
115+
public bool IsEmpty => string.IsNullOrEmpty(GeneratedCode);
116+
}
117+
118+
/// <summary>
119+
/// A virtual file system implementation for the Razor project engine.
120+
/// Since we're processing in-memory content, we don't need actual file system access.
121+
/// </summary>
122+
internal class VirtualRazorProjectFileSystem : RazorProjectFileSystem
123+
{
124+
public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath)
125+
{
126+
return Enumerable.Empty<RazorProjectItem>();
127+
}
128+
129+
public override RazorProjectItem GetItem(string path)
130+
{
131+
return new NotFoundProjectItem(string.Empty, path, FileKinds.Component);
132+
}
133+
134+
public override RazorProjectItem GetItem(string path, string? fileKind)
135+
{
136+
return new NotFoundProjectItem(string.Empty, path, fileKind ?? FileKinds.Component);
137+
}
138+
}
139+
140+
/// <summary>
141+
/// Represents a project item that was not found.
142+
/// </summary>
143+
internal class NotFoundProjectItem : RazorProjectItem
144+
{
145+
public NotFoundProjectItem(string basePath, string path, string fileKind)
146+
{
147+
BasePath = basePath;
148+
FilePath = path;
149+
FileKind = fileKind;
150+
}
151+
152+
public override string BasePath { get; }
153+
public override string FilePath { get; }
154+
public override string FileKind { get; }
155+
public override bool Exists => false;
156+
public override string PhysicalPath => FilePath;
157+
158+
public override Stream Read()
159+
{
160+
throw new InvalidOperationException("Item does not exist");
161+
}
162+
}

0 commit comments

Comments
 (0)