Skip to content

Commit 4eccaaf

Browse files
committed
feat: improved semantic token highlighting
feat: quick info support
1 parent 13ded6c commit 4eccaaf

10 files changed

Lines changed: 482 additions & 58 deletions

File tree

Apollo.Analysis.Worker/Program.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,35 @@
8686
break;
8787

8888
case "get_quick_hint":
89-
var quickHint = await monacoService.GetQuickInfoAsync(message.Payload);
89+
var qhWrapper = JsonSerializer.Deserialize<CompletionRequestWrapper>(message.Payload);
90+
ArgumentException.ThrowIfNullOrWhiteSpace(qhWrapper?.Code);
91+
ArgumentNullException.ThrowIfNull(qhWrapper?.Request);
92+
var quickHint = await monacoService.GetQuickInfoAsync(qhWrapper.Code, qhWrapper.Request);
9093

9194
var quickHintResponse = new WorkerMessage()
9295
{
9396
Action = "quick_hint_response",
9497
Payload = Convert.ToBase64String(quickHint),
9598
};
96-
99+
97100
Imports.PostMessage(quickHintResponse.ToSerialized());
98101
break;
99-
102+
103+
case "get_signature_help":
104+
var sigWrapper = JsonSerializer.Deserialize<CompletionRequestWrapper>(message.Payload);
105+
ArgumentException.ThrowIfNullOrWhiteSpace(sigWrapper?.Code);
106+
ArgumentNullException.ThrowIfNull(sigWrapper?.Request);
107+
var sigHelp = await monacoService.GetSignatureHelpAsync(sigWrapper.Code, sigWrapper.Request);
108+
109+
var sigHelpResponse = new WorkerMessage()
110+
{
111+
Action = "signature_help_response",
112+
Payload = Convert.ToBase64String(sigHelp),
113+
};
114+
115+
Imports.PostMessage(sigHelpResponse.ToSerialized());
116+
break;
117+
100118
case "get_diagnostics":
101119
try
102120
{

Apollo.Analysis/MonacoService.cs

Lines changed: 130 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Apollo.Contracts.Analysis;
33
using Apollo.Infrastructure;
44
using Apollo.Infrastructure.Workers;
5+
using Microsoft.CodeAnalysis.Classification;
56
using Microsoft.CodeAnalysis.Completion;
67
using Microsoft.CodeAnalysis.Tags;
78
using Microsoft.Extensions.Logging;
@@ -231,23 +232,41 @@ public async Task<byte[]> GetSignatureHelpAsync(string code, string signatureHel
231232
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload, _jsonOptions));
232233
}
233234

234-
public async Task<byte[]> GetQuickInfoAsync(string quickInfoRequestString)
235+
public async Task<byte[]> GetQuickInfoAsync(string code, string quickInfoRequestString)
235236
{
236-
var quickInfoRequest = JsonSerializer.Deserialize<QuickInfoRequest>(quickInfoRequestString);
237-
if (quickInfoRequest == null || _quickInfoProvider == null)
237+
try
238238
{
239-
return [];
240-
}
239+
using var doc = JsonDocument.Parse(quickInfoRequestString);
240+
var root = doc.RootElement;
241+
var path = root.GetProperty("FileName").GetString();
242+
var line = root.GetProperty("Line").GetInt32();
243+
var column = root.GetProperty("Column").GetInt32();
241244

242-
var document = _projectService.GetCurrentDocument();
243-
if (document == null)
245+
if (string.IsNullOrEmpty(path) || _quickInfoProvider == null)
246+
return [];
247+
248+
_workerLogger.LogTrace($"QuickInfo request for {path} at {line}:{column}");
249+
_projectService.UpdateDocument(path, code);
250+
_projectService.SetCurrentDocument(path);
251+
252+
var document = _projectService.GetDocument(path);
253+
if (document == null)
254+
return [];
255+
256+
var sourceText = await document.GetTextAsync();
257+
if (line < 0 || line >= sourceText.Lines.Count)
258+
return [];
259+
260+
var quickInfoRequest = new QuickInfoRequest { FileName = path, Line = line, Column = column };
261+
var quickInfoResponse = await _quickInfoProvider.Handle(quickInfoRequest, document);
262+
var payload = new ResponsePayload(quickInfoResponse, "GetQuickInfoAsync");
263+
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload, _jsonOptions));
264+
}
265+
catch (Exception ex)
244266
{
267+
_workerLogger.LogError($"Error getting quick info: {ex.Message}");
245268
return [];
246269
}
247-
248-
var quickInfoResponse = await _quickInfoProvider.Handle(quickInfoRequest, document);
249-
var payload = new ResponsePayload(quickInfoResponse, "GetQuickInfoAsync");
250-
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload, _jsonOptions));
251270
}
252271

253272
public async Task<byte[]> GetDiagnosticsAsync(string uri, Solution solution)
@@ -385,51 +404,121 @@ public async Task<byte[]> GetSemanticTokensAsync(string requestJson)
385404

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

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

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-
}
410+
SemanticTokensResult result;
399411

400-
if (_razorSemanticTokenService == null)
412+
if (isRazorFile)
401413
{
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-
}
414+
if (_razorSemanticTokenService == null || string.IsNullOrEmpty(request.RazorContent))
415+
{
416+
return SerializeTokensResponse(SemanticTokensResult.Empty);
417+
}
407418

408-
if (string.IsNullOrEmpty(request.RazorContent))
419+
result = await _razorSemanticTokenService.GetSemanticTokensAsync(
420+
request.RazorContent,
421+
request.DocumentUri);
422+
}
423+
else
409424
{
410-
_workerLogger.LogTrace("No Razor content provided");
411-
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
412-
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
413-
_jsonOptions));
425+
result = await GetCSharpSemanticTokensAsync(request.DocumentUri);
414426
}
415427

416-
var result = await _razorSemanticTokenService.GetSemanticTokensAsync(
417-
request.RazorContent,
418-
request.DocumentUri);
419-
420428
_workerLogger.LogTrace($"Returning {result.Data.Length / 5} semantic tokens");
421-
422-
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
423-
new ResponsePayload(result, "GetSemanticTokensAsync"),
424-
_jsonOptions));
429+
return SerializeTokensResponse(result);
425430
}
426431
catch (Exception ex)
427432
{
428433
_workerLogger.LogError($"Error getting semantic tokens: {ex.Message}");
429434
_workerLogger.LogTrace(ex.StackTrace ?? string.Empty);
430-
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
431-
new ResponsePayload(SemanticTokensResult.Empty, "GetSemanticTokensAsync"),
432-
_jsonOptions));
435+
return SerializeTokensResponse(SemanticTokensResult.Empty);
436+
}
437+
}
438+
439+
private byte[] SerializeTokensResponse(SemanticTokensResult result)
440+
{
441+
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(
442+
new ResponsePayload(result, "GetSemanticTokensAsync"),
443+
_jsonOptions));
444+
}
445+
446+
private async Task<SemanticTokensResult> GetCSharpSemanticTokensAsync(string documentUri)
447+
{
448+
try
449+
{
450+
var document = _projectService.GetDocument(documentUri)
451+
?? _projectService.GetCurrentDocument();
452+
453+
if (document == null)
454+
{
455+
_workerLogger.LogTrace($"No document found for C# semantic tokens: {documentUri}");
456+
return SemanticTokensResult.Empty;
457+
}
458+
459+
var text = await document.GetTextAsync();
460+
var textSpan = TextSpan.FromBounds(0, text.Length);
461+
462+
var classifiedSpans = await Classifier.GetClassifiedSpansAsync(
463+
document, textSpan);
464+
465+
var semanticSpans = classifiedSpans
466+
.Where(s => RazorSemanticTokenService.IsCSharpSemanticClassification(s.ClassificationType))
467+
.OrderBy(s => s.TextSpan.Start)
468+
.ToList();
469+
470+
if (semanticSpans.Count == 0)
471+
return SemanticTokensResult.Empty;
472+
473+
var tokens = new int[semanticSpans.Count * 5];
474+
var tokenIndex = 0;
475+
var previousLine = 0;
476+
var previousStartChar = 0;
477+
var previousSpanEnd = 0;
478+
479+
foreach (var span in semanticSpans)
480+
{
481+
if (previousSpanEnd > span.TextSpan.Start)
482+
continue;
483+
484+
var tokenType = RazorSemanticTokenService.MapCSharpClassificationToTokenType(span.ClassificationType);
485+
if (tokenType < 0)
486+
continue;
487+
488+
var linePosition = text.Lines.GetLinePositionSpan(span.TextSpan);
489+
var line = linePosition.Start.Line;
490+
var startChar = linePosition.Start.Character;
491+
var length = span.TextSpan.Length;
492+
493+
var deltaLine = line - previousLine;
494+
var deltaStartChar = deltaLine == 0
495+
? startChar - previousStartChar
496+
: startChar;
497+
498+
tokens[tokenIndex++] = deltaLine;
499+
tokens[tokenIndex++] = deltaStartChar;
500+
tokens[tokenIndex++] = length;
501+
tokens[tokenIndex++] = tokenType;
502+
tokens[tokenIndex++] = 0;
503+
504+
previousLine = line;
505+
previousStartChar = startChar;
506+
previousSpanEnd = span.TextSpan.End;
507+
}
508+
509+
if (tokenIndex < tokens.Length)
510+
tokens = tokens[..tokenIndex];
511+
512+
return new SemanticTokensResult
513+
{
514+
Data = tokens,
515+
ResultId = Guid.NewGuid().ToString()
516+
};
517+
}
518+
catch (Exception ex)
519+
{
520+
_workerLogger.LogError($"Error getting C# semantic tokens: {ex.Message}");
521+
return SemanticTokensResult.Empty;
433522
}
434523
}
435524

Apollo.Analysis/RazorSemanticTokenService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,12 @@ private int[] EncodeSemanticTokens(List<RazorSemanticToken> tokens)
681681
/// <summary>
682682
/// Check if a classification type represents a semantic token we want to highlight.
683683
/// </summary>
684+
public static bool IsCSharpSemanticClassification(string classificationType)
685+
=> IsSemanticClassification(classificationType);
686+
687+
public static int MapCSharpClassificationToTokenType(string classificationType)
688+
=> MapClassificationToTokenType(classificationType);
689+
684690
private static bool IsSemanticClassification(string classificationType)
685691
{
686692
return classificationType switch

Apollo.Client/Analysis/CodeAnalysisWorkerProxy.cs

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ internal async Task InitializeMessageListener()
9090
case "semantic_tokens_response":
9191
_semanticTokensResponse = Convert.FromBase64String(message.Payload);
9292
break;
93+
case "quick_hint_response":
94+
_quickInfoResponse = Convert.FromBase64String(message.Payload);
95+
break;
96+
case "signature_help_response":
97+
_signatureHelpResponse = Convert.FromBase64String(message.Payload);
98+
break;
99+
case "completion_resolve_response":
100+
_completionResolveResponse = Convert.FromBase64String(message.Payload);
101+
break;
93102
default:
94103
_console.AddDebug($"Unknown event: {message.Action}");
95104
_console.AddDebug(JsonSerializer.Serialize(message.Payload));
@@ -146,17 +155,80 @@ public async Task<byte[]> GetCompletionAsync(string code, string completionReque
146155

147156
public async Task<byte[]> GetCompletionResolveAsync(string completionResolveRequestString)
148157
{
149-
throw new NotImplementedException();
158+
_completionResolveResponse = null;
159+
160+
await _worker.PostMessageAsync(JsonSerializer.Serialize(new WorkerMessage
161+
{
162+
Action = "get_completion_resolve",
163+
Payload = completionResolveRequestString
164+
}));
165+
166+
for (int i = 0; i < 50; i++)
167+
{
168+
if (_completionResolveResponse == null)
169+
{
170+
await Task.Delay(100);
171+
await Task.Yield();
172+
}
173+
else
174+
{
175+
return _completionResolveResponse;
176+
}
177+
}
178+
179+
return _completionResolveResponse ?? [];
150180
}
151181

152182
public async Task<byte[]> GetSignatureHelpAsync(string code, string signatureHelpRequestString)
153183
{
154-
throw new NotImplementedException();
184+
_signatureHelpResponse = null;
185+
186+
await _worker.PostMessageAsync(JsonSerializer.Serialize(new WorkerMessage
187+
{
188+
Action = "get_signature_help",
189+
Payload = JsonSerializer.Serialize(new CompletionRequestWrapper(code, signatureHelpRequestString))
190+
}));
191+
192+
for (int i = 0; i < 50; i++)
193+
{
194+
if (_signatureHelpResponse == null)
195+
{
196+
await Task.Delay(100);
197+
await Task.Yield();
198+
}
199+
else
200+
{
201+
return _signatureHelpResponse;
202+
}
203+
}
204+
205+
return _signatureHelpResponse ?? [];
155206
}
156207

157-
public async Task<byte[]> GetQuickInfoAsync(string quickInfoRequestString)
208+
public async Task<byte[]> GetQuickInfoAsync(string code, string quickInfoRequestString)
158209
{
159-
throw new NotImplementedException();
210+
_quickInfoResponse = null;
211+
212+
await _worker.PostMessageAsync(JsonSerializer.Serialize(new WorkerMessage
213+
{
214+
Action = "get_quick_hint",
215+
Payload = JsonSerializer.Serialize(new CompletionRequestWrapper(code, quickInfoRequestString))
216+
}));
217+
218+
for (int i = 0; i < 50; i++)
219+
{
220+
if (_quickInfoResponse == null)
221+
{
222+
await Task.Delay(100);
223+
await Task.Yield();
224+
}
225+
else
226+
{
227+
return _quickInfoResponse;
228+
}
229+
}
230+
231+
return _quickInfoResponse ?? [];
160232
}
161233

162234
public async Task<byte[]> GetDiagnosticsAsync(string serializedSolution)
@@ -190,6 +262,9 @@ await _worker.PostMessageAsync(JsonSerializer.Serialize(new WorkerMessage
190262
private byte[]? _setCurrentDocumentResponse;
191263
private byte[]? _userAssemblyUpdateResponse;
192264
private byte[]? _semanticTokensResponse;
265+
private byte[]? _quickInfoResponse;
266+
private byte[]? _signatureHelpResponse;
267+
private byte[]? _completionResolveResponse;
193268

194269
public async Task<byte[]> UpdateDocumentAsync(string documentUpdateRequest)
195270
{

0 commit comments

Comments
 (0)