Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add restore support to the language server #70588

Merged
merged 4 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 class RestorableProjectsHandler(ProjectTargetFrameworkManager projectTargetFrameworkManager) : ILspServiceRequestHandler<string[]>
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
{
internal const string MethodName = "workspace/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)
{
// Restorable projects are dotnet core projects with file paths.
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
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.
var projects = projectsBuilder.Distinct().ToArray();
dibarbet marked this conversation as resolved.
Show resolved Hide resolved

// Ensure the client gets a consistent ordering.
Array.Sort(projects, StringComparer.OrdinalIgnoreCase);

return Task.FromResult(projects.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 class RestoreHandler(DotnetCliHelper dotnetCliHelper) : ILspServiceRequestHandler<RestoreParams, RestorePartialResult[]>
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
{
internal const string MethodName = "workspace/restore";
dibarbet marked this conversation as resolved.
Show resolved Hide resolved

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}"));
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
return progress.GetValues() ?? [];
}

private async Task RestoreAsync(ImmutableArray<string> pathsToRestore, BufferedProgress<RestorePartialResult> progress, CancellationToken cancellationToken)
{
foreach (var path in pathsToRestore)
{
var arguments = $"restore \"{path}\"";
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
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);
dibarbet marked this conversation as resolved.
Show resolved Hide resolved

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also get reported through the other progress reporter, since I'd assume failure to restore is mostly a user problem?

}
}

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)
Comment on lines +88 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need a change now, but we should still think about what happens if the user has a solution file and also has load on demand enabled, since this wouldn't cover the load-on-demand projects.

{
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 record RestoreParams(
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
[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 record RestorePartialResult(
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
[property: DataMember(Name = "stage")] string Stage,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does 'stage' mean here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stage indicates what overall step is happening. This value is shown in the progress bar (whereas the messages are only shown in the output window)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see. can you doc. It wasn't clear. Looking at the PR it makes more sense now. It allows the messages to be bucketed together right? So you can tell which project these are associated with?

[property: DataMember(Name = "message")] string Message
);
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,18 @@
<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>
<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 @@ -168,6 +174,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="Running_tests" xml:space="preserve">
<value>Running tests...</value>
</data>
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