Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Apollo.Analysis.Worker/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,25 @@
loggerBridge.LogTrace($"Error updating user assembly: {ex.Message}");
}
break;

case "get_semantic_tokens":
try
{
loggerBridge.LogDebug("Received semantic tokens request");
var semanticTokensResult = await monacoService.GetSemanticTokensAsync(message.Payload);
var semanticTokensResponse = new WorkerMessage
{
Action = "semantic_tokens_response",
Payload = Convert.ToBase64String(semanticTokensResult)
};
Imports.PostMessage(semanticTokensResponse.ToSerialized());
loggerBridge.LogDebug("Semantic tokens response sent");
}
catch (Exception ex)
{
loggerBridge.LogTrace($"Error getting semantic tokens: {ex.Message}");
}
break;
}
}
catch (Exception ex)
Expand Down
1 change: 1 addition & 0 deletions Apollo.Analysis/Apollo.Analysis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<PackageReference Include="Microsoft.DiaSymReader" />
<PackageReference Include="OmniSharp.Roslyn" />
<PackageReference Include="OmniSharp.Roslyn.CSharp" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" />
</ItemGroup>

</Project>
72 changes: 70 additions & 2 deletions Apollo.Analysis/MonacoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ public class MonacoService
private readonly IMetadataReferenceResolver _resolver;
private readonly ILoggerProxy _workerLogger;
private readonly RoslynProjectService _projectService;

private RoslynProject? _legacyCompletionProject;
private OmniSharpCompletionService? _legacyCompletionService;
private OmniSharpSignatureHelpService? _signatureService;
private OmniSharpQuickInfoProvider? _quickInfoProvider;

private RazorCodeExtractor? _razorExtractor;
private RazorSemanticTokenService? _razorSemanticTokenService;

private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
Expand Down Expand Up @@ -76,7 +79,11 @@ public async Task Init(string uri)

_signatureService = new OmniSharpSignatureHelpService(_projectService.Workspace);
_quickInfoProvider = new OmniSharpQuickInfoProvider(_projectService.Workspace, formattingOptions, loggerFactory);


// Initialize Razor services
_razorExtractor = new RazorCodeExtractor(_workerLogger);
_razorSemanticTokenService = new RazorSemanticTokenService(_razorExtractor, _projectService, _workerLogger);

_workerLogger.Trace("RoslynProjectService initialized successfully");
}

Expand Down Expand Up @@ -365,6 +372,67 @@ public async Task<byte[]> HandleUserAssemblyUpdateAsync(string requestJson)
}
}

public async Task<byte[]> GetSemanticTokensAsync(string requestJson)
{
try
{
var request = JsonSerializer.Deserialize<SemanticTokensRequest>(requestJson, _jsonOptions);
if (request == null)
{
_workerLogger.LogError("Failed to deserialize semantic tokens request");
return [];
}

_workerLogger.LogTrace($"Semantic tokens request for {request.DocumentUri}");

// Check if this is a Razor file
var isRazorFile = request.DocumentUri.EndsWith(".razor", StringComparison.OrdinalIgnoreCase) ||
request.DocumentUri.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase);

if (!isRazorFile)
{
_workerLogger.LogTrace($"Not a Razor file: {request.DocumentUri}");
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
_jsonOptions));
}

if (_razorSemanticTokenService == null)
{
_workerLogger.LogError("Razor semantic token service not initialized");
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
_jsonOptions));
}

if (string.IsNullOrEmpty(request.RazorContent))
{
_workerLogger.LogTrace("No Razor content provided");
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
_jsonOptions));
}

var result = await _razorSemanticTokenService.GetSemanticTokensAsync(
request.RazorContent,
request.DocumentUri);

_workerLogger.LogTrace($"Returning {result.Data.Length / 5} semantic tokens");

return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
new ResponsePayload(result, "GetSemanticTokensAsync"),
_jsonOptions));
}
catch (Exception ex)
{
_workerLogger.LogError($"Error getting semantic tokens: {ex.Message}");
_workerLogger.LogTrace(ex.StackTrace ?? string.Empty);
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
_jsonOptions));
}
}

private byte[] CreateSuccessResponse(string message)
{
var response = new { Success = true, Message = message };
Expand Down
162 changes: 162 additions & 0 deletions Apollo.Analysis/RazorCodeExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System.Collections.Immutable;
using Apollo.Infrastructure.Workers;
using Microsoft.AspNetCore.Razor.Language;

namespace Apollo.Analysis;

/// <summary>
/// Extracts C# code from Razor files using the Razor compiler.
/// Provides source mappings to translate positions between generated C# and original Razor.
/// </summary>
public class RazorCodeExtractor
{
private readonly ILoggerProxy _logger;

public RazorCodeExtractor(ILoggerProxy logger)
{
_logger = logger;
}

/// <summary>
/// Parse a Razor file and return the extraction result containing generated C# and source mappings.
/// </summary>
public RazorExtractionResult Extract(string razorContent, string filePath)
{
try
{
// Create a minimal file system for the Razor engine
var fileSystem = new VirtualRazorProjectFileSystem();

// Determine file kind based on extension
var fileKind = filePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)
? FileKinds.Legacy
: FileKinds.Component;

// Create the Razor project engine with component configuration
var projectEngine = RazorProjectEngine.Create(
RazorConfiguration.Default,
fileSystem,
builder =>
{
// Configure for Blazor component compilation
builder.SetRootNamespace("Apollo.Generated");
});

// Create a source document from the Razor content
var sourceDocument = RazorSourceDocument.Create(razorContent, filePath);

// Create and process the code document
var codeDocument = projectEngine.Process(
sourceDocument,
fileKind,
ImmutableArray<RazorSourceDocument>.Empty,
tagHelpers: null);

// Get the generated C# document
var csharpDocument = codeDocument.GetCSharpDocument();
if (csharpDocument == null)
{
_logger.LogTrace($"Failed to generate C# from Razor file: {filePath}");
return RazorExtractionResult.Empty;
}

// Log any diagnostics from Razor compilation
foreach (var diagnostic in csharpDocument.Diagnostics)
{
_logger.LogTrace($"Razor diagnostic in {filePath}: {diagnostic.GetMessage()}");
}

// Get syntax tree
var syntaxTree = codeDocument.GetSyntaxTree();

return new RazorExtractionResult
{
GeneratedCode = csharpDocument.GeneratedCode,
SourceMappings = csharpDocument.SourceMappings.ToList(),
SyntaxTree = syntaxTree
};
}
catch (Exception ex)
{
_logger.LogTrace($"Error extracting C# from Razor file {filePath}: {ex.Message}");
return RazorExtractionResult.Empty;
}
}
}

/// <summary>
/// Result of Razor extraction containing generated C# code and source mappings.
/// </summary>
public class RazorExtractionResult
{
/// <summary>
/// The generated C# code that can be analyzed by Roslyn.
/// </summary>
public string GeneratedCode { get; init; } = "";

/// <summary>
/// Source mappings that map spans in generated C# back to original Razor positions.
/// </summary>
public List<SourceMapping> SourceMappings { get; init; } = [];

/// <summary>
/// The Razor syntax tree for component detection.
/// </summary>
public RazorSyntaxTree? SyntaxTree { get; init; }

/// <summary>
/// An empty extraction result.
/// </summary>
public static RazorExtractionResult Empty => new();

/// <summary>
/// Whether this result has valid generated code.
/// </summary>
public bool IsEmpty => string.IsNullOrEmpty(GeneratedCode);
}

/// <summary>
/// A virtual file system implementation for the Razor project engine.
/// Since we're processing in-memory content, we don't need actual file system access.
/// </summary>
internal class VirtualRazorProjectFileSystem : RazorProjectFileSystem
{
public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath)
{
return Enumerable.Empty<RazorProjectItem>();
}

public override RazorProjectItem GetItem(string path)
{
return new NotFoundProjectItem(string.Empty, path, FileKinds.Component);
}

public override RazorProjectItem GetItem(string path, string? fileKind)
{
return new NotFoundProjectItem(string.Empty, path, fileKind ?? FileKinds.Component);
}
}

/// <summary>
/// Represents a project item that was not found.
/// </summary>
internal class NotFoundProjectItem : RazorProjectItem
{
public NotFoundProjectItem(string basePath, string path, string fileKind)
{
BasePath = basePath;
FilePath = path;
FileKind = fileKind;
}

public override string BasePath { get; }
public override string FilePath { get; }
public override string FileKind { get; }
public override bool Exists => false;
public override string PhysicalPath => FilePath;

public override Stream Read()
{
throw new InvalidOperationException("Item does not exist");
}
}
Loading
Loading