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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<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.CodeAnalysis.VisualBasic.Features" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.DotNet.Arcade.Sdk" Version="9.0.0-beta.24372.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(NetCoreVersion)" />
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components" Version="$(FluentUIVersion)" />
Expand Down
12 changes: 9 additions & 3 deletions src/App/Lab/LanguageServicesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ public void OnDidChangeModel(ModelChangedEvent args)
}

currentModelUrl = args.NewModelUrl;
worker.OnDidChangeModel(modelUri: currentModelUrl);
UpdateDiagnostics();
}

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

InvalidateCaches();
worker.OnDidChangeModelContent(args);

if (currentModelUrl is null)
{
logger.LogWarning("No current document to change content of.");
return;
}

worker.OnDidChangeModelContent(modelUri: currentModelUrl, args);
UpdateDiagnostics();
}

Expand All @@ -199,7 +205,7 @@ private void UpdateDiagnostics()
Debounce(ref diagnosticsDebounce, (worker, jsRuntime, currentModelUrl), static async args =>
{
var (worker, jsRuntime, currentModelUrl) = args;
var markers = await worker.GetDiagnosticsAsync();
var markers = await worker.GetDiagnosticsAsync(currentModelUrl);
var model = await BlazorMonaco.Editor.Global.GetModel(jsRuntime, currentModelUrl);
await BlazorMonaco.Editor.Global.SetModelMarkers(jsRuntime, model, MonacoConstants.MarkersOwner, markers.ToList());
},
Expand Down
47 changes: 36 additions & 11 deletions src/App/Lab/Page.razor
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@
private sealed record Input(string FileName, TextModel Model) : EditorState(Model)
{
public string FileName { get; set; } = FileName;
public string? NewContent { get; set; }
public required string? NewContent { get; set; }
}

private readonly record struct CacheInfo(DateTimeOffset? Timestamp);
Expand Down Expand Up @@ -288,6 +288,22 @@
}
}

private IEnumerable<(Input Input, bool IsConfiguration)> InputsAndConfiguration
{
get
{
foreach (var input in inputs)
{
yield return (input, false);
}

if (configuration is { } config)
{
yield return (config, true);
}
}
}

private IEnumerable<CompiledFileOutput> AllOutputs
{
get => (CurrentCompiledFile?.Outputs)
Expand Down Expand Up @@ -545,9 +561,10 @@
{
await SaveStateToUrlAsync();

configuration = new(
InitialCode.Configuration.SuggestedFileName,
await CreateModelAsync(InitialCode.Configuration.ToInputCode()));
var fileName = InitialCode.Configuration.SuggestedFileName;
var inputCode = InitialCode.Configuration.ToInputCode();
var model = await CreateModelAsync(inputCode);
configuration = new(fileName, model) { NewContent = inputCode.Text };
OnWorkspaceChanged();

await SaveStateToUrlAsync();
Expand Down Expand Up @@ -624,7 +641,7 @@

// Compile.
var input = state.ToCompilationInput();
var output = await Worker.CompileAsync(input);
var output = await Worker.CompileAsync(input, languageServicesEnabled: LanguageServices.Enabled);
compiled = (input, output, null);
outputOutdated = false;

Expand Down Expand Up @@ -1011,7 +1028,7 @@
}

/// <summary>
/// Should be called before SetModel of Monaco editor whenever (after) <see cref="inputs"/> change.
/// Should be called before SetModel of Monaco editor whenever (after) <see cref="InputsAndConfiguration"/> change.
/// </summary>
public void OnWorkspaceChanged()
{
Expand All @@ -1022,10 +1039,14 @@
return;
}

var models = inputs
.Select(i => new ModelInfo(Uri: i.Model.Uri, FileName: i.FileName) { NewContent = i.NewContent })
var models = InputsAndConfiguration
.Select(t => new ModelInfo(Uri: t.Input.Model.Uri, FileName: t.Input.FileName)
{
NewContent = t.Input.NewContent,
IsConfiguration = t.IsConfiguration,
})
.ToImmutableArray();
inputs.ForEach(i => { i.NewContent = null; });
InputsAndConfiguration.ForEach(t => { t.Input.NewContent = null; });
LanguageServices.OnDidChangeWorkspace(models);
}

Expand All @@ -1039,8 +1060,12 @@
return;
}

var models = await inputs
.SelectAsArrayAsync(static async i => new ModelInfo(Uri: i.Model.Uri, FileName: i.FileName) { NewContent = await i.Model.GetTextAsync() });
var models = await InputsAndConfiguration
.SelectAsArrayAsync(static async t => new ModelInfo(Uri: t.Input.Model.Uri, FileName: t.Input.FileName)
{
NewContent = await t.Input.Model.GetTextAsync(),
IsConfiguration = t.IsConfiguration,
});
LanguageServices.OnDidChangeWorkspace(models, updateDiagnostics: updateDiagnostics);
}

Expand Down
2 changes: 1 addition & 1 deletion src/App/Lab/SavedState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ _ when WellKnownSlugs.ShorthandToState.TryGetValue(slug, out var wellKnownState)
if (savedState.Configuration is { } savedConfiguration)
{
var input = InitialCode.Configuration.ToInputCode() with { Text = savedConfiguration };
configuration = new(input.FileName, await CreateModelAsync(input));
configuration = new(input.FileName, await CreateModelAsync(input)) { NewContent = input.Text };
}
else
{
Expand Down
18 changes: 6 additions & 12 deletions src/App/Lab/WorkerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,10 @@ private async Task<TIn> PostAndReceiveMessageAsync<TOut, TIn>(
};
}

public Task<CompiledAssembly> CompileAsync(CompilationInput input)
public Task<CompiledAssembly> CompileAsync(CompilationInput input, bool languageServicesEnabled)
{
return PostAndReceiveMessageAsync(
new WorkerInputMessage.Compile(input) { Id = messageId++ },
new WorkerInputMessage.Compile(input, languageServicesEnabled) { Id = messageId++ },
fallback: CompiledAssembly.Fail);
}

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

public void OnDidChangeModel(string modelUri)
public void OnDidChangeModelContent(string modelUri, ModelContentChangedEvent args)
{
PostMessage(
new WorkerInputMessage.OnDidChangeModel(ModelUri: modelUri) { Id = messageId++ });
new WorkerInputMessage.OnDidChangeModelContent(modelUri, args) { Id = messageId++ });
}

public void OnDidChangeModelContent(ModelContentChangedEvent args)
{
PostMessage(
new WorkerInputMessage.OnDidChangeModelContent(args) { Id = messageId++ });
}

public Task<ImmutableArray<MarkerData>> GetDiagnosticsAsync()
public Task<ImmutableArray<MarkerData>> GetDiagnosticsAsync(string modelUri)
{
return PostAndReceiveMessageAsync(
new WorkerInputMessage.GetDiagnostics() { Id = messageId++ },
new WorkerInputMessage.GetDiagnostics(modelUri) { Id = messageId++ },
deserializeAs: default(ImmutableArray<MarkerData>));
}
}
Expand Down
95 changes: 72 additions & 23 deletions src/Compiler/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,48 @@

namespace DotNetLab;

public class Compiler(
public sealed class Compiler(
ILogger<DecompilerAssemblyResolver> decompilerAssemblyResolverLogger) : ICompiler
{
private const string ToolchainHelpText = """

You can try selecting different Razor toolchain in Settings / Advanced.
""";

private (CompilationInput Input, CompiledAssembly Output)? lastResult;
public static readonly string ConfigurationGlobalUsings = """
global using DotNetLab;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
global using System;
""";

/// <summary>
/// Reused for incremental source generation.
/// </summary>
private GeneratorDriver? generatorDriver;

internal (CompilationInput Input, LiveCompilationResult Output)? LastResult { get; private set; }

public CompiledAssembly Compile(
CompilationInput input,
ImmutableDictionary<string, ImmutableArray<byte>>? assemblies,
ImmutableDictionary<string, ImmutableArray<byte>>? builtInAssemblies,
AssemblyLoadContext alc)
{
if (lastResult is { } cached)
if (LastResult is { } cached)
{
if (input.Equals(cached.Input))
{
return cached.Output;
return cached.Output.CompiledAssembly;
}
}

var result = CompileNoCache(input, assemblies, builtInAssemblies, alc);
lastResult = (input, result);
return result;
LastResult = (input, result);
return result.CompiledAssembly;
}

private CompiledAssembly CompileNoCache(
private LiveCompilationResult CompileNoCache(
CompilationInput compilationInput,
ImmutableDictionary<string, ImmutableArray<byte>>? assemblies,
ImmutableDictionary<string, ImmutableArray<byte>>? builtInAssemblies,
Expand All @@ -59,13 +66,15 @@ private CompiledAssembly CompileNoCache(
const string directory = "/";

var parseOptions = CreateDefaultParseOptions();
CSharpCompilationOptions? options = null;

var references = RefAssemblyMetadata.All;
var referenceInfos = RefAssemblies.All;

// If we have a configuration, compile and execute it.
Config.Reset();
ImmutableArray<Diagnostic> configDiagnostics;
ImmutableDictionary<string, ImmutableArray<byte>>? compilerAssembliesUsed = null;
if (compilationInput.Configuration is { } configuration)
{
if (!executeConfiguration(configuration, out configDiagnostics))
Expand All @@ -74,7 +83,7 @@ private CompiledAssembly CompileNoCache(
ImmutableArray<DiagnosticData> configDiagnosticData = configDiagnostics
.Select(d => d.ToDiagnosticData())
.ToImmutableArray();
return new CompiledAssembly(
var configResult = new CompiledAssembly(
Files: ImmutableSortedDictionary<string, CompiledFile>.Empty,
GlobalOutputs:
[
Expand All @@ -93,6 +102,7 @@ private CompiledAssembly CompileNoCache(
{
ConfigDiagnosticCount = configDiagnosticData.Length,
};
return getResult(configResult);
}

parseOptions = Config.ConfigureCSharpParseOptions(parseOptions);
Expand Down Expand Up @@ -140,7 +150,7 @@ private CompiledAssembly CompileNoCache(

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

var options = CreateDefaultCompilationOptions(outputKind);
options = CreateDefaultCompilationOptions(outputKind);

options = Config.ConfigureCSharpCompilationOptions(options);

Expand Down Expand Up @@ -335,7 +345,18 @@ .. string.IsNullOrEmpty(razorDiagnostics)
ConfigDiagnosticCount = configDiagnostics.Count(filterDiagnostic),
};

return result;
return getResult(result);

LiveCompilationResult getResult(CompiledAssembly result)
{
return new LiveCompilationResult
{
CompiledAssembly = result,
CompilerAssemblies = compilerAssembliesUsed,
CSharpParseOptions = compilerAssembliesUsed != null ? parseOptions : null,
CSharpCompilationOptions = compilerAssembliesUsed != null ? options : null,
};
}

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

Expand All @@ -346,28 +367,22 @@ bool executeConfiguration(string code, out ImmutableArray<Diagnostic> diagnostic
syntaxTrees:
[
CSharpSyntaxTree.ParseText(code, parseOptions, "Configuration.cs", Encoding.UTF8),
CSharpSyntaxTree.ParseText("""
global using DotNetLab;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
global using System;
""", parseOptions, "GlobalUsings.cs", Encoding.UTF8)
CSharpSyntaxTree.ParseText(ConfigurationGlobalUsings, parseOptions, "GlobalUsings.cs", Encoding.UTF8)
],
references:
[
..references,
..assemblies!.Values.Select(b => MetadataReference.CreateFromImage(b)),
],
options: CreateDefaultCompilationOptions(OutputKind.ConsoleApplication)
.WithSpecificDiagnosticOptions(
[
// 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
KeyValuePair.Create("CS1701", ReportDiagnostic.Suppress),
]));
options: CreateConfigurationCompilationOptions());

var emitStream = getEmitStream(configCompilation, out diagnostics);

if (emitStream == null)
if (emitStream != null)
{
compilerAssembliesUsed = assemblies;
}
else
{
// If compilation fails, it might be because older Roslyn is referenced, re-try with built-in versions.
var configCompilationWithBuiltInReferences = configCompilation.WithReferences(
Expand All @@ -379,6 +394,7 @@ bool executeConfiguration(string code, out ImmutableArray<Diagnostic> diagnostic
if (emitStream != null)
{
diagnostics = diagnosticsWithBuiltInReferences;
compilerAssembliesUsed = builtInAssemblies;
}
}

Expand Down Expand Up @@ -791,6 +807,16 @@ public static CSharpCompilationOptions CreateDefaultCompilationOptions(OutputKin
nullableContextOptions: NullableContextOptions.Enable,
concurrentBuild: false);
}

public static CSharpCompilationOptions CreateConfigurationCompilationOptions()
{
return CreateDefaultCompilationOptions(OutputKind.ConsoleApplication)
.WithSpecificDiagnosticOptions(
[
// 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
KeyValuePair.Create("CS1701", ReportDiagnostic.Suppress),
]);
}
}

public sealed class DecompilerAssemblyResolver(ILogger<DecompilerAssemblyResolver> logger, ImmutableArray<RefAssembly> references) : ICSharpCode.Decompiler.Metadata.IAssemblyResolver
Expand Down Expand Up @@ -954,3 +980,26 @@ public Result<R> Map<R>(Func<T, R> mapper)
: new Result<R>(() => mapper(value!));
}
}

/// <summary>
/// Additional data on top of <see cref="CompiledAssembly"/> that are never cached.
/// </summary>
internal sealed class LiveCompilationResult
{
public required CompiledAssembly CompiledAssembly { get; init; }

/// <summary>
/// Assemblies used to compile <see cref="CompilationInput.Configuration"/>.
/// </summary>
public required ImmutableDictionary<string, ImmutableArray<byte>>? CompilerAssemblies { get; init; }

/// <summary>
/// Set to <see langword="null"/> if the default options were used.
/// </summary>
public required CSharpParseOptions? CSharpParseOptions { get; init; }

/// <summary>
/// Set to <see langword="null"/> if the default options were used.
/// </summary>
public required CSharpCompilationOptions? CSharpCompilationOptions { get; init; }
}
Loading