|
// 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.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.AutomaticCompletion;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement
{
/// <summary>
/// When user types <c>;</c> in a statement, semicolon is added and caret is placed after the semicolon
/// </summary>
[Export(typeof(ICommandHandler))]
[Export]
[ContentType(ContentTypeNames.CSharpContentType)]
[Name(nameof(CompleteStatementCommandHandler))]
[Order(After = PredefinedCompletionNames.CompletionCommandHandler)]
internal sealed class CompleteStatementCommandHandler : IChainedCommandHandler<TypeCharCommandArgs>
{
private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry;
private readonly IEditorOperationsFactoryService _editorOperationsFactoryService;
private readonly IGlobalOptionService _globalOptions;
public CommandState GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler) => nextCommandHandler();
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CompleteStatementCommandHandler(
ITextUndoHistoryRegistry textUndoHistoryRegistry,
IEditorOperationsFactoryService editorOperationsFactoryService,
IGlobalOptionService globalOptions)
{
_textUndoHistoryRegistry = textUndoHistoryRegistry;
_editorOperationsFactoryService = editorOperationsFactoryService;
_globalOptions = globalOptions;
}
public string DisplayName => CSharpEditorResources.Complete_statement_on_semicolon;
public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
{
var willMoveSemicolon = BeforeExecuteCommand(speculative: true, args, executionContext);
if (!willMoveSemicolon)
{
// Pass this on without altering the undo stack
nextCommandHandler();
return;
}
using var transaction = CaretPreservingEditTransaction.TryCreate(CSharpEditorResources.Complete_statement_on_semicolon, args.TextView, _textUndoHistoryRegistry, _editorOperationsFactoryService);
// Determine where semicolon should be placed and move caret to location
BeforeExecuteCommand(speculative: false, args, executionContext);
// Insert the semicolon using next command handler
nextCommandHandler();
transaction?.Complete();
}
private bool BeforeExecuteCommand(bool speculative, TypeCharCommandArgs args, CommandExecutionContext executionContext)
{
if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty)
{
return false;
}
var caretOpt = args.TextView.GetCaretPoint(args.SubjectBuffer);
if (!caretOpt.HasValue)
{
return false;
}
if (!_globalOptions.GetOption(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon))
{
return false;
}
var caret = caretOpt.Value;
var document = caret.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
{
return false;
}
var cancellationToken = executionContext.OperationContext.UserCancellationToken;
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken);
if (!TryGetStartingNode(root, caret, out var currentNode, cancellationToken))
{
return false;
}
return MoveCaretToSemicolonPosition(speculative, args, document, root, originalCaret: caret, caret, syntaxFacts, currentNode,
isInsideDelimiters: false, cancellationToken);
}
/// <summary>
/// Determines which node the caret is in.
/// Must be called on the UI thread.
/// </summary>
private static bool TryGetStartingNode(
SyntaxNode root,
SnapshotPoint caret,
[NotNullWhen(true)] out SyntaxNode? startingNode,
CancellationToken cancellationToken)
{
// on the UI thread
startingNode = null;
var caretPosition = caret.Position;
var token = root.FindTokenOnLeftOfPosition(caretPosition);
if (token.SyntaxTree == null
|| token.SyntaxTree.IsEntirelyWithinComment(caretPosition, cancellationToken))
{
return false;
}
startingNode = token.GetRequiredParent();
// If the caret is before an opening delimiter or after a closing delimeter,
// start analysis with node outside of delimiters.
//
// Examples,
// `obj.ToString$()` where `token` references `(` but the caret isn't actually inside the argument list.
// `obj.ToString()$` or `obj.method()$ .method()` where `token` references `)` but the caret isn't inside the argument list.
// `defa$$ult(object)` where `token` references `default` but the caret isn't inside the parentheses.
var delimiters = startingNode.GetParentheses();
if (delimiters == default)
{
delimiters = startingNode.GetBrackets();
}
if (delimiters == default)
{
delimiters = startingNode.GetBraces();
}
var (openingDelimiter, closingDelimiter) = delimiters;
if (!openingDelimiter.IsKind(SyntaxKind.None) && openingDelimiter.Span.Start >= caretPosition
|| !closingDelimiter.IsKind(SyntaxKind.None) && closingDelimiter.Span.End <= caretPosition)
{
startingNode = startingNode.GetRequiredParent();
}
return true;
}
private static bool MoveCaretToSemicolonPosition(
bool speculative,
TypeCharCommandArgs args,
Document document,
SyntaxNode root,
SnapshotPoint originalCaret,
SnapshotPoint caret,
ISyntaxFactsService syntaxFacts,
SyntaxNode? currentNode,
bool isInsideDelimiters,
CancellationToken cancellationToken)
{
if (currentNode == null ||
IsInAStringOrCharacter(currentNode, caret))
{
// Don't complete statement. Return without moving the caret.
return false;
}
if (currentNode.Kind() is
SyntaxKind.ArgumentList or
SyntaxKind.ArrayRankSpecifier or
SyntaxKind.BracketedArgumentList or
SyntaxKind.ParenthesizedExpression or
SyntaxKind.ParameterList or
SyntaxKind.DefaultExpression or
SyntaxKind.CheckedExpression or
SyntaxKind.UncheckedExpression or
SyntaxKind.TypeOfExpression or
SyntaxKind.TupleExpression or
SyntaxKind.SwitchExpression)
{
// make sure the closing delimiter exists
if (RequiredDelimiterIsMissing(currentNode))
{
return false;
}
// set caret to just outside the delimited span and analyze again
// if caret was already in that position, return to avoid infinite loop
var newCaretPosition = currentNode.Span.End;
if (newCaretPosition == caret.Position)
{
return false;
}
var newCaret = args.SubjectBuffer.CurrentSnapshot.GetPoint(newCaretPosition);
if (!TryGetStartingNode(root, newCaret, out currentNode, cancellationToken))
return false;
return MoveCaretToSemicolonPosition(
speculative, args, document, root, originalCaret, newCaret, syntaxFacts, currentNode, isInsideDelimiters: true, cancellationToken);
}
else if (currentNode.IsKind(SyntaxKind.DoStatement))
{
if (IsInConditionOfDoStatement(currentNode, caret))
{
return MoveCaretToFinalPositionInStatement(speculative, currentNode, args, originalCaret, caret, true);
}
return false;
}
else if (syntaxFacts.IsStatement(currentNode)
|| CanHaveSemicolon(currentNode))
{
return MoveCaretToFinalPositionInStatement(speculative, currentNode, args, originalCaret, caret, isInsideDelimiters);
}
else
{
// keep caret the same, but continue analyzing with the parent of the current node
currentNode = currentNode.Parent;
return MoveCaretToSemicolonPosition(
speculative, args, document, root, originalCaret, caret, syntaxFacts, currentNode, isInsideDelimiters, cancellationToken);
}
}
private static bool CanHaveSemicolon(SyntaxNode currentNode)
{
if (currentNode.Kind() is SyntaxKind.FieldDeclaration or SyntaxKind.DelegateDeclaration or SyntaxKind.ArrowExpressionClause)
{
return true;
}
if (currentNode.IsKind(SyntaxKind.EqualsValueClause) && currentNode.IsParentKind(SyntaxKind.PropertyDeclaration))
{
return true;
}
if (currentNode is TypeDeclarationSyntax { OpenBraceToken.IsMissing: true })
{
return true;
}
if (currentNode is MethodDeclarationSyntax method)
{
if (method.Modifiers.Any(SyntaxKind.AbstractKeyword) || method.Modifiers.Any(SyntaxKind.ExternKeyword) ||
method.IsParentKind(SyntaxKind.InterfaceDeclaration))
{
return true;
}
if (method.Modifiers.Any(SyntaxKind.PartialKeyword) && method.Body is null)
{
return true;
}
}
return false;
}
private static bool IsInConditionOfDoStatement(SyntaxNode currentNode, SnapshotPoint caret)
{
if (currentNode is not DoStatementSyntax doStatement)
{
return false;
}
var condition = doStatement.Condition;
return (caret >= condition.Span.Start && caret <= condition.Span.End);
}
private static bool MoveCaretToFinalPositionInStatement(bool speculative, SyntaxNode statementNode, TypeCharCommandArgs args, SnapshotPoint originalCaret, SnapshotPoint caret, bool isInsideDelimiters)
{
if (StatementClosingDelimiterIsMissing(statementNode))
{
// Don't complete statement. Return without moving the caret.
return false;
}
if (TryGetCaretPositionToMove(statementNode, caret, isInsideDelimiters, out var targetPosition)
&& targetPosition != originalCaret)
{
if (speculative)
{
// Return an indication that moving the caret is required, but don't actually move it
return true;
}
Logger.Log(FunctionId.CommandHandler_CompleteStatement, KeyValueLogMessage.Create(LogType.UserAction, m =>
{
m[nameof(isInsideDelimiters)] = isInsideDelimiters;
m[nameof(statementNode)] = statementNode.Kind();
}));
return args.TextView.TryMoveCaretToAndEnsureVisible(targetPosition);
}
return false;
}
private static bool TryGetCaretPositionToMove(SyntaxNode statementNode, SnapshotPoint caret, bool isInsideDelimiters, out SnapshotPoint targetPosition)
{
targetPosition = default;
switch (statementNode.Kind())
{
case SyntaxKind.DoStatement:
// Move caret after the do statement's closing paren.
targetPosition = caret.Snapshot.GetPoint(((DoStatementSyntax)statementNode).CloseParenToken.Span.End);
return true;
case SyntaxKind.ForStatement:
// `For` statements can have semicolon after initializer/declaration or after condition.
// If caret is in initialer/declaration or condition, AND is inside other delimiters, complete statement
// Otherwise, return without moving the caret.
return isInsideDelimiters && TryGetForStatementCaret(caret, (ForStatementSyntax)statementNode, out targetPosition);
case SyntaxKind.ExpressionStatement:
case SyntaxKind.GotoCaseStatement:
case SyntaxKind.LocalDeclarationStatement:
case SyntaxKind.ReturnStatement:
case SyntaxKind.YieldReturnStatement:
case SyntaxKind.ThrowStatement:
case SyntaxKind.FieldDeclaration:
case SyntaxKind.DelegateDeclaration:
case SyntaxKind.ArrowExpressionClause:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.RecordDeclaration:
case SyntaxKind.EqualsValueClause:
case SyntaxKind.RecordStructDeclaration:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.StructDeclaration:
case SyntaxKind.InterfaceDeclaration:
// These statement types end in a semicolon.
// if the original caret was inside any delimiters, `caret` will be after the outermost delimiter
targetPosition = caret;
return isInsideDelimiters;
default:
// For all other statement types, don't complete statement. Return without moving the caret.
return false;
}
}
private static bool TryGetForStatementCaret(SnapshotPoint originalCaret, ForStatementSyntax forStatement, out SnapshotPoint forStatementCaret)
{
if (CaretIsInForStatementCondition(originalCaret, forStatement, out var condition))
{
forStatementCaret = GetCaretAtPosition(condition.Span.End);
}
else if (CaretIsInForStatementDeclaration(originalCaret, forStatement, out var declaration))
{
forStatementCaret = GetCaretAtPosition(declaration.Span.End);
}
else if (CaretIsInForStatementInitializers(originalCaret, forStatement))
{
forStatementCaret = GetCaretAtPosition(forStatement.Initializers.Span.End);
}
else
{
// set caret to default, we will return false
forStatementCaret = default;
}
return (forStatementCaret != default);
// Locals
SnapshotPoint GetCaretAtPosition(int position) => originalCaret.Snapshot.GetPoint(position);
}
private static bool CaretIsInForStatementCondition(int caretPosition, ForStatementSyntax forStatementSyntax, [NotNullWhen(true)] out ExpressionSyntax? condition)
{
condition = forStatementSyntax.Condition;
if (condition == null)
return false;
// If condition is null and caret is in the condition section, as in `for ( ; $$; )`,
// we will have bailed earlier due to not being inside supported delimiters
return caretPosition > condition.SpanStart && caretPosition <= condition.Span.End;
}
private static bool CaretIsInForStatementDeclaration(int caretPosition, ForStatementSyntax forStatementSyntax, [NotNullWhen(true)] out VariableDeclarationSyntax? declaration)
{
declaration = forStatementSyntax.Declaration;
if (declaration == null)
return false;
return caretPosition > declaration.Span.Start && caretPosition <= declaration.Span.End;
}
private static bool CaretIsInForStatementInitializers(int caretPosition, ForStatementSyntax forStatementSyntax)
=> forStatementSyntax.Initializers.Count != 0 &&
caretPosition > forStatementSyntax.Initializers.Span.Start &&
caretPosition <= forStatementSyntax.Initializers.Span.End;
private static bool IsInAStringOrCharacter(SyntaxNode currentNode, SnapshotPoint caret)
// Check to see if caret is before or after string
=> currentNode.Kind() is SyntaxKind.InterpolatedStringExpression or SyntaxKind.StringLiteralExpression or SyntaxKind.CharacterLiteralExpression && caret.Position < currentNode.Span.End
&& caret.Position > currentNode.SpanStart;
/// <summary>
/// Determines if a statement ends with a closing delimiter, and that closing delimiter exists.
/// </summary>
/// <remarks>
/// <para>Statements such as <c>do { } while (expression);</c> contain embedded enclosing delimiters immediately
/// preceding the semicolon. These delimiters are not part of the expression, but they behave like an argument
/// list for the purposes of identifying relevant places for statement completion:</para>
/// <list type="bullet">
/// <item><description>The closing delimiter is typically inserted by the Automatic Brace Completion feature.</description></item>
/// <item><description>It is not syntactically valid to place a semicolon <em>directly</em> within the delimiters.</description></item>
/// </list>
/// </remarks>
/// <param name="currentNode"></param>
/// <returns><see langword="true"/> if <paramref name="currentNode"/> is a statement that ends with a closing
/// delimiter, and that closing delimiter exists in the source code; otherwise, <see langword="false"/>.
/// </returns>
private static bool StatementClosingDelimiterIsMissing(SyntaxNode currentNode)
{
switch (currentNode.Kind())
{
case SyntaxKind.DoStatement:
var dostatement = (DoStatementSyntax)currentNode;
return dostatement.CloseParenToken.IsMissing;
case SyntaxKind.ForStatement:
var forStatement = (ForStatementSyntax)currentNode;
return forStatement.CloseParenToken.IsMissing;
default:
return false;
}
}
/// <summary>
/// Determines if a syntax node includes all required closing delimiters.
/// </summary>
/// <remarks>
/// <para>Some syntax nodes, such as parenthesized expressions, require a matching closing delimiter to end the
/// syntax node. If this node is omitted from the source code, the parser will automatically insert a zero-width
/// "missing" closing delimiter token to produce a valid syntax tree. This method determines if required closing
/// delimiters are present in the original source.</para>
/// </remarks>
/// <param name="currentNode"></param>
/// <returns>
/// <list type="bullet">
/// <item><description><see langword="true"/> if <paramref name="currentNode"/> requires a closing delimiter and the closing delimiter is present in the source (i.e. not missing)</description></item>
/// <item><description><see langword="true"/> if <paramref name="currentNode"/> does not require a closing delimiter</description></item>
/// <item><description>otherwise, <see langword="false"/>.</description></item>
/// </list>
/// </returns>
private static bool RequiredDelimiterIsMissing(SyntaxNode currentNode)
{
return currentNode.GetBrackets().closeBracket.IsMissing ||
currentNode.GetParentheses().closeParen.IsMissing ||
currentNode.GetBraces().closeBrace.IsMissing;
}
}
}
|