Skip to content

Commit dbe2949

Browse files
authored
Add code actions (#79)
2 parents 9e4709f + 8a2329d commit dbe2949

33 files changed

+896
-277
lines changed

.editorconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
6+
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
7+
indent_size = 2
8+
9+
[*.{cs,csx,vb,vbx}]
10+
indent_size = 4
11+
insert_final_newline = true

.globalconfig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
# CA2025: Do not pass 'IDisposable' instances into unawaited tasks
1+
# CA2016: Forward the 'CancellationToken' parameter to methods
2+
dotnet_diagnostic.CA2016.severity = warning
3+
4+
# CA2025: Do not pass 'IDisposable' instances into unawaited tasks
25
dotnet_diagnostic.CA2025.severity = suggestion

Directory.Build.props

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
<CompressionEnabled>false</CompressionEnabled>
1111
<LangVersion>preview</LangVersion>
1212

13+
<!--
14+
Needed to get diagnostics inside XML doc comments.
15+
-->
16+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
17+
<NoWarn>$(NoWarn);1573;1591;0419</NoWarn>
18+
1319
<!--
1420
Cannot trim because we dynamically execute programs
1521
which might depend on methods unreferenced at compile time.

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(AspNetCoreVersion)" />
2525
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(AspNetCoreVersion)" />
2626
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynVersion)" />
27+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeStyle" Version="$(RoslynVersion)" />
2728
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(RoslynVersion)" />
2829
<PackageVersion Include="Microsoft.DotNet.Arcade.Sdk" Version="9.0.0-beta.24372.7" />
2930
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(NetCoreVersion)" />
@@ -42,5 +43,6 @@
4243
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
4344
<!-- Pinned transitive dependencies -->
4445
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
46+
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="$(NetCoreVersion)" />
4547
</ItemGroup>
4648
</Project>

DotNetLab.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<Project Path="src/Compiler/Compiler.csproj" />
99
<Project Path="src/RazorAccess/RazorAccess.csproj" />
1010
<Project Path="src/RoslynAccess/RoslynAccess.csproj" />
11+
<Project Path="src/RoslynCodeStyleAccess/RoslynCodeStyleAccess.csproj" />
1112
<Project Path="src/RoslynWorkspaceAccess/RoslynWorkspaceAccess.csproj" />
1213
<Project Path="src/Server/Server.csproj" />
1314
<Project Path="src/Shared/Shared.csproj" />
@@ -18,6 +19,7 @@
1819
<Project Path="test/UnitTests/UnitTests.csproj" />
1920
</Folder>
2021
<Folder Name="/_/">
22+
<File Path=".editorconfig" />
2123
<File Path=".gitignore" />
2224
<File Path=".globalconfig" />
2325
<File Path="Directory.Build.props" />

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ C# and Razor compiler playground in the browser via Blazor WebAssembly. https://
1616
- Offline support (PWA).
1717
- VSCode Monaco Editor.
1818
- Multiple input sources (especially useful for interlinked Razor components).
19-
- C# Language Services (completions, live diagnostics).
19+
- C# Language Services (completions, live diagnostics, code fixes).
2020
- Configuring any C# options (e.g., LangVersion, Features, OptimizationLevel, AllowUnsafe).
2121

2222
## Development
@@ -33,6 +33,7 @@ To hit breakpoints, it is recommended to turn off the worker (in app settings).
3333
which does not depend on Roslyn/Razor from elsewhere (e.g., `Shared.csproj`).
3434
- `src/RazorAccess`: `internal` access to Razor DLLs (via fake assembly name).
3535
- `src/RoslynAccess`: `internal` access to Roslyn Compiler DLLs (via fake assembly name).
36+
- `src/RoslynCodeStyleAccess`: `internal` access to Roslyn CodeStyle DLLs (via fake assembly name).
3637
- `src/RoslynWorkspaceAccess`: `internal` access to Roslyn Workspace DLLs (via fake assembly name).
3738
- `src/Server`: a Blazor Server entrypoint for easier development of the App
3839
(it has better tooling support for hot reload and debugging).

eng/CopyDotNetDTs.targets

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<Project>
22

3-
<ItemGroup>
4-
<Content Remove="**\*.d.ts" />
5-
</ItemGroup>
3+
<ItemGroup>
4+
<Content Remove="**\*.d.ts" />
5+
</ItemGroup>
66

7-
<Target Name="CopyDotNetDTs" AfterTargets="ProcessFrameworkReferences">
8-
<ItemGroup>
9-
<_DotnetDTsPath Include="%(RuntimePack.PackageDirectory)\runtimes\browser-wasm\native\dotnet.d.ts" Condition="'%(RuntimePack.Identity)' == 'Microsoft.NETCore.App.Runtime.Mono.browser-wasm'" />
10-
</ItemGroup>
11-
<Copy SourceFiles="@(_DotnetDTsPath)" DestinationFolder="$(MSBuildProjectDirectory)\wwwroot" />
12-
</Target>
7+
<Target Name="CopyDotNetDTs" AfterTargets="ProcessFrameworkReferences">
8+
<ItemGroup>
9+
<_DotnetDTsPath Include="%(RuntimePack.PackageDirectory)\runtimes\browser-wasm\native\dotnet.d.ts" Condition="'%(RuntimePack.Identity)' == 'Microsoft.NETCore.App.Runtime.Mono.browser-wasm'" />
10+
</ItemGroup>
11+
<Copy SourceFiles="@(_DotnetDTsPath)" DestinationFolder="$(MSBuildProjectDirectory)\wwwroot" />
12+
</Target>
1313

1414
</Project>

src/App/App.csproj

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,58 @@
11
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
22

3-
<PropertyGroup>
4-
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
5-
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
6-
</PropertyGroup>
7-
8-
<ItemGroup>
9-
<PackageReference Include="Blazored.LocalStorage" />
10-
<PackageReference Include="BlazorMonaco" />
11-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
12-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" PrivateAssets="all" />
13-
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
14-
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
15-
<PackageReference Include="Microsoft.Net.Compilers.Razor.Toolset">
16-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17-
<PrivateAssets>all</PrivateAssets>
18-
</PackageReference>
19-
<PackageReference Include="protobuf-net" />
20-
<PackageReference Include="System.IO.Hashing" />
21-
</ItemGroup>
22-
23-
<ItemGroup>
24-
<ProjectReference Include="..\Worker\Worker.csproj" />
25-
</ItemGroup>
26-
27-
<ItemGroup>
28-
<Using Include="Microsoft.AspNetCore.Components" />
29-
</ItemGroup>
30-
31-
<ItemGroup>
32-
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
33-
</ItemGroup>
34-
35-
<ItemGroup>
36-
<NpmInput Include="Npm\**" Exclude="Npm\node_modules\**;Npm\package-lock.json" />
37-
<NpmOutput Include="wwwroot\js\jslib.js" />
38-
39-
<!-- Mark as Content so it's included in `service-worker-assets.js`. -->
40-
<None Remove="@(NpmOutput)" />
41-
<Content Remove="@(NpmOutput)" />
42-
<Content Include="@(NpmOutput)" />
43-
</ItemGroup>
44-
45-
<Target Name="NpmBuild" BeforeTargets="PreBuildEvent" Inputs="@(NpmInput)" Outputs="@(NpmOutput)">
46-
<Exec Command="npm install" WorkingDirectory="Npm" />
47-
<Exec Command="npm run build" WorkingDirectory="Npm" />
48-
<!--
49-
Ensure the outputs are modified (even if Webpack did not touch them
50-
because the input changes were irrelevant to build output)
51-
so this target does not re-run unnecessarily.
52-
-->
53-
<Touch Files="@(NpmOutput)" AlwaysCreate="true" />
54-
</Target>
55-
56-
<Import Project="..\..\eng\CopyDotNetDTs.targets" />
3+
<PropertyGroup>
4+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
5+
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Blazored.LocalStorage" />
10+
<PackageReference Include="BlazorMonaco" />
11+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
12+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" PrivateAssets="all" />
13+
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
14+
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
15+
<PackageReference Include="Microsoft.Net.Compilers.Razor.Toolset">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="protobuf-net" />
20+
<PackageReference Include="System.IO.Hashing" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\Worker\Worker.csproj" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<Using Include="Microsoft.AspNetCore.Components" />
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
33+
</ItemGroup>
34+
35+
<ItemGroup>
36+
<NpmInput Include="Npm\**" Exclude="Npm\node_modules\**;Npm\package-lock.json" />
37+
<NpmOutput Include="wwwroot\js\jslib.js" />
38+
39+
<!-- Mark as Content so it's included in `service-worker-assets.js`. -->
40+
<None Remove="@(NpmOutput)" />
41+
<Content Remove="@(NpmOutput)" />
42+
<Content Include="@(NpmOutput)" />
43+
</ItemGroup>
44+
45+
<Target Name="NpmBuild" BeforeTargets="PreBuildEvent" Inputs="@(NpmInput)" Outputs="@(NpmOutput)">
46+
<Exec Command="npm install" WorkingDirectory="Npm" />
47+
<Exec Command="npm run build" WorkingDirectory="Npm" />
48+
<!--
49+
Ensure the outputs are modified (even if Webpack did not touch them
50+
because the input changes were irrelevant to build output)
51+
so this target does not re-run unnecessarily.
52+
-->
53+
<Touch Files="@(NpmOutput)" AlwaysCreate="true" />
54+
</Target>
55+
56+
<Import Project="..\..\eng\CopyDotNetDTs.targets" />
5757

5858
</Project>

src/App/Lab/LanguageServicesClient.cs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ internal sealed class LanguageServicesClient(
1414
BlazorMonacoInterop blazorMonacoInterop)
1515
{
1616
private Dictionary<string, string> modelUrlToFileName = [];
17-
private IDisposable? completionProvider, semanticTokensProvider;
17+
private IDisposable? completionProvider, semanticTokensProvider, codeActionProvider;
1818
private string? currentModelUrl;
1919
private DebounceInfo completionDebounce = new(new CancellationTokenSource());
2020
private DebounceInfo diagnosticsDebounce = new(new CancellationTokenSource());
21+
private (string ModelUri, string? RangeJson, Task<string?> Result)? lastCodeActions;
2122

2223
public bool Enabled => completionProvider != null;
2324

@@ -87,7 +88,7 @@ private async Task RegisterAsync()
8788
}
8889

8990
var cSharpLanguageSelector = new LanguageSelector("csharp");
90-
completionProvider = await blazorMonacoInterop.RegisterCompletionProviderAsync(cSharpLanguageSelector, new(loggerFactory.CreateLogger<CompletionItemProviderAsync>())
91+
completionProvider = await blazorMonacoInterop.RegisterCompletionProviderAsync(cSharpLanguageSelector, new(loggerFactory)
9192
{
9293
TriggerCharacters = [" ", "(", "=", "#", ".", "<", "[", "{", "\"", "/", ":", ">", "~"],
9394
ProvideCompletionItemsFunc = (modelUri, position, context, cancellationToken) =>
@@ -96,29 +97,35 @@ private async Task RegisterAsync()
9697
ref completionDebounce,
9798
(worker, modelUri, position, context),
9899
"""{"suggestions":[],"isIncomplete":true}""",
99-
static (args, cancellationToken) => args.worker.ProvideCompletionItemsAsync(args.modelUri, args.position, args.context),
100+
static (args, cancellationToken) => args.worker.ProvideCompletionItemsAsync(args.modelUri, args.position, args.context, cancellationToken),
100101
cancellationToken);
101102
},
102-
ResolveCompletionItemFunc = (completionItem, cancellationToken) => worker.ResolveCompletionItemAsync(completionItem),
103+
ResolveCompletionItemFunc = worker.ResolveCompletionItemAsync,
103104
});
104105

105-
semanticTokensProvider = await blazorMonacoInterop.RegisterSemanticTokensProviderAsync(cSharpLanguageSelector, new SemanticTokensProvider
106+
semanticTokensProvider = await blazorMonacoInterop.RegisterSemanticTokensProviderAsync(cSharpLanguageSelector, new(loggerFactory)
106107
{
107108
Legend = new SemanticTokensLegend
108109
{
109110
TokenTypes = SemanticTokensUtil.TokenTypes.LspValues,
110111
TokenModifiers = SemanticTokensUtil.TokenModifiers.LspValues,
111112
},
112-
ProvideSemanticTokens = (modelUri, rangeJson, debug, cancellationToken) =>
113+
ProvideSemanticTokens = worker.ProvideSemanticTokensAsync,
114+
});
115+
116+
codeActionProvider = await blazorMonacoInterop.RegisterCodeActionProviderAsync(cSharpLanguageSelector, new(loggerFactory)
117+
{
118+
ProvideCodeActions = (modelUri, rangeJson, cancellationToken) =>
113119
{
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);
120+
if (lastCodeActions is { } cached &&
121+
cached.ModelUri == modelUri && cached.RangeJson == rangeJson)
122+
{
123+
return cached.Result;
124+
}
125+
126+
var result = worker.ProvideCodeActionsAsync(modelUri, rangeJson, cancellationToken);
127+
lastCodeActions = (modelUri, rangeJson, result);
128+
return result;
122129
},
123130
});
124131
}
@@ -129,6 +136,14 @@ private void Unregister()
129136
completionProvider = null;
130137
semanticTokensProvider?.Dispose();
131138
semanticTokensProvider = null;
139+
codeActionProvider?.Dispose();
140+
codeActionProvider = null;
141+
InvalidateCaches();
142+
}
143+
144+
private void InvalidateCaches()
145+
{
146+
lastCodeActions = null;
132147
}
133148

134149
public void OnDidChangeWorkspace(ImmutableArray<ModelInfo> models, bool updateDiagnostics = true)
@@ -138,6 +153,7 @@ public void OnDidChangeWorkspace(ImmutableArray<ModelInfo> models, bool updateDi
138153
return;
139154
}
140155

156+
InvalidateCaches();
141157
modelUrlToFileName = models.ToDictionary(m => m.Uri, m => m.FileName);
142158
worker.OnDidChangeWorkspace(models);
143159

@@ -166,6 +182,7 @@ public void OnDidChangeModelContent(ModelContentChangedEvent args)
166182
return;
167183
}
168184

185+
InvalidateCaches();
169186
worker.OnDidChangeModelContent(args);
170187
UpdateDiagnostics();
171188
}

0 commit comments

Comments
 (0)