/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs
620 строк
27 KB
Copilot
Have CompleteStatement handle EOF statements (#83205)
20 апр 2026, 20:31
Не верифицирован
20 апр 2026, 20:31
135e26f
Код
Авторство
О чём код?
// 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)] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class CompleteStatementCommandHandler( ITextUndoHistoryRegistry textUndoHistoryRegistry, IEditorOperationsFactoryService editorOperationsFactoryService, IGlobalOptionService globalOptions) : IChainedCommandHandler<TypeCharCommandArgs> { private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; private readonly IGlobalOptionService _globalOptions = globalOptions; private enum SemicolonBehavior { /// <summary> /// This command handler does not participate in handling the typing behavior of the current semicolon. /// </summary> None, /// <summary> /// This command handler moves the caret, but does not insert the necessary semicolon. /// </summary> Insert, /// <summary> /// This command handler moves the caret, and overtypes an existing semicolon in the process. No additional /// semicolon is needed. /// </summary> Overtype, } public CommandState GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler) => nextCommandHandler(); 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 == SemicolonBehavior.None) { // 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 if (BeforeExecuteCommand(speculative: false, args, executionContext) != SemicolonBehavior.Overtype) { // Insert the semicolon using next command handler nextCommandHandler(); } transaction?.Complete(); } private SemicolonBehavior BeforeExecuteCommand( bool speculative, TypeCharCommandArgs args, CommandExecutionContext executionContext) { if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty) return SemicolonBehavior.None; if (args.TextView.GetCaretPoint(args.SubjectBuffer) is not { } caret) return SemicolonBehavior.None; if (!_globalOptions.GetOption(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon)) return SemicolonBehavior.None; var document = caret.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); if (document == null) return SemicolonBehavior.None; var cancellationToken = executionContext.OperationContext.UserCancellationToken; var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>(); var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); if (!TryGetStartingNode(root, caret, out var tokenOnLeft, out var currentNode, cancellationToken)) return SemicolonBehavior.None; // If the user types `= new;` complete it out to `= new();` if (tokenOnLeft.Kind() is SyntaxKind.NewKeyword && currentNode is BaseObjectCreationExpressionSyntax { Parent: EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax or LocalDeclarationStatementSyntax } } } }) { if (!speculative) { var edit = args.SubjectBuffer.CreateEdit(); edit.Insert(caret, "();"); edit.Apply(); } return SemicolonBehavior.Overtype; } 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, out SyntaxToken tokenOnLeft, [NotNullWhen(true)] out SyntaxNode? startingNode, CancellationToken cancellationToken) { // on the UI thread startingNode = null; var caretPosition = caret.Position; tokenOnLeft = root.FindTokenOnLeftOfPosition(caretPosition); if (tokenOnLeft.SyntaxTree == null || tokenOnLeft.SyntaxTree.IsInNonUserCode(caretPosition, cancellationToken)) { return false; } startingNode = tokenOnLeft.GetRequiredParent(); // If the caret is before an opening delimiter or after a closing delimiter, // 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. if (HasDelimitersButCaretIsOutside(startingNode, caretPosition)) { startingNode = startingNode.GetRequiredParent(); } return true; } private static SemicolonBehavior 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 SemicolonBehavior.None; } 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.ObjectInitializerExpression or SyntaxKind.ArrayInitializerExpression or SyntaxKind.CollectionInitializerExpression or SyntaxKind.CollectionExpression or SyntaxKind.SwitchExpression) { // make sure the closing delimiter exists if (RequiredDelimiterIsMissing(currentNode)) { return SemicolonBehavior.None; } // 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 SemicolonBehavior.None; } // We know the current node has delimiters due to the Kind() check above, so isInsideDelimiters is // simply the inverse of being outside the delimiters. isInsideDelimiters = !HasDelimitersButCaretIsOutside(currentNode, caret.Position); var newCaret = args.SubjectBuffer.CurrentSnapshot.GetPoint(newCaretPosition); if (!TryGetStartingNode(root, newCaret, out _, out currentNode, cancellationToken)) return SemicolonBehavior.None; return MoveCaretToSemicolonPosition( speculative, args, document, root, originalCaret, newCaret, syntaxFacts, currentNode, isInsideDelimiters, cancellationToken); } else if (currentNode.IsKind(SyntaxKind.DoStatement)) { if (IsInConditionOfDoStatement(currentNode, caret)) { return MoveCaretToFinalPositionInStatement(speculative, currentNode, args, originalCaret, caret, isInsideDelimiters: true); } return SemicolonBehavior.None; } 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 SemicolonBehavior 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 SemicolonBehavior.None; } if (TryGetCaretPositionToMove(statementNode, caret, isInsideDelimiters, out var targetPosition) && targetPosition != originalCaret) { var overtypedExisting = AdjustPositionForExistingSemicolon(statementNode, ref targetPosition); // If this operation is speculative, return an indication that moving the caret is required, but don't // actually move it. if (!speculative) { Logger.Log(FunctionId.CommandHandler_CompleteStatement, KeyValueLogMessage.Create(LogType.UserAction, static (m, args) => { var (isInsideDelimiters, statementNode) = args; m[nameof(isInsideDelimiters)] = isInsideDelimiters; m[nameof(statementNode)] = statementNode.Kind(); }, (isInsideDelimiters, statementNode))); if (!args.TextView.TryMoveCaretToAndEnsureVisible(targetPosition)) return SemicolonBehavior.None; } return overtypedExisting ? SemicolonBehavior.Overtype : SemicolonBehavior.Insert; } return SemicolonBehavior.None; } /// <param name="statementNode">The statement that may already be followed by a semicolon</param> /// <returns>Whether we need to actively move the caret to `targetPosition` (overtype)</returns> private static bool AdjustPositionForExistingSemicolon(SyntaxNode statementNode, ref SnapshotPoint targetPosition) { // Short-circuit if we are at the position in the file if (targetPosition.Position >= targetPosition.Snapshot.Length) return false; var existingSemicolon = statementNode.FindTokenOnRightOfPosition(targetPosition, includeSkipped: true); if (existingSemicolon.IsKind(SyntaxKind.SemicolonToken) && !existingSemicolon.IsMissing) { targetPosition = new SnapshotPoint(targetPosition.Snapshot, existingSemicolon.Span.End); return true; } 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, out var relocatedPosition)) { forStatementCaret = GetCaretAtPosition(relocatedPosition); } 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, out int relocatedPosition) { if (forStatementSyntax.Initializers.Count != 0 && caretPosition > forStatementSyntax.Initializers.Span.Start && caretPosition <= forStatementSyntax.Initializers.Span.End) { // Move the caret to the first missing separator, or to the end of the initializers list if all // separators are present. for (var separatorIndex = 0; separatorIndex < forStatementSyntax.Initializers.SeparatorCount; separatorIndex++) { var separator = forStatementSyntax.Initializers.GetSeparator(separatorIndex); if (separator.IsMissing) { // We can't rely on the position of the missing separator, so move to the end of the last // initializer preceding the missing separator. relocatedPosition = forStatementSyntax.Initializers[separatorIndex].Span.End; return true; } } relocatedPosition = forStatementSyntax.Initializers.Span.End; return true; } relocatedPosition = default; return false; } private static bool IsInAStringOrCharacter(SyntaxNode currentNode, SnapshotPoint caret) { // Check to see if caret is before or after string if (currentNode.Kind() is not (SyntaxKind.InterpolatedStringExpression or SyntaxKind.StringLiteralExpression or SyntaxKind.Utf8StringLiteralExpression or SyntaxKind.CharacterLiteralExpression)) return false; if (currentNode.IsKind(SyntaxKind.StringLiteralExpression, out LiteralExpressionSyntax? literalExpression) && literalExpression.Token.Text.StartsWith("@")) { // Verbatim strings start with @", so we only consider the caret to be inside the string if it's after the " if (caret.Position <= currentNode.SpanStart + 1) return false; } else if (caret.Position <= currentNode.SpanStart) { return false; } if (currentNode.IsKind(SyntaxKind.StringLiteralExpression, out literalExpression) && (literalExpression.Token.Text.Length == 1 || literalExpression.Token.Text[^1] != '"')) { // This is an unterminated string literal, so we count the end of the span as included in the string return caret.Position <= currentNode.Span.End; } else if (currentNode.IsKind(SyntaxKind.CharacterLiteralExpression, out literalExpression) && (literalExpression.Token.Text.Length == 1 || literalExpression.Token.Text[^1] != '\'')) { // This is an unterminated character literal, so we count the end of the span as included in the character return caret.Position <= currentNode.Span.End; } else if (currentNode.IsKind(SyntaxKind.Utf8StringLiteralExpression)) { // String literals are only considered utf-8 literals if they are terminated with a "u8 sequence. Only // consider the caret inside the string if it precedes the position of the " in this sequence. return caret.Position < currentNode.Span.End - 2; } return caret.Position < currentNode.Span.End; } /// <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) { if (currentNode.GetBrackets().closeBracket.IsMissing || currentNode.GetParentheses().closeParen.IsMissing) { return true; } // If the current node has braces, we also need to make sure parent constructs are not missing braces var braces = currentNode.GetBraces(); if (braces.openBrace.IsKind(SyntaxKind.OpenBraceToken)) { if (braces.closeBrace.IsMissing) { return true; } for (var node = currentNode.Parent; node is not null; node = node.Parent) { if (node.GetBraces().closeBrace.IsMissing) return true; } } return false; } private static bool HasDelimitersButCaretIsOutside(SyntaxNode currentNode, int caretPosition) { if (currentNode.GetParentheses() is ((not SyntaxKind.None) openParenthesis, (not SyntaxKind.None) closeParenthesis)) { return openParenthesis.SpanStart >= caretPosition || closeParenthesis.Span.End <= caretPosition; } else if (currentNode.GetBrackets() is ((not SyntaxKind.None) openBracket, (not SyntaxKind.None) closeBracket)) { return openBracket.SpanStart >= caretPosition || closeBracket.Span.End <= caretPosition; } else if (currentNode.GetBraces() is ((not SyntaxKind.None) openBrace, (not SyntaxKind.None) closeBrace)) { return openBrace.SpanStart >= caretPosition || closeBrace.Span.End <= caretPosition; } else { return false; } } }