File: LanguageServer\Handler\Restore\RestoreHandler.cs
Web Access
Project: src\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj (Microsoft.CodeAnalysis.LanguageServer)
// 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 = new string[] { "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);
 
            cancellationToken.Register(() =>
            {
                process?.Kill();
            });
 
            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)
            {
                ReportProgress(progress, stageName, string.Format(LanguageServerResources.Failed_to_run_restore_on_0, path));
            }
        }
 
        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.ProjectFilePaths.Any())
        {
            return [.. request.ProjectFilePaths];
        }
 
        // 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 [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;
    }
}