File: Completion\Providers\Scripting\AbstractDirectivePathCompletionProvider.cs
Web Access
Project: ..\..\..\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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Scripting;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using System.Diagnostics;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.Completion.Providers
{
    internal abstract class AbstractDirectivePathCompletionProvider : CompletionProvider
    {
        protected static bool IsDirectorySeparator(char ch)
             => ch == '/' || (ch == '\\' && !PathUtilities.IsUnixLikePlatform);
 
        protected abstract bool TryGetStringLiteralToken(SyntaxTree tree, int position, out SyntaxToken stringLiteral, CancellationToken cancellationToken);
 
        /// <summary>
        /// <code>r</code> for metadata reference directive, <code>load</code> for source file directive.
        /// </summary>
        protected abstract string DirectiveName { get; }
 
        public sealed override async Task ProvideCompletionsAsync(CompletionContext context)
        {
            try
            {
                var document = context.Document;
                var position = context.Position;
                var cancellationToken = context.CancellationToken;
 
                var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
 
                if (!TryGetStringLiteralToken(tree, position, out var stringLiteral, cancellationToken))
                {
                    return;
                }
 
                var literalValue = stringLiteral.ToString();
 
                context.CompletionListSpan = GetTextChangeSpan(
                    quotedPath: literalValue,
                    quotedPathStart: stringLiteral.SpanStart,
                    position: position);
 
                var pathThroughLastSlash = GetPathThroughLastSlash(
                    quotedPath: literalValue,
                    quotedPathStart: stringLiteral.SpanStart,
                    position: position);
 
                await ProvideCompletionsAsync(context, pathThroughLastSlash).ConfigureAwait(false);
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General))
            {
                // nop
            }
        }
 
        public sealed override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
        {
            var lineStart = text.Lines.GetLineFromPosition(caretPosition).Start;
 
            // check if the line starts with {whitespace}#{whitespace}{DirectiveName}{whitespace}"
 
            var poundIndex = text.IndexOfNonWhiteSpace(lineStart, caretPosition - lineStart);
            if (poundIndex == -1 || text[poundIndex] != '#')
            {
                return false;
            }
 
            var directiveNameStartIndex = text.IndexOfNonWhiteSpace(poundIndex + 1, caretPosition - poundIndex - 1);
            if (directiveNameStartIndex == -1 || !text.ContentEquals(directiveNameStartIndex, DirectiveName))
            {
                return false;
            }
 
            var directiveNameEndIndex = directiveNameStartIndex + DirectiveName.Length;
            var quoteIndex = text.IndexOfNonWhiteSpace(directiveNameEndIndex, caretPosition - directiveNameEndIndex);
            if (quoteIndex == -1 || text[quoteIndex] != '"')
            {
                return false;
            }
 
            return true;
        }
 
        private static string GetPathThroughLastSlash(string quotedPath, int quotedPathStart, int position)
        {
            Contract.ThrowIfTrue(quotedPath[0] != '"');
 
            const int QuoteLength = 1;
 
            var positionInQuotedPath = position - quotedPathStart;
            var path = quotedPath[QuoteLength..positionInQuotedPath].Trim();
            var afterLastSlashIndex = AfterLastSlashIndex(path, path.Length);
 
            // We want the portion up to, and including the last slash if there is one.  That way if
            // the user pops up completion in the middle of a path (i.e. "C:\Win") then we'll
            // consider the path to be "C:\" and we will show appropriate completions.
            return afterLastSlashIndex >= 0 ? path[..afterLastSlashIndex] : path;
        }
 
        private static TextSpan GetTextChangeSpan(string quotedPath, int quotedPathStart, int position)
        {
            // We want the text change to be from after the last slash to the end of the quoted
            // path. If there is no last slash, then we want it from right after the start quote
            // character.
            var positionInQuotedPath = position - quotedPathStart;
 
            // Where we want to start tracking is right after the slash (if we have one), or else
            // right after the string starts.
            var afterLastSlashIndex = AfterLastSlashIndex(quotedPath, positionInQuotedPath);
            var afterFirstQuote = 1;
 
            var startIndex = Math.Max(afterLastSlashIndex, afterFirstQuote);
            var endIndex = quotedPath.Length;
 
            // If the string ends with a quote, the we do not want to consume that.
            if (EndsWithQuote(quotedPath))
            {
                endIndex--;
            }
 
            return TextSpan.FromBounds(startIndex + quotedPathStart, endIndex + quotedPathStart);
        }
 
        private static bool EndsWithQuote(string quotedPath)
            => quotedPath is [.., _, '"'];
 
        /// <summary>
        /// Returns the index right after the last slash that precedes 'position'.  If there is no
        /// slash in the string, -1 is returned.
        /// </summary>
        private static int AfterLastSlashIndex(string text, int position)
        {
            // Position might be out of bounds of the string (if the string is unterminated.  Make
            // sure it's within bounds.
            position = Math.Min(position, text.Length - 1);
 
            int index;
            if ((index = text.LastIndexOf('/', position)) >= 0 ||
                !PathUtilities.IsUnixLikePlatform && (index = text.LastIndexOf('\\', position)) >= 0)
            {
                return index + 1;
            }
 
            return -1;
        }
 
        protected abstract Task ProvideCompletionsAsync(CompletionContext context, string pathThroughLastSlash);
 
        protected static FileSystemCompletionHelper GetFileSystemCompletionHelper(
            Document document,
            Glyph itemGlyph,
            ImmutableArray<string> extensions,
            CompletionItemRules completionRules)
        {
            ImmutableArray<string> referenceSearchPaths;
            string? baseDirectory;
            if (document.Project.CompilationOptions?.MetadataReferenceResolver is RuntimeMetadataReferenceResolver resolver)
            {
                referenceSearchPaths = resolver.PathResolver.SearchPaths;
                baseDirectory = resolver.PathResolver.BaseDirectory;
            }
            else
            {
                referenceSearchPaths = ImmutableArray<string>.Empty;
                baseDirectory = null;
            }
 
            return new FileSystemCompletionHelper(
                Glyph.OpenFolder,
                itemGlyph,
                referenceSearchPaths,
                GetBaseDirectory(document, baseDirectory),
                extensions,
                completionRules);
        }
 
        private static string? GetBaseDirectory(Document document, string? baseDirectory)
        {
            var result = PathUtilities.GetDirectoryName(document.FilePath);
            if (!PathUtilities.IsAbsolute(result))
            {
                result = baseDirectory;
                Debug.Assert(result == null || PathUtilities.IsAbsolute(result));
            }
 
            return result;
        }
    }
}