Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for adding solution items within a different folder name #508

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,14 +1095,54 @@ public void VisualStudioVersionIsWritten()
StringCompareShould.IgnoreLineEndings);
}

private void ValidateProjectInSolution(Action<SlnProject, ProjectInSolution> customValidator, SlnProject[] projects, bool folders)
[Fact]
public void Save_WithSolutionItemsAddedToSpecificFolder_SolutionItemsExistInSpecificFolder()
{
// Arrange
string solutionFilePath = GetTempFileName();

var slnFile = new SlnFile()
{
SolutionGuid = new Guid("{6370DE27-36B7-44AE-B47A-1ECF4A6D740A}"),
};

slnFile.AddSolutionItems("docs", new[] { Path.Combine(this.TestRootPath, "README.md") });

// Act
slnFile.Save(solutionFilePath, useFolders: false);

// Assert
File.ReadAllText(solutionFilePath).ShouldBe(
@"Microsoft Visual Studio Solution File, Format Version 12.00
Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""docs"", ""docs"", ""{B283EBC2-E01F-412D-9339-FD56EF114549}""
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6370DE27-36B7-44AE-B47A-1ECF4A6D740A}
EndGlobalSection
EndGlobal
",
StringCompareShould.IgnoreLineEndings);
}

private void ValidateProjectInSolution(Action<SlnProject, ProjectInSolution> customValidator, SlnProject[] projects, bool useFolders)
{
string solutionFilePath = GetTempFileName();

SlnFile slnFile = new SlnFile();

slnFile.AddProjects(projects);
slnFile.Save(solutionFilePath, folders);
slnFile.Save(solutionFilePath, useFolders);

SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,7 @@ public void HierarchyWithCollapsedFoldersIsCorrectlyFormed()
GetFolderStructureAsString(hierarchy.Folders).ShouldBe(
$@"{_driveRoot}zoo{Path.DirectorySeparatorChar}foo - foo
{_driveRoot}zoo{Path.DirectorySeparatorChar}foo{Path.DirectorySeparatorChar}bar - bar {SlnHierarchy.Separator} qux
{_driveRoot}zoo{Path.DirectorySeparatorChar}foo{Path.DirectorySeparatorChar}foo1 - foo1
{_driveRoot}zoo{Path.DirectorySeparatorChar}foo{Path.DirectorySeparatorChar}foo1{Path.DirectorySeparatorChar}foo2 - foo2",
{_driveRoot}zoo{Path.DirectorySeparatorChar}foo{Path.DirectorySeparatorChar}foo1 - foo1 {SlnHierarchy.Separator} foo2",
StringCompareShould.IgnoreLineEndings);
}

Expand Down
30 changes: 23 additions & 7 deletions src/Microsoft.VisualStudio.SlnGen/SlnFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public sealed class SlnFile
/// <summary>
/// A list of absolute paths to include as Solution Items.
/// </summary>
private readonly List<string> _solutionItems = new ();
private readonly Dictionary<string, List<string>> _solutionItems = new ();

/// <summary>
/// Initializes a new instance of the <see cref="SlnFile" /> class.
Expand Down Expand Up @@ -118,7 +118,9 @@ public SlnFile()
/// <summary>
/// Gets a list of solution items.
/// </summary>
public IReadOnlyCollection<string> SolutionItems => _solutionItems;
public IReadOnlyDictionary<string, IReadOnlyCollection<string>> SolutionItems => _solutionItems.ToDictionary(
k => k.Key,
v => (IReadOnlyCollection<string>)v.Value.AsReadOnly());

/// <summary>
/// Gets or sets an optional Visual Studio version for the solution file.
Expand Down Expand Up @@ -342,7 +344,17 @@ public void AddProjects(IEnumerable<Project> projects, IReadOnlyDictionary<strin
/// <param name="items">An <see cref="IEnumerable{String}"/> containing items to add to the solution.</param>
public void AddSolutionItems(IEnumerable<string> items)
{
_solutionItems.AddRange(items);
_solutionItems.Add("Solution Items", items.ToList());
}

/// <summary>
/// Adds the specified solution items under the specified path.
/// </summary>
/// <param name="folderPath">The path the solution items will be added in.</param>
/// <param name="items">An <see cref="IEnumerable{String}"/> containing items to add to the solution.</param>
public void AddSolutionItems(string folderPath, IEnumerable<string> items)
{
_solutionItems.Add(folderPath, items.ToList());
}

/// <summary>
Expand Down Expand Up @@ -415,11 +427,15 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, bool col
else
{
// Just handle the solution items
if (SolutionItems.Count > 0)
foreach (var solutionItems in SolutionItems)
{
writer.WriteLine($@"Project(""{SlnFolder.FolderProjectTypeGuidString}"") = ""Solution Items"", ""Solution Items"", ""{{B283EBC2-E01F-412D-9339-FD56EF114549}}"" ");
WriteSolutionItemsProjectSection(rootPath, writer, SolutionItems);
writer.WriteLine("EndProject");
if (solutionItems.Value.Any())
{
// TODO: Create a unique Guid for multiple Solution Items
writer.WriteLine($@"Project(""{SlnFolder.FolderProjectTypeGuidString}"") = ""{solutionItems.Key}"", ""{solutionItems.Key}"", ""{{B283EBC2-E01F-412D-9339-FD56EF114549}}"" ");
WriteSolutionItemsProjectSection(rootPath, writer, solutionItems.Value);
writer.WriteLine("EndProject");
}
}
}

Expand Down
168 changes: 134 additions & 34 deletions src/Microsoft.VisualStudio.SlnGen/SlnHierarchy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,28 @@ public static SlnHierarchy CreateFromProjectDirectories(
IReadOnlyCollection<string> solutionItems,
bool collapseFolders = false)
{
SlnFolder rootFolder = GetRootFolder(projects, solutionItems);
return CreateFromProjectDirectories(
projects,
new Dictionary<string, IReadOnlyCollection<string>>
{
{ "Solution Items", solutionItems },
},
collapseFolders);
}

/// <summary>
/// Creates a <see cref="SlnHierarchy" /> based on the directory structure of the specified projects.
/// </summary>
/// <param name="projects">The set of projects that should be placed in the hierarchy.</param>
/// <param name="solutionItems">The set of solution items that should be placed in the hierarchy.</param>
/// <param name="collapseFolders">An optional value indicating whether or not folders containing a single item should be collapsed into their parent folder.</param>
/// <returns>A <see cref="SlnHierarchy" /> based on the directory structure of the specified projects.</returns>
public static SlnHierarchy CreateFromProjectDirectories(
IReadOnlyList<SlnProject> projects,
IReadOnlyDictionary<string, IReadOnlyCollection<string>> solutionItems,
bool collapseFolders = false)
{
SlnFolder rootFolder = GetRootFolder(projects, solutionItems.Values.SelectMany(x => x));

SlnHierarchy hierarchy = new SlnHierarchy(rootFolder)
{
Expand Down Expand Up @@ -98,6 +119,24 @@ public static SlnHierarchy CreateFromProjectDirectories(
public static SlnHierarchy CreateFromProjectSolutionFolder(
IReadOnlyList<SlnProject> projects,
IReadOnlyCollection<string> solutionItems)
{
return CreateFromProjectSolutionFolder(
projects,
new Dictionary<string, IReadOnlyCollection<string>>
{
{ "Solution Items", solutionItems },
});
}

/// <summary>
/// Creates a hierarchy based on solution folders declared by projects.
/// </summary>
/// <param name="projects">A <see cref="IReadOnlyList{T}" /> of projects.</param>
/// <param name="solutionItems">The set of solution items that should be placed in the hierarchy.</param>
/// <returns>A <see cref="SlnHierarchy" /> object containing solution folders and projects.</returns>
public static SlnHierarchy CreateFromProjectSolutionFolder(
IReadOnlyList<SlnProject> projects,
IReadOnlyDictionary<string, IReadOnlyCollection<string>> solutionItems)
{
SlnHierarchy hierarchy = new SlnHierarchy(new SlnFolder(string.Empty));

Expand Down Expand Up @@ -140,9 +179,67 @@ public static SlnHierarchy CreateFromProjectSolutionFolder(
}
}

// Just add to root folder
// TODO: in the future maybe solution items should have solution folder too, and ones that don't go in root
hierarchy.RootFolder.SolutionItems.AddRange(solutionItems);
foreach (var solutionItem in solutionItems)
{
string folderPath = solutionItem.Key;
IReadOnlyCollection<string> items = solutionItem.Value;

// try to get the existing folder in the solution
if (hierarchy._pathToSlnFolderMap.TryGetValue(folderPath, out SlnFolder targetFolder))
{
// the solution folder already exists
// add the solution items to the folder
targetFolder.SolutionItems.AddRange(items);
}
else
{
// the solution folder doesn't already exist
// create the solution folder by creating the parent folder if it don't already exist
SlnFolder parent = null;

// get the folder name segments of the folder path
string[] folderSegments = folderPath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);

// iterate through each folder segment
for (int i = 0; i < folderSegments.Length; i++)
{
string nestedFolderPath = string.Join(Path.DirectorySeparatorChar.ToString(), folderSegments, 0, i + 1);

// try to get the existing folder in the solution
if (!hierarchy._pathToSlnFolderMap.TryGetValue(nestedFolderPath, out SlnFolder nested))
{
// the solution folder doesn't already exist
// in order to create the folder, we need to get the parent
if (i == 0)
{
// the parent is root folder
parent = hierarchy._rootFolder;
}
else
{
string parentString = Path.GetDirectoryName(nestedFolderPath);
parent = hierarchy._pathToSlnFolderMap[parentString];
}

// create the folder
nested = new SlnFolder(nestedFolderPath)
{
Parent = parent,
};

// add the folder to the folder map
hierarchy._pathToSlnFolderMap.Add(nestedFolderPath, nested);
}
else
{
parent = nested;
}
}

// add the solution items to the folder
parent.SolutionItems.AddRange(items);
}
}

return hierarchy;
}
Expand Down Expand Up @@ -220,52 +317,55 @@ private static void CreateHierarchy(SlnHierarchy hierarchy, SlnProject project)
}
}

private static void CreateHierarchy(SlnHierarchy hierarchy, string solutionItem)
private static void CreateHierarchy(SlnHierarchy hierarchy, KeyValuePair<string, IReadOnlyCollection<string>> solutionItems)
{
FileInfo fileInfo = new FileInfo(solutionItem);
DirectoryInfo directoryInfo = fileInfo.Directory;
if (hierarchy._pathToSlnFolderMap.TryGetValue(directoryInfo!.FullName, out SlnFolder childFolder))
foreach (string solutionItem in solutionItems.Value)
{
childFolder.SolutionItems.Add(solutionItem);
return;
}
FileInfo fileInfo = new FileInfo(solutionItem);
DirectoryInfo directoryInfo = fileInfo.Directory;
if (hierarchy._pathToSlnFolderMap.TryGetValue(directoryInfo!.FullName, out SlnFolder childFolder))
{
childFolder.SolutionItems.Add(solutionItem);
return;
}

childFolder = new SlnFolder(directoryInfo.FullName);
childFolder.SolutionItems.Add(solutionItem);
hierarchy._pathToSlnFolderMap.Add(directoryInfo.FullName, childFolder);
childFolder = new SlnFolder(directoryInfo.FullName);
childFolder.SolutionItems.Add(solutionItem);
hierarchy._pathToSlnFolderMap.Add(directoryInfo.FullName, childFolder);

directoryInfo = directoryInfo.Parent;
if (directoryInfo != null)
{
while (directoryInfo != null && !string.Equals(directoryInfo.FullName, hierarchy._rootFolder.FullPath, StringComparison.OrdinalIgnoreCase))
directoryInfo = directoryInfo.Parent;
if (directoryInfo != null)
{
if (!hierarchy._pathToSlnFolderMap.TryGetValue(directoryInfo.FullName, out SlnFolder folder1))
while (directoryInfo != null && !string.Equals(directoryInfo.FullName, hierarchy._rootFolder.FullPath, StringComparison.OrdinalIgnoreCase))
{
folder1 = new SlnFolder(directoryInfo.FullName);
hierarchy._pathToSlnFolderMap.Add(directoryInfo.FullName, folder1);
if (!hierarchy._pathToSlnFolderMap.TryGetValue(directoryInfo.FullName, out SlnFolder folder1))
{
folder1 = new SlnFolder(directoryInfo.FullName);
hierarchy._pathToSlnFolderMap.Add(directoryInfo.FullName, folder1);
}

if (!folder1.Folders.Contains(childFolder))
{
folder1.Folders.Add(childFolder);
childFolder.Parent = folder1;
}

directoryInfo = directoryInfo.Parent;
childFolder = folder1;
}

if (!folder1.Folders.Contains(childFolder))
if (!hierarchy._rootFolder.Folders.Contains(childFolder))
{
folder1.Folders.Add(childFolder);
childFolder.Parent = folder1;
hierarchy._rootFolder.Folders.Add(childFolder);
childFolder.Parent = hierarchy._rootFolder;
}

directoryInfo = directoryInfo.Parent;
childFolder = folder1;
}

if (!hierarchy._rootFolder.Folders.Contains(childFolder))
{
hierarchy._rootFolder.Folders.Add(childFolder);
childFolder.Parent = hierarchy._rootFolder;
}
}
}

private static SlnFolder GetRootFolder(
IEnumerable<SlnProject> projects,
IReadOnlyCollection<string> solutionItems)
IEnumerable<string> solutionItems)
{
List<string> paths = projects.Where(i => !i.IsMainProject)
.Select(i => Directory.GetParent(i.FullPath)?.FullName ?? string.Empty)
Expand Down