File: Completion\Providers\Scripting\AbstractDirectivePathCompletionProvider.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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
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 = [];
            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;
    }
}