// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.Completion.FileSystem { internal sealed class FileSystemCompletionHelper { private readonly ICurrentWorkingDirectoryDiscoveryService _fileSystemDiscoveryService; private readonly Func _exclude; private readonly Glyph _folderGlyph; private readonly Glyph _fileGlyph; // absolute paths private readonly ImmutableArray _searchPaths; private readonly ISet _allowableExtensions; private readonly Lazy _lazyGetDrives; private readonly CompletionProvider _completionProvider; private readonly TextSpan _textChangeSpan; private readonly CompletionItemRules _itemRules; public FileSystemCompletionHelper( CompletionProvider completionProvider, TextSpan textChangeSpan, ICurrentWorkingDirectoryDiscoveryService fileSystemDiscoveryService, Glyph folderGlyph, Glyph fileGlyph, ImmutableArray searchPaths, IEnumerable allowableExtensions, Func exclude = null, CompletionItemRules itemRules = null) { Debug.Assert(searchPaths.All(path => PathUtilities.IsAbsolute(path))); _completionProvider = completionProvider; _textChangeSpan = textChangeSpan; _searchPaths = searchPaths; _allowableExtensions = allowableExtensions.Select(e => e.ToLowerInvariant()).ToSet(); _fileSystemDiscoveryService = fileSystemDiscoveryService; _folderGlyph = folderGlyph; _fileGlyph = fileGlyph; _exclude = exclude; _itemRules = itemRules; _lazyGetDrives = new Lazy(() => IOUtilities.PerformIO(Directory.GetLogicalDrives, SpecializedCollections.EmptyArray())); } public ImmutableArray GetItems(string pathSoFar, string documentPath) { if (_exclude != null && _exclude(pathSoFar)) { return ImmutableArray.Empty; } return GetFilesAndDirectories(pathSoFar, documentPath); } private CompletionItem CreateCurrentDirectoryItem() { return CommonCompletionItem.Create(".", rules: _itemRules); } private CompletionItem CreateParentDirectoryItem() { return CommonCompletionItem.Create("..", rules: _itemRules); } private CompletionItem CreateNetworkRoot(TextSpan textChangeSpan) { return CommonCompletionItem.Create("\\\\", rules: _itemRules); } private ImmutableArray GetFilesAndDirectories(string path, string basePath) { var result = ImmutableArray.CreateBuilder(); var pathKind = PathUtilities.GetPathKind(path); switch (pathKind) { case PathKind.Empty: result.Add(CreateCurrentDirectoryItem()); if (!IsDriveRoot(_fileSystemDiscoveryService.WorkingDirectory)) { result.Add(CreateParentDirectoryItem()); } result.Add(CreateNetworkRoot(_textChangeSpan)); result.AddRange(GetLogicalDrives()); result.AddRange(GetFilesAndDirectoriesInSearchPaths()); break; case PathKind.Absolute: case PathKind.RelativeToCurrentDirectory: case PathKind.RelativeToCurrentParent: case PathKind.RelativeToCurrentRoot: { var fullPath = FileUtilities.ResolveRelativePath( path, basePath, _fileSystemDiscoveryService.WorkingDirectory); if (fullPath != null) { result.AddRange(GetFilesAndDirectoriesInDirectory(fullPath)); // although it is possible to type "." here, it doesn't make any sense to do so: if (!IsDriveRoot(fullPath) && pathKind != PathKind.Absolute) { result.Add(CreateParentDirectoryItem()); } if (path == "\\" && pathKind == PathKind.RelativeToCurrentRoot) { // The user has typed only "\". In this case, we want to add "\\" to // the list. Also, the textChangeSpan needs to be backed up by one // so that it will consume the "\" when "\\" is inserted. result.Add(CreateNetworkRoot(TextSpan.FromBounds(_textChangeSpan.Start - 1, _textChangeSpan.End))); } } else { // invalid path result.Clear(); } } break; case PathKind.Relative: // although it is possible to type "." here, it doesn't make any sense to do so: result.Add(CreateParentDirectoryItem()); foreach (var searchPath in _searchPaths) { var fullPath = PathUtilities.CombineAbsoluteAndRelativePaths(searchPath, path); // search paths are always absolute: Debug.Assert(PathUtilities.IsAbsolute(fullPath)); result.AddRange(GetFilesAndDirectoriesInDirectory(fullPath)); } break; case PathKind.RelativeToDriveDirectory: // these paths are not supported break; default: throw ExceptionUtilities.Unreachable; } return result.AsImmutable(); } private static bool IsDriveRoot(string fullPath) { return IOUtilities.PerformIO(() => new DirectoryInfo(fullPath).Parent == null); } private IEnumerable GetFilesAndDirectoriesInDirectory(string fullDirectoryPath) { Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath)); if (IOUtilities.PerformIO(() => Directory.Exists(fullDirectoryPath))) { var directoryInfo = IOUtilities.PerformIO(() => new DirectoryInfo(fullDirectoryPath)); if (directoryInfo != null) { return from child in GetFileSystemInfos(directoryInfo) where ShouldShow(child) where CanAccess(child) select this.CreateCompletion(child); } } return SpecializedCollections.EmptyEnumerable(); } private CompletionItem CreateCompletion(FileSystemInfo child) { return CommonCompletionItem.Create( child.Name, glyph: child is DirectoryInfo ? _folderGlyph : _fileGlyph, description: child.FullName.ToSymbolDisplayParts(), rules: _itemRules); } private bool ShouldShow(FileSystemInfo child) { // Get the attributes. If we can't, assume it's hidden. var attributes = IOUtilities.PerformIO(() => child.Attributes, FileAttributes.Hidden); // Don't show hidden/system files. if ((attributes & FileAttributes.Hidden) != 0 || (attributes & FileAttributes.System) != 0) { return false; } if (child is DirectoryInfo) { return true; } if (child is FileInfo) { return _allowableExtensions.Count == 0 || _allowableExtensions.Contains(Path.GetExtension(child.Name).ToLowerInvariant()); } return false; } private bool CanAccess(FileSystemInfo info) { return info.TypeSwitch( (DirectoryInfo d) => CanAccessDirectory(d), (FileInfo f) => CanAccessFile(f)); } private bool CanAccessFile(FileInfo file) { var accessControl = IOUtilities.PerformIO(file.GetAccessControl); // Quick and dirty check. If we can't even get the access control object, then we // can't access the file. if (accessControl == null) { return false; } // TODO(cyrusn): Actually add checks here. return true; } private bool CanAccessDirectory(DirectoryInfo directory) { var accessControl = IOUtilities.PerformIO(directory.GetAccessControl); // Quick and dirty check. If we can't even get the access control object, then we // can't access the file. if (accessControl == null) { return false; } // TODO(cyrusn): Do more checks here. return true; } private IEnumerable GetFilesAndDirectoriesInSearchPaths() { return _searchPaths.SelectMany(GetFilesAndDirectoriesInDirectory); } private IEnumerable GetLogicalDrives() { // First, we may have a filename, so let's include all drives return from d in _lazyGetDrives.Value where d.Length > 0 && (d.Last() == Path.DirectorySeparatorChar || d.Last() == Path.AltDirectorySeparatorChar) let text = d.Substring(0, d.Length - 1) select CommonCompletionItem.Create(text, glyph: _folderGlyph, rules: _itemRules); } private static FileSystemInfo[] GetFileSystemInfos(DirectoryInfo directoryInfo) { return IOUtilities.PerformIO(directoryInfo.GetFileSystemInfos, SpecializedCollections.EmptyArray()); } } }