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
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
indent_style = space

[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
indent_size = 2

[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
5 changes: 4 additions & 1 deletion .globalconfig
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# CA2025: Do not pass 'IDisposable' instances into unawaited tasks
# CA2016: Forward the 'CancellationToken' parameter to methods
dotnet_diagnostic.CA2016.severity = warning

# CA2025: Do not pass 'IDisposable' instances into unawaited tasks
dotnet_diagnostic.CA2025.severity = suggestion
6 changes: 6 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
<CompressionEnabled>false</CompressionEnabled>
<LangVersion>preview</LangVersion>

<!--
Needed to get diagnostics inside XML doc comments.
-->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1573;1591;0419</NoWarn>

<!--
Cannot trim because we dynamically execute programs
which might depend on methods unreferenced at compile time.
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeStyle" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.DotNet.Arcade.Sdk" Version="9.0.0-beta.24372.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(NetCoreVersion)" />
Expand All @@ -42,5 +43,6 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
<!-- Pinned transitive dependencies -->
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="$(NetCoreVersion)" />
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions DotNetLab.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<Project Path="src/Compiler/Compiler.csproj" />
<Project Path="src/RazorAccess/RazorAccess.csproj" />
<Project Path="src/RoslynAccess/RoslynAccess.csproj" />
<Project Path="src/RoslynCodeStyleAccess/RoslynCodeStyleAccess.csproj" />
<Project Path="src/RoslynWorkspaceAccess/RoslynWorkspaceAccess.csproj" />
<Project Path="src/Server/Server.csproj" />
<Project Path="src/Shared/Shared.csproj" />
Expand All @@ -18,6 +19,7 @@
<Project Path="test/UnitTests/UnitTests.csproj" />
</Folder>
<Folder Name="/_/">
<File Path=".editorconfig" />
<File Path=".gitignore" />
<File Path=".globalconfig" />
<File Path="Directory.Build.props" />
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ C# and Razor compiler playground in the browser via Blazor WebAssembly. https://
- Offline support (PWA).
- VSCode Monaco Editor.
- Multiple input sources (especially useful for interlinked Razor components).
- C# Language Services (completions, live diagnostics).
- C# Language Services (completions, live diagnostics, code fixes).
- Configuring any C# options (e.g., LangVersion, Features, OptimizationLevel, AllowUnsafe).

## Development
Expand All @@ -33,6 +33,7 @@ To hit breakpoints, it is recommended to turn off the worker (in app settings).
which does not depend on Roslyn/Razor from elsewhere (e.g., `Shared.csproj`).
- `src/RazorAccess`: `internal` access to Razor DLLs (via fake assembly name).
- `src/RoslynAccess`: `internal` access to Roslyn Compiler DLLs (via fake assembly name).
- `src/RoslynCodeStyleAccess`: `internal` access to Roslyn CodeStyle DLLs (via fake assembly name).
- `src/RoslynWorkspaceAccess`: `internal` access to Roslyn Workspace DLLs (via fake assembly name).
- `src/Server`: a Blazor Server entrypoint for easier development of the App
(it has better tooling support for hot reload and debugging).
Expand Down
18 changes: 9 additions & 9 deletions eng/CopyDotNetDTs.targets
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project>

<ItemGroup>
<Content Remove="**\*.d.ts" />
</ItemGroup>
<ItemGroup>
<Content Remove="**\*.d.ts" />
</ItemGroup>

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

</Project>
108 changes: 54 additions & 54 deletions src/App/App.csproj
Original file line number Diff line number Diff line change
@@ -1,58 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" />
<PackageReference Include="BlazorMonaco" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" PrivateAssets="all" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
<PackageReference Include="Microsoft.Net.Compilers.Razor.Toolset">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="protobuf-net" />
<PackageReference Include="System.IO.Hashing" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Worker\Worker.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.AspNetCore.Components" />
</ItemGroup>

<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>

<ItemGroup>
<NpmInput Include="Npm\**" Exclude="Npm\node_modules\**;Npm\package-lock.json" />
<NpmOutput Include="wwwroot\js\jslib.js" />

<!-- Mark as Content so it's included in `service-worker-assets.js`. -->
<None Remove="@(NpmOutput)" />
<Content Remove="@(NpmOutput)" />
<Content Include="@(NpmOutput)" />
</ItemGroup>

<Target Name="NpmBuild" BeforeTargets="PreBuildEvent" Inputs="@(NpmInput)" Outputs="@(NpmOutput)">
<Exec Command="npm install" WorkingDirectory="Npm" />
<Exec Command="npm run build" WorkingDirectory="Npm" />
<!--
Ensure the outputs are modified (even if Webpack did not touch them
because the input changes were irrelevant to build output)
so this target does not re-run unnecessarily.
-->
<Touch Files="@(NpmOutput)" AlwaysCreate="true" />
</Target>

<Import Project="..\..\eng\CopyDotNetDTs.targets" />
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" />
<PackageReference Include="BlazorMonaco" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" PrivateAssets="all" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
<PackageReference Include="Microsoft.Net.Compilers.Razor.Toolset">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="protobuf-net" />
<PackageReference Include="System.IO.Hashing" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Worker\Worker.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.AspNetCore.Components" />
</ItemGroup>

<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>

<ItemGroup>
<NpmInput Include="Npm\**" Exclude="Npm\node_modules\**;Npm\package-lock.json" />
<NpmOutput Include="wwwroot\js\jslib.js" />

<!-- Mark as Content so it's included in `service-worker-assets.js`. -->
<None Remove="@(NpmOutput)" />
<Content Remove="@(NpmOutput)" />
<Content Include="@(NpmOutput)" />
</ItemGroup>

<Target Name="NpmBuild" BeforeTargets="PreBuildEvent" Inputs="@(NpmInput)" Outputs="@(NpmOutput)">
<Exec Command="npm install" WorkingDirectory="Npm" />
<Exec Command="npm run build" WorkingDirectory="Npm" />
<!--
Ensure the outputs are modified (even if Webpack did not touch them
because the input changes were irrelevant to build output)
so this target does not re-run unnecessarily.
-->
<Touch Files="@(NpmOutput)" AlwaysCreate="true" />
</Target>

<Import Project="..\..\eng\CopyDotNetDTs.targets" />

</Project>
45 changes: 31 additions & 14 deletions src/App/Lab/LanguageServicesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ internal sealed class LanguageServicesClient(
BlazorMonacoInterop blazorMonacoInterop)
{
private Dictionary<string, string> modelUrlToFileName = [];
private IDisposable? completionProvider, semanticTokensProvider;
private IDisposable? completionProvider, semanticTokensProvider, codeActionProvider;
private string? currentModelUrl;
private DebounceInfo completionDebounce = new(new CancellationTokenSource());
private DebounceInfo diagnosticsDebounce = new(new CancellationTokenSource());
private (string ModelUri, string? RangeJson, Task<string?> Result)? lastCodeActions;

public bool Enabled => completionProvider != null;

Expand Down Expand Up @@ -87,7 +88,7 @@ private async Task RegisterAsync()
}

var cSharpLanguageSelector = new LanguageSelector("csharp");
completionProvider = await blazorMonacoInterop.RegisterCompletionProviderAsync(cSharpLanguageSelector, new(loggerFactory.CreateLogger<CompletionItemProviderAsync>())
completionProvider = await blazorMonacoInterop.RegisterCompletionProviderAsync(cSharpLanguageSelector, new(loggerFactory)
{
TriggerCharacters = [" ", "(", "=", "#", ".", "<", "[", "{", "\"", "/", ":", ">", "~"],
ProvideCompletionItemsFunc = (modelUri, position, context, cancellationToken) =>
Expand All @@ -96,29 +97,35 @@ private async Task RegisterAsync()
ref completionDebounce,
(worker, modelUri, position, context),
"""{"suggestions":[],"isIncomplete":true}""",
static (args, cancellationToken) => args.worker.ProvideCompletionItemsAsync(args.modelUri, args.position, args.context),
static (args, cancellationToken) => args.worker.ProvideCompletionItemsAsync(args.modelUri, args.position, args.context, cancellationToken),
cancellationToken);
},
ResolveCompletionItemFunc = (completionItem, cancellationToken) => worker.ResolveCompletionItemAsync(completionItem),
ResolveCompletionItemFunc = worker.ResolveCompletionItemAsync,
});

semanticTokensProvider = await blazorMonacoInterop.RegisterSemanticTokensProviderAsync(cSharpLanguageSelector, new SemanticTokensProvider
semanticTokensProvider = await blazorMonacoInterop.RegisterSemanticTokensProviderAsync(cSharpLanguageSelector, new(loggerFactory)
{
Legend = new SemanticTokensLegend
{
TokenTypes = SemanticTokensUtil.TokenTypes.LspValues,
TokenModifiers = SemanticTokensUtil.TokenModifiers.LspValues,
},
ProvideSemanticTokens = (modelUri, rangeJson, debug, cancellationToken) =>
ProvideSemanticTokens = worker.ProvideSemanticTokensAsync,
});

codeActionProvider = await blazorMonacoInterop.RegisterCodeActionProviderAsync(cSharpLanguageSelector, new(loggerFactory)
{
ProvideCodeActions = (modelUri, rangeJson, cancellationToken) =>
{
return DebounceAsync(
ref diagnosticsDebounce,
(worker, modelUri, debug, rangeJson),
// Fallback value when cancelled is `null` which causes an exception to be thrown
// instead of returning empty tokens which would cause the semantic colorization to disappear.
null,
static (args, cancellationToken) => args.worker.ProvideSemanticTokensAsync(args.modelUri, args.rangeJson, args.debug),
cancellationToken);
if (lastCodeActions is { } cached &&
cached.ModelUri == modelUri && cached.RangeJson == rangeJson)
{
return cached.Result;
}

var result = worker.ProvideCodeActionsAsync(modelUri, rangeJson, cancellationToken);
lastCodeActions = (modelUri, rangeJson, result);
return result;
},
});
}
Expand All @@ -129,6 +136,14 @@ private void Unregister()
completionProvider = null;
semanticTokensProvider?.Dispose();
semanticTokensProvider = null;
codeActionProvider?.Dispose();
codeActionProvider = null;
InvalidateCaches();
}

private void InvalidateCaches()
{
lastCodeActions = null;
}

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

InvalidateCaches();
modelUrlToFileName = models.ToDictionary(m => m.Uri, m => m.FileName);
worker.OnDidChangeWorkspace(models);

Expand Down Expand Up @@ -166,6 +182,7 @@ public void OnDidChangeModelContent(ModelContentChangedEvent args)
return;
}

InvalidateCaches();
worker.OnDidChangeModelContent(args);
UpdateDiagnostics();
}
Expand Down
Loading