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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Roslyn.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using StreamJsonRpc;
using Xunit.Abstractions;
using LSP = Roslyn.LanguageServer.Protocol;

Expand Down Expand Up @@ -74,9 +75,10 @@ await File.WriteAllTextAsync(projectPath, $"""
documents: files,
locations: annotatedLocations);

// Perform restore and mock up project restore client handler
// Perform restore
ProcessUtilities.Run("dotnet", $"restore --project {projectPath}");
Copy link
Member

Choose a reason for hiding this comment

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

Do you feel the new WorkDoneProgress code is exercised well by the existing tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

Restore wasn't really exercised by the tests at all really. We likely need integration tests for this that actually do the full load / restore loop.

lspClient.AddClientLocalRpcTarget(ProjectDependencyHelper.ProjectNeedsRestoreName, (string[] projectFilePaths) => { });

lspClient.AddClientLocalRpcTarget(new WorkDoneProgressTarget());

// Listen for project initialization
var projectInitialized = new TaskCompletionSource();
Expand All @@ -92,6 +94,15 @@ await File.WriteAllTextAsync(projectPath, $"""
return lspClient;
}

private class WorkDoneProgressTarget
{
[JsonRpcMethod(Methods.WindowWorkDoneProgressCreateName, UseSingleObjectParameterDeserialization = true)]
public Task HandleCreateWorkDoneProgress(WorkDoneProgressCreateParams _1, CancellationToken _2) => Task.CompletedTask;

[JsonRpcMethod(Methods.ProgressNotificationName, UseSingleObjectParameterDeserialization = true)]
public Task HandleProgress((string token, object value) _1, CancellationToken _2) => Task.CompletedTask;
}

private protected static Dictionary<string, IList<LSP.Location>> GetAnnotatedLocations(DocumentUri codeUri, SourceText text, ImmutableDictionary<string, ImmutableArray<TextSpan>> spanMap)
{
var locations = new Dictionary<string, IList<LSP.Location>>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ internal sealed class CanonicalMiscFilesProjectLoader : LanguageServerProjectLoa
{
private readonly Lazy<string> _canonicalDocumentPath;

/// <summary>
/// Avoid showing restore notifications for misc files - it ends up being noisy and confusing
/// as every file is a misc file on first open until we detect a project for it.
/// </summary>
protected override bool EnableProgressReporting => false;

public CanonicalMiscFilesProjectLoader(
LanguageServerWorkspaceFactory workspaceFactory,
IFileChangeWatcher fileChangeWatcher,
Expand All @@ -37,7 +43,8 @@ public CanonicalMiscFilesProjectLoader(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
: base(
workspaceFactory,
fileChangeWatcher,
Expand All @@ -46,7 +53,8 @@ public CanonicalMiscFilesProjectLoader(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider)
binLogPathProvider,
dotnetCliHelper)
{
_canonicalDocumentPath = new Lazy<string>(() =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public FileBasedProgramsProjectSystem(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
: base(
workspaceFactory,
fileChangeWatcher,
Expand All @@ -53,7 +54,8 @@ public FileBasedProgramsProjectSystem(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider)
binLogPathProvider,
dotnetCliHelper)
{
_lspServices = lspServices;
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>();
Expand All @@ -66,7 +68,8 @@ public FileBasedProgramsProjectSystem(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider);
binLogPathProvider,
dotnetCliHelper);
}

private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ internal sealed class FileBasedProgramsWorkspaceProviderFactory(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider) : ILspMiscellaneousFilesWorkspaceProviderFactory
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper) : ILspMiscellaneousFilesWorkspaceProviderFactory
{
public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices)
{
Expand All @@ -48,6 +49,7 @@ public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorksp
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider);
binLogPathProvider,
dotnetCliHelper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal abstract class LanguageServerProjectLoader
private readonly ILogger _logger;
private readonly ProjectLoadTelemetryReporter _projectLoadTelemetryReporter;
private readonly IBinLogPathProvider _binLogPathProvider;
private readonly DotnetCliHelper _dotnetCliHelper;
protected readonly ImmutableDictionary<string, string> AdditionalProperties;

/// <summary>
Expand Down Expand Up @@ -84,6 +85,11 @@ public sealed record Primordial(ProjectSystemProjectFactory PrimordialProjectFac
public sealed record LoadedTargets(ImmutableArray<LoadedProject> LoadedProjectTargets) : ProjectLoadState;
}

/// <summary>
/// Indicates whether loads should report UI progress to the client for this loader.
/// </summary>
protected virtual bool EnableProgressReporting => true;

protected LanguageServerProjectLoader(
LanguageServerWorkspaceFactory workspaceFactory,
IFileChangeWatcher fileChangeWatcher,
Expand All @@ -92,7 +98,8 @@ protected LanguageServerProjectLoader(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
{
_workspaceFactory = workspaceFactory;
_fileChangeWatcher = fileChangeWatcher;
Expand All @@ -101,6 +108,7 @@ protected LanguageServerProjectLoader(
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectLoader));
_projectLoadTelemetryReporter = projectLoadTelemetry;
_binLogPathProvider = binLogPathProvider;
_dotnetCliHelper = dotnetCliHelper;

AdditionalProperties = BuildAdditionalProperties(serverConfigurationFactory.ServerConfiguration);

Expand Down Expand Up @@ -176,12 +184,8 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList<ProjectToLoad

if (GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableAutomaticRestore) && projectsThatNeedRestore.Any())
{
// Tell the client to restore any projects with unresolved dependencies.
// This should eventually move entirely server side once we have a mechanism for reporting generic project load progress.
// Tracking: https://github.com/dotnet/vscode-csharp/issues/6675
//
// The request blocks to ensure we aren't trying to run a design time build at the same time as a restore.
await ProjectDependencyHelper.SendProjectNeedsRestoreRequestAsync(projectsThatNeedRestore, cancellationToken);
// This request blocks to ensure we aren't trying to run a design time build at the same time as a restore.
await ProjectDependencyHelper.RestoreProjectsAsync(projectsThatNeedRestore, EnableProgressReporting, _dotnetCliHelper, _logger, cancellationToken);
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public LanguageServerProjectSystem(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
: base(
workspaceFactory,
fileChangeWatcher,
Expand All @@ -44,7 +45,8 @@ public LanguageServerProjectSystem(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider)
binLogPathProvider,
dotnetCliHelper)
{
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectSystem));
_hostProjectFactory = workspaceFactory.HostProjectFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.PooledObjects;
Expand All @@ -16,8 +16,6 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal static class ProjectDependencyHelper
{
internal const string ProjectNeedsRestoreName = "workspace/_roslyn_projectNeedsRestore";

internal static bool NeedsRestore(ProjectFileInfo newProjectFileInfo, ProjectFileInfo? previousProjectFileInfo, ILogger logger)
{
if (previousProjectFileInfo is null)
Expand Down Expand Up @@ -121,20 +119,24 @@ static bool SatisfiesVersion(VersionRange requestedVersionRange, NuGetVersion pr
}
}

internal static async Task SendProjectNeedsRestoreRequestAsync(ImmutableArray<string> projectPaths, CancellationToken cancellationToken)
internal static async Task RestoreProjectsAsync(ImmutableArray<string> projectPaths, bool enableProgressReporting, DotnetCliHelper dotnetCliHelper, ILogger logger, CancellationToken cancellationToken)
{
if (projectPaths.IsEmpty)
return;

Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through.");

var languageServerManager = LanguageServerHost.Instance.GetRequiredLspService<IClientLanguageServerManager>();
var workDoneProgressManager = LanguageServerHost.Instance.GetRequiredLspService<WorkDoneProgressManager>();

// Ensure we only pass unique paths back to be restored.
var unresolvedParams = new UnresolvedDependenciesParams([.. projectPaths.Distinct()]);
await languageServerManager.SendRequestAsync(ProjectNeedsRestoreName, unresolvedParams, cancellationToken);
try
{
await RestoreHandler.RestoreAsync(projectPaths, workDoneProgressManager, dotnetCliHelper, logger, enableProgressReporting, cancellationToken);
}
catch (OperationCanceledException)
{
// Restore was cancelled. This is not a failure, it just leaves the project unrestored or partially restored (same as if the user cancelled a CLI restore).
// We don't want this exception to bubble up to the project load queue however as it may need to additional work after this call.
logger.LogWarning("Project restore was canceled.");
}
}

private sealed record UnresolvedDependenciesParams(
[property: JsonPropertyName("projectFilePaths")] string[] ProjectFilePaths);
}
Loading
Loading