File: BraceCompletion\AbstractBraceCompletionService.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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.BraceCompletion;
 
internal abstract class AbstractBraceCompletionService : IBraceCompletionService
{
    protected abstract ISyntaxFacts SyntaxFacts { get; }
 
    protected abstract char OpeningBrace { get; }
    protected abstract char ClosingBrace { get; }
 
    /// <summary>
    /// Whether or not this brace completion session actually needs semantics to work (and thus should get a semantic model).
    /// </summary>
    protected virtual bool NeedsSemantics => false;
 
    /// <summary>
    /// Returns if the token is a valid opening token kind for this brace completion service.
    /// </summary>
    protected abstract bool IsValidOpeningBraceToken(SyntaxToken token);
 
    /// <summary>
    /// Returns if the token is a valid closing token kind for this brace completion service.
    /// </summary>
    protected abstract bool IsValidClosingBraceToken(SyntaxToken token);
 
    public abstract bool AllowOverType(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken);
 
    public Task<bool> HasBraceCompletionAsync(BraceCompletionContext context, Document document, CancellationToken cancellationToken)
    {
        if (!context.HasCompletionForOpeningBrace(OpeningBrace))
        {
            return Task.FromResult(false);
        }
 
        var openingToken = context.GetOpeningToken();
        if (!NeedsSemantics)
        {
            return Task.FromResult(IsValidOpenBraceTokenAtPosition(context.Document.Text, openingToken, context.OpeningPoint));
        }
 
        // Pass along a document with frozen partial semantics.  Brace completion is a highly latency sensitive
        // operation.  We don't want to wait on things like source generators to figure things out.
        return IsValidOpenBraceTokenAtPositionAsync(document.WithFrozenPartialSemantics(cancellationToken), openingToken, context.OpeningPoint, cancellationToken);
    }
 
    public BraceCompletionResult GetBraceCompletion(BraceCompletionContext context)
    {
        Debug.Assert(context.HasCompletionForOpeningBrace(OpeningBrace));
 
        var closingPoint = context.ClosingPoint;
        var braceTextEdit = new TextChange(TextSpan.FromBounds(closingPoint, closingPoint), ClosingBrace.ToString());
 
        // The caret location should be in between the braces.
        var originalOpeningLinePosition = context.Document.Text.Lines.GetLinePosition(context.OpeningPoint);
        var caretLocation = new LinePosition(originalOpeningLinePosition.Line, originalOpeningLinePosition.Character + 1);
        return new BraceCompletionResult([braceTextEdit], caretLocation);
    }
 
    public virtual BraceCompletionResult? GetTextChangesAfterCompletion(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken)
        => null;
 
    public virtual BraceCompletionResult? GetTextChangeAfterReturn(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken)
        => null;
 
    public virtual bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken)
    {
        if (OpeningBrace != brace)
        {
            return false;
        }
 
        // check that the user is not typing in a string literal or comment
        var syntaxFactsService = document.LanguageServices.GetRequiredService<ISyntaxFactsService>();
 
        return !syntaxFactsService.IsInNonUserCode(document.SyntaxTree, openingPosition, cancellationToken);
    }
 
    public BraceCompletionContext? GetCompletedBraceContext(ParsedDocument document, StructuredAnalyzerConfigOptions fallbackOptions, int caretLocation)
    {
        var leftToken = document.Root.FindTokenOnLeftOfPosition(caretLocation);
        var rightToken = document.Root.FindTokenOnRightOfPosition(caretLocation);
 
        if (IsValidOpeningBraceToken(leftToken) && IsValidClosingBraceToken(rightToken))
        {
            return new BraceCompletionContext(document, fallbackOptions, leftToken.GetLocation().SourceSpan.Start, rightToken.GetLocation().SourceSpan.End, caretLocation);
        }
 
        return null;
    }
 
    /// <summary>
    /// Only called if <see cref="NeedsSemantics"/> returns true;
    /// </summary>
    protected virtual Task<bool> IsValidOpenBraceTokenAtPositionAsync(Document document, SyntaxToken token, int position, CancellationToken cancellationToken)
    {
        // Subclass should have overridden this.
        throw ExceptionUtilities.Unreachable();
    }
 
    /// <summary>
    /// Checks if the already inserted token is a valid opening token at the position in the document.
    /// By default checks that the opening token is a valid token at the position and not in skipped token trivia.
    /// </summary>
    protected virtual bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position)
        => token.SpanStart == position && IsValidOpeningBraceToken(token) && !ParentIsSkippedTokensTriviaOrNull(this.SyntaxFacts, token);
 
    /// <summary>
    /// Returns true when the current position is inside user code (e.g. not strings) and the closing token
    /// matches the expected closing token for this brace completion service.
    /// Helper method used by <see cref="AllowOverType(BraceCompletionContext, CancellationToken)"/> implementations.
    /// </summary>
    protected bool AllowOverTypeInUserCodeWithValidClosingToken(BraceCompletionContext context, CancellationToken cancellationToken)
    {
        var tree = context.Document.SyntaxTree;
        var syntaxFactsService = context.Document.LanguageServices.GetRequiredService<ISyntaxFactsService>();
 
        return !syntaxFactsService.IsInNonUserCode(tree, context.CaretLocation, cancellationToken)
            && CheckClosingTokenKind(context.Document, context.ClosingPoint);
    }
 
    /// <summary>
    /// Returns true when the closing token matches the expected closing token for this brace completion service.
    /// Used by <see cref="AllowOverType(BraceCompletionContext, CancellationToken)"/> implementations
    /// when the over type could be triggered from outside of user code (e.g. overtyping end quotes in a string).
    /// </summary>
    protected bool AllowOverTypeWithValidClosingToken(BraceCompletionContext context)
    {
        return CheckClosingTokenKind(context.Document, context.ClosingPoint);
    }
 
    protected static bool ParentIsSkippedTokensTriviaOrNull(ISyntaxFacts syntaxFacts, SyntaxToken token)
        => token.Parent == null || syntaxFacts.IsSkippedTokensTrivia(token.Parent);
 
    /// <summary>
    /// Checks that the token at the closing position is a valid closing token.
    /// </summary>
    private bool CheckClosingTokenKind(ParsedDocument document, int closingPosition)
    {
        var closingToken = document.Root.FindTokenFromEnd(closingPosition, includeZeroWidth: false, findInsideTrivia: true);
        return IsValidClosingBraceToken(closingToken);
    }
 
    public static class CurlyBrace
    {
        public const char OpenCharacter = '{';
        public const char CloseCharacter = '}';
    }
 
    public static class Parenthesis
    {
        public const char OpenCharacter = '(';
        public const char CloseCharacter = ')';
    }
 
    public static class Bracket
    {
        public const char OpenCharacter = '[';
        public const char CloseCharacter = ']';
    }
 
    public static class LessAndGreaterThan
    {
        public const char OpenCharacter = '<';
        public const char CloseCharacter = '>';
    }
 
    public static class DoubleQuote
    {
        public const char OpenCharacter = '"';
        public const char CloseCharacter = '"';
    }
 
    public static class SingleQuote
    {
        public const char OpenCharacter = '\'';
        public const char CloseCharacter = '\'';
    }
 
    /// <summary>
    /// Determines if inserting the opening brace at the location could be an attempt to
    /// escape a previously inserted opening brace.
    /// E.g. they are trying to type $"{{"
    /// </summary>
    protected static bool CouldEscapePreviousOpenBrace(char openingBrace, int position, SourceText text)
    {
        var index = position - 1;
        var openBraceCount = 0;
        while (index >= 0)
        {
            if (text[index] == openingBrace)
            {
                openBraceCount++;
            }
            else
            {
                break;
            }
 
            index--;
        }
 
        if (openBraceCount > 0 && openBraceCount % 2 == 1)
        {
            return true;
        }
 
        return false;
    }
}