Skip to content

Commit ae88eef

Browse files
authored
Add C# semantic tokens provider (#77)
1 parent d29e535 commit ae88eef

File tree

15 files changed

+1019
-4
lines changed

15 files changed

+1019
-4
lines changed

src/App/Lab/LanguageServices.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal sealed class LanguageServices(
1414
BlazorMonacoInterop blazorMonacoInterop)
1515
{
1616
private Dictionary<string, string> modelUrlToFileName = [];
17-
private IDisposable? completionProvider;
17+
private IDisposable? completionProvider, semanticTokensProvider;
1818
private string? currentModelUrl;
1919
private DebounceInfo completionDebounce = new(new CancellationTokenSource());
2020
private DebounceInfo diagnosticsDebounce = new(new CancellationTokenSource());
@@ -101,12 +101,34 @@ private async Task RegisterAsync()
101101
},
102102
ResolveCompletionItemFunc = (completionItem, cancellationToken) => worker.ResolveCompletionItemAsync(completionItem),
103103
});
104+
105+
semanticTokensProvider = await blazorMonacoInterop.RegisterSemanticTokensProviderAsync(cSharpLanguageSelector, new SemanticTokensProvider
106+
{
107+
Legend = new SemanticTokensLegend
108+
{
109+
TokenTypes = SemanticTokensUtil.TokenTypes.LspValues,
110+
TokenModifiers = SemanticTokensUtil.TokenModifiers.LspValues,
111+
},
112+
ProvideSemanticTokens = (modelUri, rangeJson, debug, cancellationToken) =>
113+
{
114+
return DebounceAsync(
115+
ref diagnosticsDebounce,
116+
(worker, modelUri, debug, rangeJson),
117+
// Fallback value when cancelled is `null` which causes an exception to be thrown
118+
// instead of returning empty tokens which would cause the semantic colorization to disappear.
119+
null,
120+
static (args, cancellationToken) => args.worker.ProvideSemanticTokensAsync(args.modelUri, args.rangeJson, args.debug),
121+
cancellationToken);
122+
},
123+
});
104124
}
105125

106126
private void Unregister()
107127
{
108128
completionProvider?.Dispose();
109129
completionProvider = null;
130+
semanticTokensProvider?.Dispose();
131+
semanticTokensProvider = null;
110132
}
111133

112134
public void OnDidChangeWorkspace(ImmutableArray<ModelInfo> models, bool updateDiagnostics = true)

src/App/Lab/Page.razor

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@inject LanguageServices LanguageServices
77
@inject InputOutputCache Cache
88
@inject TemplateCache TemplateCache
9+
@inject BlazorMonacoInterop BlazorMonacoInterop
910
@inject ILogger<Page> Logger
1011

1112
<PageTitle>.NET Lab</PageTitle>
@@ -219,6 +220,7 @@
219220
private readonly List<Input> inputs = new();
220221
private readonly Dictionary<string, EditorState> outputStates = new();
221222
private bool editorInitializationStarted;
223+
private bool monacoThemeDefined;
222224
private DotNetObjectReference<Page>? dotNetObjectReference;
223225
private Action? unregisterEventListeners;
224226
private IJSObjectReference module = null!;
@@ -390,9 +392,28 @@
390392
};
391393
}
392394

393-
public static string GetMonacoTheme(bool dark)
395+
public string GetMonacoTheme(bool dark)
394396
{
395-
return dark ? "vs-dark" : "vs";
397+
if (monacoThemeDefined)
398+
{
399+
return dark ? CustomMonacoTheme.Dark : CustomMonacoTheme.Light;
400+
}
401+
402+
return dark ? BuiltInMonacoTheme.Dark : BuiltInMonacoTheme.Light;
403+
}
404+
405+
private async Task DefineMonacoThemeAsync()
406+
{
407+
Debug.Assert(!monacoThemeDefined);
408+
await CustomMonacoTheme.DefineAsync(JSRuntime);
409+
monacoThemeDefined = true;
410+
bool dark = settings.Theme switch
411+
{
412+
DesignThemeModes.Dark => true,
413+
DesignThemeModes.Light => false,
414+
_ => loadedDarkTheme,
415+
};
416+
await BlazorMonaco.Editor.Global.SetTheme(JSRuntime, GetMonacoTheme(dark: dark));
396417
}
397418

398419
private async Task OnInputPresetSelectedAsync(MenuChangeEventArgs args)
@@ -894,6 +915,8 @@
894915

895916
editorInitializationStarted = true;
896917

918+
await BlazorMonacoInterop.EnableSemanticHighlightingAsync(inputEditor.Id);
919+
await DefineMonacoThemeAsync();
897920
module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Lab/Page.razor.js?v=3");
898921
dotNetObjectReference = DotNetObjectReference.Create(this);
899922
unregisterEventListeners = await module.InvokeAsync<Action>("registerEventListeners", dotNetObjectReference);

src/App/Lab/Settings.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
290290
[Parameter, EditorRequired] public required StandaloneCodeEditor OutputEditor { get; set; }
291291
[CascadingParameter] public required Page Page { get; set; }
292292

293+
public DesignThemeModes Theme => theme;
294+
293295
private bool DebugLogs
294296
{
295297
get => Logging.LogLevel <= LogLevel.Debug;
@@ -738,7 +740,7 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
738740
{
739741
if (vimDisposable is null)
740742
{
741-
vimDisposable = await JSRuntime.InvokeAsync<IJSObjectReference>("jslib.EnableVimMode", "input-editor", "vim-status");
743+
vimDisposable = await JSRuntime.InvokeAsync<IJSObjectReference>("jslib.EnableVimMode", InputEditor.Id, "vim-status");
742744
}
743745
}
744746
else

src/App/Lab/WorkerController.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,13 @@ public Task<string> ProvideCompletionItemsAsync(string modelUri, Position positi
435435
deserializeAs: default(string));
436436
}
437437

438+
public Task<string?> ProvideSemanticTokensAsync(string modelUri, string? rangeJson, bool debug)
439+
{
440+
return PostAndReceiveMessageAsync(
441+
new WorkerInputMessage.ProvideSemanticTokens(modelUri, rangeJson, debug) { Id = messageId++ },
442+
deserializeAs: default(string));
443+
}
444+
438445
public void OnDidChangeWorkspace(ImmutableArray<ModelInfo> models)
439446
{
440447
PostMessage(

src/App/Utils/Monaco/BlazorMonacoInterop.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ private static partial JSObject RegisterCompletionProvider(
2121
string[]? triggerCharacters,
2222
[JSMarshalAs<JSType.Any>] object completionItemProvider);
2323

24+
[JSImport("enableSemanticHighlighting", moduleName)]
25+
private static partial void EnableSemanticHighlighting(string editorId);
26+
27+
[JSImport("registerSemanticTokensProvider", moduleName)]
28+
private static partial JSObject RegisterSemanticTokensProvider(
29+
string language,
30+
string legend,
31+
[JSMarshalAs<JSType.Any>] object provider);
32+
2433
[JSImport("dispose", moduleName)]
2534
private static partial void DisposeDisposable(JSObject disposable);
2635

@@ -57,6 +66,19 @@ internal static async Task<string> ProvideCompletionItemsAsync(
5766
return json;
5867
}
5968

69+
[JSExport]
70+
internal static async Task<string?> ProvideSemanticTokensAsync(
71+
[JSMarshalAs<JSType.Any>] object providerReference,
72+
string modelUri,
73+
string? rangeJson,
74+
bool debug,
75+
JSObject token)
76+
{
77+
var provider = ((DotNetObjectReference<SemanticTokensProvider>)providerReference).Value;
78+
string? json = await provider.ProvideSemanticTokensAsync(modelUri, rangeJson, debug, ToCancellationToken(token));
79+
return json;
80+
}
81+
6082
public async Task<IDisposable> RegisterCompletionProviderAsync(
6183
LanguageSelector language,
6284
CompletionItemProviderAsync completionItemProvider)
@@ -69,6 +91,24 @@ public async Task<IDisposable> RegisterCompletionProviderAsync(
6991
return new Disposable(disposable);
7092
}
7193

94+
public async Task EnableSemanticHighlightingAsync(string editorId)
95+
{
96+
await EnsureInitializedAsync();
97+
EnableSemanticHighlighting(editorId);
98+
}
99+
100+
public async Task<IDisposable> RegisterSemanticTokensProviderAsync(
101+
LanguageSelector language,
102+
SemanticTokensProvider provider)
103+
{
104+
await EnsureInitializedAsync();
105+
JSObject disposable = RegisterSemanticTokensProvider(
106+
JsonSerializer.Serialize(language, BlazorMonacoJsonContext.Default.LanguageSelector),
107+
JsonSerializer.Serialize(provider.Legend, BlazorMonacoJsonContext.Default.SemanticTokensLegend),
108+
DotNetObjectReference.Create(provider));
109+
return new Disposable(disposable);
110+
}
111+
72112
public static CancellationToken ToCancellationToken(JSObject token)
73113
{
74114
// No need to initialize, we already must have called other APIs.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using Microsoft.JSInterop;
2+
3+
namespace DotNetLab;
4+
5+
internal static class CustomMonacoTheme
6+
{
7+
public const string Light = "dotnetlab-light";
8+
public const string Dark = "dotnetlab-dark";
9+
10+
public static async Task DefineAsync(IJSRuntime jsRuntime)
11+
{
12+
// https://github.com/dotnet/vscode-csharp/blob/adf1a040d73c6cbe72d18c516b2c5ceb59b538e3/themes/vs2019_light.json
13+
await BlazorMonaco.Editor.Global.DefineTheme(jsRuntime, Light, new()
14+
{
15+
Base = BuiltInMonacoTheme.Light,
16+
Inherit = true,
17+
Colors = [],
18+
Rules =
19+
[
20+
new() { Token = "comment", Foreground = "008000" },
21+
new() { Token = "excludedCode", Foreground = "808080" },
22+
new() { Token = "variable", Foreground = "001080" },
23+
new() { Token = "keyword", Foreground = "0000ff" },
24+
new() { Token = "keywordControl", Foreground = "af00db" },
25+
new() { Token = "number", Foreground = "098658" },
26+
new() { Token = "operator", Foreground = "000000" },
27+
new() { Token = "operatorOverloaded", Foreground = "000000" },
28+
new() { Token = "macro", Foreground = "0000ff" },
29+
new() { Token = "string", Foreground = "a31515" },
30+
new() { Token = "text", Foreground = "000000" },
31+
new() { Token = "preprocessorText", Foreground = "a31515" },
32+
new() { Token = "punctuation", Foreground = "222222" },
33+
new() { Token = "stringVerbatim", Foreground = "a31515" },
34+
new() { Token = "stringEscapeCharacter", Foreground = "ff0000" },
35+
new() { Token = "class", Foreground = "267f99" },
36+
new() { Token = "recordClassName", Foreground = "267f99" },
37+
new() { Token = "delegateName", Foreground = "267f99" },
38+
new() { Token = "enum", Foreground = "267f99" },
39+
new() { Token = "interface", Foreground = "267f99" },
40+
new() { Token = "moduleName", Foreground = "222222" },
41+
new() { Token = "struct", Foreground = "267f99" },
42+
new() { Token = "recordStructName", Foreground = "267f99" },
43+
new() { Token = "typeParameter", Foreground = "267f99" },
44+
new() { Token = "fieldName", Foreground = "222222" },
45+
new() { Token = "enumMember", Foreground = "222222" },
46+
new() { Token = "constantName", Foreground = "222222" },
47+
new() { Token = "parameter", Foreground = "001080" },
48+
new() { Token = "method", Foreground = "795e26" },
49+
new() { Token = "extensionMethodName", Foreground = "795e26" },
50+
new() { Token = "property", Foreground = "222222" },
51+
new() { Token = "event", Foreground = "222222" },
52+
new() { Token = "namespace", Foreground = "222222" },
53+
new() { Token = "label", Foreground = "000000" },
54+
new() { Token = "xmlDocCommentAttributeName", Foreground = "282828" },
55+
new() { Token = "xmlDocCommentAttributeQuotes", Foreground = "282828" },
56+
new() { Token = "xmlDocCommentAttributeValue", Foreground = "282828" },
57+
new() { Token = "xmlDocCommentCDataSection", Foreground = "808080" },
58+
new() { Token = "xmlDocCommentComment", Foreground = "008000" },
59+
new() { Token = "xmlDocCommentDelimiter", Foreground = "808080" },
60+
new() { Token = "xmlDocCommentEntityReference", Foreground = "008000" },
61+
new() { Token = "xmlDocCommentName", Foreground = "808080" },
62+
new() { Token = "xmlDocCommentProcessingInstruction", Foreground = "808080" },
63+
new() { Token = "xmlDocCommentText", Foreground = "008000" },
64+
new() { Token = "xmlLiteralAttributeName", Foreground = "ff0000" },
65+
new() { Token = "xmlLiteralAttributeQuotes", Foreground = "0000ff" },
66+
new() { Token = "xmlLiteralAttributeValue", Foreground = "0000ff" },
67+
new() { Token = "xmlLiteralCDataSection", Foreground = "0000ff" },
68+
new() { Token = "xmlLiteralComment", Foreground = "008000" },
69+
new() { Token = "xmlLiteralDelimiter", Foreground = "800000" },
70+
new() { Token = "xmlLiteralEmbeddedExpression", Foreground = "000000" },
71+
new() { Token = "xmlLiteralEntityReference", Foreground = "ff0000" },
72+
new() { Token = "xmlLiteralName", Foreground = "800000" },
73+
new() { Token = "xmlLiteralProcessingInstruction", Foreground = "0000ff" },
74+
new() { Token = "xmlLiteralText", Foreground = "0000ff" },
75+
new() { Token = "regexComment", Foreground = "008000" },
76+
new() { Token = "regexCharacterClass", Foreground = "0073ff" },
77+
new() { Token = "regexAnchor", Foreground = "ff00c1" },
78+
new() { Token = "regexQuantifier", Foreground = "ff00c1" },
79+
new() { Token = "regexGrouping", Foreground = "05c3ba" },
80+
new() { Token = "regexAlternation", Foreground = "05c3ba" },
81+
new() { Token = "regexText", Foreground = "800000" },
82+
new() { Token = "regexSelfEscapedCharacter", Foreground = "800000" },
83+
new() { Token = "regexOtherEscape", Foreground = "9e5b71" },
84+
new() { Token = "jsonComment", Foreground = "008000" },
85+
new() { Token = "jsonNumber", Foreground = "098658" },
86+
new() { Token = "jsonString", Foreground = "a31515" },
87+
new() { Token = "jsonKeyword", Foreground = "0000ff" },
88+
new() { Token = "jsonText", Foreground = "000000" },
89+
new() { Token = "jsonOperator", Foreground = "000000" },
90+
new() { Token = "jsonPunctuation", Foreground = "000000" },
91+
new() { Token = "jsonArray", Foreground = "000000" },
92+
new() { Token = "jsonObject", Foreground = "000000" },
93+
new() { Token = "jsonPropertyName", Foreground = "0451a5" },
94+
new() { Token = "jsonConstructorName", Foreground = "795e26" },
95+
],
96+
});
97+
98+
// https://github.com/dotnet/vscode-csharp/blob/adf1a040d73c6cbe72d18c516b2c5ceb59b538e3/themes/vs2019_dark.json
99+
await BlazorMonaco.Editor.Global.DefineTheme(jsRuntime, Dark, new()
100+
{
101+
Base = BuiltInMonacoTheme.Dark,
102+
Inherit = true,
103+
Colors = [],
104+
Rules =
105+
[
106+
new() { Token = "comment", Foreground = "6a9955" },
107+
new() { Token = "keyword", Foreground = "569cd6" },
108+
new() { Token = "keywordControl", Foreground = "c586c0" },
109+
new() { Token = "number", Foreground = "b5cea8" },
110+
new() { Token = "operator", Foreground = "d4d4d4" },
111+
new() { Token = "string", Foreground = "ce9178" },
112+
new() { Token = "class", Foreground = "4ec9b0" },
113+
new() { Token = "interface", Foreground = "b8d7a3" },
114+
new() { Token = "struct", Foreground = "86c691" },
115+
new() { Token = "enum", Foreground = "b8d7a3" },
116+
new() { Token = "enumMember", Foreground = "d4d4d4" },
117+
new() { Token = "delegateName", Foreground = "4ec9b0" },
118+
new() { Token = "method", Foreground = "dcdcaa" },
119+
new() { Token = "extensionMethodName", Foreground = "dcdcaa" },
120+
new() { Token = "preprocessorText", Foreground = "d4d4d4" },
121+
new() { Token = "xmlDocCommentComment", Foreground = "608b4e" },
122+
new() { Token = "xmlDocCommentName", Foreground = "569cd6" },
123+
new() { Token = "xmlDocCommentDelimiter", Foreground = "808080" },
124+
new() { Token = "xmlDocCommentAttributeName", Foreground = "c8c8c8" },
125+
new() { Token = "xmlDocCommentAttributeValue", Foreground = "c8c8c8" },
126+
new() { Token = "xmlDocCommentCDataSection", Foreground = "e9d585" },
127+
new() { Token = "xmlDocCommentText", Foreground = "608b4e" },
128+
new() { Token = "punctuation", Foreground = "d4d4d4" },
129+
new() { Token = "variable", Foreground = "9cdcfe" },
130+
new() { Token = "property", Foreground = "d4d4d4" },
131+
new() { Token = "parameter", Foreground = "9cdcfe" },
132+
new() { Token = "fieldName", Foreground = "d4d4d4" },
133+
new() { Token = "event", Foreground = "d4d4d4" },
134+
new() { Token = "namespace", Foreground = "d4d4d4" },
135+
new() { Token = "typeParameter", Foreground = "b8d7a3" },
136+
new() { Token = "constantName", Foreground = "d4d4d4" },
137+
new() { Token = "stringVerbatim", Foreground = "ce9178" },
138+
new() { Token = "stringEscapeCharacter", Foreground = "d7ba7d" },
139+
new() { Token = "excludedCode", Foreground = "808080" },
140+
new() { Token = "macro", Foreground = "808080" },
141+
new() { Token = "label", Foreground = "c8c8c8" },
142+
new() { Token = "operatorOverloaded", Foreground = "d4d4d4" },
143+
new() { Token = "regexComment", Foreground = "57a64a" },
144+
new() { Token = "regexCharacterClass", Foreground = "2eabfe" },
145+
new() { Token = "regexAnchor", Foreground = "f979ae" },
146+
new() { Token = "regexQuantifier", Foreground = "f979ae" },
147+
new() { Token = "regexGrouping", Foreground = "05c3ba" },
148+
new() { Token = "regexAlternation", Foreground = "05c3ba" },
149+
new() { Token = "regexSelfEscapedCharacter", Foreground = "d69d85" },
150+
new() { Token = "regexOtherEscape", Foreground = "ffd68f" },
151+
new() { Token = "regexText", Foreground = "d69d85" },
152+
],
153+
});
154+
}
155+
}
156+
157+
internal static class BuiltInMonacoTheme
158+
{
159+
public const string Light = "vs";
160+
public const string Dark = "vs-dark";
161+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace DotNetLab;
2+
3+
/// <summary>
4+
/// Combination of Monaco Editor's
5+
/// <see href="https://code.visualstudio.com/api/references/vscode-api#DocumentSemanticTokensProvider">DocumentSemanticTokensProvider</see> and
6+
/// <see href="https://code.visualstudio.com/api/references/vscode-api#DocumentRangeSemanticTokensProvider">DocumentRangeSemanticTokensProvider</see>.
7+
/// </summary>
8+
internal sealed class SemanticTokensProvider
9+
{
10+
public required SemanticTokensLegend Legend { get; init; }
11+
12+
public delegate Task<string?> ProvideSemanticTokensDelegate(
13+
string modelUri,
14+
string? rangeJson,
15+
bool debug,
16+
CancellationToken cancellationToken);
17+
18+
public required ProvideSemanticTokensDelegate ProvideSemanticTokens { get; init; }
19+
20+
public Task<string?> ProvideSemanticTokensAsync(
21+
string modelUri,
22+
string? rangeJson,
23+
bool debug,
24+
CancellationToken cancellationToken)
25+
{
26+
return ProvideSemanticTokens(modelUri, rangeJson, debug, cancellationToken);
27+
}
28+
}

0 commit comments

Comments
 (0)