File: Completion\FileSystemCompletionHelper.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Completion;
 
internal class FileSystemCompletionHelper
{
    private static readonly char[] s_windowsDirectorySeparator = ['\\'];
 
    private readonly Glyph _folderGlyph;
    private readonly Glyph _fileGlyph;
 
    // absolute paths
    private readonly ImmutableArray<string> _searchPaths;
    private readonly string? _baseDirectory;
 
    private readonly ImmutableArray<string> _allowableExtensions;
    private readonly CompletionItemRules _itemRules;
 
    public FileSystemCompletionHelper(
        Glyph folderGlyph,
        Glyph fileGlyph,
        ImmutableArray<string> searchPaths,
        string? baseDirectory,
        ImmutableArray<string> allowableExtensions,
        CompletionItemRules itemRules)
    {
        Debug.Assert(searchPaths.All(PathUtilities.IsAbsolute));
        Debug.Assert(baseDirectory == null || PathUtilities.IsAbsolute(baseDirectory));
 
        _searchPaths = searchPaths;
        _baseDirectory = baseDirectory;
        _allowableExtensions = allowableExtensions;
        _folderGlyph = folderGlyph;
        _fileGlyph = fileGlyph;
        _itemRules = itemRules;
    }
 
    // virtual for testing
    protected virtual string[] GetLogicalDrives()
        => IOUtilities.PerformIO(Directory.GetLogicalDrives, []);
 
    // virtual for testing
    protected virtual bool DirectoryExists(string fullPath)
    {
        Debug.Assert(PathUtilities.IsAbsolute(fullPath));
        return Directory.Exists(fullPath);
    }
 
    // virtual for testing
    protected virtual IEnumerable<string> EnumerateDirectories(string fullDirectoryPath)
    {
        Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath));
        return IOUtilities.PerformIO(() => Directory.EnumerateDirectories(fullDirectoryPath), []);
    }
 
    // virtual for testing
    protected virtual IEnumerable<string> EnumerateFiles(string fullDirectoryPath)
    {
        Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath));
        return IOUtilities.PerformIO(() => Directory.EnumerateFiles(fullDirectoryPath), []);
    }
 
    // virtual for testing
    protected virtual bool IsVisibleFileSystemEntry(string fullPath)
    {
        Debug.Assert(PathUtilities.IsAbsolute(fullPath));
        return IOUtilities.PerformIO(() => (File.GetAttributes(fullPath) & (FileAttributes.Hidden | FileAttributes.System)) == 0, false);
    }
 
    private CompletionItem CreateNetworkRoot()
        => CommonCompletionItem.Create(
            "\\\\",
            displayTextSuffix: "",
            glyph: null,
            description: "\\\\".ToSymbolDisplayParts(),
            rules: _itemRules);
 
    private CompletionItem CreateUnixRoot()
        => CommonCompletionItem.Create(
            "/",
            displayTextSuffix: "",
            glyph: _folderGlyph,
            description: "/".ToSymbolDisplayParts(),
            rules: _itemRules);
 
    private CompletionItem CreateFileSystemEntryItem(string fullPath, bool isDirectory)
        => CommonCompletionItem.Create(
            PathUtilities.GetFileName(fullPath),
            displayTextSuffix: "",
            glyph: isDirectory ? _folderGlyph : _fileGlyph,
            description: fullPath.ToSymbolDisplayParts(),
            rules: _itemRules);
 
    private CompletionItem CreateLogicalDriveItem(string drive)
        => CommonCompletionItem.Create(
            drive,
            displayTextSuffix: "",
            glyph: _folderGlyph,
            description: drive.ToSymbolDisplayParts(),
            rules: _itemRules);
 
    public Task<ImmutableArray<CompletionItem>> GetItemsAsync(string directoryPath, CancellationToken cancellationToken)
        => Task.Run(() => GetItems(directoryPath, cancellationToken), cancellationToken);
 
    private ImmutableArray<CompletionItem> GetItems(string directoryPath, CancellationToken cancellationToken)
    {
        if (!PathUtilities.IsUnixLikePlatform && directoryPath == "\\")
        {
            // The user has typed only "\".  In this case, we want to add "\\" to the list.  
            return [CreateNetworkRoot()];
        }
 
        var result = ArrayBuilder<CompletionItem>.GetInstance();
 
        var pathKind = PathUtilities.GetPathKind(directoryPath);
        switch (pathKind)
        {
            case PathKind.Empty:
                // base directory
                if (_baseDirectory != null)
                {
                    result.AddRange(GetItemsInDirectory(_baseDirectory, cancellationToken));
                }
 
                // roots
                if (PathUtilities.IsUnixLikePlatform)
                {
                    result.AddRange(CreateUnixRoot());
                }
                else
                {
                    foreach (var drive in GetLogicalDrives())
                    {
                        result.Add(CreateLogicalDriveItem(drive.TrimEnd(s_windowsDirectorySeparator)));
                    }
 
                    result.Add(CreateNetworkRoot());
                }
 
                // entries on search paths
                foreach (var searchPath in _searchPaths)
                {
                    result.AddRange(GetItemsInDirectory(searchPath, cancellationToken));
                }
 
                break;
 
            case PathKind.Absolute:
            case PathKind.RelativeToCurrentDirectory:
            case PathKind.RelativeToCurrentParent:
            case PathKind.RelativeToCurrentRoot:
                var fullDirectoryPath = FileUtilities.ResolveRelativePath(directoryPath, basePath: null, baseDirectory: _baseDirectory);
                if (fullDirectoryPath != null)
                {
                    result.AddRange(GetItemsInDirectory(fullDirectoryPath, cancellationToken));
                }
                else
                {
                    // invalid path
                    result.Clear();
                }
 
                break;
 
            case PathKind.Relative:
 
                // base directory:
                if (_baseDirectory != null)
                {
                    result.AddRange(GetItemsInDirectory(PathUtilities.CombineAbsoluteAndRelativePaths(_baseDirectory, directoryPath)!, cancellationToken));
                }
 
                // search paths:
                foreach (var searchPath in _searchPaths)
                {
                    result.AddRange(GetItemsInDirectory(PathUtilities.CombineAbsoluteAndRelativePaths(searchPath, directoryPath)!, cancellationToken));
                }
 
                break;
 
            case PathKind.RelativeToDriveDirectory:
                // Paths "C:dir" are not supported, but when the path doesn't include any directory, i.e. "C:",
                // we return the drive itself.
                if (directoryPath.Length == 2)
                {
                    result.Add(CreateLogicalDriveItem(directoryPath));
                }
 
                break;
 
            default:
                throw ExceptionUtilities.UnexpectedValue(pathKind);
        }
 
        return result.ToImmutableAndFree();
    }
 
    private IEnumerable<CompletionItem> GetItemsInDirectory(string fullDirectoryPath, CancellationToken cancellationToken)
    {
        Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath));
 
        cancellationToken.ThrowIfCancellationRequested();
 
        if (!DirectoryExists(fullDirectoryPath))
        {
            yield break;
        }
 
        cancellationToken.ThrowIfCancellationRequested();
 
        foreach (var directory in EnumerateDirectories(fullDirectoryPath))
        {
            if (IsVisibleFileSystemEntry(directory))
            {
                yield return CreateFileSystemEntryItem(directory, isDirectory: true);
            }
        }
 
        cancellationToken.ThrowIfCancellationRequested();
 
        foreach (var file in EnumerateFiles(fullDirectoryPath))
        {
            if (_allowableExtensions.Length != 0 &&
                !_allowableExtensions.Contains(
                    PathUtilities.GetExtension(file),
                    PathUtilities.IsUnixLikePlatform ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase))
            {
                continue;
            }
 
            cancellationToken.ThrowIfCancellationRequested();
 
            if (IsVisibleFileSystemEntry(file))
            {
                yield return CreateFileSystemEntryItem(file, isDirectory: false);
            }
        }
    }
 
    internal TestAccessor GetTestAccessor()
        => new(this);
 
    internal readonly struct TestAccessor(FileSystemCompletionHelper fileSystemCompletionHelper)
    {
        private readonly FileSystemCompletionHelper _fileSystemCompletionHelper = fileSystemCompletionHelper;
 
        internal ImmutableArray<CompletionItem> GetItems(string directoryPath, CancellationToken cancellationToken)
            => _fileSystemCompletionHelper.GetItems(directoryPath, cancellationToken);
    }
}