| File: BraceCompletion\AbstractBraceCompletionService.cs | Web Access |
| Project: src\roslyn\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 ValueTask<bool> HasBraceCompletionAsync(BraceCompletionContext context, Document document, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (!context.HasCompletionForOpeningBrace(OpeningBrace)) return ValueTask.FromResult(false); var openingToken = context.GetOpeningToken(); if (!NeedsSemantics) return ValueTask.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 ValueTask<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; } }