Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 5 additions & 2 deletions sources/assets/Stride.Core.Assets/PackageSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using Stride.Core;
using Stride.Core.Assets.Analysis;
using Stride.Core.Assets.Diagnostics;
Expand Down Expand Up @@ -787,10 +788,12 @@ public static void Load(string filePath, PackageSessionResult sessionResult, Pac
SolutionProject? firstProject = null;

// If we have a solution, load all packages
if (string.Equals(Path.GetExtension(filePath), ".sln", StringComparison.InvariantCultureIgnoreCase))
if (Regex.IsMatch(Path.GetExtension(filePath), @"\.slnf?$", RegexOptions.IgnoreCase))
{
// The session should save back its changes to the solution
var solution = session.VSSolution = VisualStudio.Solution.FromFile(filePath);
VisualStudio.Solution solution = session.VSSolution = Path.GetExtension(filePath).Equals(".sln", StringComparison.InvariantCultureIgnoreCase)
? VisualStudio.Solution.FromFile(filePath)
: VisualStudio.Solution.FromSolutionFilter(filePath);

// Keep header
var versionHeader = solution.Properties.FirstOrDefault(x => x.Name == "VisualStudioVersion");
Expand Down
68 changes: 45 additions & 23 deletions sources/assets/Stride.Core.Assets/PackageSessionHelper.Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions;
using NuGet.ProjectModel;
using Stride.Core.Extensions;
using Stride.Core.VisualStudio;
Expand All @@ -18,33 +19,29 @@ internal partial class PackageSessionHelper
{
try
{
// Solution file: extract projects
var solutionDirectory = Path.GetDirectoryName(fullPath) ?? "";
var solution = Solution.FromFile(fullPath);

foreach (var project in solution.Projects)
if (Path.GetExtension(fullPath).Equals(Package.PackageFileExtension, StringComparison.InvariantCultureIgnoreCase))
{
if (project.TypeGuid == KnownProjectTypeGuid.CSharp || project.TypeGuid == KnownProjectTypeGuid.CSharpNewSystem)
var packageVersion = await TryGetPackageVersion(fullPath);
if (packageVersion is not null)
{
var projectPath = project.FullPath;
var projectAssetsJsonPath = Path.Combine(Path.GetDirectoryName(projectPath), "obj", LockFileFormat.AssetsFileName);
#if !STRIDE_LAUNCHER && !STRIDE_VSPACKAGE
if (!File.Exists(projectAssetsJsonPath))
{
var log = new Stride.Core.Diagnostics.LoggerResult();
await VSProjectHelper.RestoreNugetPackages(log, projectPath);
}
#endif
if (File.Exists(projectAssetsJsonPath))
return packageVersion;
}
}
else if (Regex.IsMatch(Path.GetExtension(fullPath), @"\.slnf?$", RegexOptions.IgnoreCase))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a compile-time regex.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the regex to a static readonly field.

    public static readonly Regex SolutionFileRegex = new(@"\.slnf?$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was more thinking of using the new GeneratedRegex attribute that triggers a source generator.

See

#if NET7_0_OR_GREATER
private static readonly Regex SemanticVersionRegex = GetSemanticVersionRegex();
private static readonly Regex StrictSemanticVersionRegex = GetStrictSemanticVersionRegex();
#else
private static readonly Regex SemanticVersionRegex = new Regex(@"^(?<Version>\d+(\s*\.\s*\d+){0,3})(?<Release>-[0-9a-z]*[\.0-9a-z-]*)?(?<BuildMetadata>\+[0-9a-z]*[\.0-9a-z-]*)?$", Flags);
private static readonly Regex StrictSemanticVersionRegex = new Regex(@"^(?<Version>\d+(\.\d+){2})(?<Release>-[0-9a-z]*[\.0-9a-z-]*)?(?<BuildMetadata>\+[0-9a-z]*[\.0-9a-z-]*)?$", Flags);
#endif
and
#if NET7_0_OR_GREATER
[GeneratedRegex(@"^(?<Version>\d+(\s*\.\s*\d+){0,3})(?<Release>-[0-9a-z]*[\.0-9a-z-]*)?(?<BuildMetadata>\+[0-9a-z]*[\.0-9a-z-]*)?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)]
private static partial Regex GetSemanticVersionRegex();
[GeneratedRegex(@"^(?<Version>\d+(\.\d+){2})(?<Release>-[0-9a-z]*[\.0-9a-z-]*)?(?<BuildMetadata>\+[0-9a-z]*[\.0-9a-z-]*)?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)]
private static partial Regex GetStrictSemanticVersionRegex();
#endif

{
// Solution file: extract projects
var solution = Path.GetExtension(fullPath).Equals(".sln", StringComparison.InvariantCultureIgnoreCase)
? Solution.FromFile(fullPath)
: Solution.FromSolutionFilter(fullPath);
foreach (var project in solution.Projects)
{
if (project.TypeGuid == KnownProjectTypeGuid.CSharp || project.TypeGuid == KnownProjectTypeGuid.CSharpNewSystem)
{
var format = new LockFileFormat();
var projectAssets = format.Read(projectAssetsJsonPath);
foreach (var library in projectAssets.Libraries)
var projectPath = project.FullPath;
var projectVersion = await TryGetPackageVersion(projectPath);
if (projectVersion is not null)
{
if ((library.Type == "package" || library.Type == "project") && (library.Name == "Stride.Engine" || library.Name == "Xenko.Engine"))
{
return new PackageVersion(library.Version.ToString());
}
return projectVersion;
}
}
}
Expand Down Expand Up @@ -92,4 +89,29 @@ internal static void RemovePackageSections(Project project)
project.Sections.Remove(solutionPackageIdentifier);
}
}

private static async Task<PackageVersion?> TryGetPackageVersion(string projectPath)
{
var projectAssetsJsonPath = Path.Combine(Path.GetDirectoryName(projectPath), "obj", LockFileFormat.AssetsFileName);
#if !STRIDE_LAUNCHER && !STRIDE_VSPACKAGE
if (!File.Exists(projectAssetsJsonPath))
{
var log = new Stride.Core.Diagnostics.LoggerResult();
await VSProjectHelper.RestoreNugetPackages(log, projectPath);
}
#endif
if (File.Exists(projectAssetsJsonPath))
{
var format = new LockFileFormat();
var projectAssets = format.Read(projectAssetsJsonPath);
foreach (var library in projectAssets.Libraries)
{
if ((library.Type == "package" || library.Type == "project") && (library.Name == "Stride.Engine" || library.Name == "Xenko.Engine"))
{
return new PackageVersion(library.Version.ToString());
}
}
}
return null;
}
}
56 changes: 56 additions & 0 deletions sources/core/Stride.Core.Design/VisualStudio/Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,60 @@ public static Solution FromStream(string solutionFullPath, Stream stream)
solution.FullPath = solutionFullPath;
return solution;
}

/// <summary>
/// Loads a filtered solution from a solution filter file (.slnf).
/// </summary>
/// <param name="solutionFilterPath">The solution filter full path.</param>
/// <returns>A filtered Solution.</returns>
public static Solution FromSolutionFilter(string solutionFilterPath)
{
using var stream = new FileStream(solutionFilterPath, FileMode.Open, FileAccess.Read);
return FromSolutionFilterStream(solutionFilterPath, stream);
}

/// <summary>
/// Loads a filtered solution from a solution filter stream (.slnf).
/// </summary>
/// <param name="solutionFilterPath">The solution filter full path.</param>
/// <param name="stream">The stream containing the solution filter data.</param>
/// <returns>A filtered Solution.</returns>
public static Solution FromSolutionFilterStream(string solutionFilterPath, Stream stream)
{
var solutionFilter = SolutionFilter.FromStream(solutionFilterPath, stream);

// The solution filter contains a reference to the base solution
var baseSolutionPath = solutionFilter.SolutionPath;
var baseSolution = FromFile(baseSolutionPath);

// Create a new solution with only the filtered projects
var filteredSolution = new Solution();
filteredSolution.FullPath = baseSolution.FullPath;
filteredSolution.Headers.AddRange(baseSolution.Headers);
filteredSolution.Properties.AddRange(baseSolution.Properties);
filteredSolution.GlobalSections.AddRange(baseSolution.GlobalSections);

// Build a dictionary for quick project path lookup
var projectPathMap = baseSolution.Projects
.Where(project => !project.IsSolutionFolder)
.ToDictionary(
project => project.GetRelativePath(baseSolution).Replace('/', Path.DirectorySeparatorChar),
project => project,
StringComparer.OrdinalIgnoreCase);

// Add projects by path
foreach (var projectPath in solutionFilter.ProjectPaths)
{
if (projectPathMap.TryGetValue(projectPath, out var project))
{
// Only add if not already added by GUID
if (!filteredSolution.Projects.Contains(project.Guid))
{
filteredSolution.Projects.Add(project);
}
}
}

return filteredSolution;
}
}
92 changes: 92 additions & 0 deletions sources/core/Stride.Core.Design/VisualStudio/SolutionFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#region License

// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// This file is distributed under MIT License. See LICENSE.md for details.

#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Stride.Core.VisualStudio;

/// <summary>
/// Represents a Visual Studio solution filter file (.slnf).
/// </summary>
public class SolutionFilter
{
/// <summary>
/// Initializes a new instance of the <see cref="SolutionFilter"/> class.
/// </summary>
public SolutionFilter()
{
SolutionPath = string.Empty;
ProjectPaths = [];
}

/// <summary>
/// Gets or sets the path to the solution file referenced by this solution filter.
/// </summary>
public string SolutionPath { get; set; }

/// <summary>
/// Gets the list of project paths included in the solution filter.
/// </summary>
public List<string> ProjectPaths { get; } = [];

/// <summary>
/// Loads a solution filter from a file path.
/// </summary>
/// <param name="solutionFilterPath">The full path to the solution filter file.</param>
/// <returns>A populated SolutionFilter instance.</returns>
public static SolutionFilter FromFile(string solutionFilterPath)
{
using var stream = new FileStream(solutionFilterPath, FileMode.Open, FileAccess.Read);
return FromStream(solutionFilterPath, stream);
}

/// <summary>
/// Loads a solution filter from a stream.
/// </summary>
/// <param name="solutionFilterPath">The full path to the solution filter file.</param>
/// <param name="stream">The stream containing the solution filter data.</param>
/// <returns>A populated SolutionFilter instance.</returns>
public static SolutionFilter FromStream(string solutionFilterPath, Stream stream)
{
using var filterReader = new SolutionFilterReader(solutionFilterPath, stream);
return filterReader.ReadSolutionFilterFile();
}
}

/// <summary>
/// JSON model for deserializing solution filter files.
/// </summary>
internal class SolutionFilterData
{
/// <summary>
/// Gets or sets the solution information.
/// </summary>
[JsonPropertyName("solution")]
public SolutionInfo? Solution { get; set; }

/// <summary>
/// Represents solution information in a solution filter file.
/// </summary>
public class SolutionInfo
{
/// <summary>
/// Gets or sets the relative path to the solution file.
/// </summary>
[JsonPropertyName("path")]
public string? Path { get; set; }

/// <summary>
/// Gets or sets the list of project paths in the solution filter.
/// </summary>
[JsonPropertyName("projects")]
public List<string>? Projects { get; set; }
}
}
109 changes: 109 additions & 0 deletions sources/core/Stride.Core.Design/VisualStudio/SolutionFilterReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#region License

// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// This file is distributed under MIT License. See LICENSE.md for details.

#endregion

using System;
using System.IO;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace Stride.Core.VisualStudio;

internal class SolutionFilterReader : IDisposable
{
private readonly string solutionFilterPath;
private readonly string solutionFilterDirectory;
private StreamReader? reader;
private bool disposed;

/// <summary>
/// Initializes a new instance of the <see cref="SolutionFilterReader"/> class.
/// </summary>
/// <param name="solutionFilterPath">The solution filter path.</param>
public SolutionFilterReader(string solutionFilterPath)
: this(solutionFilterPath, new FileStream(solutionFilterPath, FileMode.Open, FileAccess.Read))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SolutionFilterReader"/> class.
/// </summary>
/// <param name="solutionFilterPath">The solution filter path.</param>
/// <param name="stream">The stream containing the solution filter data.</param>
public SolutionFilterReader(string solutionFilterPath, Stream stream)
{
this.solutionFilterPath = solutionFilterPath;
solutionFilterDirectory = Path.GetDirectoryName(solutionFilterPath) ?? string.Empty;
reader = new StreamReader(stream);
}

/// <summary>
/// Reads the solution filter file and returns a SolutionFilter instance.
/// </summary>
/// <returns>A populated SolutionFilter instance.</returns>
public SolutionFilter ReadSolutionFilterFile()
{
#if NET7_0_OR_GREATER
ObjectDisposedException.ThrowIf(disposed, this);
#else
if (disposed) throw new ObjectDisposedException(nameof(SolutionFilterReader));
#endif

var solutionFilter = new SolutionFilter();

try
{
// Read and deserialize the JSON content
var jsonContent = reader!.ReadToEnd();
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

var filterData = JsonSerializer.Deserialize<SolutionFilterData>(jsonContent, options);

if (filterData?.Solution?.Path == null)
{
throw new SolutionFileException($"Invalid solution filter file: {solutionFilterPath}. Missing or invalid 'solution.path' property.");
}

// Resolve the solution path relative to the solution filter
var relativeSolutionPath = filterData.Solution.Path.Replace('\\', Path.DirectorySeparatorChar);
solutionFilter.SolutionPath = Path.GetFullPath(Path.Combine(solutionFilterDirectory, relativeSolutionPath));

// Process project paths
if (filterData.Solution.Projects != null)
{
foreach (var projectPath in filterData.Solution.Projects)
{
if (!string.IsNullOrEmpty(projectPath))
{
solutionFilter.ProjectPaths.Add(projectPath.Replace('\\', Path.DirectorySeparatorChar));
}
}
}
}
catch (JsonException ex)
{
throw new SolutionFileException($"Error parsing solution filter file: {solutionFilterPath}", ex);
}

return solutionFilter;
}

/// <summary>
/// Disposes resources used by the reader.
/// </summary>
public void Dispose()
{
disposed = true;
if (reader != null)
{
reader.Dispose();
reader = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ public static async Task<UFile> BrowseForExistingProject(IViewModelServiceProvid
var initialDirectory = InternalSettings.FileDialogLastOpenSessionDirectory.GetValue();
var filters = new List<FilePickerFilter>
{
new("Solution or package files") { Patterns = [EditorViewModel.SolutionFileExtension, EditorViewModel.PackageFileExtension]},
new("Solution or package files") { Patterns = [EditorViewModel.SolutionFileExtension, EditorViewModel.SolutionFilterFileExtension, EditorViewModel.PackageFileExtension]},
new("Solution file") { Patterns = [EditorViewModel.SolutionFileExtension]},
new("Solution filter file") { Patterns = [EditorViewModel.SolutionFilterFileExtension]},
new("Package file") { Patterns = [EditorViewModel.PackageFileExtension]},
};
var filePaths = await OpenFileDialog(serviceProvider, false, initialDirectory, filters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public abstract class EditorViewModel : ViewModelBase
{
public const string PackageFileExtension = Package.PackageFileExtension;
public const string SolutionFileExtension = ".sln";
public const string SolutionFilterFileExtension = ".slnf";
private SessionViewModel session;

protected EditorViewModel(IViewModelServiceProvider serviceProvider, MostRecentlyUsedFileCollection mru, string editorName, string editorVersionMajor)
Expand Down