// 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.Collections.Generic; using System.Collections.Immutable; using System.Threading; 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.Indentation; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Indentation; internal sealed class CSharpSmartTokenFormatter : ISmartTokenFormatter { private readonly IndentationOptions _options; private readonly ImmutableArray<AbstractFormattingRule> _formattingRules; private readonly CompilationUnitSyntax _root; private readonly SourceText _text; public CSharpSmartTokenFormatter( IndentationOptions options, ImmutableArray<AbstractFormattingRule> formattingRules, CompilationUnitSyntax root, SourceText text) { Contract.ThrowIfNull(root); _options = options; _formattingRules = formattingRules; _root = root; _text = text; } public IList<TextChange> FormatRange( SyntaxToken startToken, SyntaxToken endToken, CancellationToken cancellationToken) { Contract.ThrowIfTrue(startToken.Kind() is SyntaxKind.None or SyntaxKind.EndOfFileToken); Contract.ThrowIfTrue(endToken.Kind() is SyntaxKind.None or SyntaxKind.EndOfFileToken); var smartTokenformattingRules = _formattingRules; var common = startToken.GetCommonRoot(endToken); RoslynDebug.AssertNotNull(common); // if there are errors, do not touch lines // Exception 1: In the case of try-catch-finally block, a try block without a catch/finally block is considered incomplete // but we would like to apply line operation in a completed try block even if there is no catch/finally block // Exception 2: Similar behavior for do-while if (common.ContainsDiagnostics && !CloseBraceOfTryOrDoBlock(endToken)) { smartTokenformattingRules = [new NoLineChangeFormattingRule(), .. _formattingRules]; } var formatter = CSharpSyntaxFormatting.Instance; var result = formatter.GetFormattingResult( _root, [TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End)], _options.FormattingOptions, smartTokenformattingRules, cancellationToken); return result.GetTextChanges(cancellationToken); } private static bool CloseBraceOfTryOrDoBlock(SyntaxToken endToken) { return endToken.IsKind(SyntaxKind.CloseBraceToken) && endToken.Parent.IsKind(SyntaxKind.Block) && endToken.Parent.Parent?.Kind() is SyntaxKind.TryStatement or SyntaxKind.DoStatement; } public IList<TextChange> FormatToken(SyntaxToken token, CancellationToken cancellationToken) { Contract.ThrowIfTrue(token.Kind() is SyntaxKind.None or SyntaxKind.EndOfFileToken); // get previous token var previousToken = token.GetPreviousToken(includeZeroWidth: true); if (previousToken.Kind() == SyntaxKind.None) { // no previous token. nothing to format return []; } // This is a heuristic to prevent brace completion from breaking user expectation/muscle memory in common scenarios (see Devdiv:823958). // Formatter uses FindToken on the position, which returns token to left, if there is nothing to the right and returns token to the right // if there exists one. If the shape is "{|}", we're including '}' in the formatting range. Avoid doing that to improve verbatim typing // in the following special scenarios. var adjustedEndPosition = token.Span.End; if (token.IsKind(SyntaxKind.OpenBraceToken) && (token.Parent.IsInitializerForArrayOrCollectionCreationExpression() || token.Parent is AnonymousObjectCreationExpressionSyntax)) { var nextToken = token.GetNextToken(includeZeroWidth: true); if (nextToken.IsKind(SyntaxKind.CloseBraceToken)) { // Format upto '{' and exclude '}' adjustedEndPosition = token.SpanStart; } } ImmutableArray<AbstractFormattingRule> smartTokenFormattingRules = [new SmartTokenFormattingRule(), .. _formattingRules]; var adjustedStartPosition = previousToken.SpanStart; if (token.IsKind(SyntaxKind.OpenBraceToken) && _options.IndentStyle != FormattingOptions2.IndentStyle.Smart) { RoslynDebug.AssertNotNull(token.SyntaxTree); if (token.IsFirstTokenOnLine(_text)) { adjustedStartPosition = token.SpanStart; } } var formatter = CSharpSyntaxFormatting.Instance; var result = formatter.GetFormattingResult( _root, [TextSpan.FromBounds(adjustedStartPosition, adjustedEndPosition)], _options.FormattingOptions, smartTokenFormattingRules, cancellationToken); return result.GetTextChanges(cancellationToken); } private class NoLineChangeFormattingRule : AbstractFormattingRule { public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) { // no line operation. no line changes what so ever var lineOperation = base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); if (lineOperation != null) { // ignore force if same line option if (lineOperation.Option == AdjustNewLinesOption.ForceLinesIfOnSingleLine) { return null; } // basically means don't ever put new line if there isn't already one, but do // indentation. return FormattingOperations.CreateAdjustNewLinesOperation(line: 0, option: AdjustNewLinesOption.PreserveLines); } return null; } } private sealed class SmartTokenFormattingRule : NoLineChangeFormattingRule { public override void AddSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node, in NextSuppressOperationAction nextOperation) { // don't suppress anything } public override AdjustSpacesOperation? GetAdjustSpacesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustSpacesOperation nextOperation) { var spaceOperation = base.GetAdjustSpacesOperation(in previousToken, in currentToken, in nextOperation); // if there is force space operation, convert it to ForceSpaceIfSingleLine operation. // (force space basically means remove all line breaks) if (spaceOperation != null && spaceOperation.Option == AdjustSpacesOption.ForceSpaces) { return FormattingOperations.CreateAdjustSpacesOperation(spaceOperation.Space, AdjustSpacesOption.ForceSpacesIfOnSingleLine); } return spaceOperation; } } } |