Skip to content

Commit 9023424

Browse files
authored
Improve language services in edge cases (#80)
2 parents dbe2949 + 479e412 commit 9023424

28 files changed

+610
-261
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynVersion)" />
2727
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeStyle" Version="$(RoslynVersion)" />
2828
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(RoslynVersion)" />
29+
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Features" Version="$(RoslynVersion)" />
2930
<PackageVersion Include="Microsoft.DotNet.Arcade.Sdk" Version="9.0.0-beta.24372.7" />
3031
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(NetCoreVersion)" />
3132
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components" Version="$(FluentUIVersion)" />

src/App/Lab/LanguageServicesClient.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ public void OnDidChangeModel(ModelChangedEvent args)
171171
}
172172

173173
currentModelUrl = args.NewModelUrl;
174-
worker.OnDidChangeModel(modelUri: currentModelUrl);
175174
UpdateDiagnostics();
176175
}
177176

@@ -183,7 +182,14 @@ public void OnDidChangeModelContent(ModelContentChangedEvent args)
183182
}
184183

185184
InvalidateCaches();
186-
worker.OnDidChangeModelContent(args);
185+
186+
if (currentModelUrl is null)
187+
{
188+
logger.LogWarning("No current document to change content of.");
189+
return;
190+
}
191+
192+
worker.OnDidChangeModelContent(modelUri: currentModelUrl, args);
187193
UpdateDiagnostics();
188194
}
189195

@@ -199,7 +205,7 @@ private void UpdateDiagnostics()
199205
Debounce(ref diagnosticsDebounce, (worker, jsRuntime, currentModelUrl), static async args =>
200206
{
201207
var (worker, jsRuntime, currentModelUrl) = args;
202-
var markers = await worker.GetDiagnosticsAsync();
208+
var markers = await worker.GetDiagnosticsAsync(currentModelUrl);
203209
var model = await BlazorMonaco.Editor.Global.GetModel(jsRuntime, currentModelUrl);
204210
await BlazorMonaco.Editor.Global.SetModelMarkers(jsRuntime, model, MonacoConstants.MarkersOwner, markers.ToList());
205211
},

src/App/Lab/Page.razor

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@
256256
private sealed record Input(string FileName, TextModel Model) : EditorState(Model)
257257
{
258258
public string FileName { get; set; } = FileName;
259-
public string? NewContent { get; set; }
259+
public required string? NewContent { get; set; }
260260
}
261261

262262
private readonly record struct CacheInfo(DateTimeOffset? Timestamp);
@@ -288,6 +288,22 @@
288288
}
289289
}
290290

291+
private IEnumerable<(Input Input, bool IsConfiguration)> InputsAndConfiguration
292+
{
293+
get
294+
{
295+
foreach (var input in inputs)
296+
{
297+
yield return (input, false);
298+
}
299+
300+
if (configuration is { } config)
301+
{
302+
yield return (config, true);
303+
}
304+
}
305+
}
306+
291307
private IEnumerable<CompiledFileOutput> AllOutputs
292308
{
293309
get => (CurrentCompiledFile?.Outputs)
@@ -545,9 +561,10 @@
545561
{
546562
await SaveStateToUrlAsync();
547563

548-
configuration = new(
549-
InitialCode.Configuration.SuggestedFileName,
550-
await CreateModelAsync(InitialCode.Configuration.ToInputCode()));
564+
var fileName = InitialCode.Configuration.SuggestedFileName;
565+
var inputCode = InitialCode.Configuration.ToInputCode();
566+
var model = await CreateModelAsync(inputCode);
567+
configuration = new(fileName, model) { NewContent = inputCode.Text };
551568
OnWorkspaceChanged();
552569

553570
await SaveStateToUrlAsync();
@@ -624,7 +641,7 @@
624641

625642
// Compile.
626643
var input = state.ToCompilationInput();
627-
var output = await Worker.CompileAsync(input);
644+
var output = await Worker.CompileAsync(input, languageServicesEnabled: LanguageServices.Enabled);
628645
compiled = (input, output, null);
629646
outputOutdated = false;
630647

@@ -1011,7 +1028,7 @@
10111028
}
10121029

10131030
/// <summary>
1014-
/// Should be called before SetModel of Monaco editor whenever (after) <see cref="inputs"/> change.
1031+
/// Should be called before SetModel of Monaco editor whenever (after) <see cref="InputsAndConfiguration"/> change.
10151032
/// </summary>
10161033
public void OnWorkspaceChanged()
10171034
{
@@ -1022,10 +1039,14 @@
10221039
return;
10231040
}
10241041

1025-
var models = inputs
1026-
.Select(i => new ModelInfo(Uri: i.Model.Uri, FileName: i.FileName) { NewContent = i.NewContent })
1042+
var models = InputsAndConfiguration
1043+
.Select(t => new ModelInfo(Uri: t.Input.Model.Uri, FileName: t.Input.FileName)
1044+
{
1045+
NewContent = t.Input.NewContent,
1046+
IsConfiguration = t.IsConfiguration,
1047+
})
10271048
.ToImmutableArray();
1028-
inputs.ForEach(i => { i.NewContent = null; });
1049+
InputsAndConfiguration.ForEach(t => { t.Input.NewContent = null; });
10291050
LanguageServices.OnDidChangeWorkspace(models);
10301051
}
10311052

@@ -1039,8 +1060,12 @@
10391060
return;
10401061
}
10411062

1042-
var models = await inputs
1043-
.SelectAsArrayAsync(static async i => new ModelInfo(Uri: i.Model.Uri, FileName: i.FileName) { NewContent = await i.Model.GetTextAsync() });
1063+
var models = await InputsAndConfiguration
1064+
.SelectAsArrayAsync(static async t => new ModelInfo(Uri: t.Input.Model.Uri, FileName: t.Input.FileName)
1065+
{
1066+
NewContent = await t.Input.Model.GetTextAsync(),
1067+
IsConfiguration = t.IsConfiguration,
1068+
});
10441069
LanguageServices.OnDidChangeWorkspace(models, updateDiagnostics: updateDiagnostics);
10451070
}
10461071

src/App/Lab/SavedState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ _ when WellKnownSlugs.ShorthandToState.TryGetValue(slug, out var wellKnownState)
9898
if (savedState.Configuration is { } savedConfiguration)
9999
{
100100
var input = InitialCode.Configuration.ToInputCode() with { Text = savedConfiguration };
101-
configuration = new(input.FileName, await CreateModelAsync(input));
101+
configuration = new(input.FileName, await CreateModelAsync(input)) { NewContent = input.Text };
102102
}
103103
else
104104
{

src/App/Lab/WorkerController.cs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,10 @@ private async Task<TIn> PostAndReceiveMessageAsync<TOut, TIn>(
394394
};
395395
}
396396

397-
public Task<CompiledAssembly> CompileAsync(CompilationInput input)
397+
public Task<CompiledAssembly> CompileAsync(CompilationInput input, bool languageServicesEnabled)
398398
{
399399
return PostAndReceiveMessageAsync(
400-
new WorkerInputMessage.Compile(input) { Id = messageId++ },
400+
new WorkerInputMessage.Compile(input, languageServicesEnabled) { Id = messageId++ },
401401
fallback: CompiledAssembly.Fail);
402402
}
403403

@@ -475,22 +475,16 @@ public void OnDidChangeWorkspace(ImmutableArray<ModelInfo> models)
475475
new WorkerInputMessage.OnDidChangeWorkspace(models) { Id = messageId++ });
476476
}
477477

478-
public void OnDidChangeModel(string modelUri)
478+
public void OnDidChangeModelContent(string modelUri, ModelContentChangedEvent args)
479479
{
480480
PostMessage(
481-
new WorkerInputMessage.OnDidChangeModel(ModelUri: modelUri) { Id = messageId++ });
481+
new WorkerInputMessage.OnDidChangeModelContent(modelUri, args) { Id = messageId++ });
482482
}
483483

484-
public void OnDidChangeModelContent(ModelContentChangedEvent args)
485-
{
486-
PostMessage(
487-
new WorkerInputMessage.OnDidChangeModelContent(args) { Id = messageId++ });
488-
}
489-
490-
public Task<ImmutableArray<MarkerData>> GetDiagnosticsAsync()
484+
public Task<ImmutableArray<MarkerData>> GetDiagnosticsAsync(string modelUri)
491485
{
492486
return PostAndReceiveMessageAsync(
493-
new WorkerInputMessage.GetDiagnostics() { Id = messageId++ },
487+
new WorkerInputMessage.GetDiagnostics(modelUri) { Id = messageId++ },
494488
deserializeAs: default(ImmutableArray<MarkerData>));
495489
}
496490
}

src/Compiler/Compiler.cs

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,48 @@
1515

1616
namespace DotNetLab;
1717

18-
public class Compiler(
18+
public sealed class Compiler(
1919
ILogger<DecompilerAssemblyResolver> decompilerAssemblyResolverLogger) : ICompiler
2020
{
2121
private const string ToolchainHelpText = """
2222
2323
You can try selecting different Razor toolchain in Settings / Advanced.
2424
""";
2525

26-
private (CompilationInput Input, CompiledAssembly Output)? lastResult;
26+
public static readonly string ConfigurationGlobalUsings = """
27+
global using DotNetLab;
28+
global using Microsoft.CodeAnalysis;
29+
global using Microsoft.CodeAnalysis.CSharp;
30+
global using System;
31+
""";
2732

2833
/// <summary>
2934
/// Reused for incremental source generation.
3035
/// </summary>
3136
private GeneratorDriver? generatorDriver;
3237

38+
internal (CompilationInput Input, LiveCompilationResult Output)? LastResult { get; private set; }
39+
3340
public CompiledAssembly Compile(
3441
CompilationInput input,
3542
ImmutableDictionary<string, ImmutableArray<byte>>? assemblies,
3643
ImmutableDictionary<string, ImmutableArray<byte>>? builtInAssemblies,
3744
AssemblyLoadContext alc)
3845
{
39-
if (lastResult is { } cached)
46+
if (LastResult is { } cached)
4047
{
4148
if (input.Equals(cached.Input))
4249
{
43-
return cached.Output;
50+
return cached.Output.CompiledAssembly;
4451
}
4552
}
4653

4754
var result = CompileNoCache(input, assemblies, builtInAssemblies, alc);
48-
lastResult = (input, result);
49-
return result;
55+
LastResult = (input, result);
56+
return result.CompiledAssembly;
5057
}
5158

52-
private CompiledAssembly CompileNoCache(
59+
private LiveCompilationResult CompileNoCache(
5360
CompilationInput compilationInput,
5461
ImmutableDictionary<string, ImmutableArray<byte>>? assemblies,
5562
ImmutableDictionary<string, ImmutableArray<byte>>? builtInAssemblies,
@@ -59,13 +66,15 @@ private CompiledAssembly CompileNoCache(
5966
const string directory = "/";
6067

6168
var parseOptions = CreateDefaultParseOptions();
69+
CSharpCompilationOptions? options = null;
6270

6371
var references = RefAssemblyMetadata.All;
6472
var referenceInfos = RefAssemblies.All;
6573

6674
// If we have a configuration, compile and execute it.
6775
Config.Reset();
6876
ImmutableArray<Diagnostic> configDiagnostics;
77+
ImmutableDictionary<string, ImmutableArray<byte>>? compilerAssembliesUsed = null;
6978
if (compilationInput.Configuration is { } configuration)
7079
{
7180
if (!executeConfiguration(configuration, out configDiagnostics))
@@ -74,7 +83,7 @@ private CompiledAssembly CompileNoCache(
7483
ImmutableArray<DiagnosticData> configDiagnosticData = configDiagnostics
7584
.Select(d => d.ToDiagnosticData())
7685
.ToImmutableArray();
77-
return new CompiledAssembly(
86+
var configResult = new CompiledAssembly(
7887
Files: ImmutableSortedDictionary<string, CompiledFile>.Empty,
7988
GlobalOutputs:
8089
[
@@ -93,6 +102,7 @@ private CompiledAssembly CompileNoCache(
93102
{
94103
ConfigDiagnosticCount = configDiagnosticData.Length,
95104
};
105+
return getResult(configResult);
96106
}
97107

98108
parseOptions = Config.ConfigureCSharpParseOptions(parseOptions);
@@ -140,7 +150,7 @@ private CompiledAssembly CompileNoCache(
140150

141151
var outputKind = GetDefaultOutputKind(cSharpSources.Select(s => s.SyntaxTree));
142152

143-
var options = CreateDefaultCompilationOptions(outputKind);
153+
options = CreateDefaultCompilationOptions(outputKind);
144154

145155
options = Config.ConfigureCSharpCompilationOptions(options);
146156

@@ -335,7 +345,18 @@ .. string.IsNullOrEmpty(razorDiagnostics)
335345
ConfigDiagnosticCount = configDiagnostics.Count(filterDiagnostic),
336346
};
337347

338-
return result;
348+
return getResult(result);
349+
350+
LiveCompilationResult getResult(CompiledAssembly result)
351+
{
352+
return new LiveCompilationResult
353+
{
354+
CompiledAssembly = result,
355+
CompilerAssemblies = compilerAssembliesUsed,
356+
CSharpParseOptions = compilerAssembliesUsed != null ? parseOptions : null,
357+
CSharpCompilationOptions = compilerAssembliesUsed != null ? options : null,
358+
};
359+
}
339360

340361
static bool filterDiagnostic(Diagnostic d) => d.Severity != DiagnosticSeverity.Hidden;
341362

@@ -346,28 +367,22 @@ bool executeConfiguration(string code, out ImmutableArray<Diagnostic> diagnostic
346367
syntaxTrees:
347368
[
348369
CSharpSyntaxTree.ParseText(code, parseOptions, "Configuration.cs", Encoding.UTF8),
349-
CSharpSyntaxTree.ParseText("""
350-
global using DotNetLab;
351-
global using Microsoft.CodeAnalysis;
352-
global using Microsoft.CodeAnalysis.CSharp;
353-
global using System;
354-
""", parseOptions, "GlobalUsings.cs", Encoding.UTF8)
370+
CSharpSyntaxTree.ParseText(ConfigurationGlobalUsings, parseOptions, "GlobalUsings.cs", Encoding.UTF8)
355371
],
356372
references:
357373
[
358374
..references,
359375
..assemblies!.Values.Select(b => MetadataReference.CreateFromImage(b)),
360376
],
361-
options: CreateDefaultCompilationOptions(OutputKind.ConsoleApplication)
362-
.WithSpecificDiagnosticOptions(
363-
[
364-
// warning CS1701: Assuming assembly reference 'System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' used by 'Microsoft.CodeAnalysis.CSharp' matches identity 'System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' of 'System.Runtime', you may need to supply runtime policy
365-
KeyValuePair.Create("CS1701", ReportDiagnostic.Suppress),
366-
]));
377+
options: CreateConfigurationCompilationOptions());
367378

368379
var emitStream = getEmitStream(configCompilation, out diagnostics);
369380

370-
if (emitStream == null)
381+
if (emitStream != null)
382+
{
383+
compilerAssembliesUsed = assemblies;
384+
}
385+
else
371386
{
372387
// If compilation fails, it might be because older Roslyn is referenced, re-try with built-in versions.
373388
var configCompilationWithBuiltInReferences = configCompilation.WithReferences(
@@ -379,6 +394,7 @@ bool executeConfiguration(string code, out ImmutableArray<Diagnostic> diagnostic
379394
if (emitStream != null)
380395
{
381396
diagnostics = diagnosticsWithBuiltInReferences;
397+
compilerAssembliesUsed = builtInAssemblies;
382398
}
383399
}
384400

@@ -791,6 +807,16 @@ public static CSharpCompilationOptions CreateDefaultCompilationOptions(OutputKin
791807
nullableContextOptions: NullableContextOptions.Enable,
792808
concurrentBuild: false);
793809
}
810+
811+
public static CSharpCompilationOptions CreateConfigurationCompilationOptions()
812+
{
813+
return CreateDefaultCompilationOptions(OutputKind.ConsoleApplication)
814+
.WithSpecificDiagnosticOptions(
815+
[
816+
// warning CS1701: Assuming assembly reference 'System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' used by 'Microsoft.CodeAnalysis.CSharp' matches identity 'System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' of 'System.Runtime', you may need to supply runtime policy
817+
KeyValuePair.Create("CS1701", ReportDiagnostic.Suppress),
818+
]);
819+
}
794820
}
795821

796822
public sealed class DecompilerAssemblyResolver(ILogger<DecompilerAssemblyResolver> logger, ImmutableArray<RefAssembly> references) : ICSharpCode.Decompiler.Metadata.IAssemblyResolver
@@ -954,3 +980,26 @@ public Result<R> Map<R>(Func<T, R> mapper)
954980
: new Result<R>(() => mapper(value!));
955981
}
956982
}
983+
984+
/// <summary>
985+
/// Additional data on top of <see cref="CompiledAssembly"/> that are never cached.
986+
/// </summary>
987+
internal sealed class LiveCompilationResult
988+
{
989+
public required CompiledAssembly CompiledAssembly { get; init; }
990+
991+
/// <summary>
992+
/// Assemblies used to compile <see cref="CompilationInput.Configuration"/>.
993+
/// </summary>
994+
public required ImmutableDictionary<string, ImmutableArray<byte>>? CompilerAssemblies { get; init; }
995+
996+
/// <summary>
997+
/// Set to <see langword="null"/> if the default options were used.
998+
/// </summary>
999+
public required CSharpParseOptions? CSharpParseOptions { get; init; }
1000+
1001+
/// <summary>
1002+
/// Set to <see langword="null"/> if the default options were used.
1003+
/// </summary>
1004+
public required CSharpCompilationOptions? CSharpCompilationOptions { get; init; }
1005+
}

0 commit comments

Comments
 (0)