| File: BraceCompletion\CurlyBraceCompletionService.cs | Web Access |
| Project: src\roslyn\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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.Generic; using System.Collections.Immutable; using System.Composition; using System.Diagnostics; using System.Threading; using Microsoft.CodeAnalysis.BraceCompletion; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Formatting; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; [ExportBraceCompletionService(LanguageNames.CSharp), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class CurlyBraceCompletionService() : AbstractCurlyBraceOrBracketCompletionService { protected override char OpeningBrace => CurlyBrace.OpenCharacter; protected override char ClosingBrace => CurlyBrace.CloseCharacter; public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) { // Only potentially valid for curly brace completion if not in an interpolation brace completion context. if (OpeningBrace == brace && InterpolationBraceCompletionService.IsPositionInInterpolationContext(document, openingPosition)) { return false; } return base.CanProvideBraceCompletion(brace, openingPosition, document, cancellationToken); } protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.OpenBraceToken) && !token.Parent.IsKind(SyntaxKind.Interpolation); protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CloseBraceToken); protected override int AdjustFormattingEndPoint(ParsedDocument document, int startPoint, int endPoint) { // Only format outside of the completed braces if they're on the same line for array/collection/object initializer expressions. // Example: `var x = new int[]{}`: // Correct: `var x = new int[] {}` // Incorrect: `var x = new int[] { }` // This is a heuristic to prevent brace completion from breaking user expectation/muscle memory in common scenarios. // see bug Devdiv:823958 if (document.Text.Lines.GetLineFromPosition(startPoint) == document.Text.Lines.GetLineFromPosition(endPoint)) { var startToken = document.Root.FindToken(startPoint, findInsideTrivia: true); if (IsValidOpeningBraceToken(startToken) && (startToken.Parent?.IsInitializerForArrayOrCollectionCreationExpression() == true || startToken.Parent is AnonymousObjectCreationExpressionSyntax)) { // Since the braces are next to each other the span to format is everything up to the opening brace start. endPoint = startToken.SpanStart; } } return endPoint; } protected override ImmutableArray<AbstractFormattingRule> GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options) { var indentStyle = options.IndentStyle; return [BraceCompletionFormattingRule.ForIndentStyle(indentStyle)]; } private sealed class BraceCompletionFormattingRule : BaseFormattingRule { private static readonly Predicate<SuppressOperation> s_predicate = o => o.Option.IsOn(SuppressOption.NoWrapping); private static readonly ImmutableArray<BraceCompletionFormattingRule> s_instances = [ new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.None), new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.Block), new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.Smart), ]; private readonly FormattingOptions2.IndentStyle _indentStyle; private readonly CSharpSyntaxFormattingOptions _options; public BraceCompletionFormattingRule(FormattingOptions2.IndentStyle indentStyle) : this(indentStyle, CSharpSyntaxFormattingOptions.Default) { } private BraceCompletionFormattingRule(FormattingOptions2.IndentStyle indentStyle, CSharpSyntaxFormattingOptions options) { _indentStyle = indentStyle; _options = options; } public static AbstractFormattingRule ForIndentStyle(FormattingOptions2.IndentStyle indentStyle) { Debug.Assert(s_instances[(int)indentStyle]._indentStyle == indentStyle); return s_instances[(int)indentStyle]; } public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions options) { var newOptions = options as CSharpSyntaxFormattingOptions ?? CSharpSyntaxFormattingOptions.Default; if (_options.NewLines == newOptions.NewLines) { return this; } return new BraceCompletionFormattingRule(_indentStyle, newOptions); } private static bool? NeedsNewLine(in SyntaxToken currentToken, CSharpSyntaxFormattingOptions options) { if (!currentToken.IsKind(SyntaxKind.OpenBraceToken)) { return null; } // If we're inside any of the following expressions check if the option for // braces on new lines in object / array initializers is set before we attempt // to move the open brace location to a new line. // new MyObject { // new List<int> { // int[] arr = { // = new[] { // = new int[] { if (currentToken.Parent is (kind: SyntaxKind.ObjectInitializerExpression or SyntaxKind.CollectionInitializerExpression or SyntaxKind.ArrayInitializerExpression or SyntaxKind.ImplicitArrayCreationExpression or SyntaxKind.WithInitializerExpression or SyntaxKind.PropertyPatternClause)) { return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInObjectCollectionArrayInitializers); } var currentTokenParentParent = currentToken.Parent?.Parent; // * { - in the property accessor context if (currentTokenParentParent is AccessorDeclarationSyntax) { return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAccessors); } // * { - in the anonymous Method context if (currentTokenParentParent.IsKind(SyntaxKind.AnonymousMethodExpression)) { return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAnonymousMethods); } // new { - Anonymous object creation if (currentToken.Parent.IsKind(SyntaxKind.AnonymousObjectCreationExpression)) { return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAnonymousTypes); } // * { - in the control statement context if (IsControlBlock(currentToken.Parent)) { return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInControlBlocks); } // * { - in the simple Lambda context if (currentTokenParentParent is (kind: SyntaxKind.SimpleLambdaExpression or SyntaxKind.ParenthesizedLambdaExpression)) { return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInLambdaExpressionBody); } // * { - in the member declaration context if (currentTokenParentParent is MemberDeclarationSyntax) { return currentTokenParentParent is BasePropertyDeclarationSyntax ? options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInProperties) : options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInMethods); } // * { - in the type declaration context if (currentToken.Parent is BaseTypeDeclarationSyntax or NamespaceDeclarationSyntax) { return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInTypes); } return null; } private static bool IsControlBlock(SyntaxNode? node) { if (node.IsKind(SyntaxKind.SwitchStatement)) { return true; } var parentKind = node?.Parent?.Kind(); switch (parentKind.GetValueOrDefault()) { case SyntaxKind.IfStatement: case SyntaxKind.ElseClause: case SyntaxKind.WhileStatement: case SyntaxKind.DoStatement: case SyntaxKind.ForEachStatement: case SyntaxKind.ForEachVariableStatement: case SyntaxKind.UsingStatement: case SyntaxKind.ForStatement: case SyntaxKind.TryStatement: case SyntaxKind.CatchClause: case SyntaxKind.FinallyClause: case SyntaxKind.LockStatement: case SyntaxKind.CheckedStatement: case SyntaxKind.UncheckedStatement: case SyntaxKind.SwitchSection: case SyntaxKind.FixedStatement: case SyntaxKind.UnsafeStatement: return true; default: return false; } } public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) { var needsNewLine = NeedsNewLine(currentToken, _options); return needsNewLine switch { true => CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines), false => null, _ => base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation), }; } public override void AddAlignTokensOperations(List<AlignTokensOperation> list, SyntaxNode node, in NextAlignTokensOperationAction nextOperation) { base.AddAlignTokensOperations(list, node, in nextOperation); if (_indentStyle == FormattingOptions2.IndentStyle.Block) { var bracePair = node.GetBracePair(); if (bracePair.IsValidBracketOrBracePair()) { // If the user has set block style indentation and we're in a valid brace pair // then make sure we align the close brace to the open brace. AddAlignIndentationOfTokensToBaseTokenOperation(list, node, bracePair.openBrace, [bracePair.closeBrace], AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine); } } } public override void AddSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node, in NextSuppressOperationAction nextOperation) { base.AddSuppressOperations(list, node, in nextOperation); // not sure exactly what is happening here, but removing the bellow causes the indentation to be wrong. // remove suppression rules for array and collection initializer if (node.IsInitializerForArrayOrCollectionCreationExpression()) { // remove any suppression operation list.RemoveAll(s_predicate); } } } }