Skip to content

Commit

Permalink
Merge pull request #70588 from dibarbet/nuget_restore
Browse files Browse the repository at this point in the history
Add restore support to the language server
  • Loading branch information
dibarbet authored Oct 30, 2023
2 parents 3226945 + 7b7ba12 commit 79d1884
Show file tree
Hide file tree
Showing 18 changed files with 668 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.Composition;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer;

/// <summary>
/// Handler that allows the client to retrieve a set of restorable projects.
/// Used to populate a list of projects that can be restored.
/// </summary>
[ExportCSharpVisualBasicStatelessLspService(typeof(RestorableProjectsHandler)), Shared]
[Method(MethodName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class RestorableProjectsHandler(ProjectTargetFrameworkManager projectTargetFrameworkManager) : ILspServiceRequestHandler<string[]>
{
internal const string MethodName = "workspace/_roslyn_restorableProjects";

public bool MutatesSolutionState => false;

public bool RequiresLSPSolution => true;

public Task<string[]> HandleRequestAsync(RequestContext context, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(context.Solution);

using var _ = ArrayBuilder<string>.GetInstance(out var projectsBuilder);
foreach (var project in context.Solution.Projects)
{
// To restore via the dotnet CLI, we must have a file path and it must be a .NET core project.
if (project.FilePath != null && projectTargetFrameworkManager.IsDotnetCoreProject(project.Id))
{
projectsBuilder.Add(project.FilePath);
}
}

// We may have multiple projects with the same file path in multi-targeting scenarios.
// They'll all get restored together so we only want one result per project file.
projectsBuilder.RemoveDuplicates();

// Ensure the client gets a consistent ordering.
projectsBuilder.Sort(StringComparer.OrdinalIgnoreCase);

return Task.FromResult(projectsBuilder.ToArray());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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 Microsoft.CodeAnalysis.Host.Mef;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler;

/// <summary>
/// Given an input project (or none), runs restore on the project and streams the output
/// back to the client to display.
/// </summary>
[ExportCSharpVisualBasicStatelessLspService(typeof(RestoreHandler)), Shared]
[Method(MethodName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class RestoreHandler(DotnetCliHelper dotnetCliHelper) : ILspServiceRequestHandler<RestoreParams, RestorePartialResult[]>
{
internal const string MethodName = "workspace/_roslyn_restore";

public bool MutatesSolutionState => false;

public bool RequiresLSPSolution => true;

public async Task<RestorePartialResult[]> HandleRequestAsync(RestoreParams request, RequestContext context, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(context.Solution);
using var progress = BufferedProgress.Create(request.PartialResultToken);

progress.Report(new RestorePartialResult(LanguageServerResources.Restore, LanguageServerResources.Restore_started));

var restorePaths = GetRestorePaths(request, context.Solution, context);
if (restorePaths.IsEmpty)
{
progress.Report(new RestorePartialResult(LanguageServerResources.Restore, LanguageServerResources.Nothing_found_to_restore));
return progress.GetValues() ?? [];
}

await RestoreAsync(restorePaths, progress, cancellationToken);

progress.Report(new RestorePartialResult(LanguageServerResources.Restore, $"{LanguageServerResources.Restore_complete}{Environment.NewLine}"));
return progress.GetValues() ?? [];
}

private async Task RestoreAsync(ImmutableArray<string> pathsToRestore, BufferedProgress<RestorePartialResult> progress, CancellationToken cancellationToken)
{
foreach (var path in pathsToRestore)
{
var arguments = $"restore \"{path}\"";
var workingDirectory = Path.GetDirectoryName(path);
var stageName = string.Format(LanguageServerResources.Restoring_0, Path.GetFileName(path));
ReportProgress(progress, stageName, string.Format(LanguageServerResources.Running_dotnet_restore_on_0, path));

var process = dotnetCliHelper.Run(arguments, workingDirectory, shouldLocalizeOutput: true);

process.OutputDataReceived += (sender, args) => ReportProgress(progress, stageName, args.Data);
process.ErrorDataReceived += (sender, args) => ReportProgress(progress, stageName, args.Data);
process.BeginOutputReadLine();
process.BeginErrorReadLine();

await process.WaitForExitAsync(cancellationToken);

if (process.ExitCode != 0)
{
throw new InvalidOperationException(LanguageServerResources.Failed_to_run_restore_see_output_for_details);
}
}

static void ReportProgress(BufferedProgress<RestorePartialResult> progress, string stage, string? restoreOutput)
{
if (restoreOutput != null)
{
progress.Report(new RestorePartialResult(stage, restoreOutput));
}
}
}

private static ImmutableArray<string> GetRestorePaths(RestoreParams request, Solution solution, RequestContext context)
{
if (request.ProjectFilePath != null)
{
return ImmutableArray.Create(request.ProjectFilePath);
}

// No file paths were specified - this means we should restore all projects in the solution.
// If there is a valid solution path, use that as the restore path.
if (solution.FilePath != null)
{
return ImmutableArray.Create(solution.FilePath);
}

// We don't have an addressable solution, so lets find all addressable projects.
// We can only restore projects with file paths as we are using the dotnet CLI to address them.
// We also need to remove duplicates as in multi targeting scenarios there will be multiple projects with the same file path.
var projects = solution.Projects
.Select(p => p.FilePath)
.WhereNotNull()
.Distinct()
.ToImmutableArray();

context.TraceInformation($"Found {projects.Length} restorable projects from {solution.Projects.Count()} projects in solution");
return projects;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.Serialization;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Newtonsoft.Json;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler;

[DataContract]
internal sealed record RestoreParams(
[property: DataMember(Name = "projectFilePath"), JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? ProjectFilePath
) : IPartialResultParams<RestorePartialResult>
{
[DataMember(Name = Methods.PartialResultTokenName)]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public IProgress<RestorePartialResult>? PartialResultToken { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// 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.Serialization;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler;

[DataContract]
internal sealed record RestorePartialResult(
[property: DataMember(Name = "stage")] string Stage,
[property: DataMember(Name = "message")] string Message
);
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@
<data name="Failed_to_read_runsettings_file_at_0:1" xml:space="preserve">
<value>Failed to read .runsettings file at {0}:{1}</value>
</data>
<data name="Failed_to_run_restore_see_output_for_details" xml:space="preserve">
<value>Failed to run restore, see output for details</value>
</data>
<data name="Found_0_tests_in_1" xml:space="preserve">
<value>Found {0} tests in {1}</value>
</data>
Expand All @@ -167,6 +170,9 @@
<data name="Message" xml:space="preserve">
<value>Message</value>
</data>
<data name="Nothing_found_to_restore" xml:space="preserve">
<value>Nothing found to restore</value>
</data>
<data name="No_test_methods_found_in_requested_range" xml:space="preserve">
<value>No test methods found in requested range</value>
</data>
Expand All @@ -179,6 +185,21 @@
<data name="Projects_failed_to_load_because_MSBuild_could_not_be_found" xml:space="preserve">
<value>Projects failed to load because the .NET Framework build tools could not be found. Try installing Visual Studio or the Visual Studio Build Tools package, or check the logs for details.</value>
</data>
<data name="Restore" xml:space="preserve">
<value>Restore</value>
</data>
<data name="Restore_complete" xml:space="preserve">
<value>Restore complete</value>
</data>
<data name="Restore_started" xml:space="preserve">
<value>Restore started</value>
</data>
<data name="Restoring_0" xml:space="preserve">
<value>Restoring {0}</value>
</data>
<data name="Running_dotnet_restore_on_0" xml:space="preserve">
<value>Running dotnet restore on {0}</value>
</data>
<data name="Project_0_loaded_by_CSharp_Dev_Kit" xml:space="preserve">
<value>Project {0} loaded by C# Dev Kit</value>
<comment>The placeholder is a name of a file</comment>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 79d1884

Please sign in to comment.