diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs
new file mode 100644
index 0000000000000..b439d7672ad3d
--- /dev/null
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs
@@ -0,0 +1,189 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// Uncomment this to test run-api locally.
+// Eventually when a new enough SDK is adopted in-repo we can remove this
+//#define RoslynTestRunApi
+
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
+using Microsoft.Extensions.Logging;
+using Roslyn.LanguageServer.Protocol;
+using Roslyn.Test.Utilities;
+using Xunit.Abstractions;
+
+namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
+
+///
+/// Goal of these tests:
+/// - Ensure that the various request/response forms work as expected in basic scenarios.
+/// - Ensure that various properties on the response are populated in a reasonable way.
+/// Non-goals:
+/// - Thorough behavioral testing.
+/// - Testing of more intricate behaviors which are subject to change.
+///
+public sealed class VirtualProjectXmlProviderTests : AbstractLanguageServerHostTests
+{
+ public VirtualProjectXmlProviderTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
+ {
+ }
+
+ private class EnableRunApiTests : ExecutionCondition
+ {
+ public override bool ShouldSkip =>
+#if RoslynTestRunApi
+ false;
+#else
+ true;
+#endif
+
+ public override string SkipReason => $"Compilation symbol 'RoslynTestRunApi' is not defined.";
+ }
+
+ private async Task GetProjectXmlProviderAsync()
+ {
+ var (exportProvider, _) = await LanguageServerTestComposition.CreateExportProviderAsync(
+ LoggerFactory, includeDevKitComponents: false, MefCacheDirectory.Path, extensionPaths: null);
+ return exportProvider.GetExportedValue();
+ }
+
+ [Fact]
+ public async Task GetProjectXml_FileBasedProgram_SdkTooOld_01()
+ {
+ var projectProvider = await GetProjectXmlProviderAsync();
+
+ var tempDir = TempRoot.CreateDirectory();
+ var appFile = tempDir.CreateFile("app.cs");
+ await appFile.WriteAllTextAsync("""
+ Console.WriteLine("Hello, world!");
+ """);
+
+ var globalJsonFile = tempDir.CreateFile("global.json");
+ globalJsonFile.WriteAllBytes(Encoding.UTF8.GetBytes("""
+ {
+ "sdk": {
+ "version": "9.0.105"
+ }
+ }
+ """));
+
+ var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
+ Assert.Null(contentNullable);
+ }
+
+ [ConditionalFact(typeof(EnableRunApiTests))]
+ public async Task GetProjectXml_FileBasedProgram_01()
+ {
+ var projectProvider = await GetProjectXmlProviderAsync();
+
+ var tempDir = TempRoot.CreateDirectory();
+ var appFile = tempDir.CreateFile("app.cs");
+ await appFile.WriteAllTextAsync("""
+ Console.WriteLine("Hello, world!");
+ """);
+
+ var globalJsonFile = tempDir.CreateFile("global.json");
+ await globalJsonFile.WriteAllTextAsync("""
+ {
+ "sdk": {
+ "version": "10.0.100-preview.5.25265.12"
+ }
+ }
+ """);
+
+ var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
+ var content = contentNullable.Value;
+ var virtualProjectXml = content.VirtualProjectXml;
+ LoggerFactory.CreateLogger().LogTrace(virtualProjectXml);
+
+ Assert.Contains("net10.0", virtualProjectXml);
+ Assert.Contains("", virtualProjectXml);
+ Assert.Empty(content.Diagnostics);
+ }
+
+ [ConditionalFact(typeof(EnableRunApiTests))]
+ public async Task GetProjectXml_NonFileBasedProgram_01()
+ {
+ var projectProvider = await GetProjectXmlProviderAsync();
+
+ var tempDir = TempRoot.CreateDirectory();
+ var appFile = tempDir.CreateFile("app.cs");
+ await appFile.WriteAllTextAsync("""
+ public class C
+ {
+ }
+ """);
+
+ var globalJsonFile = tempDir.CreateFile("global.json");
+ await globalJsonFile.WriteAllTextAsync("""
+ {
+ "sdk": {
+ "version": "10.0.100-preview.5.25265.12"
+ }
+ }
+ """);
+
+ var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
+ var content = contentNullable.Value;
+ LoggerFactory.CreateLogger().LogTrace(content.VirtualProjectXml);
+
+ Assert.Contains("net10.0", content.VirtualProjectXml);
+ Assert.Contains("", content.VirtualProjectXml);
+ Assert.Empty(content.Diagnostics);
+ }
+
+ [ConditionalFact(typeof(EnableRunApiTests))]
+ public async Task GetProjectXml_BadPath_01()
+ {
+ var projectProvider = await GetProjectXmlProviderAsync();
+
+ var tempDir = TempRoot.CreateDirectory();
+
+ var globalJsonFile = tempDir.CreateFile("global.json");
+ await globalJsonFile.WriteAllTextAsync("""
+ {
+ "sdk": {
+ "version": "10.0.100-preview.5.25265.12"
+ }
+ }
+ """);
+
+ var content = await projectProvider.GetVirtualProjectContentAsync(Path.Combine(tempDir.Path, "BAD"), CancellationToken.None);
+ Assert.Null(content);
+ }
+
+ [ConditionalFact(typeof(EnableRunApiTests))]
+ public async Task GetProjectXml_BadDirective_01()
+ {
+ var projectProvider = await GetProjectXmlProviderAsync();
+
+ var tempDir = TempRoot.CreateDirectory();
+ var appFile = tempDir.CreateFile("app.cs");
+ await appFile.WriteAllTextAsync("""
+ #:package Newtonsoft.Json@13.0.3
+ #:BAD
+ Console.WriteLine("Hello, world!");
+ """);
+
+ var globalJsonFile = tempDir.CreateFile("global.json");
+ await globalJsonFile.WriteAllTextAsync("""
+ {
+ "sdk": {
+ "version": "10.0.100-preview.5.25265.12"
+ }
+ }
+ """);
+
+ var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
+ var content = contentNullable.Value;
+ var diagnostic = content.Diagnostics.Single();
+ Assert.Contains("Unrecognized directive 'BAD'", diagnostic.Message);
+ Assert.Equal(appFile.Path, diagnostic.Location.Path);
+
+ // LinePositionSpan is not deserializing properly.
+ // Address when implementing editor squiggles. https://github.com/dotnet/roslyn/issues/78688
+ Assert.Equal("(0,0)-(0,0)", diagnostic.Location.Span.ToString());
+ }
+}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs
index c2d0c09b8c067..3b9d6766e950b 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs
@@ -68,7 +68,7 @@ private async Task GetDotnetSdkFolderFromDotnetExecutableAsync(string pr
return dotnetSdkFolderPath;
}
- public Process Run(string[] arguments, string? workingDirectory, bool shouldLocalizeOutput)
+ public Process Run(string[] arguments, string? workingDirectory, bool shouldLocalizeOutput, bool redirectStandardInput = false)
{
_logger.LogDebug($"Running dotnet CLI command at {_dotnetExecutablePath.Value} in directory {workingDirectory} with arguments {arguments}");
@@ -76,6 +76,7 @@ public Process Run(string[] arguments, string? workingDirectory, bool shouldLoca
{
CreateNoWindow = true,
UseShellExecute = false,
+ RedirectStandardInput = redirectStandardInput,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
similarity index 83%
rename from src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsProjectSystem.cs
rename to src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
index f60b31557eeef..981095c2d0817 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
@@ -4,8 +4,10 @@
using System.Collections.Immutable;
using System.Security;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Features.Workspaces;
using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.MSBuild;
@@ -22,7 +24,7 @@
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager;
-namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
+namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
/// Handles loading both miscellaneous files and file-based program projects.
internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoader, ILspMiscellaneousFilesWorkspaceProvider
@@ -30,10 +32,12 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad
private readonly ILspServices _lspServices;
private readonly ILogger _logger;
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
+ private readonly VirtualProjectXmlProvider _projectXmlProvider;
public FileBasedProgramsProjectSystem(
ILspServices lspServices,
IMetadataAsSourceFileService metadataAsSourceFileService,
+ VirtualProjectXmlProvider projectXmlProvider,
LanguageServerWorkspaceFactory workspaceFactory,
IFileChangeWatcher fileChangeWatcher,
IGlobalOptionService globalOptionService,
@@ -57,6 +61,7 @@ public FileBasedProgramsProjectSystem(
_lspServices = lspServices;
_logger = loggerFactory.CreateLogger();
_metadataAsSourceFileService = metadataAsSourceFileService;
+ _projectXmlProvider = projectXmlProvider;
}
public Workspace Workspace => ProjectFactory.Workspace;
@@ -124,20 +129,33 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool
await UnloadProjectAsync(documentPath);
}
- protected override async Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync(
+ protected override async Task TryLoadProjectInMSBuildHostAsync(
BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken)
{
- const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
- var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
+ var content = await _projectXmlProvider.GetVirtualProjectContentAsync(documentPath, cancellationToken);
+ if (content is not var (virtualProjectContent, diagnostics))
+ {
+ // 'GetVirtualProjectContentAsync' will log errors when it fails
+ return null;
+ }
- var loader = ProjectFactory.CreateFileTextLoader(documentPath);
- var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
- var (virtualProjectContent, isFileBasedProgram) = VirtualCSharpFileBasedProgramProject.MakeVirtualProjectContent(documentPath, textAndVersion.Text);
+ foreach (var diagnostic in diagnostics)
+ {
+ // https://github.com/dotnet/roslyn/issues/78688: Surface diagnostics in editor
+ _logger.LogError($"{diagnostic.Location.Path}{diagnostic.Location.Span.Start}: {diagnostic.Message}");
+ }
// When loading a virtual project, the path to the on-disk source file is not used. Instead the path is adjusted to end with .csproj.
// This is necessary in order to get msbuild to apply the standard c# props/targets to the project.
- var virtualProjectPath = VirtualCSharpFileBasedProgramProject.GetVirtualProjectPath(documentPath);
+ var virtualProjectPath = VirtualProjectXmlProvider.GetVirtualProjectPath(documentPath);
+
+ var loader = ProjectFactory.CreateFileTextLoader(documentPath);
+ var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
+ var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text);
+
+ const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
+ var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken);
- return (loadedFile, hasAllInformation: isFileBasedProgram, preferred: buildHostKind, actual: buildHostKind);
+ return new RemoteProjectLoadResult(loadedFile, HasAllInformation: isFileBasedProgram, Preferred: buildHostKind, Actual: buildHostKind);
}
}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsWorkspaceProviderFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs
similarity index 77%
rename from src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsWorkspaceProviderFactory.cs
rename to src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs
index de90d3175e244..3754d38086a01 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsWorkspaceProviderFactory.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs
@@ -6,6 +6,7 @@
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.MSBuild;
@@ -15,7 +16,7 @@
using Microsoft.CommonLanguageServerProtocol.Framework;
using Microsoft.Extensions.Logging;
-namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
+namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
///
/// Service to create instances.
@@ -27,6 +28,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class FileBasedProgramsWorkspaceProviderFactory(
IMetadataAsSourceFileService metadataAsSourceFileService,
+ VirtualProjectXmlProvider projectXmlProvider,
LanguageServerWorkspaceFactory workspaceFactory,
IFileChangeWatcher fileChangeWatcher,
IGlobalOptionService globalOptionService,
@@ -38,6 +40,17 @@ internal sealed class FileBasedProgramsWorkspaceProviderFactory(
{
public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices)
{
- return new FileBasedProgramsProjectSystem(lspServices, metadataAsSourceFileService, workspaceFactory, fileChangeWatcher, globalOptionService, loggerFactory, listenerProvider, projectLoadTelemetry, serverConfigurationFactory, binLogPathProvider);
+ return new FileBasedProgramsProjectSystem(
+ lspServices,
+ metadataAsSourceFileService,
+ projectXmlProvider,
+ workspaceFactory,
+ fileChangeWatcher,
+ globalOptionService,
+ loggerFactory,
+ listenerProvider,
+ projectLoadTelemetry,
+ serverConfigurationFactory,
+ binLogPathProvider);
}
}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/RunApiModels.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/RunApiModels.cs
new file mode 100644
index 0000000000000..91dfc531e14e2
--- /dev/null
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/RunApiModels.cs
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms
+{
+ [JsonDerivedType(typeof(GetProject), nameof(GetProject))]
+ internal abstract class RunApiInput
+ {
+ private RunApiInput() { }
+
+ public sealed class GetProject : RunApiInput
+ {
+ public string? ArtifactsPath { get; init; }
+ public required string EntryPointFileFullPath { get; init; }
+ }
+ }
+
+ [JsonDerivedType(typeof(Error), nameof(Error))]
+ [JsonDerivedType(typeof(Project), nameof(Project))]
+ internal abstract class RunApiOutput
+ {
+ private RunApiOutput() { }
+
+ public const int LatestKnownVersion = 1;
+
+ [JsonPropertyOrder(-1)]
+ public int Version { get; }
+
+ public sealed class Error : RunApiOutput
+ {
+ public required string Message { get; init; }
+ public required string Details { get; init; }
+ }
+
+ public sealed class Project : RunApiOutput
+ {
+ public required string Content { get; init; }
+ public required ImmutableArray Diagnostics { get; init; }
+ }
+ }
+ internal sealed class SimpleDiagnostic
+ {
+ public required Position Location { get; init; }
+ public required string Message { get; init; }
+
+ ///
+ /// An adapter of that ensures we JSON-serialize only the necessary fields.
+ ///
+ public readonly struct Position
+ {
+ public string Path { get; init; }
+ public LinePositionSpan Span { get; init; }
+
+ public static implicit operator Position(FileLinePositionSpan fileLinePositionSpan) => new()
+ {
+ Path = fileLinePositionSpan.Path,
+ Span = fileLinePositionSpan.Span,
+ };
+ }
+ }
+
+ [JsonSerializable(typeof(RunApiInput))]
+ [JsonSerializable(typeof(RunApiOutput))]
+ internal partial class RunFileApiJsonSerializerContext : JsonSerializerContext;
+}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs
new file mode 100644
index 0000000000000..96e9a08c32fb0
--- /dev/null
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs
@@ -0,0 +1,109 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.Extensions.Logging;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
+
+[Export(typeof(VirtualProjectXmlProvider)), Shared]
+[method: ImportingConstructor]
+[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+internal class VirtualProjectXmlProvider(DotnetCliHelper dotnetCliHelper, ILoggerFactory loggerFactory)
+{
+ private readonly ILogger _logger = loggerFactory.CreateLogger();
+
+ internal async Task<(string VirtualProjectXml, ImmutableArray Diagnostics)?> GetVirtualProjectContentAsync(string documentFilePath, CancellationToken cancellationToken)
+ {
+ var workingDirectory = Path.GetDirectoryName(documentFilePath);
+ var process = dotnetCliHelper.Run(["run-api"], workingDirectory, shouldLocalizeOutput: true, redirectStandardInput: true);
+
+ cancellationToken.Register(() =>
+ {
+ process?.Kill();
+ });
+
+ var input = new RunApiInput.GetProject() { EntryPointFileFullPath = documentFilePath };
+ var inputJson = JsonSerializer.Serialize(input, RunFileApiJsonSerializerContext.Default.RunApiInput);
+ await process.StandardInput.WriteAsync(inputJson);
+ process.StandardInput.Close();
+
+ // Debug severity is used for these because we think it will be common for the user environment to have too old of an SDK for the call to work.
+ // Rather than representing a hard error condition, it represents a condition where we need to gracefully downgrade the experience.
+ process.ErrorDataReceived += (sender, args) => _logger.LogDebug($"dotnet run-api: {args.Data}");
+ process.BeginErrorReadLine();
+
+ var responseJson = await process.StandardOutput.ReadLineAsync(cancellationToken);
+ await process.WaitForExitAsync(cancellationToken);
+
+ if (process.ExitCode != 0)
+ {
+ _logger.LogDebug($"dotnet run-api exited with exit code '{process.ExitCode}'.");
+ return null;
+ }
+
+ if (string.IsNullOrWhiteSpace(responseJson))
+ {
+ _logger.LogError($"dotnet run-api exited with exit code 0, but did not return any response.");
+ return null;
+ }
+
+ try
+ {
+ var response = JsonSerializer.Deserialize(responseJson, RunFileApiJsonSerializerContext.Default.RunApiOutput);
+ if (response is RunApiOutput.Error error)
+ {
+ _logger.LogError($"dotnet run-api version: {error.Version}. Latest known version: {RunApiOutput.LatestKnownVersion}");
+ _logger.LogError($"dotnet run-api returned error: '{error.Message}'");
+ return null;
+ }
+
+ if (response is RunApiOutput.Project project)
+ {
+ if (project.Version > RunApiOutput.LatestKnownVersion)
+ {
+ _logger.LogWarning($"'dotnet run-api' version '{project.Version}' is newer than latest known version {RunApiOutput.LatestKnownVersion}");
+ }
+
+ return (project.Content, project.Diagnostics);
+ }
+
+ throw ExceptionUtilities.UnexpectedValue(response);
+ }
+ catch (JsonException ex)
+ {
+ // In this case, run-api returned 0 exit code, but gave us back JSON that we don't know how to parse.
+ _logger.LogError(ex, "Could not deserialize run-api response.");
+ return null;
+ }
+ }
+
+ ///
+ /// Adjusts a path to a file-based program for use in passing the virtual project to msbuild.
+ /// (msbuild needs the path to end in .csproj to recognize as a C# project and apply all the standard props/targets to it.)
+ ///
+ internal static string GetVirtualProjectPath(string documentFilePath)
+ => Path.ChangeExtension(documentFilePath, ".csproj");
+
+ internal static bool IsFileBasedProgram(string documentFilePath, SourceText text)
+ {
+ // TODO: this needs to be adjusted to be more sustainable.
+ // When we adopt the dotnet run-api, we need to get rid of this or adjust it to be more sustainable (e.g. using the appropriate document to get a syntax tree)
+ var tree = CSharpSyntaxTree.ParseText(text, options: CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview), path: documentFilePath);
+ var root = tree.GetRoot();
+ var isFileBasedProgram = root.GetLeadingTrivia().Any(SyntaxKind.IgnoredDirectiveTrivia) || root.ChildNodes().Any(node => node.IsKind(SyntaxKind.GlobalStatement));
+ return isFileBasedProgram;
+ }
+}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs
index b16e6c6ccf0db..3b4f7d4ce7044 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs
@@ -180,9 +180,11 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedListLoads a project in the MSBuild host.
/// Caller needs to catch exceptions to avoid bringing down the project loader queue.
- protected abstract Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync(
+ protected abstract Task TryLoadProjectInMSBuildHostAsync(
BuildHostProcessManager buildHostProcessManager, string projectPath, CancellationToken cancellationToken);
/// True if the project needs a NuGet restore, false otherwise.
@@ -203,13 +205,17 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
try
{
- if (await TryLoadProjectInMSBuildHostAsync(buildHostProcessManager, projectPath, cancellationToken)
- is not var (remoteProjectFile, hasAllInformation, preferredBuildHostKind, actualBuildHostKind))
+ var remoteProjectLoadResult = await TryLoadProjectInMSBuildHostAsync(buildHostProcessManager, projectPath, cancellationToken);
+ if (remoteProjectLoadResult is null)
{
+ // Note that this is a fairly common condition, e.g. for VB projects.
+ // In the file-based programs primordial case, no 'LoadedProject' is produced for the project,
+ // and therefore no reloading is performed for it after failing to load it once (in this code path).
_logger.LogWarning($"Unable to load project '{projectPath}'.");
return false;
}
+ (RemoteProjectFile remoteProjectFile, bool hasAllInformation, BuildHostProcessKind preferredBuildHostKind, BuildHostProcessKind actualBuildHostKind) = remoteProjectLoadResult;
if (preferredBuildHostKind != actualBuildHostKind)
preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind;
@@ -249,15 +255,10 @@ is not var (remoteProjectFile, hasAllInformation, preferredBuildHostKind, actual
var (target, targetAlreadyExists) = await GetOrCreateProjectTargetAsync(previousProjectTargets, loadedProjectInfo);
newProjectTargetsBuilder.Add(target);
- if (targetAlreadyExists)
- {
- // https://github.com/dotnet/roslyn/issues/78561: Automatic restore should run even when the target is already loaded
- _ = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger);
- }
- else
+ var (targetTelemetryInfo, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger);
+ needsRestore |= targetNeedsRestore;
+ if (!targetAlreadyExists)
{
- var (targetTelemetryInfo, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger);
- needsRestore |= targetNeedsRestore;
telemetryInfos[loadedProjectInfo] = targetTelemetryInfo with { IsSdkStyle = preferredBuildHostKind == BuildHostProcessKind.NetCore };
}
}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
index 64d3082ef258f..47cc6a8462d3a 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
@@ -91,7 +91,7 @@ public async Task OpenProjectsAsync(ImmutableArray projectFilePaths)
await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync();
}
- protected override async Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync(
+ protected override async Task TryLoadProjectInMSBuildHostAsync(
BuildHostProcessManager buildHostProcessManager, string projectPath, CancellationToken cancellationToken)
{
if (!_projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Ignore, out var languageName))
@@ -101,6 +101,6 @@ public async Task OpenProjectsAsync(ImmutableArray projectFilePaths)
var (buildHost, actualBuildHostKind) = await buildHostProcessManager.GetBuildHostWithFallbackAsync(preferredBuildHostKind, projectPath, cancellationToken);
var loadedFile = await buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken);
- return (loadedFile, hasAllInformation: true, preferredBuildHostKind, actualBuildHostKind);
+ return new RemoteProjectLoadResult(loadedFile, HasAllInformation: true, preferredBuildHostKind, actualBuildHostKind);
}
}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VirtualProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VirtualProject.cs
deleted file mode 100644
index 8bada428d0f98..0000000000000
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VirtualProject.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Runtime.InteropServices;
-using System.Security;
-using System.Security.Cryptography;
-using System.Text;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Text;
-using Roslyn.Utilities;
-
-namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
-
-///
-/// This will be replaced invoke dotnet run-api command implemented in https://github.com/dotnet/sdk/pull/48749
-///
-internal static class VirtualCSharpFileBasedProgramProject
-{
- ///
- /// Adjusts a path to a file-based program for use in passing the virtual project to msbuild.
- /// (msbuild needs the path to end in .csproj to recognize as a C# project and apply all the standard props/targets to it.)
- ///
- internal static string GetVirtualProjectPath(string documentFilePath)
- => Path.ChangeExtension(documentFilePath, ".csproj");
-
- #region TODO: Copy-pasted from dotnet run-api. Delete when run-api is adopted.
- // See https://github.com/dotnet/sdk/blob/b5dbc69cc28676ac6ea615654c8016a11b75e747/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs#L10
- private static class Sha256Hasher
- {
- public static string Hash(string text)
- {
- byte[] bytes = Encoding.UTF8.GetBytes(text);
- byte[] hash = SHA256.HashData(bytes);
-#if NET9_0_OR_GREATER
- return Convert.ToHexStringLower(hash);
-#else
- return Convert.ToHexString(hash).ToLowerInvariant();
-#endif
- }
-
- public static string HashWithNormalizedCasing(string text)
- {
- return Hash(text.ToUpperInvariant());
- }
- }
-
- // TODO: this is a copy of SDK run-api code. Must delete when adopting run-api.
- // See https://github.com/dotnet/sdk/blob/5a4292947487a9d34f4256c1d17fb3dc26859174/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs#L449
- internal static string GetArtifactsPath(string entryPointFileFullPath)
- {
- // We want a location where permissions are expected to be restricted to the current user.
- string directory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- ? Path.GetTempPath()
- : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
-
- // Include entry point file name so the directory name is not completely opaque.
- string fileName = Path.GetFileNameWithoutExtension(entryPointFileFullPath);
- string hash = Sha256Hasher.HashWithNormalizedCasing(entryPointFileFullPath);
- string directoryName = $"{fileName}-{hash}";
-
- return Path.Join(directory, "dotnet", "runfile", directoryName);
- }
- #endregion
-
- internal static (string virtualProjectXml, bool isFileBasedProgram) MakeVirtualProjectContent(string documentFilePath, SourceText text)
- {
- Contract.ThrowIfFalse(PathUtilities.IsAbsolute(documentFilePath));
- // NB: this is a temporary solution for running our heuristic.
- // When we adopt the dotnet run-api, we need to get rid of this or adjust it to be more sustainable (e.g. using the appropriate document to get a syntax tree)
- var tree = CSharpSyntaxTree.ParseText(text, options: CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview), path: documentFilePath);
- var root = tree.GetRoot();
- var isFileBasedProgram = root.GetLeadingTrivia().Any(SyntaxKind.IgnoredDirectiveTrivia) || root.ChildNodes().Any(node => node.IsKind(SyntaxKind.GlobalStatement));
-
- var artifactsPath = GetArtifactsPath(documentFilePath);
-
- var targetFramework = Environment.GetEnvironmentVariable("DOTNET_RUN_FILE_TFM") ?? "net10.0";
-
- var virtualProjectXml = $"""
-
-
-
- false
- {SecurityElement.Escape(artifactsPath)}
-
-
-
-
-
-
- Exe
- {SecurityElement.Escape(targetFramework)}
- enable
- enable
-
-
-
- false
-
-
-
- preview
-
-
-
- $(Features);FileBasedProgram
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" />
-
-
-
-
-
-
-
-
- """;
-
- return (virtualProjectXml, isFileBasedProgram);
- }
-}