diff --git a/src/OmniSharp.DotNet/DotNetWorkspace.cs b/src/OmniSharp.DotNet/DotNetWorkspace.cs index 70b6c328f8..165a7615da 100644 --- a/src/OmniSharp.DotNet/DotNetWorkspace.cs +++ b/src/OmniSharp.DotNet/DotNetWorkspace.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.ProjectModel.Graph; +using OmniSharp.DotNet.Projects; namespace OmniSharp.DotNet { - public class DotNetWorkspace : Workspace { private readonly HashSet _projects = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -16,13 +15,9 @@ public class DotNetWorkspace : Workspace public DotNetWorkspace(string initialPath) : base(ProjectReaderSettings.ReadFromEnvironment(), true) { - var paths = ResolveProjectPath(initialPath); - if (paths != null && paths.Any()) + foreach (var path in ProjectSearcher.Search(initialPath)) { - foreach (var path in paths) - { - AddProject(path); - } + AddProject(path); } } @@ -50,7 +45,7 @@ public IReadOnlyList GetAllProjects() public IReadOnlyList GetProjectContexts(string projectPath) { return (IReadOnlyList)GetProjectContextCollection(projectPath)?.ProjectContexts.AsReadOnly() ?? - Enumerable.Empty().ToList().AsReadOnly(); + Array.Empty(); } /// @@ -103,45 +98,6 @@ protected override IEnumerable BuildProjectContexts(Project proj } } - private static List ResolveProjectPath(string projectPath) - { - if (File.Exists(projectPath)) - { - var filename = Path.GetFileName(projectPath); - if (!Project.FileName.Equals(filename, StringComparison.OrdinalIgnoreCase) && - !GlobalSettings.FileName.Equals(filename, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - projectPath = Path.GetDirectoryName(projectPath); - } - - if (File.Exists(Path.Combine(projectPath, Project.FileName))) - { - return new List { projectPath }; - } - - if (File.Exists(Path.Combine(projectPath, GlobalSettings.FileName))) - { - var root = ProjectRootResolver.ResolveRootDirectory(projectPath); - GlobalSettings globalSettings; - if (GlobalSettings.TryGetGlobalSettings(projectPath, out globalSettings)) - { - return globalSettings.ProjectSearchPaths - .Select(searchPath => Path.Combine(globalSettings.DirectoryPath, searchPath)) - .Where(actualPath => Directory.Exists(actualPath)) - .SelectMany(actualPath => Directory.GetDirectories(actualPath)) - .Where(actualPath => File.Exists(Path.Combine(actualPath, Project.FileName))) - .Select(path => Path.GetFullPath(path)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - } - } - - return null; - } - private static IEnumerable GetProjectReferences(ProjectContext context) { var projectDescriptions = context.LibraryManager @@ -156,7 +112,7 @@ private static IEnumerable GetProjectReferences(ProjectConte continue; } - // if this is an assembly reference then don't threat it as project reference + // if this is an assembly reference then don't treat it as project reference if (!string.IsNullOrEmpty(description.TargetFrameworkInfo?.AssemblyPath)) { continue; diff --git a/src/OmniSharp.DotNet/Projects/ProjectSearcher.cs b/src/OmniSharp.DotNet/Projects/ProjectSearcher.cs index 5fcc5f9cbd..cae944f5e3 100644 --- a/src/OmniSharp.DotNet/Projects/ProjectSearcher.cs +++ b/src/OmniSharp.DotNet/Projects/ProjectSearcher.cs @@ -1,66 +1,86 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Microsoft.DotNet.ProjectModel; namespace OmniSharp.DotNet.Projects { public class ProjectSearcher { - public static IEnumerable Search(string solutionRoot) + public static IEnumerable Search(string directory) { - return Search(solutionRoot, maxDepth: 5); + return Search(directory, maxDepth: 5); } - public static IEnumerable Search(string solutionRoot, int maxDepth) + public static IEnumerable Search(string directory, int maxDepth) { - var dir = new DirectoryInfo(solutionRoot); - if (!dir.Exists) + if (!Directory.Exists(directory)) { - return Enumerable.Empty(); + return Array.Empty(); } - if (File.Exists(Path.Combine(solutionRoot, Project.FileName))) + // Is there a project.json file in this directory? If so, return it. + var projectFilePath = Path.Combine(directory, Project.FileName); + if (File.Exists(projectFilePath)) { - return new string[] { solutionRoot }; + return new string[] { projectFilePath }; } - else if (File.Exists(Path.Combine(solutionRoot, GlobalSettings.FileName))) + + // Is there a global.json file in this directory? If so, use that to search. + if (File.Exists(Path.Combine(directory, GlobalSettings.FileName))) { - return FindProjectsThroughGlobalJson(solutionRoot); + return FindProjectsThroughGlobalJson(directory); } - else + + // Otherwise, perform a general search through the file system. + return FindProjects(directory, maxDepth); + } + + // TODO: Replace with proper tuple when we move to C# 7 + private struct DirectoryAndDepth + { + public readonly string Directory; + public readonly int Depth; + + private DirectoryAndDepth(string directory, int depth) { - return FindProjects(solutionRoot, maxDepth); + this.Directory = directory; + this.Depth = depth; } + + public static DirectoryAndDepth Create(string directory, int depth) + => new DirectoryAndDepth(directory, depth); } - private static IEnumerable FindProjects(string root, int maxDepth) + private static IEnumerable FindProjects(string rootDirectory, int maxDepth) { - var result = new List(); - var stack = new Stack>(); + var result = new SortedSet(StringComparer.OrdinalIgnoreCase); + var stack = new Stack(); - stack.Push(Tuple.Create(new DirectoryInfo(root), 0)); + stack.Push(DirectoryAndDepth.Create(rootDirectory, 0)); - while (stack.Any()) + while (stack.Count > 0) { - var next = stack.Pop(); - var currentFolder = next.Item1; - var depth = next.Item2; + var current = stack.Pop(); - if (!currentFolder.Exists) + if (!Directory.Exists(current.Directory)) { continue; } - else if (currentFolder.GetFiles(Project.FileName).Any()) + + // Did we find a project.json? + var projectFilePath = Path.Combine(current.Directory, Project.FileName); + if (File.Exists(projectFilePath)) { - result.Add(Path.Combine(currentFolder.FullName, Project.FileName)); + result.Add(projectFilePath); } - else if (depth < maxDepth) + + // If we're not already at maximum depth, go ahead and search child directories. + if (current.Depth < maxDepth) { - foreach (var sub in currentFolder.GetDirectories()) + foreach (var childDirectory in Directory.GetDirectories(current.Directory)) { - stack.Push(Tuple.Create(sub, depth + 1)); + stack.Push(DirectoryAndDepth.Create(childDirectory, current.Depth + 1)); } } } @@ -73,19 +93,38 @@ private static IEnumerable FindProjectsThroughGlobalJson(string root) GlobalSettings globalSettings; if (GlobalSettings.TryGetGlobalSettings(root, out globalSettings)) { - return globalSettings.ProjectSearchPaths - .Select(searchPath => Path.Combine(globalSettings.DirectoryPath, searchPath)) - .Where(actualPath => Directory.Exists(actualPath)) - .SelectMany(actualPath => Directory.GetDirectories(actualPath)) - .Where(actualPath => File.Exists(Path.Combine(actualPath, Project.FileName))) - .Select(path => Path.GetFullPath(path)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - } - else - { - return Enumerable.Empty(); + var projectPaths = new SortedSet(StringComparer.OrdinalIgnoreCase); + var searchDirectories = new Queue(); + + // Look in global.json 'projects' search paths and their immediate children + foreach (var searchPath in globalSettings.ProjectSearchPaths) + { + var searchDirectory = Path.Combine(globalSettings.DirectoryPath, searchPath); + if (Directory.Exists(searchDirectory)) + { + searchDirectories.Enqueue(searchDirectory); + + foreach (var childDirectory in Directory.GetDirectories(searchDirectory)) + { + searchDirectories.Enqueue(childDirectory); + } + } + } + + while (searchDirectories.Count > 0) + { + var searchDirectory = searchDirectories.Dequeue(); + var projectFilePath = Path.Combine(searchDirectory, Project.FileName); + if (File.Exists(projectFilePath)) + { + projectPaths.Add(Path.GetFullPath(projectFilePath)); + } + } + + return projectPaths; } + + return Array.Empty(); } } } \ No newline at end of file diff --git a/tests/OmniSharp.DotNet.Tests/ProjectSearcherTest.cs b/tests/OmniSharp.DotNet.Tests/ProjectSearcherTest.cs deleted file mode 100644 index 284f4c6ca6..0000000000 --- a/tests/OmniSharp.DotNet.Tests/ProjectSearcherTest.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.IO; -using System.Linq; -using OmniSharp.DotNet.Projects; -using TestCommon; -using Xunit; - -namespace OmniSharp.DotNet.Tests -{ - public class ProjectSearcherTest - { - private readonly TestsContext _context; - - public ProjectSearcherTest() - { - _context = TestsContext.Default; - } - - [Theory] - [InlineData("ProjectSearchSample01", "ProjectSearchSample01")] - [InlineData("ProjectSearchSample03", "Project1")] - [InlineData("ProjectSearchSample04", "ProjectSearchSample04")] - public void SingleResultExpect(string testSampleName, string projectName) - { - var projectPath = _context.GetTestSample(testSampleName); - var project = ProjectSearcher.Search(projectPath).Single(); - - Assert.Equal(projectName, Path.GetFileName(project)); - } - - [Fact] - public void NoneProjectJson() - { - var projectPath = _context.GetTestSample("ProjectSearchSample02"); - Assert.Empty(ProjectSearcher.Search(projectPath)); - } - - [Fact] - public void RecursivelySearch() - { - var projectPath = _context.GetTestSample("ProjectSearchSample05"); - var results = ProjectSearcher.Search(projectPath); - - Assert.Equal(3, results.Count()); - } - - [Fact] - public void GlobalJsonExpand() - { - var projectPath = _context.GetTestSample("ProjectSearchSample06"); - var results = ProjectSearcher.Search(projectPath); - - Assert.Equal(2, results.Count()); - } - - [Fact] - public void GlobalJsonFindNothing() - { - var projectPath = _context.GetTestSample("ProjectSearchSample07"); - Assert.Empty(ProjectSearcher.Search(projectPath)); - } - } -} \ No newline at end of file diff --git a/tests/OmniSharp.DotNet.Tests/ProjectSearcherTests.cs b/tests/OmniSharp.DotNet.Tests/ProjectSearcherTests.cs new file mode 100644 index 0000000000..2795f431f6 --- /dev/null +++ b/tests/OmniSharp.DotNet.Tests/ProjectSearcherTests.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using System.Linq; +using OmniSharp.DotNet.Projects; +using TestCommon; +using Xunit; + +namespace OmniSharp.DotNet.Tests +{ + public class ProjectSearcherTests + { + private readonly TestsContext _context; + + public ProjectSearcherTests() + { + _context = TestsContext.Default; + } + + private string GetLocation(string filePath) + { + var directoryPath = File.Exists(filePath) + ? Path.GetDirectoryName(filePath) + : filePath; + + if (directoryPath.StartsWith(_context.TestSamples, StringComparison.OrdinalIgnoreCase)) + { + directoryPath = directoryPath.Substring(_context.TestSamples.Length); + } + + directoryPath = directoryPath.Replace(Path.DirectorySeparatorChar, '/'); + + if (directoryPath.Length > 0 && directoryPath[0] == '/') + { + directoryPath = directoryPath.Substring(1); + } + + return directoryPath; + } + + [Theory] + [InlineData("ProjectSearchSample01", "ProjectSearchSample01")] + [InlineData("ProjectSearchSample03", "ProjectSearchSample03/src/Project1")] + [InlineData("ProjectSearchSample04", "ProjectSearchSample04")] + public void SingleResultExpect(string testSampleName, string projectName) + { + var directory = _context.GetTestSample(testSampleName); + var projectFilePath = ProjectSearcher.Search(directory).Single(); + + Assert.Equal(projectName, GetLocation(projectFilePath)); + } + + [Fact] + public void NoneProjectJson() + { + var directory = _context.GetTestSample("ProjectSearchSample02"); + var projectFilePaths = ProjectSearcher.Search(directory); + + Assert.Empty(projectFilePaths); + } + + [Fact] + public void RecursivelySearch() + { + var directory = _context.GetTestSample("ProjectSearchSample05"); + var projectFilePaths = ProjectSearcher.Search(directory); + var locations = projectFilePaths.Select(GetLocation); + + var expected = new [] + { + "ProjectSearchSample05/src/Project1", + "ProjectSearchSample05/src/Project2/Embed", + "ProjectSearchSample05/src/Project2", + "ProjectSearchSample05/test/Test01" + }; + + Assert.Equal(expected, locations); + } + + [Fact] + public void GlobalJsonExpand() + { + var directory = _context.GetTestSample("ProjectSearchSample06"); + var projectFilePaths = ProjectSearcher.Search(directory); + var locations = projectFilePaths.Select(GetLocation); + + var expected = new [] + { + "ProjectSearchSample06/src/Project1", + "ProjectSearchSample06/src/Project2", + }; + + Assert.Equal(expected, locations); + } + + [Fact] + public void GlobalJsonFindNothing() + { + var directory = _context.GetTestSample("ProjectSearchSample07"); + var projectFilePaths = ProjectSearcher.Search(directory); + + Assert.Empty(projectFilePaths); + } + + [Fact] + public void GlobalJsonTopLevelFolders() + { + var directory = _context.GetTestSample("ProjectSearchSample08"); + var projectFilePaths = ProjectSearcher.Search(directory); + var locations = projectFilePaths.Select(GetLocation); + + var expected = new [] + { + "ProjectSearchSample08/Project1", + "ProjectSearchSample08/Project2", + "ProjectSearchSample08/Test1", + }; + + Assert.Equal(expected, locations); + } + } +} \ No newline at end of file diff --git a/tests/TestCommon/TestsContext.cs b/tests/TestCommon/TestsContext.cs index 8f3cf03f18..035fa879a0 100644 --- a/tests/TestCommon/TestsContext.cs +++ b/tests/TestCommon/TestsContext.cs @@ -8,20 +8,18 @@ public class TestsContext private TestsContext() { - // AppContext is .NET Core only - var basedir = Directory.GetCurrentDirectory(); - var current = basedir; + var currentDirectory = Directory.GetCurrentDirectory(); var solutionFile = "OmniSharp.sln"; - while (!File.Exists(Path.Combine(current, solutionFile))) + while (!File.Exists(Path.Combine(currentDirectory, solutionFile))) { - current = Path.GetDirectoryName(current); - if (Path.GetPathRoot(current) == current) + currentDirectory = Path.GetDirectoryName(currentDirectory); + if (Path.GetPathRoot(currentDirectory) == currentDirectory) { break; } } - SolutionRoot = current; + SolutionRoot = currentDirectory; TestRoot = Path.Combine(SolutionRoot, "tests"); TestSamples = Path.Combine(TestRoot, "TestSamples"); } diff --git a/tests/TestSamples/ProjectSearchSample08/Project1/project.json b/tests/TestSamples/ProjectSearchSample08/Project1/project.json new file mode 100644 index 0000000000..b1166148e9 --- /dev/null +++ b/tests/TestSamples/ProjectSearchSample08/Project1/project.json @@ -0,0 +1,9 @@ +{ + "version": "1.0.0", + "dependencies": { + "Microsoft.NETCore.App": "1.0.0" + }, + "frameworks": { + "netcoreapp1.0": {} + } +} \ No newline at end of file diff --git a/tests/TestSamples/ProjectSearchSample08/Project2/project.json b/tests/TestSamples/ProjectSearchSample08/Project2/project.json new file mode 100644 index 0000000000..b1166148e9 --- /dev/null +++ b/tests/TestSamples/ProjectSearchSample08/Project2/project.json @@ -0,0 +1,9 @@ +{ + "version": "1.0.0", + "dependencies": { + "Microsoft.NETCore.App": "1.0.0" + }, + "frameworks": { + "netcoreapp1.0": {} + } +} \ No newline at end of file diff --git a/tests/TestSamples/ProjectSearchSample08/Test1/project.json b/tests/TestSamples/ProjectSearchSample08/Test1/project.json new file mode 100644 index 0000000000..b1166148e9 --- /dev/null +++ b/tests/TestSamples/ProjectSearchSample08/Test1/project.json @@ -0,0 +1,9 @@ +{ + "version": "1.0.0", + "dependencies": { + "Microsoft.NETCore.App": "1.0.0" + }, + "frameworks": { + "netcoreapp1.0": {} + } +} \ No newline at end of file diff --git a/tests/TestSamples/ProjectSearchSample08/global.json b/tests/TestSamples/ProjectSearchSample08/global.json new file mode 100644 index 0000000000..de303e7043 --- /dev/null +++ b/tests/TestSamples/ProjectSearchSample08/global.json @@ -0,0 +1,7 @@ +{ + "projects": [ + "Project1", + "Project2", + "Test1" + ] +} \ No newline at end of file