Add Explorer Workbench with hierarchical, off-UI Storage analysis.

Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 12:43:05 +02:00
commit e9aba73552
130 changed files with 15110 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
namespace Explorer.Domain;
public static class PathRules
{
public const string ExtendedPrefix = @"\\?\";
public const string ExtendedUncPrefix = @"\\?\UNC\";
public static string ToExtended(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return path;
}
if (path.StartsWith(ExtendedPrefix, StringComparison.Ordinal))
{
return path;
}
if (path.StartsWith(@"\\", StringComparison.Ordinal))
{
return ExtendedUncPrefix + path[2..];
}
return ExtendedPrefix + path;
}
public static string FromExtended(string path)
{
if (path.StartsWith(ExtendedUncPrefix, StringComparison.OrdinalIgnoreCase))
{
return @"\\" + path[ExtendedUncPrefix.Length..];
}
if (path.StartsWith(ExtendedPrefix, StringComparison.Ordinal))
{
return path[ExtendedPrefix.Length..];
}
return path;
}
public static bool IsUnc(string path)
{
var p = FromExtended(path);
return p.StartsWith(@"\\", StringComparison.Ordinal);
}
public static string NormalizeDirectorySeparators(string path)
=> path.Replace('/', '\\');
public static string Combine(string root, string relative)
{
root = FromExtended(root).TrimEnd('\\');
relative = relative.Replace('/', '\\').TrimStart('\\');
if (string.IsNullOrEmpty(relative))
{
return EnsureDirectoryTrailingSlashIfRoot(root);
}
return root + "\\" + relative;
}
public static string MakeRelative(string root, string fullPath)
{
var r = FromExtended(root).TrimEnd('\\');
var f = FromExtended(fullPath).TrimEnd('\\');
if (f.Equals(r, StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
if (f.StartsWith(r + "\\", StringComparison.OrdinalIgnoreCase))
{
return f[(r.Length + 1)..];
}
return f;
}
public static string CanonicalUncRoot(string path)
{
var p = FromExtended(path).TrimEnd('\\');
if (!p.StartsWith(@"\\", StringComparison.Ordinal))
{
return p;
}
var parts = p[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
return @"\\" + p[2..].ToLowerInvariant();
}
return @"\\" + parts[0].ToLowerInvariant() + "\\" + parts[1];
}
public static string Parent(string path)
{
var p = FromExtended(path).TrimEnd('\\');
if (p.Length >= 2 && p[1] == ':' && p.Length <= 3)
{
return p.Length == 2 ? p + "\\" : p + (p.EndsWith('\\') ? "" : "\\");
}
if (IsUnc(p))
{
var root = CanonicalUncRoot(p);
if (p.Equals(root, StringComparison.OrdinalIgnoreCase))
{
return root;
}
}
var idx = p.LastIndexOf('\\');
if (idx <= 0)
{
return p;
}
if (idx == 2 && p[1] == ':')
{
return p[..3];
}
return p[..idx];
}
public static bool IsDriveRoot(string path)
{
var p = FromExtended(path).TrimEnd('\\');
return p.Length == 2 && p[1] == ':';
}
public static string EnsureDirectoryTrailingSlashIfRoot(string path)
{
var p = FromExtended(path);
if (p.Length == 2 && p[1] == ':')
{
return p + "\\";
}
return p;
}
public static string GetFileName(string path)
{
var p = FromExtended(path).TrimEnd('\\');
var idx = p.LastIndexOf('\\');
return idx < 0 ? p : p[(idx + 1)..];
}
public static string JoinDisplay(string? root, string pathRel)
{
if (string.IsNullOrWhiteSpace(root))
{
return pathRel;
}
return string.IsNullOrWhiteSpace(pathRel) ? EnsureDirectoryTrailingSlashIfRoot(root) : Combine(root, pathRel);
}
public static string ShortenDisplay(string path, int maxChars = 64)
{
var p = FromExtended(path);
if (p.Length <= maxChars)
{
return p;
}
var unc = IsUnc(p);
var parts = p.TrimEnd('\\').Split('\\', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length <= 2)
{
return p;
}
string prefix;
var start = 0;
if (unc)
{
prefix = @"\\" + parts[0] + "\\" + parts[1];
start = 2;
}
else
{
prefix = parts.Length > 2 ? parts[0] + "\\" + parts[1] : parts[0];
start = parts.Length > 2 ? 2 : 1;
}
var leaf = parts[^1];
var parent = parts.Length - 1 > start ? parts[^2] + "\\" + leaf : leaf;
var candidate = prefix + @"\…\" + parent;
if (candidate.Length <= maxChars)
{
return candidate;
}
candidate = prefix + @"\…\" + leaf;
return candidate.Length <= maxChars ? candidate : prefix + @"\…";
}
}