Skip to content

Conversation

@MartinZikmund
Copy link
Member

GitHub Issue: closes #22436

PR Type:

What is the current behavior? πŸ€”

What is the new behavior? πŸš€

PR Checklist βœ…

Please check if your PR fulfills the following requirements:

Other information ℹ️

Copilot AI review requested due to automatic review settings January 22, 2026 14:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces centralized solution discovery functionality for the DevServer by creating a new SolutionDiscovery utility class. The implementation enables DevServer to automatically discover solution files in subdirectories (up to 2 levels deep), with prioritization for common source directories like "src", "source", and "app". This allows developers to start DevServer from the repository root even when solution files are in subdirectories.

Changes:

  • Created new SolutionDiscovery helper class with intelligent directory traversal and prioritization
  • Refactored two existing solution discovery implementations to use the centralized utility
  • Linked the new helper class into Uno.UI.RemoteControl.Host project

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/Uno.UI.DevServer.Cli/Helpers/SolutionDiscovery.cs New utility class that discovers .sln and .slnx files in current directory and subdirectories with configurable search depth, prioritization, and directory skipping
src/Uno.UI.DevServer.Cli/Mcp/DevServerMonitor.cs Refactored to use SolutionDiscovery.DiscoverSolutions() instead of inline directory enumeration
src/Uno.UI.RemoteControl.Host/Program.Command.cs Refactored to use SolutionDiscovery.DiscoverFirstSolution() instead of inline directory enumeration
src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj Added file link to include SolutionDiscovery.cs from the CLI project

πŸ’‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +166
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;

namespace Uno.UI.DevServer.Cli.Helpers;

/// <summary>
/// Provides methods to discover solution files in the current directory and subdirectories.
/// This allows the DevServer to be started from a parent directory (e.g., repository root)
/// when solution files are located in subdirectories like 'src/'.
/// </summary>
internal static class SolutionDiscovery
{
/// <summary>
/// Prioritized subdirectory names to search first. These are common source directory names.
/// </summary>
private static readonly string[] PrioritizedSubdirectories = ["src", "source", "app"];

/// <summary>
/// Directory names to skip during search. These typically contain build artifacts or dependencies.
/// </summary>
private static readonly HashSet<string> SkippedDirectories = new(StringComparer.OrdinalIgnoreCase)
{
"bin", "obj", "node_modules", "packages", ".git", ".vs", ".idea"
};

/// <summary>
/// Maximum depth to search for solution files in subdirectories.
/// </summary>
private const int MaxSearchDepth = 2;

/// <summary>
/// Discovers all solution files starting from the specified directory.
/// First searches the current directory, then subdirectories up to MaxSearchDepth.
/// Results are ordered by depth (shallower first), then alphabetically.
/// </summary>
/// <param name="startDirectory">The directory to start searching from.</param>
/// <param name="logger">Optional logger for diagnostic output.</param>
/// <returns>Array of full paths to discovered solution files, or empty array if none found.</returns>
public static string[] DiscoverSolutions(string startDirectory, ILogger? logger = null)
{
var solutions = new List<(string path, int depth)>();

// First, check current directory (depth 0)
var currentDirSolutions = GetSolutionFilesInDirectory(startDirectory);
if (currentDirSolutions.Length > 0)
{
logger?.LogDebug("Found {Count} solution(s) in current directory: {Directory}", currentDirSolutions.Length, startDirectory);
return currentDirSolutions.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToArray();
}

// Search subdirectories up to MaxSearchDepth
logger?.LogDebug("No solutions found in current directory, searching subdirectories...");
SearchSubdirectories(startDirectory, 1, solutions, logger);

if (solutions.Count == 0)
{
logger?.LogDebug("No solution files found in {Directory} or its subdirectories", startDirectory);
return [];
}

// Order by depth first, then alphabetically
var result = solutions
.OrderBy(s => s.depth)
.ThenBy(s => s.path, StringComparer.OrdinalIgnoreCase)
.Select(s => s.path)
.ToArray();

logger?.LogDebug("Found {Count} solution(s) in subdirectories", result.Length);
return result;
}

/// <summary>
/// Discovers the first solution file starting from the specified directory.
/// Convenience method that returns the first result from DiscoverSolutions.
/// </summary>
/// <param name="startDirectory">The directory to start searching from.</param>
/// <param name="logger">Optional logger for diagnostic output.</param>
/// <returns>Full path to the first discovered solution file, or null if none found.</returns>
public static string? DiscoverFirstSolution(string startDirectory, ILogger? logger = null)
{
return DiscoverSolutions(startDirectory, logger).FirstOrDefault();
}

private static void SearchSubdirectories(string directory, int currentDepth, List<(string path, int depth)> results, ILogger? logger)
{
if (currentDepth > MaxSearchDepth)
{
return;
}

IEnumerable<string> subdirectories;
try
{
subdirectories = Directory.EnumerateDirectories(directory);
}
catch (UnauthorizedAccessException)
{
return;
}
catch (DirectoryNotFoundException)
{
return;
}

// Sort subdirectories: prioritized ones first, then alphabetically
var sortedSubdirs = subdirectories
.Select(d => new { Path = d, Name = Path.GetFileName(d) })
.Where(d => !ShouldSkipDirectory(d.Name))
.OrderBy(d => GetDirectoryPriority(d.Name))
.ThenBy(d => d.Name, StringComparer.OrdinalIgnoreCase)
.Select(d => d.Path)
.ToList();

foreach (var subdir in sortedSubdirs)
{
var solutions = GetSolutionFilesInDirectory(subdir);
foreach (var solution in solutions)
{
results.Add((solution, currentDepth));
logger?.LogDebug("Found solution at depth {Depth}: {Path}", currentDepth, solution);
}

// Recurse into subdirectories
SearchSubdirectories(subdir, currentDepth + 1, results, logger);
}
}

private static string[] GetSolutionFilesInDirectory(string directory)
{
try
{
return Directory
.EnumerateFiles(directory, "*.sln")
.Concat(Directory.EnumerateFiles(directory, "*.slnx"))
.ToArray();
}
catch (UnauthorizedAccessException)
{
return [];
}
catch (DirectoryNotFoundException)
{
return [];
}
}

private static bool ShouldSkipDirectory(string directoryName)
{
// Skip hidden directories (starting with . or _)
if (directoryName.StartsWith('.') || directoryName.StartsWith('_'))
{
return true;
}

return SkippedDirectories.Contains(directoryName);
}

private static int GetDirectoryPriority(string directoryName)
{
var index = Array.FindIndex(PrioritizedSubdirectories, p => p.Equals(directoryName, StringComparison.OrdinalIgnoreCase));
return index >= 0 ? index : int.MaxValue;
}
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The new SolutionDiscovery utility class lacks test coverage. Since this repository has comprehensive test coverage (including the Uno.UI.RemoteControl.DevServer.Tests project), and this class contains non-trivial logic including directory traversal, prioritization, error handling, and depth limits, it should have unit tests to ensure correctness. Consider adding tests that verify:

  • Discovery of solutions in the current directory
  • Discovery in prioritized subdirectories (src, source, app)
  • Respect for MaxSearchDepth limit
  • Proper skipping of excluded directories (bin, obj, node_modules, etc.)
  • Handling of both .sln and .slnx extensions
  • Error handling for UnauthorizedAccessException and DirectoryNotFoundException
  • Ordering of results (depth first, then alphabetically)

Copilot uses AI. Check for mistakes.
@unodevops
Copy link
Contributor

πŸ€– Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-22437/wasm-skia-net9/index.html

@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22437/docs/index.html

@unodevops
Copy link
Contributor

⚠️⚠️ The build 192538 has failed on Uno.UI - CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recognize solution file in src for DevServer

3 participants