Skip to content

Commit

Permalink
Merge pull request #2121 from JoeRobich/solution-filter
Browse files Browse the repository at this point in the history
Support Solution filter (.slnf)
  • Loading branch information
JoeRobich authored Mar 25, 2021
2 parents a8578ac + 84df396 commit f1962bd
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 16 deletions.
4 changes: 2 additions & 2 deletions src/OmniSharp.Host/Services/OmniSharpEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ public OmniSharpEnvironment(
{
TargetDirectory = path;
}
else if (File.Exists(path) && Path.GetExtension(path).Equals(".sln", StringComparison.OrdinalIgnoreCase))
else if (File.Exists(path) && (Path.GetExtension(path).Equals(".sln", StringComparison.OrdinalIgnoreCase) || Path.GetExtension(path).Equals(".slnf", StringComparison.OrdinalIgnoreCase)))
{
SolutionFilePath = path;
TargetDirectory = Path.GetDirectoryName(path);
}

if (TargetDirectory == null)
{
throw new ArgumentException("OmniSharp only supports being launched with a directory path or a path to a solution (.sln) file.", nameof(path));
throw new ArgumentException("OmniSharp only supports being launched with a directory path or a path to a solution (.sln, .slnf) file.", nameof(path));
}

HostProcessId = hostPid;
Expand Down
41 changes: 27 additions & 14 deletions src/OmniSharp.MSBuild/ProjectSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,21 @@ public void Initalize(IConfiguration configuration)
}
}

public Task WaitForIdleAsync() { return _manager.WaitForQueueEmptyAsync(); }
public Task WaitForIdleAsync() { return _manager.WaitForQueueEmptyAsync(); }

private IEnumerable<(string, ProjectIdInfo)> GetInitialProjectPathsAndIds()
{
// If a solution was provided, use it.
if (!string.IsNullOrEmpty(_environment.SolutionFilePath))
{
_solutionFileOrRootPath = _environment.SolutionFilePath;
return GetProjectPathsAndIdsFromSolution(_environment.SolutionFilePath);
return GetProjectPathsAndIdsFromSolutionOrFilter(_environment.SolutionFilePath, out _solutionFileOrRootPath);
}

// Otherwise, assume that the path provided is a directory and look for a solution there.
var solutionFilePath = FindSolutionFilePath(_environment.TargetDirectory, _logger);
if (!string.IsNullOrEmpty(solutionFilePath))
{
_solutionFileOrRootPath = solutionFilePath;
return GetProjectPathsAndIdsFromSolution(solutionFilePath);
return GetProjectPathsAndIdsFromSolutionOrFilter(solutionFilePath, out _solutionFileOrRootPath);
}

// Finally, if there isn't a single solution immediately available,
Expand All @@ -158,10 +156,20 @@ public void Initalize(IConfiguration configuration)
});
}

private IEnumerable<(string, ProjectIdInfo)> GetProjectPathsAndIdsFromSolution(string solutionFilePath)
private IEnumerable<(string, ProjectIdInfo)> GetProjectPathsAndIdsFromSolutionOrFilter(string solutionOrFilterFilePath, out string solutionFilePath)
{
_logger.LogInformation($"Detecting projects in '{solutionFilePath}'.");
_logger.LogInformation($"Detecting projects in '{solutionOrFilterFilePath}'.");

solutionFilePath = solutionOrFilterFilePath;

var projectFilter = ImmutableHashSet<string>.Empty;
if (SolutionFilterReader.IsSolutionFilterFilename(solutionOrFilterFilePath) &&
!SolutionFilterReader.TryRead(solutionOrFilterFilePath, out solutionFilePath, out projectFilter))
{
throw new InvalidSolutionFileException($"Solution filter file was invalid.");
}

var solutionFolder = Path.GetDirectoryName(solutionFilePath);
var solutionFile = SolutionFile.ParseFile(solutionFilePath);
var processedProjects = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var result = new List<(string, ProjectIdInfo)>();
Expand Down Expand Up @@ -196,10 +204,14 @@ public void Initalize(IConfiguration configuration)
continue;
}

// Solution files are assumed to contain relative paths to project files with Windows-style slashes.
var projectFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar);
projectFilePath = Path.Combine(_environment.TargetDirectory, projectFilePath);
projectFilePath = Path.GetFullPath(projectFilePath);
// Solution files contain relative paths to project files with Windows-style slashes.
var relativeProjectfilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar);
var projectFilePath = Path.GetFullPath(Path.Combine(solutionFolder, relativeProjectfilePath));
if (!projectFilter.IsEmpty &&
!projectFilter.Contains(projectFilePath))
{
continue;
}

// Have we seen this project? If so, move on.
if (processedProjects.Contains(projectFilePath))
Expand All @@ -225,11 +237,12 @@ public void Initalize(IConfiguration configuration)

private static string FindSolutionFilePath(string rootPath, ILogger logger)
{
// currently, Directory.GetFiles collects files that the file extension has 'sln' prefix.
// this causes collecting unexpected files like 'x.slnx', or 'x.slnproj'.
// currently, Directory.GetFiles on Windows collects files that the file extension has 'sln' prefix, while
// GetFiles on Mono looks for an exact match. Use an approach that works for both.
// see https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netframework-4.7.2 ('Note' description)
var solutionsFilePaths = Directory.GetFiles(rootPath, "*.sln").Where(x => Path.GetExtension(x).Equals(".sln", StringComparison.OrdinalIgnoreCase)).ToArray();
var result = SolutionSelector.Pick(solutionsFilePaths, rootPath);
var solutionFiltersFilePaths = Directory.GetFiles(rootPath, "*.slnf").Where(x => Path.GetExtension(x).Equals(".slnf", StringComparison.OrdinalIgnoreCase)).ToArray();
var result = SolutionSelector.Pick(solutionsFilePaths.Concat(solutionFiltersFilePaths).ToArray(), rootPath);

if (result.Message != null)
{
Expand Down
70 changes: 70 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/SolutionFilterReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Immutable;
using System.IO;
using Newtonsoft.Json.Linq;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal static class SolutionFilterReader
{
public static bool IsSolutionFilterFilename(string filename)
{
return Path.GetExtension(filename).Equals(".slnf", StringComparison.OrdinalIgnoreCase);
}

public static bool TryRead(string filterFilename, out string solutionFilename, out ImmutableHashSet<string> projectFilter)
{
try
{
var filterDirectory = Path.GetDirectoryName(filterFilename);

var document = JObject.Parse(File.ReadAllText(filterFilename));
var solution = document["solution"];
// Convert directory separators to the platform's default, since that is what MSBuild provide us.
var solutionPath = ((string)solution?["path"])?.Replace('\\', Path.DirectorySeparatorChar);

solutionFilename = Path.GetFullPath(Path.Combine(filterDirectory, solutionPath));
if (!File.Exists(solutionFilename))
{
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}

// The base directory for projects is the solution folder.
var solutionDirectory = Path.GetDirectoryName(solutionFilename);

var filterProjects = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
var projects = (JArray)solution?["projects"] ?? new JArray();
foreach (string project in projects)
{
// Convert directory separators to the platform's default, since that is what MSBuild provide us.
var projectPath = project?.Replace('\\', Path.DirectorySeparatorChar);
if (projectPath is null)
{
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}

var projectFilename = Path.GetFullPath(Path.Combine(solutionDirectory, projectPath));
if (!File.Exists(projectFilename))
{
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}

// Fill the filter with the absolute project paths.
filterProjects.Add(projectFilename);
}

projectFilter = filterProjects.ToImmutable();
return true;
}
catch
{
solutionFilename = string.Empty;
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace ProjectAndSolution
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"solution": {
"path": "Solution\\ProjectAndSolutionFilter.sln",
"projects": [
"..\\Project\\ProjectAndSolutionFilter.csproj"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectAndSolutionFilter", "..\Project\ProjectAndSolutionFilter.csproj", "{A4C2694D-AEB4-4CB1-8951-5290424EF883}"
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(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x64.ActiveCfg = Debug|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x64.Build.0 = Debug|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x86.ActiveCfg = Debug|x86
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x86.Build.0 = Debug|x86
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|Any CPU.Build.0 = Release|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x64.ActiveCfg = Release|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x64.Build.0 = Release|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x86.ActiveCfg = Release|x86
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x86.Build.0 = Release|x86
EndGlobalSection
EndGlobal
28 changes: 28 additions & 0 deletions tests/OmniSharp.MSBuild.Tests/WorkspaceInformationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,34 @@ public async Task TestProjectAndSolution()
Assert.Equal("netcoreapp3.1", targetFramework.ShortName);
}

[Fact]
public async Task TestProjectAndSolutionFilter()
{
using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectAndSolutionFilter"))
using (var host = CreateMSBuildTestHost(testProject.Directory))
{
var workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync();

Assert.Equal("ProjectAndSolutionFilter.sln", Path.GetFileName(workspaceInfo.SolutionPath));
Assert.NotNull(workspaceInfo.Projects);
var project = Assert.Single(workspaceInfo.Projects);

Assert.Equal("ProjectAndSolutionFilter", project.AssemblyName);
Assert.Equal("bin/Debug/netcoreapp2.1/", project.OutputPath.EnsureForwardSlashes());
Assert.Equal("obj/Debug/netcoreapp2.1/", project.IntermediateOutputPath.EnsureForwardSlashes());
var expectedTargetPath = $"{testProject.Directory}/Project/{project.OutputPath}ProjectAndSolutionFilter.dll".EnsureForwardSlashes();
Assert.Equal(expectedTargetPath, project.TargetPath.EnsureForwardSlashes());
Assert.Equal("Debug", project.Configuration);
Assert.Equal("AnyCPU", project.Platform);
Assert.True(project.IsExe);
Assert.False(project.IsUnityProject);

Assert.Equal(".NETCoreApp,Version=v2.1", project.TargetFramework);
var targetFramework = Assert.Single(project.TargetFrameworks);
Assert.Equal("netcoreapp2.1", targetFramework.ShortName);
}
}

[Fact]
public async Task ProjectAndSolutionWithProjectSection()
{
Expand Down

0 comments on commit f1962bd

Please sign in to comment.