Show folder names immediately and refresh stale index sizes without walking the whole drive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 11:21:20 +02:00
parent 6fc7506eb7
commit e2916aef9c
88 changed files with 5373 additions and 544 deletions

View File

@@ -16,6 +16,7 @@ public sealed class BrowseService
private readonly UiPreferencesStore _preferences;
private readonly IElevatedScanService? _elevation;
private readonly IRecycleBinCatalog? _recycle;
private readonly IKnownUserFolderCatalog? _knownFolders;
public BrowseService(
IFileSystemEnumerator enumerator,
@@ -26,7 +27,8 @@ public sealed class BrowseService
CloudPlaceStore cloudPlaces,
UiPreferencesStore preferences,
IElevatedScanService? elevation = null,
IRecycleBinCatalog? recycle = null)
IRecycleBinCatalog? recycle = null,
IKnownUserFolderCatalog? knownFolders = null)
{
_enumerator = enumerator;
_volumes = volumes;
@@ -37,6 +39,7 @@ public sealed class BrowseService
_preferences = preferences;
_elevation = elevation;
_recycle = recycle;
_knownFolders = knownFolders;
}
public async Task<FolderListing> ListThisPcAsync(CancellationToken cancellationToken = default)
@@ -56,6 +59,41 @@ public sealed class BrowseService
return new FolderListing { Path = listing.Path, Items = items };
}
public FolderListing ListHome()
{
var items = (_knownFolders?.ListExisting() ?? [])
.Select(folder => new FileSystemItem
{
FullPath = folder.Path,
Name = folder.Name,
DisplayName = folder.Name,
IsDirectory = true,
Attributes = AttributeFlags.Directory
})
.ToList();
return new FolderListing { Path = LocationRoots.Home, Items = items };
}
public FolderListing ListFavorites()
{
var items = new List<FileSystemItem>();
foreach (var path in FavoriteFolders.Normalize(_preferences.Load().FavoriteFolders))
{
var exists = Directory.Exists(path);
var name = FavoriteDisplayName(path);
items.Add(new FileSystemItem
{
FullPath = path,
Name = name,
DisplayName = exists ? name : $"{name} (Offline)",
IsDirectory = true,
Attributes = AttributeFlags.Directory
});
}
return new FolderListing { Path = LocationRoots.Favorites, Items = items };
}
public async Task<FolderListing> ListNetworkAsync(CancellationToken cancellationToken = default)
{
var listing = await ListSourcesAsync(LocationRoots.Network, source => source.Kind.IsNetwork(), cancellationToken)
@@ -172,17 +210,6 @@ public sealed class BrowseService
BrowseViewport? viewport = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var source = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
if (source is { IsIndexed: true } && _preferences.Load().IndexArchiveContents)
{
var archiveListing = await TryListArchiveAsync(source, path, cancellationToken).ConfigureAwait(false);
if (archiveListing is not null)
{
yield return CompleteDelta(archiveListing);
yield break;
}
}
if (path == LocationRoots.RecycleBin || IsRecycleBinPath(path))
{
yield return CompleteDelta(ListRecycleBin(path));
@@ -190,13 +217,35 @@ public sealed class BrowseService
}
var reachable = _volumes.IsPathReachable(path);
if (!reachable)
if (MightBeArchiveListing(path, reachable))
{
yield return CompleteDelta(await ListOfflineAsync(source, path, cancellationToken).ConfigureAwait(false));
var archiveSource = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
if (archiveSource is { IsIndexed: true })
{
var archiveListing = await TryListArchiveAsync(archiveSource, path, cancellationToken).ConfigureAwait(false);
if (archiveListing is not null)
{
yield return CompleteDelta(archiveListing);
yield break;
}
}
if (!reachable)
{
yield return CompleteDelta(await ListOfflineAsync(archiveSource, path, cancellationToken).ConfigureAwait(false));
yield break;
}
}
else if (!reachable)
{
var offlineSource = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
yield return CompleteDelta(await ListOfflineAsync(offlineSource, path, cancellationToken).ConfigureAwait(false));
yield break;
}
await foreach (var delta in ListLiveProgressiveAsync(path, source, viewport, cancellationToken).ConfigureAwait(false))
var sourceTask = _sources.FindByPathAsync(path, cancellationToken);
_ = MarkReachableInBackground(path, cancellationToken);
await foreach (var delta in ListLiveProgressiveAsync(path, sourceTask, viewport, cancellationToken).ConfigureAwait(false))
{
yield return delta;
}
@@ -205,6 +254,64 @@ public sealed class BrowseService
public bool CanBrowseArchive(string name)
=> _preferences.Load().IndexArchiveContents && ArchiveFormats.IsArchive(name);
private bool MightBeArchiveListing(string path, bool reachable)
{
if (!_preferences.Load().IndexArchiveContents)
{
return false;
}
if (!reachable)
{
return true;
}
var name = Path.GetFileName(path.TrimEnd('\\', '/'));
return !string.IsNullOrEmpty(name) && ArchiveFormats.IsArchive(name);
}
private async Task MarkReachableInBackground(string path, CancellationToken cancellationToken)
{
try
{
await _sources.MarkReachableAsync(path, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
catch
{
// Listing already started from the live filesystem.
}
}
private async Task<Dictionary<string, IndexEntry>> LoadIndexAfterSourceAsync(
Task<Source?> sourceTask,
string path,
CancellationToken cancellationToken)
{
Source? source;
try
{
source = await sourceTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch
{
return new Dictionary<string, IndexEntry>(StringComparer.Ordinal);
}
if (source is not { IsIndexed: true })
{
return new Dictionary<string, IndexEntry>(StringComparer.Ordinal);
}
return await LoadIndexChildrenAsync(source, path, cancellationToken).ConfigureAwait(false);
}
private static void ApplyDelta(
List<FileSystemItem> items,
Dictionary<string, int> byPath,
@@ -293,46 +400,43 @@ public sealed class BrowseService
private async IAsyncEnumerable<BrowseDelta> ListLiveProgressiveAsync(
string path,
Source? source,
Task<Source?> sourceTask,
BrowseViewport? viewport,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var prefs = _preferences.Load();
var spaceCache = new Dictionary<string, VolumeSpace>(StringComparer.OrdinalIgnoreCase);
var indexTask = source is { IsIndexed: true }
? LoadIndexChildrenAsync(source, path, cancellationToken)
: Task.FromResult(new Dictionary<string, IndexEntry>(StringComparer.Ordinal));
Task<Dictionary<string, IndexEntry>>? indexTask = null;
Task<Dictionary<string, IndexEntry>> KickIndex()
=> indexTask ??= LoadIndexAfterSourceAsync(sourceTask, path, cancellationToken);
var batch = new List<FileSystemItem>(BrowseHydration.PublishBatch);
var all = new List<FileSystemItem>();
var byPath = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, IndexEntry>? indexMap = indexTask.IsCompletedSuccessfully ? indexTask.Result : null;
var sink = new FileEnumerationSink();
var firstFlush = true;
await foreach (var raw in StreamEnumerationAsync(path, sink, cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
if (indexMap is null && indexTask.IsCompleted)
{
indexMap = await indexTask.ConfigureAwait(false);
}
var item = Annotate(raw, sizeFromIndex: false, prefs, probeAccess: false);
var item = Annotate(
raw.Overlay(hydration: raw.Hydration & ~(ItemHydrationFlags.Index | ItemHydrationFlags.Provider)),
sizeFromIndex: false,
prefs,
probeAccess: false);
if (!LocationVisibility.ShouldShow(item.Location, prefs))
{
continue;
}
if (indexMap is not null)
{
item = OverlayIndex(item, indexMap, prefs);
}
item = AttachVolumeSpace(item, spaceCache);
Upsert(all, byPath, item);
batch.Add(item);
if (batch.Count >= BrowseHydration.PublishBatch)
var limit = firstFlush ? BrowseHydration.FirstPublish : BrowseHydration.PublishBatch;
if (batch.Count >= limit)
{
firstFlush = false;
yield return new BrowseDelta
{
Path = path,
@@ -340,6 +444,7 @@ public sealed class BrowseService
CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location
};
batch.Clear();
_ = KickIndex();
}
}
@@ -352,11 +457,7 @@ public sealed class BrowseService
CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location
};
if (indexMap is null)
{
indexMap = await indexTask.ConfigureAwait(false);
}
var indexMap = await KickIndex().ConfigureAwait(false);
var indexUpdates = OverlayPendingIndex(all, byPath, indexMap, prefs);
if (indexUpdates.Count > 0)
{
@@ -368,6 +469,7 @@ public sealed class BrowseService
};
}
var source = sourceTask.IsCompletedSuccessfully ? sourceTask.Result : await sourceTask.ConfigureAwait(false);
var accessUpdates = await ProbeAccessDeniedAsync(all, byPath, prefs, source?.Kind, cancellationToken)
.ConfigureAwait(false);
if (accessUpdates.Count > 0)
@@ -506,6 +608,7 @@ public sealed class BrowseService
allocatedSizeBytes: item.AllocatedSizeBytes ?? entry.AllocatedSizeBytes,
fileId: item.FileId ?? entry.FileId,
cloud: cloud,
indexedChildCount: entry.ChildFileCount + entry.ChildDirCount,
hydration: item.Hydration | ItemHydrationFlags.Index);
return Annotate(hydrated, sizeFromIndex: item.IsDirectory && entry.AggregateSize > 0, preferences, probeAccess: false);
}
@@ -757,6 +860,28 @@ public sealed class BrowseService
return item.Overlay(freeSpaceBytes: space.FreeBytes, capacityBytes: space.CapacityBytes);
}
public int CountVisibleChildren(string path)
{
var live = _enumerator.EnumerateChildrenSafe(path, out var error);
if (error is not null)
{
return -1;
}
var prefs = _preferences.Load();
var count = 0;
foreach (var item in live)
{
var location = LocationClassifier.Classify(item.FullPath, item.Name, item.Attributes, item.IsDirectory);
if (LocationVisibility.ShouldShow(location, prefs) && !location.IsRecycleBin)
{
count++;
}
}
return count;
}
private IReadOnlyList<FileSystemItem> AttachVolumeSpace(IReadOnlyList<FileSystemItem> items)
{
var cache = new Dictionary<string, VolumeSpace>(StringComparer.OrdinalIgnoreCase);
@@ -813,6 +938,12 @@ public sealed class BrowseService
.ToList();
}
private static string FavoriteDisplayName(string path)
{
var name = PathRules.GetFileName(path.TrimEnd('\\'));
return string.IsNullOrEmpty(name) ? path : name;
}
private static string FormatBytes(long bytes)
{
string[] units = ["B", "KB", "MB", "GB", "TB", "PB"];