From 2c26ee9e75254a82fb86bfed918deb601273454a Mon Sep 17 00:00:00 2001 From: Dmitry Goncharenko Date: Sun, 28 Oct 2018 20:39:30 -0700 Subject: [PATCH 1/6] On demand projects loading when working with big C# codebases --- .../Services/IProjectSystem.cs | 6 +++ src/OmniSharp.Cake/CakeProjectSystem.cs | 6 +++ src/OmniSharp.DotNet/DotNetProjectSystem.cs | 6 +++ .../Options/MSBuildOptions.cs | 6 +++ src/OmniSharp.MSBuild/ProjectManager.cs | 47 ++++++++++++++++++- src/OmniSharp.MSBuild/ProjectSystem.cs | 21 ++++++++- src/OmniSharp.Plugins/Plugin.cs | 6 +++ .../Helpers/ProjectSystemExtensions.cs | 30 ++++++++++++ .../Services/Buffer/UpdateBufferService.cs | 10 +++- .../Services/Diagnostics/CodeCheckService.cs | 9 +++- .../Services/Navigation/FindUsagesService.cs | 9 +++- .../Refactoring/V2/GetCodeActionsService.cs | 9 ++++ .../Structure/BlockStructureService.cs | 13 ++++- .../Structure/CodeStructureService.cs | 9 +++- .../Structure/MembersAsTreeService.cs | 10 +++- src/OmniSharp.Roslyn/OmniSharpWorkspace.cs | 28 ++++++++++- src/OmniSharp.Script/ScriptProjectSystem.cs | 6 +++ .../EndpointMiddlewareFacts.cs | 5 ++ 18 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs diff --git a/src/OmniSharp.Abstractions/Services/IProjectSystem.cs b/src/OmniSharp.Abstractions/Services/IProjectSystem.cs index ab459e2bb2..31291f969a 100644 --- a/src/OmniSharp.Abstractions/Services/IProjectSystem.cs +++ b/src/OmniSharp.Abstractions/Services/IProjectSystem.cs @@ -29,5 +29,11 @@ public interface IProjectSystem /// The file path to the project to retrieve. Alternatively, /// a file path to a document within a proejct may be specified. Task GetProjectModelAsync(string filePath); + + /// + /// This project system will try to locate projects that reference given file and will wait until the projects and their references are fully loaded. + /// + /// File for which to locate and load projects. + Task WaitForProjectsToLoadForFileAsync(string filePath); } } diff --git a/src/OmniSharp.Cake/CakeProjectSystem.cs b/src/OmniSharp.Cake/CakeProjectSystem.cs index 63d1b60860..4d1322b49b 100644 --- a/src/OmniSharp.Cake/CakeProjectSystem.cs +++ b/src/OmniSharp.Cake/CakeProjectSystem.cs @@ -258,6 +258,12 @@ public Task GetProjectModelAsync(string filePath) return Task.FromResult(new CakeContextModel(filePath)); } + public Task WaitForProjectsToLoadForFileAsync(string filePath) + { + // At the moment this project system doesn't support on demand projects loading + return Task.CompletedTask; + } + private ProjectInfo GetProjectFileInfo(string path) { return !_projects.TryGetValue(path, out ProjectInfo projectFileInfo) ? null : projectFileInfo; diff --git a/src/OmniSharp.DotNet/DotNetProjectSystem.cs b/src/OmniSharp.DotNet/DotNetProjectSystem.cs index 796deda7dc..33d0b56f1f 100644 --- a/src/OmniSharp.DotNet/DotNetProjectSystem.cs +++ b/src/OmniSharp.DotNet/DotNetProjectSystem.cs @@ -152,6 +152,12 @@ public void Update(bool allowRestore) } } + public Task WaitForProjectsToLoadForFileAsync(string filePath) + { + // At the moment this project system doesn't support on demand projects loading + return Task.CompletedTask; + } + private void UpdateProject(string projectDirectory) { _logger.LogInformation($"Update project {projectDirectory}"); diff --git a/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs b/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs index 3da2887828..31ef89849b 100644 --- a/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs +++ b/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs @@ -8,6 +8,12 @@ internal class MSBuildOptions public string Platform { get; set; } public bool EnablePackageAutoRestore { get; set; } + /// + /// If true, MSBuild project system will only be loading projects for files that were opened in the editor + /// as well as referenced projects, recursively. + /// + public bool OnDemandProjectsLoad { get; set; } + /// /// When set to true, the MSBuild project system will attempt to resolve the path to the MSBuild /// SDKs for a project by running 'dotnet --info' and retrieving the path. This is only needed diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index a9c5fb8d4b..6bd6611079 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -17,6 +18,7 @@ using OmniSharp.MSBuild.Models.Events; using OmniSharp.MSBuild.Notification; using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Options; using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; using OmniSharp.Utilities; @@ -39,12 +41,15 @@ public ProjectToUpdate(string filePath, bool allowAutoRestore) } private readonly ILogger _logger; + private readonly IOmniSharpEnvironment _environment; + private readonly MSBuildOptions _options; private readonly IEventEmitter _eventEmitter; private readonly IFileSystemWatcher _fileSystemWatcher; private readonly MetadataFileReferenceCache _metadataFileReferenceCache; private readonly PackageDependencyChecker _packageDependencyChecker; private readonly ProjectFileInfoCollection _projectFiles; private readonly HashSet _failedToLoadProjectFiles; + private readonly ConcurrentDictionary _projectsRequestedOnDemand; private readonly ProjectLoader _projectLoader; private readonly OmniSharpWorkspace _workspace; private readonly ImmutableArray _eventSinks; @@ -57,15 +62,27 @@ public ProjectToUpdate(string filePath, bool allowAutoRestore) private readonly FileSystemNotificationCallback _onDirectoryFileChanged; - public ProjectManager(ILoggerFactory loggerFactory, IEventEmitter eventEmitter, IFileSystemWatcher fileSystemWatcher, MetadataFileReferenceCache metadataFileReferenceCache, PackageDependencyChecker packageDependencyChecker, ProjectLoader projectLoader, OmniSharpWorkspace workspace, ImmutableArray eventSinks) + public ProjectManager(IOmniSharpEnvironment environment, + ILoggerFactory loggerFactory, + MSBuildOptions options, + IEventEmitter eventEmitter, + IFileSystemWatcher fileSystemWatcher, + MetadataFileReferenceCache metadataFileReferenceCache, + PackageDependencyChecker packageDependencyChecker, + ProjectLoader projectLoader, + OmniSharpWorkspace workspace, + ImmutableArray eventSinks) { + _environment = environment; _logger = loggerFactory.CreateLogger(); + _options = options ?? new MSBuildOptions(); _eventEmitter = eventEmitter; _fileSystemWatcher = fileSystemWatcher; _metadataFileReferenceCache = metadataFileReferenceCache; _packageDependencyChecker = packageDependencyChecker; _projectFiles = new ProjectFileInfoCollection(); _failedToLoadProjectFiles = new HashSet(StringComparer.OrdinalIgnoreCase); + _projectsRequestedOnDemand = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _projectLoader = projectLoader; _workspace = workspace; _eventSinks = eventSinks; @@ -75,6 +92,33 @@ public ProjectManager(ILoggerFactory loggerFactory, IEventEmitter eventEmitter, _processLoopTask = Task.Run(() => ProcessLoopAsync(_processLoopCancellation.Token)); _onDirectoryFileChanged = OnDirectoryFileChanged; + + if (_options.OnDemandProjectsLoad) + { + _workspace.DocumentRequested += OnWorkspaceDocumentRequested; + } + } + + private void OnWorkspaceDocumentRequested(object sender, DocumentRequestedEventArgs args) + { + // Search and queue for loading C# projects that are likely to reference the requested file + string projectDir = Path.GetDirectoryName(args.DocumentPath); + while(projectDir != null && projectDir.StartsWith(_environment.TargetDirectory.TrimEnd(Path.DirectorySeparatorChar))) + { + List csProjFiles = Directory.EnumerateFiles(projectDir, "*.csproj", SearchOption.TopDirectoryOnly).ToList(); + if (csProjFiles.Count > 0) + { + foreach(string csProjFile in csProjFiles) + { + if (_projectsRequestedOnDemand.TryAdd(csProjFile, 0 /*unused*/)) + { + QueueProjectUpdate(csProjFile, allowAutoRestore:true); + } + } + return; + } + projectDir = Path.GetDirectoryName(projectDir); + } } protected override void DisposeCore(bool disposing) @@ -93,7 +137,6 @@ protected override void DisposeCore(bool disposing) public void QueueProjectUpdate(string projectFilePath, bool allowAutoRestore) { - _logger.LogInformation($"Queue project update for '{projectFilePath}'"); _queue.Post(new ProjectToUpdate(projectFilePath, allowAutoRestore)); } diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs index a8b8cb5633..8a8756075f 100644 --- a/src/OmniSharp.MSBuild/ProjectSystem.cs +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -98,7 +98,14 @@ public void Initalize(IConfiguration configuration) _packageDependencyChecker = new PackageDependencyChecker(_loggerFactory, _eventEmitter, _dotNetCli, _options); _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory, _sdksPathResolver); - _manager = new ProjectManager(_loggerFactory, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, _loader, _workspace, _eventSinks); + _manager = new ProjectManager(_environment, _loggerFactory, _options, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, + _loader, _workspace, _eventSinks); + + if (_options.OnDemandProjectsLoad) + { + _logger.LogInformation($"Skip loading projects listed in solution file or under target directory because {Key}:{nameof(MSBuildOptions.OnDemandProjectsLoad)} is true."); + return; + } var initialProjectPaths = GetInitialProjectPaths(); @@ -214,5 +221,17 @@ async Task IProjectSystem.GetProjectModelAsync(string filePath) return new MSBuildProjectInfo(projectFileInfo); } + + public async Task WaitForProjectsToLoadForFileAsync(string filePath) + { + if (_options.OnDemandProjectsLoad) + { + // Request the document only to make sure that projects referencing it are queued for loading by the project system. + _workspace.GetDocument(filePath); + // Wait for all queued projects to load to ensure that workspace is fully up to date before this method completes. + // If the project for the document was loaded before and there are no other projects to load at the moment, the call below will be no-op. + await _manager.WaitForQueueEmptyAsync(); + } + } } } diff --git a/src/OmniSharp.Plugins/Plugin.cs b/src/OmniSharp.Plugins/Plugin.cs index 4e13c6be07..6dc4c39c99 100644 --- a/src/OmniSharp.Plugins/Plugin.cs +++ b/src/OmniSharp.Plugins/Plugin.cs @@ -115,5 +115,11 @@ public Task GetProjectModelAsync(string filePath) // TODO: Call out to process return Task.FromResult(null); } + + public Task WaitForProjectsToLoadForFileAsync(string filePath) + { + // TODO: Call out to process + return Task.CompletedTask; + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs new file mode 100644 index 0000000000..c158069a31 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs @@ -0,0 +1,30 @@ +using OmniSharp.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace OmniSharp.Helpers +{ + public static class ProjectSystemExtensions + { + public static async Task WaitForAllProjectsToLoadForFileAsync(this IEnumerable projectSystems, string filePath) + { + if (filePath != null) + { + await Task.WhenAll(GetProjectSystemsForFile(projectSystems, filePath).Select(ps => ps.WaitForProjectsToLoadForFileAsync(filePath))); + } + } + + private static IEnumerable GetProjectSystemsForFile(IEnumerable projectSystems, string filePath) + { + foreach (IProjectSystem projectSystem in projectSystems) + { + if (projectSystem.Extensions.Any(extension => filePath.EndsWith(extension, StringComparison.OrdinalIgnoreCase))) + { + yield return projectSystem; + } + } + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs index d980c2d569..5de1821153 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs @@ -1,8 +1,11 @@ +using System.Collections.Generic; using System.Composition; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.UpdateBuffer; +using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Buffer { @@ -10,15 +13,20 @@ namespace OmniSharp.Roslyn.CSharp.Services.Buffer public class UpdateBufferService : IRequestHandler { private OmniSharpWorkspace _workspace; + private readonly IEnumerable _projectSystems; [ImportingConstructor] - public UpdateBufferService(OmniSharpWorkspace workspace) + public UpdateBufferService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) { _workspace = workspace; + _projectSystems = projectSystems; } public async Task Handle(UpdateBufferRequest request) { + // Waiting until the document is fully formed in memory (for project systems that have this ability) + // before applying updates to it helps to reduce chances for the compiler getting confused and producing invalid compilation errors. + await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); await _workspace.BufferManager.UpdateBufferAsync(request); return true; } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs index 4a09494f81..38d1c32c79 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs @@ -7,6 +7,7 @@ using OmniSharp.Mef; using OmniSharp.Models; using OmniSharp.Models.CodeCheck; +using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics { @@ -14,15 +15,21 @@ namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics public class CodeCheckService : IRequestHandler { private OmniSharpWorkspace _workspace; + private readonly IEnumerable _projectSystems; [ImportingConstructor] - public CodeCheckService(OmniSharpWorkspace workspace) + public CodeCheckService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) { _workspace = workspace; + _projectSystems = projectSystems; } public async Task Handle(CodeCheckRequest request) { + // Waiting until the document is fully formed in memory (for project systems that have this ability) + // helps to reduce chances of returning invalid list of errors while compilation is still in progress. + await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); + var documents = request.FileName != null ? _workspace.GetDocuments(request.FileName) : _workspace.CurrentSolution.Projects.SelectMany(project => project.Documents); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs index 0107a6650c..9d9b3819f4 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs @@ -10,6 +10,7 @@ using OmniSharp.Mef; using OmniSharp.Models; using OmniSharp.Models.FindUsages; +using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Navigation { @@ -17,16 +18,22 @@ namespace OmniSharp.Roslyn.CSharp.Services.Navigation public class FindUsagesService : IRequestHandler { private readonly OmniSharpWorkspace _workspace; + private readonly IEnumerable _projectSystems; [ImportingConstructor] - public FindUsagesService(OmniSharpWorkspace workspace) + public FindUsagesService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) { _workspace = workspace; + _projectSystems = projectSystems; } public async Task Handle(FindUsagesRequest request) { + // Waiting until all projects relevant to the document are fully loaded (for project systems that have this ability) + // helps to produce more complete list of usages for a symbol. + await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); var document = _workspace.GetDocument(request.FileName); + var response = new QuickFixResponse(); if (document != null) { diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs index efc47cdb3a..447cd45c5e 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; +using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.V2.CodeActions; using OmniSharp.Roslyn.CSharp.Services.CodeActions; @@ -14,18 +15,26 @@ namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 [OmniSharpHandler(OmniSharpEndpoints.V2.GetCodeActions, LanguageNames.CSharp)] public class GetCodeActionsService : BaseCodeActionService { + private readonly IEnumerable _projectSystems; + [ImportingConstructor] public GetCodeActionsService( OmniSharpWorkspace workspace, CodeActionHelper helper, [ImportMany] IEnumerable providers, + [ImportMany] IEnumerable projectSystems, ILoggerFactory loggerFactory) : base(workspace, helper, providers, loggerFactory.CreateLogger()) { + _projectSystems = projectSystems; } public override async Task Handle(GetCodeActionsRequest request) { + // Waiting until the document is fully formed in memory (for project systems that have this ability) + // helps to reduce chances of returning invalid list of code actions while compilation is still in progress. + await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); + var availableActions = await GetAvailableCodeActions(request); return new GetCodeActionsResponse diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs index b62fe3ce9b..58cb2df70d 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions; +using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.V2; using OmniSharp.Services; @@ -29,13 +30,15 @@ public class BlockStructureService : IRequestHandler _projectSystems; [ImportingConstructor] - public BlockStructureService(IAssemblyLoader loader, OmniSharpWorkspace workspace) + public BlockStructureService(IAssemblyLoader loader, OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) { _workspace = workspace; _loader = loader; _featureAssembly = _loader.LazyLoad(Configuration.RoslynFeatures); + _projectSystems = projectSystems; _blockStructureService = _featureAssembly.LazyGetType("Microsoft.CodeAnalysis.Structure.BlockStructureService"); _blockStructure = _featureAssembly.LazyGetType("Microsoft.CodeAnalysis.Structure.BlockStructure"); @@ -50,7 +53,15 @@ public BlockStructureService(IAssemblyLoader loader, OmniSharpWorkspace workspac public async Task Handle(BlockStructureRequest request) { + // Waiting until the document is fully formed in memory (for project systems that have this ability) + // helps to reduce chances of returning invalid document block structure while compilation is still in progress. + await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); var document = _workspace.GetDocument(request.FileName); + if (document == null) + { + return new BlockStructureResponse(); + } + var text = await document.GetTextAsync(); var service = _blockStructureService.LazyGetMethod("GetService").InvokeStatic(new[] { document }); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs index 42c1373d44..936a16195f 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions; +using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.V2; using OmniSharp.Models.V2.CodeStructure; @@ -20,18 +21,24 @@ public class CodeStructureService : IRequestHandler _propertyProviders; + private readonly IEnumerable _projectSystems; [ImportingConstructor] public CodeStructureService( OmniSharpWorkspace workspace, - [ImportMany] IEnumerable propertyProviders) + [ImportMany] IEnumerable propertyProviders, + [ImportMany] IEnumerable projectSystems) { _workspace = workspace; _propertyProviders = propertyProviders; + _projectSystems = projectSystems; } public async Task Handle(CodeStructureRequest request) { + // Waiting until the document is fully formed in memory (for project systems that have this ability) + // helps to reduce chances of returning invalid/incomplete document code structure while compilation is still in progress. + await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); var document = _workspace.GetDocument(request.FileName); if (document == null) { diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs index 3cf164a837..33db00b630 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs @@ -3,8 +3,10 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using OmniSharp.Abstractions.Services; +using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.MembersTree; +using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Structure { @@ -13,16 +15,22 @@ public class MembersAsTreeService : IRequestHandler _discovers; + private readonly IEnumerable _projectSystems; [ImportingConstructor] - public MembersAsTreeService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable featureDiscovers) + public MembersAsTreeService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable featureDiscovers, [ImportMany] IEnumerable projectSystems) { _workspace = workspace; _discovers = featureDiscovers; + _projectSystems = projectSystems; } public async Task Handle(MembersTreeRequest request) { + // Waiting until the document is fully formed in memory (for project systems that have this ability) + // helps to reduce chances of returning invalid/incomplete member structure for the document while compilation is still in progress. + await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); + return new FileMemberTree() { TopLevelTypeDefinitions = await StructureComputer.Compute(_workspace.GetDocuments(request.FileName), _discovers) diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index 2006e775be..4431d9d0b8 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -24,7 +24,7 @@ public class OmniSharpWorkspace : Workspace public BufferManager BufferManager { get; private set; } private readonly ILogger _logger; - + public event EventHandler DocumentRequested; private readonly ConcurrentDictionary miscDocumentsProjectInfos = new ConcurrentDictionary(); [ImportingConstructor] @@ -209,12 +209,14 @@ public void OnDocumentChanged(DocumentId documentId, SourceText text) public DocumentId GetDocumentId(string filePath) { + OnDocumentRequested(filePath); var documentIds = CurrentSolution.GetDocumentIdsWithFilePath(filePath); return documentIds.FirstOrDefault(); } public IEnumerable GetDocuments(string filePath) { + OnDocumentRequested(filePath); return CurrentSolution .GetDocumentIdsWithFilePath(filePath) .Select(id => CurrentSolution.GetDocument(id)); @@ -363,5 +365,29 @@ public override async Task LoadTextAndVersionAsync( return textAndVersion; } } + + private void OnDocumentRequested(string filePath) + { + EventHandler handler = DocumentRequested; + if (handler != null) + { + handler(this, new DocumentRequestedEventArgs(filePath)); + } + } + } + + public class DocumentRequestedEventArgs : EventArgs + { + public string DocumentPath { get; } + + public DocumentRequestedEventArgs(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentNullException(nameof(filePath)); + } + + DocumentPath = filePath; + } } } diff --git a/src/OmniSharp.Script/ScriptProjectSystem.cs b/src/OmniSharp.Script/ScriptProjectSystem.cs index 3f9737985d..2c91baf15d 100644 --- a/src/OmniSharp.Script/ScriptProjectSystem.cs +++ b/src/OmniSharp.Script/ScriptProjectSystem.cs @@ -180,5 +180,11 @@ Task IProjectSystem.GetWorkspaceModelAsync(WorkspaceInformationRequest r } return Task.FromResult(new ScriptContextModelCollection(scriptContextModels)); } + + public Task WaitForProjectsToLoadForFileAsync(string filePath) + { + // At the moment this project system doesn't support on demand projects loading + return Task.CompletedTask; + } } } diff --git a/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs b/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs index 780f51e85d..bf89dae691 100644 --- a/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs +++ b/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs @@ -89,6 +89,11 @@ public Task GetProjectModelAsync(string path) throw new NotImplementedException(); } + public Task WaitForProjectsToLoadForFileAsync(string filePath) + { + throw new NotImplementedException(); + } + public void Initalize(IConfiguration configuration) { } } From 81cea7384c513671488be100d8d85e1f511cc088 Mon Sep 17 00:00:00 2001 From: Dmitry Goncharenko Date: Fri, 2 Nov 2018 14:49:56 -0700 Subject: [PATCH 2/6] Allow on-demand load for projects outside of OmniSharpEnvironment.TargetDirectory --- src/OmniSharp.MSBuild/ProjectManager.cs | 13 +++++++------ src/OmniSharp.MSBuild/ProjectSystem.cs | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index 6bd6611079..75fe1f9175 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -41,7 +41,6 @@ public ProjectToUpdate(string filePath, bool allowAutoRestore) } private readonly ILogger _logger; - private readonly IOmniSharpEnvironment _environment; private readonly MSBuildOptions _options; private readonly IEventEmitter _eventEmitter; private readonly IFileSystemWatcher _fileSystemWatcher; @@ -62,8 +61,7 @@ public ProjectToUpdate(string filePath, bool allowAutoRestore) private readonly FileSystemNotificationCallback _onDirectoryFileChanged; - public ProjectManager(IOmniSharpEnvironment environment, - ILoggerFactory loggerFactory, + public ProjectManager(ILoggerFactory loggerFactory, MSBuildOptions options, IEventEmitter eventEmitter, IFileSystemWatcher fileSystemWatcher, @@ -73,7 +71,6 @@ public ProjectManager(IOmniSharpEnvironment environment, OmniSharpWorkspace workspace, ImmutableArray eventSinks) { - _environment = environment; _logger = loggerFactory.CreateLogger(); _options = options ?? new MSBuildOptions(); _eventEmitter = eventEmitter; @@ -101,9 +98,12 @@ public ProjectManager(IOmniSharpEnvironment environment, private void OnWorkspaceDocumentRequested(object sender, DocumentRequestedEventArgs args) { - // Search and queue for loading C# projects that are likely to reference the requested file + // Search and queue for loading C# projects that are likely to reference the requested file. + // C# source files are located pretty much always in the same folder with project file that is referencing them or in a subfolder. + // Do not limit search by OmniSharpEnvironment.TargetDirectory to allow for loading on demand projects that are outside of it, + // i.e. in VSCode case potentially outside of the folder opened in the IDE. string projectDir = Path.GetDirectoryName(args.DocumentPath); - while(projectDir != null && projectDir.StartsWith(_environment.TargetDirectory.TrimEnd(Path.DirectorySeparatorChar))) + while(projectDir != null) { List csProjFiles = Directory.EnumerateFiles(projectDir, "*.csproj", SearchOption.TopDirectoryOnly).ToList(); if (csProjFiles.Count > 0) @@ -137,6 +137,7 @@ protected override void DisposeCore(bool disposing) public void QueueProjectUpdate(string projectFilePath, bool allowAutoRestore) { + _logger.LogInformation($"Queue project update for '{projectFilePath}'"); _queue.Post(new ProjectToUpdate(projectFilePath, allowAutoRestore)); } diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs index 8a8756075f..0177c7d603 100644 --- a/src/OmniSharp.MSBuild/ProjectSystem.cs +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -98,7 +98,7 @@ public void Initalize(IConfiguration configuration) _packageDependencyChecker = new PackageDependencyChecker(_loggerFactory, _eventEmitter, _dotNetCli, _options); _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory, _sdksPathResolver); - _manager = new ProjectManager(_environment, _loggerFactory, _options, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, + _manager = new ProjectManager(_loggerFactory, _options, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, _loader, _workspace, _eventSinks); if (_options.OnDemandProjectsLoad) From 17d1bf9d730e4e56459c1ef1ceae3ac787910bbe Mon Sep 17 00:00:00 2001 From: Dmitry Goncharenko Date: Fri, 2 Nov 2018 20:45:06 -0700 Subject: [PATCH 3/6] Added UTs --- src/OmniSharp.Roslyn/OmniSharpWorkspace.cs | 6 +- .../AbstractMSBuildTestFixture.cs | 6 +- .../OnDemandProjectsLoadTests.cs | 92 +++++++++++++++++++ 3 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index 4431d9d0b8..01bd0c7836 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -368,11 +368,7 @@ public override async Task LoadTextAndVersionAsync( private void OnDocumentRequested(string filePath) { - EventHandler handler = DocumentRequested; - if (handler != null) - { - handler(this, new DocumentRequestedEventArgs(filePath)); - } + DocumentRequested?.Invoke(this, new DocumentRequestedEventArgs(filePath)); } } diff --git a/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs b/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs index 0029e03549..407066f1ce 100644 --- a/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs +++ b/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs @@ -30,10 +30,12 @@ public void Dispose() (_msbuildLocator as IDisposable)?.Dispose(); } - protected OmniSharpTestHost CreateMSBuildTestHost(string path, IEnumerable additionalExports = null) + protected OmniSharpTestHost CreateMSBuildTestHost(string path, IEnumerable additionalExports = null, + IEnumerable> configurationData = null) { var environment = new OmniSharpEnvironment(path, logLevel: LogLevel.Trace); - var serviceProvider = TestServiceProvider.Create(this.TestOutput, environment, this.LoggerFactory, _assemblyLoader, _msbuildLocator); + var serviceProvider = TestServiceProvider.Create(this.TestOutput, environment, this.LoggerFactory, _assemblyLoader, _msbuildLocator, + configurationData); return OmniSharpTestHost.Create(serviceProvider, additionalExports); } diff --git a/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs b/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs new file mode 100644 index 0000000000..74d6ed0bdd --- /dev/null +++ b/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using OmniSharp.Models; +using OmniSharp.Models.MembersTree; +using OmniSharp.Models.V2.CodeActions; +using OmniSharp.MSBuild.Models; +using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; +using OmniSharp.Roslyn.CSharp.Services.Structure; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.MSBuild.Tests +{ + public class OnDemandProjectsLoadTests : AbstractMSBuildTestFixture + { + public OnDemandProjectsLoadTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public async Task LoadProjectsOnDemandOneByOne() + { + var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) + using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) + { + MSBuildWorkspaceInfo workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + // Expect empty workspace initially since no documents have been requested yet + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(0, workspaceInfo.Projects.Count); + + // Requesting library document should load only that project + GetCodeActionsService codeActionHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.GetCodeActions); + GetCodeActionsResponse codeActionResponse = await codeActionHandler.Handle( + new GetCodeActionsRequest { FileName = Path.Combine(testProject.Directory, "Lib", "Class1.cs") }); + workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + Assert.NotNull(codeActionResponse); + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(1, workspaceInfo.Projects.Count); + Assert.Equal("Lib.csproj", Path.GetFileName(workspaceInfo.Projects[0].Path)); + + // Requesting app document should load that project as well + QuickFixResponse codeCheckResponse = await host.RequestCodeCheckAsync(Path.Combine(testProject.Directory, "App", "Program.cs")); + workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + Assert.NotNull(codeCheckResponse); + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(2, workspaceInfo.Projects.Count); + Assert.Equal("App.csproj", Path.GetFileName(workspaceInfo.Projects[0].Path)); + Assert.Equal("Lib.csproj", Path.GetFileName(workspaceInfo.Projects[1].Path)); + } + } + + [Fact] + public async Task LoadProjectAndItsReferenceOnDemand() + { + var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) + using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) + { + MSBuildWorkspaceInfo workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + // Expect empty workspace initially since no documents have been requested yet + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(0, workspaceInfo.Projects.Count); + + // Requesting app document should load both projects + MembersAsTreeService membersAsTreeService = host.GetRequestHandler(OmniSharpEndpoints.MembersTree); + var request = new MembersTreeRequest { FileName = Path.Combine(testProject.Directory, "App", "Program.cs") }; + FileMemberTree response = await membersAsTreeService.Handle(request); + workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + Assert.NotNull(request); + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(2, workspaceInfo.Projects.Count); + Assert.Equal("App.csproj", Path.GetFileName(workspaceInfo.Projects[0].Path)); + Assert.Equal("Lib.csproj", Path.GetFileName(workspaceInfo.Projects[1].Path)); + } + } + } +} From 0ee5c3b81e317077219e40f4c2885e8b1af59d06 Mon Sep 17 00:00:00 2001 From: Dmitry Goncharenko Date: Tue, 4 Dec 2018 20:54:48 -0800 Subject: [PATCH 4/6] Per CR feedback switched to GetDocument*FromFullProjectModelAsync; added MSBuildOptions.OnDemandProjectsLoadSearchStopsAt --- .../Services/IProjectSystem.cs | 6 -- src/OmniSharp.Cake/CakeProjectSystem.cs | 6 -- src/OmniSharp.DotNet/DotNetProjectSystem.cs | 6 -- .../Options/MSBuildOptions.cs | 7 +- src/OmniSharp.MSBuild/ProjectManager.cs | 33 +++++++-- src/OmniSharp.MSBuild/ProjectSystem.cs | 12 ---- src/OmniSharp.Plugins/Plugin.cs | 6 -- .../Helpers/ProjectSystemExtensions.cs | 30 -------- .../Services/Buffer/UpdateBufferService.cs | 13 +--- .../Services/Diagnostics/CodeCheckService.cs | 12 +--- .../Services/Navigation/FindUsagesService.cs | 12 +--- .../Refactoring/V2/BaseCodeActionService.cs | 3 +- .../Refactoring/V2/GetCodeActionsService.cs | 9 --- .../Structure/BlockStructureService.cs | 13 ++-- .../Structure/CodeStructureService.cs | 12 +--- .../Structure/MembersAsTreeService.cs | 13 +--- src/OmniSharp.Roslyn/BufferManager.cs | 20 +++++- src/OmniSharp.Roslyn/OmniSharpWorkspace.cs | 48 +++++++------ src/OmniSharp.Script/ScriptProjectSystem.cs | 6 -- .../EndpointMiddlewareFacts.cs | 5 -- .../OnDemandProjectsLoadTests.cs | 70 +++++++++++++++++++ 21 files changed, 171 insertions(+), 171 deletions(-) delete mode 100644 src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs diff --git a/src/OmniSharp.Abstractions/Services/IProjectSystem.cs b/src/OmniSharp.Abstractions/Services/IProjectSystem.cs index 31291f969a..ab459e2bb2 100644 --- a/src/OmniSharp.Abstractions/Services/IProjectSystem.cs +++ b/src/OmniSharp.Abstractions/Services/IProjectSystem.cs @@ -29,11 +29,5 @@ public interface IProjectSystem /// The file path to the project to retrieve. Alternatively, /// a file path to a document within a proejct may be specified. Task GetProjectModelAsync(string filePath); - - /// - /// This project system will try to locate projects that reference given file and will wait until the projects and their references are fully loaded. - /// - /// File for which to locate and load projects. - Task WaitForProjectsToLoadForFileAsync(string filePath); } } diff --git a/src/OmniSharp.Cake/CakeProjectSystem.cs b/src/OmniSharp.Cake/CakeProjectSystem.cs index 4d1322b49b..63d1b60860 100644 --- a/src/OmniSharp.Cake/CakeProjectSystem.cs +++ b/src/OmniSharp.Cake/CakeProjectSystem.cs @@ -258,12 +258,6 @@ public Task GetProjectModelAsync(string filePath) return Task.FromResult(new CakeContextModel(filePath)); } - public Task WaitForProjectsToLoadForFileAsync(string filePath) - { - // At the moment this project system doesn't support on demand projects loading - return Task.CompletedTask; - } - private ProjectInfo GetProjectFileInfo(string path) { return !_projects.TryGetValue(path, out ProjectInfo projectFileInfo) ? null : projectFileInfo; diff --git a/src/OmniSharp.DotNet/DotNetProjectSystem.cs b/src/OmniSharp.DotNet/DotNetProjectSystem.cs index 33d0b56f1f..796deda7dc 100644 --- a/src/OmniSharp.DotNet/DotNetProjectSystem.cs +++ b/src/OmniSharp.DotNet/DotNetProjectSystem.cs @@ -152,12 +152,6 @@ public void Update(bool allowRestore) } } - public Task WaitForProjectsToLoadForFileAsync(string filePath) - { - // At the moment this project system doesn't support on demand projects loading - return Task.CompletedTask; - } - private void UpdateProject(string projectDirectory) { _logger.LogInformation($"Update project {projectDirectory}"); diff --git a/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs b/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs index 6c2dee512d..d693731864 100644 --- a/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs +++ b/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs @@ -8,12 +8,17 @@ internal class MSBuildOptions public string Platform { get; set; } public bool EnablePackageAutoRestore { get; set; } - /// + /// /// If true, MSBuild project system will only be loading projects for files that were opened in the editor /// as well as referenced projects, recursively. /// public bool OnDemandProjectsLoad { get; set; } + /// + /// Search for a .csproj to load on demand is stopped after a folder containing a file or folder from the list is encountered. + /// + public string[] OnDemandProjectsLoadSearchStopsAt { get; set; } = new[] { ".git" }; + /// /// When set to true, the MSBuild project system will attempt to resolve the path to the MSBuild /// SDKs for a project by running 'dotnet --info' and retrieving the path. This is only needed diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index 75fe1f9175..51f663ead7 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -92,17 +92,17 @@ public ProjectManager(ILoggerFactory loggerFactory, if (_options.OnDemandProjectsLoad) { - _workspace.DocumentRequested += OnWorkspaceDocumentRequested; + _workspace.AddWaitForProjectModelReadyEventHandler(WaitForProjectModelReadyAsync); } } - private void OnWorkspaceDocumentRequested(object sender, DocumentRequestedEventArgs args) + private async Task WaitForProjectModelReadyAsync(string documentPath) { // Search and queue for loading C# projects that are likely to reference the requested file. // C# source files are located pretty much always in the same folder with project file that is referencing them or in a subfolder. // Do not limit search by OmniSharpEnvironment.TargetDirectory to allow for loading on demand projects that are outside of it, // i.e. in VSCode case potentially outside of the folder opened in the IDE. - string projectDir = Path.GetDirectoryName(args.DocumentPath); + string projectDir = Path.GetDirectoryName(documentPath); while(projectDir != null) { List csProjFiles = Directory.EnumerateFiles(projectDir, "*.csproj", SearchOption.TopDirectoryOnly).ToList(); @@ -115,10 +115,35 @@ private void OnWorkspaceDocumentRequested(object sender, DocumentRequestedEventA QueueProjectUpdate(csProjFile, allowAutoRestore:true); } } - return; + + break; } + + bool foundStopAtItem = false; + foreach (string stopAtItemName in _options.OnDemandProjectsLoadSearchStopsAt) + { + string itemPath = Path.Combine(projectDir, stopAtItemName); + if (File.Exists(itemPath) || Directory.Exists(itemPath)) + { + foundStopAtItem = true; + break; + } + } + + if (foundStopAtItem) + { + _logger.LogTrace($"Couldn't find project to load for '{documentPath}'"); + break; + } + projectDir = Path.GetDirectoryName(projectDir); } + + // Wait for all queued projects to load to ensure that workspace is fully up to date before this method completes. + // If the project for the document was loaded before and there are no other projects to load at the moment, the call below will be no-op. + _logger.LogTrace($"Started waiting for projects queue to be empty when requested '{documentPath}'"); + await WaitForQueueEmptyAsync(); + _logger.LogTrace($"Stopped waiting for projects queue to be empty when requested '{documentPath}'"); } protected override void DisposeCore(bool disposing) diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs index 3da163e6e5..e6a8302590 100644 --- a/src/OmniSharp.MSBuild/ProjectSystem.cs +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -225,17 +225,5 @@ async Task IProjectSystem.GetProjectModelAsync(string filePath) return new MSBuildProjectInfo(projectFileInfo); } - - public async Task WaitForProjectsToLoadForFileAsync(string filePath) - { - if (_options.OnDemandProjectsLoad) - { - // Request the document only to make sure that projects referencing it are queued for loading by the project system. - _workspace.GetDocument(filePath); - // Wait for all queued projects to load to ensure that workspace is fully up to date before this method completes. - // If the project for the document was loaded before and there are no other projects to load at the moment, the call below will be no-op. - await _manager.WaitForQueueEmptyAsync(); - } - } } } diff --git a/src/OmniSharp.Plugins/Plugin.cs b/src/OmniSharp.Plugins/Plugin.cs index 6dc4c39c99..4e13c6be07 100644 --- a/src/OmniSharp.Plugins/Plugin.cs +++ b/src/OmniSharp.Plugins/Plugin.cs @@ -115,11 +115,5 @@ public Task GetProjectModelAsync(string filePath) // TODO: Call out to process return Task.FromResult(null); } - - public Task WaitForProjectsToLoadForFileAsync(string filePath) - { - // TODO: Call out to process - return Task.CompletedTask; - } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs deleted file mode 100644 index c158069a31..0000000000 --- a/src/OmniSharp.Roslyn.CSharp/Helpers/ProjectSystemExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using OmniSharp.Services; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace OmniSharp.Helpers -{ - public static class ProjectSystemExtensions - { - public static async Task WaitForAllProjectsToLoadForFileAsync(this IEnumerable projectSystems, string filePath) - { - if (filePath != null) - { - await Task.WhenAll(GetProjectSystemsForFile(projectSystems, filePath).Select(ps => ps.WaitForProjectsToLoadForFileAsync(filePath))); - } - } - - private static IEnumerable GetProjectSystemsForFile(IEnumerable projectSystems, string filePath) - { - foreach (IProjectSystem projectSystem in projectSystems) - { - if (projectSystem.Extensions.Any(extension => filePath.EndsWith(extension, StringComparison.OrdinalIgnoreCase))) - { - yield return projectSystem; - } - } - } - } -} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs index 5de1821153..2b9634042e 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; using System.Composition; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.UpdateBuffer; -using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Buffer { @@ -13,21 +10,17 @@ namespace OmniSharp.Roslyn.CSharp.Services.Buffer public class UpdateBufferService : IRequestHandler { private OmniSharpWorkspace _workspace; - private readonly IEnumerable _projectSystems; [ImportingConstructor] - public UpdateBufferService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) + public UpdateBufferService(OmniSharpWorkspace workspace) { _workspace = workspace; - _projectSystems = projectSystems; } public async Task Handle(UpdateBufferRequest request) { - // Waiting until the document is fully formed in memory (for project systems that have this ability) - // before applying updates to it helps to reduce chances for the compiler getting confused and producing invalid compilation errors. - await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); - await _workspace.BufferManager.UpdateBufferAsync(request); + // To properly handle the request wait until all projects are loaded. + await _workspace.BufferManager.UpdateBufferInFullProjectModelAsync(request); return true; } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs index 38d1c32c79..c61a07b2ab 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs @@ -7,7 +7,6 @@ using OmniSharp.Mef; using OmniSharp.Models; using OmniSharp.Models.CodeCheck; -using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics { @@ -15,23 +14,18 @@ namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics public class CodeCheckService : IRequestHandler { private OmniSharpWorkspace _workspace; - private readonly IEnumerable _projectSystems; [ImportingConstructor] - public CodeCheckService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) + public CodeCheckService(OmniSharpWorkspace workspace) { _workspace = workspace; - _projectSystems = projectSystems; } public async Task Handle(CodeCheckRequest request) { - // Waiting until the document is fully formed in memory (for project systems that have this ability) - // helps to reduce chances of returning invalid list of errors while compilation is still in progress. - await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); - var documents = request.FileName != null - ? _workspace.GetDocuments(request.FileName) + // To properly handle the request wait until all projects are loaded. + ? await _workspace.GetDocumentsFromFullProjectModelAsync(request.FileName) : _workspace.CurrentSolution.Projects.SelectMany(project => project.Documents); var quickFixes = await documents.FindDiagnosticLocationsAsync(_workspace); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs index 9d9b3819f4..f7b3b9824c 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindUsagesService.cs @@ -10,7 +10,6 @@ using OmniSharp.Mef; using OmniSharp.Models; using OmniSharp.Models.FindUsages; -using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Navigation { @@ -18,22 +17,17 @@ namespace OmniSharp.Roslyn.CSharp.Services.Navigation public class FindUsagesService : IRequestHandler { private readonly OmniSharpWorkspace _workspace; - private readonly IEnumerable _projectSystems; [ImportingConstructor] - public FindUsagesService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) + public FindUsagesService(OmniSharpWorkspace workspace) { _workspace = workspace; - _projectSystems = projectSystems; } public async Task Handle(FindUsagesRequest request) { - // Waiting until all projects relevant to the document are fully loaded (for project systems that have this ability) - // helps to produce more complete list of usages for a symbol. - await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); - var document = _workspace.GetDocument(request.FileName); - + // To produce complete list of usages for symbols in the document wait until all projects are loaded. + var document = await _workspace.GetDocumentFromFullProjectModelAsync(request.FileName); var response = new QuickFixResponse(); if (document != null) { diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs index 9d74803837..c8598a6192 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs @@ -62,7 +62,8 @@ protected BaseCodeActionService(OmniSharpWorkspace workspace, CodeActionHelper h protected async Task> GetAvailableCodeActions(ICodeActionRequest request) { - var document = this.Workspace.GetDocument(request.FileName); + // To produce a complete list of code actions for the document wait until all projects are loaded. + var document = await this.Workspace.GetDocumentFromFullProjectModelAsync(request.FileName); if (document == null) { return Array.Empty(); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs index 447cd45c5e..efc47cdb3a 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; -using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.V2.CodeActions; using OmniSharp.Roslyn.CSharp.Services.CodeActions; @@ -15,26 +14,18 @@ namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 [OmniSharpHandler(OmniSharpEndpoints.V2.GetCodeActions, LanguageNames.CSharp)] public class GetCodeActionsService : BaseCodeActionService { - private readonly IEnumerable _projectSystems; - [ImportingConstructor] public GetCodeActionsService( OmniSharpWorkspace workspace, CodeActionHelper helper, [ImportMany] IEnumerable providers, - [ImportMany] IEnumerable projectSystems, ILoggerFactory loggerFactory) : base(workspace, helper, providers, loggerFactory.CreateLogger()) { - _projectSystems = projectSystems; } public override async Task Handle(GetCodeActionsRequest request) { - // Waiting until the document is fully formed in memory (for project systems that have this ability) - // helps to reduce chances of returning invalid list of code actions while compilation is still in progress. - await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); - var availableActions = await GetAvailableCodeActions(request); return new GetCodeActionsResponse diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs index 58cb2df70d..e26e2c9d01 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Structure/BlockStructureService.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions; -using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.V2; using OmniSharp.Services; @@ -30,15 +29,13 @@ public class BlockStructureService : IRequestHandler _projectSystems; [ImportingConstructor] - public BlockStructureService(IAssemblyLoader loader, OmniSharpWorkspace workspace, [ImportMany] IEnumerable projectSystems) + public BlockStructureService(IAssemblyLoader loader, OmniSharpWorkspace workspace) { _workspace = workspace; _loader = loader; _featureAssembly = _loader.LazyLoad(Configuration.RoslynFeatures); - _projectSystems = projectSystems; _blockStructureService = _featureAssembly.LazyGetType("Microsoft.CodeAnalysis.Structure.BlockStructureService"); _blockStructure = _featureAssembly.LazyGetType("Microsoft.CodeAnalysis.Structure.BlockStructure"); @@ -53,13 +50,11 @@ public BlockStructureService(IAssemblyLoader loader, OmniSharpWorkspace workspac public async Task Handle(BlockStructureRequest request) { - // Waiting until the document is fully formed in memory (for project systems that have this ability) - // helps to reduce chances of returning invalid document block structure while compilation is still in progress. - await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); - var document = _workspace.GetDocument(request.FileName); + // To provide complete code structure for the document wait until all projects are loaded. + var document = await _workspace.GetDocumentFromFullProjectModelAsync(request.FileName); if (document == null) { - return new BlockStructureResponse(); + return null; } var text = await document.GetTextAsync(); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs index 936a16195f..e3d0b661e6 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Structure/CodeStructureService.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions; -using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.V2; using OmniSharp.Models.V2.CodeStructure; @@ -21,25 +20,20 @@ public class CodeStructureService : IRequestHandler _propertyProviders; - private readonly IEnumerable _projectSystems; [ImportingConstructor] public CodeStructureService( OmniSharpWorkspace workspace, - [ImportMany] IEnumerable propertyProviders, - [ImportMany] IEnumerable projectSystems) + [ImportMany] IEnumerable propertyProviders) { _workspace = workspace; _propertyProviders = propertyProviders; - _projectSystems = projectSystems; } public async Task Handle(CodeStructureRequest request) { - // Waiting until the document is fully formed in memory (for project systems that have this ability) - // helps to reduce chances of returning invalid/incomplete document code structure while compilation is still in progress. - await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); - var document = _workspace.GetDocument(request.FileName); + // To provide complete code structure for the document wait until all projects are loaded. + var document = await _workspace.GetDocumentFromFullProjectModelAsync(request.FileName); if (document == null) { return null; diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs index 33db00b630..489e07c239 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Structure/MembersAsTreeService.cs @@ -3,10 +3,8 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using OmniSharp.Abstractions.Services; -using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.MembersTree; -using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Structure { @@ -15,25 +13,20 @@ public class MembersAsTreeService : IRequestHandler _discovers; - private readonly IEnumerable _projectSystems; [ImportingConstructor] - public MembersAsTreeService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable featureDiscovers, [ImportMany] IEnumerable projectSystems) + public MembersAsTreeService(OmniSharpWorkspace workspace, [ImportMany] IEnumerable featureDiscovers) { _workspace = workspace; _discovers = featureDiscovers; - _projectSystems = projectSystems; } public async Task Handle(MembersTreeRequest request) { - // Waiting until the document is fully formed in memory (for project systems that have this ability) - // helps to reduce chances of returning invalid/incomplete member structure for the document while compilation is still in progress. - await _projectSystems.WaitForAllProjectsToLoadForFileAsync(request.FileName); - return new FileMemberTree() { - TopLevelTypeDefinitions = await StructureComputer.Compute(_workspace.GetDocuments(request.FileName), _discovers) + // To provide complete members tree for the document wait until all projects are loaded. + TopLevelTypeDefinitions = await StructureComputer.Compute(await _workspace.GetDocumentsFromFullProjectModelAsync(request.FileName), _discovers) }; } } diff --git a/src/OmniSharp.Roslyn/BufferManager.cs b/src/OmniSharp.Roslyn/BufferManager.cs index 07a3b5e91e..0c97851870 100644 --- a/src/OmniSharp.Roslyn/BufferManager.cs +++ b/src/OmniSharp.Roslyn/BufferManager.cs @@ -29,7 +29,17 @@ public BufferManager(OmniSharpWorkspace workspace, IFileSystemWatcher fileSystem _onFileChanged = OnFileChanged; } - public async Task UpdateBufferAsync(Request request) + public Task UpdateBufferInFullProjectModelAsync(Request request) + { + return UpdateBufferCoreAsync(request, inFullProjectModel: true); + } + + public Task UpdateBufferAsync(Request request) + { + return UpdateBufferCoreAsync(request, inFullProjectModel: false); + } + + private async Task UpdateBufferCoreAsync(Request request, bool inFullProjectModel) { var buffer = request.Buffer; var changes = request.Changes; @@ -46,8 +56,12 @@ public async Task UpdateBufferAsync(Request request) var solution = _workspace.CurrentSolution; - var documentIds = solution.GetDocumentIdsWithFilePath(request.FileName); - if (!documentIds.IsEmpty) + // To properly update the buffer wait until all projects are loaded. + var documentIds = inFullProjectModel ? + await _workspace.GetDocumentIdsWithFilePathFromFullProjectModelAsync(request.FileName) : + solution.GetDocumentIdsWithFilePath(request.FileName); + + if (documentIds.Any()) { if (changes == null) { diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index 01bd0c7836..b115639e4a 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -24,7 +24,9 @@ public class OmniSharpWorkspace : Workspace public BufferManager BufferManager { get; private set; } private readonly ILogger _logger; - public event EventHandler DocumentRequested; + + private readonly ConcurrentBag> _waitForProjectModelReadyEventHandlers = new ConcurrentBag>(); + private readonly ConcurrentDictionary miscDocumentsProjectInfos = new ConcurrentDictionary(); [ImportingConstructor] @@ -37,6 +39,11 @@ public OmniSharpWorkspace(HostServicesAggregator aggregator, ILoggerFactory logg public override bool CanOpenDocuments => true; + public void AddWaitForProjectModelReadyEventHandler(Func eventHandler) + { + _waitForProjectModelReadyEventHandlers.Add(eventHandler); + } + public override void OpenDocument(DocumentId documentId, bool activate = true) { var doc = this.CurrentSolution.GetDocument(documentId); @@ -209,14 +216,12 @@ public void OnDocumentChanged(DocumentId documentId, SourceText text) public DocumentId GetDocumentId(string filePath) { - OnDocumentRequested(filePath); var documentIds = CurrentSolution.GetDocumentIdsWithFilePath(filePath); return documentIds.FirstOrDefault(); } public IEnumerable GetDocuments(string filePath) { - OnDocumentRequested(filePath); return CurrentSolution .GetDocumentIdsWithFilePath(filePath) .Select(id => CurrentSolution.GetDocument(id)); @@ -235,6 +240,24 @@ public Document GetDocument(string filePath) return CurrentSolution.GetDocument(documentId); } + public async Task> GetDocumentsFromFullProjectModelAsync(string filePath) + { + await OnWaitForProjectModelReadyAsync(filePath); + return GetDocuments(filePath); + } + + public async Task GetDocumentFromFullProjectModelAsync(string filePath) + { + await OnWaitForProjectModelReadyAsync(filePath); + return GetDocument(filePath); + } + + public async Task> GetDocumentIdsWithFilePathFromFullProjectModelAsync(string filePath) + { + await OnWaitForProjectModelReadyAsync(filePath); + return CurrentSolution.GetDocumentIdsWithFilePath(filePath); + } + public override bool CanApplyChange(ApplyChangesKind feature) { return true; @@ -366,24 +389,9 @@ public override async Task LoadTextAndVersionAsync( } } - private void OnDocumentRequested(string filePath) + private Task OnWaitForProjectModelReadyAsync(string filePath) { - DocumentRequested?.Invoke(this, new DocumentRequestedEventArgs(filePath)); + return Task.WhenAll(_waitForProjectModelReadyEventHandlers.Select(h => h(filePath))); } } - - public class DocumentRequestedEventArgs : EventArgs - { - public string DocumentPath { get; } - - public DocumentRequestedEventArgs(string filePath) - { - if (string.IsNullOrWhiteSpace(filePath)) - { - throw new ArgumentNullException(nameof(filePath)); - } - - DocumentPath = filePath; - } - } } diff --git a/src/OmniSharp.Script/ScriptProjectSystem.cs b/src/OmniSharp.Script/ScriptProjectSystem.cs index 2c91baf15d..3f9737985d 100644 --- a/src/OmniSharp.Script/ScriptProjectSystem.cs +++ b/src/OmniSharp.Script/ScriptProjectSystem.cs @@ -180,11 +180,5 @@ Task IProjectSystem.GetWorkspaceModelAsync(WorkspaceInformationRequest r } return Task.FromResult(new ScriptContextModelCollection(scriptContextModels)); } - - public Task WaitForProjectsToLoadForFileAsync(string filePath) - { - // At the moment this project system doesn't support on demand projects loading - return Task.CompletedTask; - } } } diff --git a/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs b/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs index bf89dae691..780f51e85d 100644 --- a/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs +++ b/tests/OmniSharp.Http.Tests/EndpointMiddlewareFacts.cs @@ -89,11 +89,6 @@ public Task GetProjectModelAsync(string path) throw new NotImplementedException(); } - public Task WaitForProjectsToLoadForFileAsync(string filePath) - { - throw new NotImplementedException(); - } - public void Initalize(IConfiguration configuration) { } } diff --git a/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs b/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs index 74d6ed0bdd..429a7f2576 100644 --- a/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs @@ -4,6 +4,7 @@ using OmniSharp.Models; using OmniSharp.Models.MembersTree; using OmniSharp.Models.V2.CodeActions; +using OmniSharp.Models.V2.CodeStructure; using OmniSharp.MSBuild.Models; using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; @@ -88,5 +89,74 @@ public async Task LoadProjectAndItsReferenceOnDemand() Assert.Equal("Lib.csproj", Path.GetFileName(workspaceInfo.Projects[1].Path)); } } + + [Fact] + public async Task OnDemandProjectsLoadSearchStopsAtFile() + { + var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) + using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) + { + MSBuildWorkspaceInfo workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + // Expect empty workspace initially since no documents have been requested yet + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(0, workspaceInfo.Projects.Count); + + // Create a subfolder containing both "stop-search-at" marker file ('.git') and the file to load + string stopAtFolder = Path.Combine(testProject.Directory, "App", "StopHere"); + Directory.CreateDirectory(stopAtFolder); + string fileToRequest = Path.Combine(stopAtFolder, "Empty.cs"); + File.WriteAllText(fileToRequest, "class empty {}"); + File.WriteAllText(Path.Combine(stopAtFolder, ".git"), string.Empty); + + // Requesting a file should should stop search since it will find stop-search at file in the same folder + CodeStructureService service = host.GetRequestHandler(OmniSharpEndpoints.V2.CodeStructure); + var request = new CodeStructureRequest { FileName = fileToRequest }; + CodeStructureResponse response = await service.Handle(request); + workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + // No project is expected to be found/loaded + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(0, workspaceInfo.Projects.Count); + } + } + + [Fact] + public async Task OnDemandProjectsLoadSearchStopsAtFolder() + { + var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) + using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) + { + MSBuildWorkspaceInfo workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + // Expect empty workspace initially since no documents have been requested yet + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(0, workspaceInfo.Projects.Count); + + // Create a subfolder containing "stop-search-at" marker folder '.git' + Directory.CreateDirectory(Path.Combine(testProject.Directory, "App", "StopHere", ".git")); + + string fileToRequestFolder = Path.Combine(testProject.Directory, "App", "StopHere", "EmptyClassLib"); + Directory.CreateDirectory(fileToRequestFolder); + string fileToRequest = Path.Combine(fileToRequestFolder, "Empty.cs"); + File.WriteAllText(fileToRequest, "class empty {}"); + + // Requesting a file should should stop search since it will find stop-search at file in the same folder + CodeStructureService service = host.GetRequestHandler(OmniSharpEndpoints.V2.CodeStructure); + var request = new CodeStructureRequest { FileName = fileToRequest }; + CodeStructureResponse response = await service.Handle(request); + workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); + + // No project is expected to be found/loaded + Assert.Null(workspaceInfo.SolutionPath); + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(0, workspaceInfo.Projects.Count); + } + } } } From aa0d7d3780384b831c20f62dcf028ed103733681 Mon Sep 17 00:00:00 2001 From: Dmitry Goncharenko Date: Sun, 9 Dec 2018 21:29:28 -0800 Subject: [PATCH 5/6] Revert changes for UpdateBufferService --- .../Services/Buffer/UpdateBufferService.cs | 3 +-- src/OmniSharp.Roslyn/BufferManager.cs | 20 +++---------------- src/OmniSharp.Roslyn/OmniSharpWorkspace.cs | 6 ------ 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs index 2b9634042e..d980c2d569 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Buffer/UpdateBufferService.cs @@ -19,8 +19,7 @@ public UpdateBufferService(OmniSharpWorkspace workspace) public async Task Handle(UpdateBufferRequest request) { - // To properly handle the request wait until all projects are loaded. - await _workspace.BufferManager.UpdateBufferInFullProjectModelAsync(request); + await _workspace.BufferManager.UpdateBufferAsync(request); return true; } } diff --git a/src/OmniSharp.Roslyn/BufferManager.cs b/src/OmniSharp.Roslyn/BufferManager.cs index 0c97851870..07a3b5e91e 100644 --- a/src/OmniSharp.Roslyn/BufferManager.cs +++ b/src/OmniSharp.Roslyn/BufferManager.cs @@ -29,17 +29,7 @@ public BufferManager(OmniSharpWorkspace workspace, IFileSystemWatcher fileSystem _onFileChanged = OnFileChanged; } - public Task UpdateBufferInFullProjectModelAsync(Request request) - { - return UpdateBufferCoreAsync(request, inFullProjectModel: true); - } - - public Task UpdateBufferAsync(Request request) - { - return UpdateBufferCoreAsync(request, inFullProjectModel: false); - } - - private async Task UpdateBufferCoreAsync(Request request, bool inFullProjectModel) + public async Task UpdateBufferAsync(Request request) { var buffer = request.Buffer; var changes = request.Changes; @@ -56,12 +46,8 @@ private async Task UpdateBufferCoreAsync(Request request, bool inFullProjectMode var solution = _workspace.CurrentSolution; - // To properly update the buffer wait until all projects are loaded. - var documentIds = inFullProjectModel ? - await _workspace.GetDocumentIdsWithFilePathFromFullProjectModelAsync(request.FileName) : - solution.GetDocumentIdsWithFilePath(request.FileName); - - if (documentIds.Any()) + var documentIds = solution.GetDocumentIdsWithFilePath(request.FileName); + if (!documentIds.IsEmpty) { if (changes == null) { diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index b115639e4a..db0654e8ec 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -252,12 +252,6 @@ public async Task GetDocumentFromFullProjectModelAsync(string filePath return GetDocument(filePath); } - public async Task> GetDocumentIdsWithFilePathFromFullProjectModelAsync(string filePath) - { - await OnWaitForProjectModelReadyAsync(filePath); - return CurrentSolution.GetDocumentIdsWithFilePath(filePath); - } - public override bool CanApplyChange(ApplyChangesKind feature) { return true; From f5f44e41b86a829fbfbc9fa2754b38b092f9dfc6 Mon Sep 17 00:00:00 2001 From: Dmitry Goncharenko Date: Sun, 16 Dec 2018 18:44:44 -0800 Subject: [PATCH 6/6] Addressed more PR feedback --- .../Options/MSBuildOptions.cs | 7 +- src/OmniSharp.MSBuild/ProjectManager.cs | 37 +++------ src/OmniSharp.MSBuild/ProjectSystem.cs | 4 +- src/OmniSharp.Roslyn/OmniSharpWorkspace.cs | 8 +- .../App/App.csproj | 12 +++ .../App/Program.cs | 12 +++ .../DeepProjectTransitiveReference.sln | 64 ++++++++++++++++ .../Lib1/Class1.cs | 8 ++ .../Lib1/Lib1.csproj | 11 +++ .../Lib2/Class2.cs | 8 ++ .../Lib2/Lib2.csproj | 7 ++ ...dTests.cs => LoadProjectsOnDemandTests.cs} | 75 +++++-------------- 12 files changed, 158 insertions(+), 95 deletions(-) create mode 100644 test-assets/test-projects/DeepProjectTransitiveReference/App/App.csproj create mode 100644 test-assets/test-projects/DeepProjectTransitiveReference/App/Program.cs create mode 100644 test-assets/test-projects/DeepProjectTransitiveReference/DeepProjectTransitiveReference.sln create mode 100644 test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Class1.cs create mode 100644 test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Lib1.csproj create mode 100644 test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Class2.cs create mode 100644 test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Lib2.csproj rename tests/OmniSharp.MSBuild.Tests/{OnDemandProjectsLoadTests.cs => LoadProjectsOnDemandTests.cs} (58%) diff --git a/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs b/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs index d693731864..bf03468862 100644 --- a/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs +++ b/src/OmniSharp.MSBuild/Options/MSBuildOptions.cs @@ -12,12 +12,7 @@ internal class MSBuildOptions /// If true, MSBuild project system will only be loading projects for files that were opened in the editor /// as well as referenced projects, recursively. /// - public bool OnDemandProjectsLoad { get; set; } - - /// - /// Search for a .csproj to load on demand is stopped after a folder containing a file or folder from the list is encountered. - /// - public string[] OnDemandProjectsLoadSearchStopsAt { get; set; } = new[] { ".git" }; + public bool LoadProjectsOnDemand { get; set; } /// /// When set to true, the MSBuild project system will attempt to resolve the path to the MSBuild diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index 1b94ff6481..259bbd7cc5 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -68,7 +68,7 @@ public ProjectManager(ILoggerFactory loggerFactory, MetadataFileReferenceCache metadataFileReferenceCache, PackageDependencyChecker packageDependencyChecker, ProjectLoader projectLoader, - OmniSharpWorkspace workspace, + OmniSharpWorkspace workspace, ImmutableArray eventSinks) { _logger = loggerFactory.CreateLogger(); @@ -90,22 +90,24 @@ public ProjectManager(ILoggerFactory loggerFactory, _onDirectoryFileChanged = OnDirectoryFileChanged; - if (_options.OnDemandProjectsLoad) + if (_options.LoadProjectsOnDemand) { - _workspace.AddWaitForProjectModelReadyEventHandler(WaitForProjectModelReadyAsync); + _workspace.AddWaitForProjectModelReadyHandler(WaitForProjectModelReadyAsync); } } private async Task WaitForProjectModelReadyAsync(string documentPath) { // Search and queue for loading C# projects that are likely to reference the requested file. - // C# source files are located pretty much always in the same folder with project file that is referencing them or in a subfolder. - // Do not limit search by OmniSharpEnvironment.TargetDirectory to allow for loading on demand projects that are outside of it, - // i.e. in VSCode case potentially outside of the folder opened in the IDE. + // C# source files are located pretty much always in the same folder with their project file or in a subfolder below. + // Search up the root folder to enable on-demand project load in additional scenarios like the following: + // - A subfolder in a big codebase was opened in VSCode and then a document was opened that is located outside of the subfoler. + // - A workspace was opened in VSCode that includes multiple subfolders from a big codebase. + // - Documents from different codebases are opened in the same VSCode workspace. string projectDir = Path.GetDirectoryName(documentPath); - while(projectDir != null) + do { - List csProjFiles = Directory.EnumerateFiles(projectDir, "*.csproj", SearchOption.TopDirectoryOnly).ToList(); + var csProjFiles = Directory.EnumerateFiles(projectDir, "*.csproj", SearchOption.TopDirectoryOnly).ToList(); if (csProjFiles.Count > 0) { foreach(string csProjFile in csProjFiles) @@ -119,25 +121,8 @@ private async Task WaitForProjectModelReadyAsync(string documentPath) break; } - bool foundStopAtItem = false; - foreach (string stopAtItemName in _options.OnDemandProjectsLoadSearchStopsAt) - { - string itemPath = Path.Combine(projectDir, stopAtItemName); - if (File.Exists(itemPath) || Directory.Exists(itemPath)) - { - foundStopAtItem = true; - break; - } - } - - if (foundStopAtItem) - { - _logger.LogTrace($"Couldn't find project to load for '{documentPath}'"); - break; - } - projectDir = Path.GetDirectoryName(projectDir); - } + } while(projectDir != null); // Wait for all queued projects to load to ensure that workspace is fully up to date before this method completes. // If the project for the document was loaded before and there are no other projects to load at the moment, the call below will be no-op. diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs index e6a8302590..0165e45067 100644 --- a/src/OmniSharp.MSBuild/ProjectSystem.cs +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -102,9 +102,9 @@ public void Initalize(IConfiguration configuration) _manager = new ProjectManager(_loggerFactory, _options, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, _loader, _workspace, _eventSinks); - if (_options.OnDemandProjectsLoad) + if (_options.LoadProjectsOnDemand) { - _logger.LogInformation($"Skip loading projects listed in solution file or under target directory because {Key}:{nameof(MSBuildOptions.OnDemandProjectsLoad)} is true."); + _logger.LogInformation($"Skip loading projects listed in solution file or under target directory because {Key}:{nameof(MSBuildOptions.LoadProjectsOnDemand)} is true."); return; } diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index db0654e8ec..b6d38d6c16 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -25,7 +25,7 @@ public class OmniSharpWorkspace : Workspace private readonly ILogger _logger; - private readonly ConcurrentBag> _waitForProjectModelReadyEventHandlers = new ConcurrentBag>(); + private readonly ConcurrentBag> _waitForProjectModelReadyHandlers = new ConcurrentBag>(); private readonly ConcurrentDictionary miscDocumentsProjectInfos = new ConcurrentDictionary(); @@ -39,9 +39,9 @@ public OmniSharpWorkspace(HostServicesAggregator aggregator, ILoggerFactory logg public override bool CanOpenDocuments => true; - public void AddWaitForProjectModelReadyEventHandler(Func eventHandler) + public void AddWaitForProjectModelReadyHandler(Func handler) { - _waitForProjectModelReadyEventHandlers.Add(eventHandler); + _waitForProjectModelReadyHandlers.Add(handler); } public override void OpenDocument(DocumentId documentId, bool activate = true) @@ -385,7 +385,7 @@ public override async Task LoadTextAndVersionAsync( private Task OnWaitForProjectModelReadyAsync(string filePath) { - return Task.WhenAll(_waitForProjectModelReadyEventHandlers.Select(h => h(filePath))); + return Task.WhenAll(_waitForProjectModelReadyHandlers.Select(h => h(filePath))); } } } diff --git a/test-assets/test-projects/DeepProjectTransitiveReference/App/App.csproj b/test-assets/test-projects/DeepProjectTransitiveReference/App/App.csproj new file mode 100644 index 0000000000..2df4528b98 --- /dev/null +++ b/test-assets/test-projects/DeepProjectTransitiveReference/App/App.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp1.1 + + + + + + + diff --git a/test-assets/test-projects/DeepProjectTransitiveReference/App/Program.cs b/test-assets/test-projects/DeepProjectTransitiveReference/App/Program.cs new file mode 100644 index 0000000000..be4278310c --- /dev/null +++ b/test-assets/test-projects/DeepProjectTransitiveReference/App/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace App +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/test-assets/test-projects/DeepProjectTransitiveReference/DeepProjectTransitiveReference.sln b/test-assets/test-projects/DeepProjectTransitiveReference/DeepProjectTransitiveReference.sln new file mode 100644 index 0000000000..403cc3c7d2 --- /dev/null +++ b/test-assets/test-projects/DeepProjectTransitiveReference/DeepProjectTransitiveReference.sln @@ -0,0 +1,64 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App\App.csproj", "{632DFE45-B56E-4158-8F27-45E2BA0BAFCF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib1", "Lib1\Lib1.csproj", "{CE41561B-5D13-4688-8686-EEFF744BE8B5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib2", "Lib2\Lib2.csproj", "{BAC03617-3A2B-431E-A9E4-49441B745C4F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Debug|x64.ActiveCfg = Debug|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Debug|x64.Build.0 = Debug|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Debug|x86.ActiveCfg = Debug|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Debug|x86.Build.0 = Debug|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Release|Any CPU.Build.0 = Release|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Release|x64.ActiveCfg = Release|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Release|x64.Build.0 = Release|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Release|x86.ActiveCfg = Release|Any CPU + {632DFE45-B56E-4158-8F27-45E2BA0BAFCF}.Release|x86.Build.0 = Release|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Debug|x64.Build.0 = Debug|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Debug|x86.Build.0 = Debug|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Release|Any CPU.Build.0 = Release|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Release|x64.ActiveCfg = Release|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Release|x64.Build.0 = Release|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Release|x86.ActiveCfg = Release|Any CPU + {CE41561B-5D13-4688-8686-EEFF744BE8B5}.Release|x86.Build.0 = Release|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Debug|x64.ActiveCfg = Debug|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Debug|x64.Build.0 = Debug|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Debug|x86.ActiveCfg = Debug|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Debug|x86.Build.0 = Debug|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Release|Any CPU.Build.0 = Release|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Release|x64.ActiveCfg = Release|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Release|x64.Build.0 = Release|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Release|x86.ActiveCfg = Release|Any CPU + {BAC03617-3A2B-431E-A9E4-49441B745C4F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4006CDC8-9E1D-438A-9749-2B3303F9074C} + EndGlobalSection +EndGlobal diff --git a/test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Class1.cs b/test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Class1.cs new file mode 100644 index 0000000000..44e7c59c4d --- /dev/null +++ b/test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Class1.cs @@ -0,0 +1,8 @@ +using System; + +namespace Lib +{ + public class Class1 + { + } +} diff --git a/test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Lib1.csproj b/test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Lib1.csproj new file mode 100644 index 0000000000..8b93623e25 --- /dev/null +++ b/test-assets/test-projects/DeepProjectTransitiveReference/Lib1/Lib1.csproj @@ -0,0 +1,11 @@ + + + + netstandard1.3 + + + + + + + diff --git a/test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Class2.cs b/test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Class2.cs new file mode 100644 index 0000000000..4345f9e5f7 --- /dev/null +++ b/test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Class2.cs @@ -0,0 +1,8 @@ +using System; + +namespace Lib +{ + public class Class2 + { + } +} diff --git a/test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Lib2.csproj b/test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Lib2.csproj new file mode 100644 index 0000000000..b81db56b71 --- /dev/null +++ b/test-assets/test-projects/DeepProjectTransitiveReference/Lib2/Lib2.csproj @@ -0,0 +1,7 @@ + + + + netstandard1.3 + + + diff --git a/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs b/tests/OmniSharp.MSBuild.Tests/LoadProjectsOnDemandTests.cs similarity index 58% rename from tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs rename to tests/OmniSharp.MSBuild.Tests/LoadProjectsOnDemandTests.cs index 429a7f2576..729a9e13ef 100644 --- a/tests/OmniSharp.MSBuild.Tests/OnDemandProjectsLoadTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/LoadProjectsOnDemandTests.cs @@ -15,17 +15,17 @@ namespace OmniSharp.MSBuild.Tests { - public class OnDemandProjectsLoadTests : AbstractMSBuildTestFixture + public class LoadProjectsOnDemandTests : AbstractMSBuildTestFixture { - public OnDemandProjectsLoadTests(ITestOutputHelper output) + public LoadProjectsOnDemandTests(ITestOutputHelper output) : base(output) { } [Fact] - public async Task LoadProjectsOnDemandOneByOne() + public async Task LoadOnDemandProjectsOneByOne() { - var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; + var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.LoadProjectsOnDemand)}"] = "true" }; using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) { @@ -62,9 +62,9 @@ public async Task LoadProjectsOnDemandOneByOne() } [Fact] - public async Task LoadProjectAndItsReferenceOnDemand() + public async Task LoadOnDemandProjectAndItsReference() { - var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; + var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.LoadProjectsOnDemand)}"] = "true" }; using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) { @@ -91,44 +91,10 @@ public async Task LoadProjectAndItsReferenceOnDemand() } [Fact] - public async Task OnDemandProjectsLoadSearchStopsAtFile() + public async Task LoadOnDemandProjectWithTwoLevelsOfTransitiveReferences() { - var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; - using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) - using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) - { - MSBuildWorkspaceInfo workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); - - // Expect empty workspace initially since no documents have been requested yet - Assert.Null(workspaceInfo.SolutionPath); - Assert.NotNull(workspaceInfo.Projects); - Assert.Equal(0, workspaceInfo.Projects.Count); - - // Create a subfolder containing both "stop-search-at" marker file ('.git') and the file to load - string stopAtFolder = Path.Combine(testProject.Directory, "App", "StopHere"); - Directory.CreateDirectory(stopAtFolder); - string fileToRequest = Path.Combine(stopAtFolder, "Empty.cs"); - File.WriteAllText(fileToRequest, "class empty {}"); - File.WriteAllText(Path.Combine(stopAtFolder, ".git"), string.Empty); - - // Requesting a file should should stop search since it will find stop-search at file in the same folder - CodeStructureService service = host.GetRequestHandler(OmniSharpEndpoints.V2.CodeStructure); - var request = new CodeStructureRequest { FileName = fileToRequest }; - CodeStructureResponse response = await service.Handle(request); - workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); - - // No project is expected to be found/loaded - Assert.Null(workspaceInfo.SolutionPath); - Assert.NotNull(workspaceInfo.Projects); - Assert.Equal(0, workspaceInfo.Projects.Count); - } - } - - [Fact] - public async Task OnDemandProjectsLoadSearchStopsAtFolder() - { - var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.OnDemandProjectsLoad)}"] = "true" }; - using (var testProject = await TestAssets.Instance.GetTestProjectAsync("TwoProjectsWithSolution")) + var configData = new Dictionary { [$"MsBuild:{nameof(MSBuildOptions.LoadProjectsOnDemand)}"] = "true" }; + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("DeepProjectTransitiveReference")) using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: configData)) { MSBuildWorkspaceInfo workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); @@ -138,24 +104,19 @@ public async Task OnDemandProjectsLoadSearchStopsAtFolder() Assert.NotNull(workspaceInfo.Projects); Assert.Equal(0, workspaceInfo.Projects.Count); - // Create a subfolder containing "stop-search-at" marker folder '.git' - Directory.CreateDirectory(Path.Combine(testProject.Directory, "App", "StopHere", ".git")); - - string fileToRequestFolder = Path.Combine(testProject.Directory, "App", "StopHere", "EmptyClassLib"); - Directory.CreateDirectory(fileToRequestFolder); - string fileToRequest = Path.Combine(fileToRequestFolder, "Empty.cs"); - File.WriteAllText(fileToRequest, "class empty {}"); - - // Requesting a file should should stop search since it will find stop-search at file in the same folder - CodeStructureService service = host.GetRequestHandler(OmniSharpEndpoints.V2.CodeStructure); - var request = new CodeStructureRequest { FileName = fileToRequest }; - CodeStructureResponse response = await service.Handle(request); + // Requesting the document should load project App, its reference Lib1 and Lib2 that is referenced by Lib1 + MembersAsTreeService membersAsTreeService = host.GetRequestHandler(OmniSharpEndpoints.MembersTree); + var request = new MembersTreeRequest { FileName = Path.Combine(testProject.Directory, "App", "Program.cs") }; + FileMemberTree response = await membersAsTreeService.Handle(request); workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync(); - // No project is expected to be found/loaded + Assert.NotNull(request); Assert.Null(workspaceInfo.SolutionPath); Assert.NotNull(workspaceInfo.Projects); - Assert.Equal(0, workspaceInfo.Projects.Count); + Assert.Equal(3, workspaceInfo.Projects.Count); + Assert.Equal("App.csproj", Path.GetFileName(workspaceInfo.Projects[0].Path)); + Assert.Equal("Lib1.csproj", Path.GetFileName(workspaceInfo.Projects[1].Path)); + Assert.Equal("Lib2.csproj", Path.GetFileName(workspaceInfo.Projects[2].Path)); } } }