From 7008258e6a37a3860086aaf569210b40180bb276 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Thu, 31 Jul 2025 22:28:29 +1000 Subject: [PATCH] use vs persistence --- .../VsPersistence/IntermediateModels.cs | 24 +++++ .../VsPersistence/SharpIdeModels.cs | 20 ++++ .../VsPersistence/VsPersistenceMapper.cs | 93 +++++++++++++++++++ .../SharpIDE.Application.csproj | 2 + .../Components/SolutionExplorer.razor | 4 + src/SharpIDE.Photino/Layout/MainLayout.razor | 7 +- .../Properties/launchSettings.json | 6 +- 7 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/IntermediateModels.cs create mode 100644 src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs create mode 100644 src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/VsPersistenceMapper.cs diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/IntermediateModels.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/IntermediateModels.cs new file mode 100644 index 0000000..c27475c --- /dev/null +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/IntermediateModels.cs @@ -0,0 +1,24 @@ +using Microsoft.VisualStudio.SolutionPersistence.Model; + +namespace SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; + +public class IntermediateSolutionModel +{ + public required string Name { get; set; } + public required string FilePath { get; set; } + public required List Projects { get; set; } + public required List SolutionFolders { get; set; } +} + +public class IntermediateSlnFolderModel +{ + public required SolutionFolderModel Model { get; set; } + public required List Folders { get; set; } + public required List Projects { get; set; } +} + +public class IntermediateProjectModel +{ + public required SolutionProjectModel Model { get; set; } + public required string FullFilePath { get; set; } +} diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs new file mode 100644 index 0000000..e548fc6 --- /dev/null +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs @@ -0,0 +1,20 @@ +namespace SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; + +public class SharpIdeSolutionModel +{ + public required string Name { get; set; } + public required string FilePath { get; set; } + public required List Projects { get; set; } + public required List Folders { get; set; } +} +public class SharpIdeSolutionFolder +{ + public required string Name { get; set; } + public required List Folders { get; set; } + public required List Projects { get; set; } +} +public class SharpIdeProjectModel +{ + public required string Name { get; set; } + public required string FilePath { get; set; } +} diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/VsPersistenceMapper.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/VsPersistenceMapper.cs new file mode 100644 index 0000000..34c11a0 --- /dev/null +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/VsPersistenceMapper.cs @@ -0,0 +1,93 @@ +using Ardalis.GuardClauses; +using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.VisualStudio.SolutionPersistence.Serializer; + +namespace SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; + +public class VsPersistenceMapper +{ + public static async Task GetSolutionModel(string solutionFilePath, CancellationToken cancellationToken = default) + { + // This intermediate model is pretty much useless, but I have left it around as we grab the project nodes with it, which we might use later. + var intermediateModel = await GetIntermediateModel(solutionFilePath, cancellationToken); + + var solutionName = Path.GetFileName(solutionFilePath); + var solutionModel = new SharpIdeSolutionModel + { + Name = solutionName, + FilePath = solutionFilePath, + Projects = intermediateModel.Projects.Select(GetSharpIdeProjectModel).ToList(), + Folders = intermediateModel.SolutionFolders.Select(s => new SharpIdeSolutionFolder + { + Name = s.Model.Name, + Folders = s.Folders.Select(GetSharpIdeSolutionFolder).ToList(), + Projects = s.Projects.Select(GetSharpIdeProjectModel).ToList() + }).ToList(), + }; + + return solutionModel; + } + private static SharpIdeProjectModel GetSharpIdeProjectModel(IntermediateProjectModel projectModel) => new SharpIdeProjectModel() + { + Name = projectModel.Model.DisplayName!, + FilePath = projectModel.Model.FilePath, + }; + + private static SharpIdeSolutionFolder GetSharpIdeSolutionFolder(IntermediateSlnFolderModel folderModel) => new SharpIdeSolutionFolder() + { + Name = folderModel.Model.Name, + Folders = folderModel.Folders.Select(GetSharpIdeSolutionFolder).ToList(), + Projects = folderModel.Projects.Select(GetSharpIdeProjectModel).ToList() + }; + + public static async Task GetIntermediateModel(string solutionFilePath, + CancellationToken cancellationToken = default) + { + var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath); + Guard.Against.Null(serializer, nameof(serializer)); + var vsSolution = await serializer.OpenAsync(solutionFilePath, cancellationToken); + + var rootFolders = vsSolution.SolutionFolders + .Where(f => f.Parent is null) + .Select(f => BuildFolderTree(f, vsSolution.SolutionFolders, vsSolution.SolutionProjects)) + .ToList(); + + var solutionModel = new IntermediateSolutionModel + { + Name = Path.GetFileName(solutionFilePath), + FilePath = solutionFilePath, + Projects = vsSolution.SolutionProjects.Where(p => p.Parent is null).Select(s => new IntermediateProjectModel + { + Model = s, + FullFilePath = Path.GetFullPath(s.FilePath) + }).ToList(), + SolutionFolders = rootFolders + }; + return solutionModel; + } + + private static IntermediateSlnFolderModel BuildFolderTree(SolutionFolderModel folder, + IReadOnlyList allSolutionFolders, IReadOnlyList allSolutionProjects) + { + var childFolders = allSolutionFolders + .Where(f => f.Parent == folder) + .Select(f => BuildFolderTree(f, allSolutionFolders, allSolutionProjects)) + .ToList(); + + var projectsInFolder = allSolutionProjects + .Where(p => p.Parent == folder) + .Select(s => new IntermediateProjectModel + { + Model = s, + FullFilePath = Path.GetFullPath(s.FilePath) + }) + .ToList(); + + return new IntermediateSlnFolderModel + { + Model = folder, + Folders = childFolders, + Projects = projectsInFolder + }; + } +} diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj index 4f584ca..361e40d 100644 --- a/src/SharpIDE.Application/SharpIDE.Application.csproj +++ b/src/SharpIDE.Application/SharpIDE.Application.csproj @@ -7,12 +7,14 @@ + + diff --git a/src/SharpIDE.Photino/Components/SolutionExplorer.razor b/src/SharpIDE.Photino/Components/SolutionExplorer.razor index 9e29879..8da037f 100644 --- a/src/SharpIDE.Photino/Components/SolutionExplorer.razor +++ b/src/SharpIDE.Photino/Components/SolutionExplorer.razor @@ -1,6 +1,7 @@ @using Ardalis.GuardClauses @using Microsoft.Build.Construction @using SharpIDE.Application.Features.SolutionDiscovery +@using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence @if (_solutionFile is null) { @@ -19,6 +20,9 @@ [Parameter, EditorRequired] public string SolutionFilePath { get; set; } = null!; + [Parameter, EditorRequired] + public SharpIdeSolutionModel SolutionModel { get; set; } = null!; + private SolutionFile _solutionFile = null!; private List _rootNodes = []; private Dictionary _folders = new(); diff --git a/src/SharpIDE.Photino/Layout/MainLayout.razor b/src/SharpIDE.Photino/Layout/MainLayout.razor index 5e56b81..6e950a8 100644 --- a/src/SharpIDE.Photino/Layout/MainLayout.razor +++ b/src/SharpIDE.Photino/Layout/MainLayout.razor @@ -1,4 +1,6 @@ @using SharpIDE.Application.Features.Build +@using SharpIDE.Application.Features.SolutionDiscovery +@using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence @inherits LayoutComponentBase @inject IDialogService DialogService @@ -11,7 +13,7 @@ @if (_solutionFilePath is not null) { - + } @* *@ @@ -35,6 +37,7 @@ } private string? _solutionFilePath; + private SharpIdeSolutionModel? _solutionModel; protected override async Task OnInitializedAsync() { @@ -46,5 +49,7 @@ _solutionFilePath = solutionFilePath; await BuildService.BuildSolutionAsync(_solutionFilePath); + var solutionModel = await RoslynTest.Analyse(_solutionFilePath); + _solutionModel = solutionModel; } } diff --git a/src/SharpIDE.Photino/Properties/launchSettings.json b/src/SharpIDE.Photino/Properties/launchSettings.json index 98ea0ab..2ff19a1 100644 --- a/src/SharpIDE.Photino/Properties/launchSettings.json +++ b/src/SharpIDE.Photino/Properties/launchSettings.json @@ -5,7 +5,8 @@ "commandName": "Project", "dotnetRunMessages": true, "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development" + "DOTNET_ENVIRONMENT": "Development", + "MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE" : "true" } }, "(Watch)": { @@ -15,7 +16,8 @@ "commandLineArgs": "watch run", "environmentVariables": { "DOTNET_ENVIRONMENT": "Development", - "DOTNET_WATCH_RESTART_ON_RUDE_EDIT": "true" + "DOTNET_WATCH_RESTART_ON_RUDE_EDIT": "true", + "MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE" : "true" } } }