diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs index 405854da388d7..66d904d79c94e 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs @@ -71,7 +71,7 @@ private async Task GetDotnetSdkFolderFromDotnetExecutableAsync(string pr 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}"); + _logger.LogDebug($"Running dotnet CLI command at {_dotnetExecutablePath.Value} in directory {workingDirectory} with arguments '{string.Join(' ', arguments)}'"); var startInfo = new ProcessStartInfo(_dotnetExecutablePath.Value) { diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs index 7a2c74612d7ad..254822bbf2534 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.ProjectSystem; @@ -25,7 +26,8 @@ internal sealed class LoadedProject : IDisposable private readonly ProjectSystemProject _projectSystemProject; private readonly ProjectSystemProjectOptionsProcessor _optionsProcessor; - private readonly IFileChangeContext _fileChangeContext; + private readonly IFileChangeContext _sourceFileChangeContext; + private readonly IFileChangeContext _projectFileChangeContext; private readonly ProjectTargetFrameworkManager _targetFrameworkManager; /// @@ -53,22 +55,16 @@ public LoadedProject(ProjectSystemProject projectSystemProject, SolutionServices // TODO: we only should listen for add/removals here, but we can't specify such a filter now _projectDirectory = Path.GetDirectoryName(_projectFilePath)!; - _fileChangeContext = fileWatcher.CreateContext([new(_projectDirectory, [".cs", ".cshtml", ".razor"])]); - _fileChangeContext.FileChanged += FileChangedContext_FileChanged; + _sourceFileChangeContext = fileWatcher.CreateContext([new(_projectDirectory, [".cs", ".cshtml", ".razor"])]); + _sourceFileChangeContext.FileChanged += SourceFileChangeContext_FileChanged; - // Start watching for file changes for the project file as well - _fileChangeContext.EnqueueWatchingFile(_projectFilePath); + _projectFileChangeContext = fileWatcher.CreateContext([]); + _projectFileChangeContext.FileChanged += ProjectFileChangeContext_FileChanged; + _projectFileChangeContext.EnqueueWatchingFile(_projectFilePath); } - private void FileChangedContext_FileChanged(object? sender, string filePath) + private void SourceFileChangeContext_FileChanged(object? sender, string filePath) { - // If the project file itself changed, we almost certainly need to reload the project. - if (string.Equals(filePath, _projectFilePath, StringComparison.OrdinalIgnoreCase)) - { - NeedsReload?.Invoke(this, EventArgs.Empty); - return; - } - var matchers = _mostRecentFileMatchers?.Value; if (matchers is null) { @@ -92,6 +88,11 @@ private void FileChangedContext_FileChanged(object? sender, string filePath) } } + private void ProjectFileChangeContext_FileChanged(object? sender, string filePath) + { + NeedsReload?.Invoke(this, EventArgs.Empty); + } + public event EventHandler? NeedsReload; public string? GetTargetFramework() @@ -105,7 +106,8 @@ private void FileChangedContext_FileChanged(object? sender, string filePath) /// public void Dispose() { - _fileChangeContext.Dispose(); + _sourceFileChangeContext.Dispose(); + _projectFileChangeContext.Dispose(); _optionsProcessor.Dispose(); _projectSystemProject.RemoveFromWorkspace(); } @@ -221,7 +223,7 @@ public void Dispose() document => _projectSystemProject.RemoveDynamicSourceFile(document.FilePath), "Project {0} now has {1} dynamic file(s)."); - WatchProjectAssetsFile(newProjectInfo, _fileChangeContext); + WatchProjectAssetsFile(newProjectInfo); var needsRestore = ProjectDependencyHelper.NeedsRestore(newProjectInfo, _mostRecentFileInfo, logger); @@ -272,7 +274,7 @@ void UpdateProjectSystemProjectCollection(IEnumerable loadedCollection, IE logger.LogTrace(logMessage, projectFullPathWithTargetFramework, newItems.Count); } - void WatchProjectAssetsFile(ProjectFileInfo currentProjectInfo, IFileChangeContext fileChangeContext) + void WatchProjectAssetsFile(ProjectFileInfo currentProjectInfo) { if (_mostRecentFileInfo?.ProjectAssetsFilePath == currentProjectInfo.ProjectAssetsFilePath) { @@ -282,14 +284,9 @@ void WatchProjectAssetsFile(ProjectFileInfo currentProjectInfo, IFileChangeConte // Dispose of the last once since we're changing the file we're watching. _mostRecentProjectAssetsFileWatcher?.Dispose(); - - IWatchedFile? currentWatcher = null; - if (currentProjectInfo.ProjectAssetsFilePath != null) - { - currentWatcher = fileChangeContext.EnqueueWatchingFile(currentProjectInfo.ProjectAssetsFilePath); - } - - _mostRecentProjectAssetsFileWatcher = currentWatcher; + _mostRecentProjectAssetsFileWatcher = currentProjectInfo.ProjectAssetsFilePath is { } assetsFilePath + ? _projectFileChangeContext.EnqueueWatchingFile(assetsFilePath) + : null; } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs index e419f2ee621c1..5de95267c4daf 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Composition; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.Extensions.Logging; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler; @@ -17,7 +18,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler; [Method(MethodName)] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class RestoreHandler(DotnetCliHelper dotnetCliHelper) : ILspServiceRequestHandler +internal sealed class RestoreHandler(DotnetCliHelper dotnetCliHelper, ILoggerFactory loggerFactory) : ILspServiceRequestHandler { internal const string MethodName = "workspace/_roslyn_restore"; @@ -25,6 +26,8 @@ internal sealed class RestoreHandler(DotnetCliHelper dotnetCliHelper) : ILspServ public bool RequiresLSPSolution => true; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + public async Task HandleRequestAsync(RestoreParams request, RequestContext context, CancellationToken cancellationToken) { Contract.ThrowIfNull(context.Solution); @@ -35,18 +38,31 @@ public async Task HandleRequestAsync(RestoreParams reque var restorePaths = GetRestorePaths(request, context.Solution, context); if (restorePaths.IsEmpty) { + _logger.LogDebug($"Restore was requested but no paths were provided."); progress.Report(new RestorePartialResult(LanguageServerResources.Restore, LanguageServerResources.Nothing_found_to_restore)); return progress.GetValues() ?? []; } - await RestoreAsync(restorePaths, progress, cancellationToken); + _logger.LogDebug($"Running restore on {restorePaths.Length} paths, starting with '{restorePaths.First()}'."); + bool success = await RestoreAsync(restorePaths, progress, cancellationToken); progress.Report(new RestorePartialResult(LanguageServerResources.Restore, $"{LanguageServerResources.Restore_complete}{Environment.NewLine}")); + if (success) + { + _logger.LogDebug($"Restore completed successfully."); + } + else + { + _logger.LogError($"Restore completed with errors. See '.NET NuGet Restore' output window for more details."); + } + return progress.GetValues() ?? []; } - private async Task RestoreAsync(ImmutableArray pathsToRestore, BufferedProgress progress, CancellationToken cancellationToken) + /// True if all restore invocations exited with code 0. Otherwise, false. + private async Task RestoreAsync(ImmutableArray pathsToRestore, BufferedProgress progress, CancellationToken cancellationToken) { + bool success = true; foreach (var path in pathsToRestore) { var arguments = new string[] { "restore", path }; @@ -71,9 +87,12 @@ private async Task RestoreAsync(ImmutableArray pathsToRestore, BufferedP if (process.ExitCode != 0) { ReportProgress(progress, stageName, string.Format(LanguageServerResources.Failed_to_run_restore_on_0, path)); + success = false; } } + return success; + static void ReportProgress(BufferedProgress progress, string stage, string? restoreOutput) { if (restoreOutput != null)