diff --git a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBracketCompletionTests.cs b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBracketCompletionTests.cs index 8f3f2fc8b7b83..8aaf2260a4842 100644 --- a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBracketCompletionTests.cs +++ b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBracketCompletionTests.cs @@ -258,7 +258,16 @@ class C { void M(object o) { - _ = o is $$ + _ = o is$$ + } +} +"; + var expectedBeforeReturn = @" +class C +{ + void M(object o) + { + _ = o is [] } } "; @@ -267,17 +276,17 @@ class C { void M(object o) { - _ = o is [ -] + _ = o is + [ + + ] } } "; using var session = CreateSession(code); CheckStart(session.Session); - // Open bracket probably should be moved to new line - // Close bracket probably should be aligned with open bracket - // Tracked by https://github.com/dotnet/roslyn/issues/57244 - CheckReturn(session.Session, 0, expected); + CheckText(session.Session, expectedBeforeReturn); + CheckReturn(session.Session, 12, expected); } internal static Holder CreateSession(string code) diff --git a/src/EditorFeatures/CSharpTest/Formatting/Indentation/SmartIndenterEnterOnTokenTests.cs b/src/EditorFeatures/CSharpTest/Formatting/Indentation/SmartIndenterEnterOnTokenTests.cs index c2479a5ff807f..f8d087ef82fef 100644 --- a/src/EditorFeatures/CSharpTest/Formatting/Indentation/SmartIndenterEnterOnTokenTests.cs +++ b/src/EditorFeatures/CSharpTest/Formatting/Indentation/SmartIndenterEnterOnTokenTests.cs @@ -1369,12 +1369,10 @@ void Main(object o) ] } }"; - // Expected indentation probably should be 12 instead - // Tracked by https://github.com/dotnet/roslyn/issues/57244 await AssertIndentNotUsingSmartTokenFormatterButUsingIndenterAsync( code, indentationLine: 7, - expectedIndentation: 8); + expectedIndentation: 12); } [Trait(Traits.Feature, Traits.Features.SmartIndent)] diff --git a/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs new file mode 100644 index 0000000000000..59f1d7906edb5 --- /dev/null +++ b/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs @@ -0,0 +1,268 @@ +// 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.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.BraceCompletion; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Formatting.Rules; +using Microsoft.CodeAnalysis.Indentation; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +{ + internal abstract class AbstractCurlyBraceOrBracketCompletionService : AbstractBraceCompletionService + { + /// + /// Annotation used to find the closing brace location after formatting changes are applied. + /// The closing brace location is then used as the caret location. + /// + private static readonly SyntaxAnnotation s_closingBraceSyntaxAnnotation = new(nameof(s_closingBraceSyntaxAnnotation)); + + protected abstract ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options); + + protected abstract int AdjustFormattingEndPoint(SourceText text, SyntaxNode root, int startPoint, int endPoint); + + public sealed override async Task GetTextChangesAfterCompletionAsync(BraceCompletionContext context, IndentationOptions options, CancellationToken cancellationToken) + { + // After the closing brace is completed we need to format the span from the opening point to the closing point. + // E.g. when the user triggers completion for an if statement ($$ is the caret location) we insert braces to get + // if (true){$$} + // We then need to format this to + // if (true) { $$} + + if (!options.AutoFormattingOptions.FormatOnCloseBrace) + { + return null; + } + + var (formattingChanges, finalCurlyBraceEnd) = await FormatTrackingSpanAsync( + context.Document, + context.OpeningPoint, + context.ClosingPoint, + // We're not trying to format the indented block here, so no need to pass in additional rules. + braceFormattingIndentationRules: ImmutableArray.Empty, + options, + cancellationToken).ConfigureAwait(false); + + if (formattingChanges.IsEmpty) + { + return null; + } + + // The caret location should be at the start of the closing brace character. + var originalText = await context.Document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var formattedText = originalText.WithChanges(formattingChanges); + var caretLocation = formattedText.Lines.GetLinePosition(finalCurlyBraceEnd - 1); + + return new BraceCompletionResult(formattingChanges, caretLocation); + } + + private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, int closingBraceEndPoint) + { + // Set the start point to the character after the opening brace. + var start = openingPosition + 1; + // Set the end point to the closing brace start character position. + var end = closingBraceEndPoint - 1; + + for (var i = start; i < end; i++) + { + if (!char.IsWhiteSpace(text[i])) + { + return false; + } + } + + return true; + } + + public sealed override async Task GetTextChangeAfterReturnAsync( + BraceCompletionContext context, + IndentationOptions options, + CancellationToken cancellationToken) + { + var document = context.Document; + var closingPoint = context.ClosingPoint; + var openingPoint = context.OpeningPoint; + var originalDocumentText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + + // check whether shape of the braces are what we support + // shape must be either "{|}" or "{ }". | is where caret is. otherwise, we don't do any special behavior + if (!ContainsOnlyWhitespace(originalDocumentText, openingPoint, closingPoint)) + { + return null; + } + + var openingPointLine = originalDocumentText.Lines.GetLineFromPosition(openingPoint).LineNumber; + var closingPointLine = originalDocumentText.Lines.GetLineFromPosition(closingPoint).LineNumber; + + // If there are already multiple empty lines between the braces, don't do anything. + // We need to allow a single empty line between the braces to account for razor scenarios where they insert a line. + if (closingPointLine - openingPointLine > 2) + { + return null; + } + + // If there is not already an empty line inserted between the braces, insert one. + TextChange? newLineEdit = null; + var textToFormat = originalDocumentText; + if (closingPointLine - openingPointLine == 1) + { + var newLineString = options.FormattingOptions.NewLine; + newLineEdit = new TextChange(new TextSpan(closingPoint - 1, 0), newLineString); + textToFormat = originalDocumentText.WithChanges(newLineEdit.Value); + + // Modify the closing point location to adjust for the newly inserted line. + closingPoint += newLineString.Length; + } + + // Format the text that contains the newly inserted line. + var (formattingChanges, newClosingPoint) = await FormatTrackingSpanAsync( + document.WithText(textToFormat), + openingPoint, + closingPoint, + braceFormattingIndentationRules: GetBraceFormattingIndentationRulesAfterReturn(options), + options, + cancellationToken).ConfigureAwait(false); + + closingPoint = newClosingPoint; + var formattedText = textToFormat.WithChanges(formattingChanges); + + // Get the empty line between the curly braces. + var desiredCaretLine = GetLineBetweenCurlys(closingPoint, formattedText); + Debug.Assert(desiredCaretLine.GetFirstNonWhitespacePosition() == null, "the line between the formatted braces is not empty"); + + // Set the caret position to the properly indented column in the desired line. + var newDocument = document.WithText(formattedText); + var newDocumentText = await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); + var caretPosition = GetIndentedLinePosition(newDocument, newDocumentText, desiredCaretLine.LineNumber, cancellationToken); + + // The new line edit is calculated against the original text, d0, to get text d1. + // The formatting edits are calculated against d1 to get text d2. + // Merge the formatting and new line edits into a set of whitespace only text edits that all apply to d0. + var overallChanges = newLineEdit != null ? GetMergedChanges(newLineEdit.Value, formattingChanges, formattedText) : formattingChanges; + return new BraceCompletionResult(overallChanges, caretPosition); + + static TextLine GetLineBetweenCurlys(int closingPosition, SourceText text) + { + var closingBraceLineNumber = text.Lines.GetLineFromPosition(closingPosition - 1).LineNumber; + return text.Lines[closingBraceLineNumber - 1]; + } + + static LinePosition GetIndentedLinePosition(Document document, SourceText sourceText, int lineNumber, CancellationToken cancellationToken) + { + var indentationService = document.GetRequiredLanguageService(); + var indentation = indentationService.GetIndentation(document, lineNumber, cancellationToken); + + var baseLinePosition = sourceText.Lines.GetLinePosition(indentation.BasePosition); + var offsetOfBacePosition = baseLinePosition.Character; + var totalOffset = offsetOfBacePosition + indentation.Offset; + var indentedLinePosition = new LinePosition(lineNumber, totalOffset); + return indentedLinePosition; + } + + static ImmutableArray GetMergedChanges(TextChange newLineEdit, ImmutableArray formattingChanges, SourceText formattedText) + { + var newRanges = TextChangeRangeExtensions.Merge( + ImmutableArray.Create(newLineEdit.ToTextChangeRange()), + formattingChanges.SelectAsArray(f => f.ToTextChangeRange())); + + using var _ = ArrayBuilder.GetInstance(out var mergedChanges); + var amountToShift = 0; + foreach (var newRange in newRanges) + { + var newTextChangeSpan = newRange.Span; + // Get the text to put in the text change by looking at the span in the formatted text. + // As the new range start is relative to the original text, we need to adjust it assuming the previous changes were applied + // to get the correct start location in the formatted text. + // E.g. with changes + // 1. Insert "hello" at 2 + // 2. Insert "goodbye" at 3 + // "goodbye" is after "hello" at location 3 + 5 (length of "hello") in the new text. + var newTextChangeText = formattedText.GetSubText(new TextSpan(newRange.Span.Start + amountToShift, newRange.NewLength)).ToString(); + amountToShift += (newRange.NewLength - newRange.Span.Length); + mergedChanges.Add(new TextChange(newTextChangeSpan, newTextChangeText)); + } + + return mergedChanges.ToImmutable(); + } + } + + /// + /// Formats the span between the opening and closing points, options permitting. + /// Returns the text changes that should be applied to the input document to + /// get the formatted text and the end of the close curly brace in the formatted text. + /// + private async Task<(ImmutableArray textChanges, int finalBraceEnd)> FormatTrackingSpanAsync( + Document document, + int openingPoint, + int closingPoint, + ImmutableArray braceFormattingIndentationRules, + IndentationOptions options, + CancellationToken cancellationToken) + { + // Annotate the original closing brace so we can find it after formatting. + document = await GetDocumentWithAnnotatedClosingBraceAsync(document, closingPoint, cancellationToken).ConfigureAwait(false); + + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var startPoint = openingPoint; + var endPoint = AdjustFormattingEndPoint(text, root, startPoint, closingPoint); + + if (options.AutoFormattingOptions.IndentStyle == FormattingOptions.IndentStyle.Smart) + { + // Set the formatting start point to be the beginning of the first word to the left + // of the opening brace location. + // skip whitespace + while (startPoint >= 0 && char.IsWhiteSpace(text[startPoint])) + { + startPoint--; + } + + // skip tokens in the first word to the left. + startPoint--; + while (startPoint >= 0 && !char.IsWhiteSpace(text[startPoint])) + { + startPoint--; + } + } + + var spanToFormat = TextSpan.FromBounds(Math.Max(startPoint, 0), endPoint); + var rules = document.GetFormattingRules(spanToFormat, braceFormattingIndentationRules); + var services = document.Project.Solution.Workspace.Services; + var result = Formatter.GetFormattingResult( + root, SpecializedCollections.SingletonEnumerable(spanToFormat), services, options.FormattingOptions, rules, cancellationToken); + if (result == null) + { + return (ImmutableArray.Empty, closingPoint); + } + + var newRoot = result.GetFormattedRoot(cancellationToken); + var newClosingPoint = newRoot.GetAnnotatedTokens(s_closingBraceSyntaxAnnotation).Single().SpanStart + 1; + + var textChanges = result.GetTextChanges(cancellationToken).ToImmutableArray(); + return (textChanges, newClosingPoint); + + async Task GetDocumentWithAnnotatedClosingBraceAsync(Document document, int closingBraceEndPoint, CancellationToken cancellationToken) + { + var originalRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var closeBraceToken = originalRoot.FindToken(closingBraceEndPoint - 1); + Debug.Assert(IsValidClosingBraceToken(closeBraceToken)); + + var newCloseBraceToken = closeBraceToken.WithAdditionalAnnotations(s_closingBraceSyntaxAnnotation); + var root = originalRoot.ReplaceToken(closeBraceToken, newCloseBraceToken); + return document.WithSyntaxRoot(root); + } + } + } +} diff --git a/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs index 56674ced58531..b18a0e50878fd 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs @@ -3,16 +3,25 @@ // 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.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.BraceCompletion; +using Microsoft.CodeAnalysis.CSharp.Formatting; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Indentation; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion { [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class BracketBraceCompletionService : AbstractBraceCompletionService + internal class BracketBraceCompletionService : AbstractCurlyBraceOrBracketCompletionService { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -30,5 +39,44 @@ public override Task AllowOverTypeAsync(BraceCompletionContext context, Ca protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.OpenBracketToken); protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CloseBracketToken); + + protected override int AdjustFormattingEndPoint(SourceText text, SyntaxNode root, int startPoint, int endPoint) + => endPoint; + + protected override ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options) + { + return ImmutableArray.Create(BracketCompletionFormattingRule.Instance); + } + + private sealed class BracketCompletionFormattingRule : BaseFormattingRule + { + public static readonly AbstractFormattingRule Instance = new BracketCompletionFormattingRule(); + + public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + if (currentToken.IsKind(SyntaxKind.OpenBracketToken) && currentToken.Parent.IsKind(SyntaxKind.ListPattern)) + { + // For list patterns we format brackets as though they are a block, so when formatting after Return + // we add a newline + return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines); + } + + return base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); + } + + public override void AddAlignTokensOperations(List list, SyntaxNode node, in NextAlignTokensOperationAction nextOperation) + { + base.AddAlignTokensOperations(list, node, in nextOperation); + + var bracketPair = node.GetBracketPair(); + if (bracketPair.IsValidBracketOrBracePair() && node is ListPatternSyntax) + { + // For list patterns we format brackets as though they are a block, so ensure the close bracket + // is aligned with the open bracket + AddAlignIndentationOfTokensToBaseTokenOperation(list, node, bracketPair.openBracket, + SpecializedCollections.SingletonEnumerable(bracketPair.closeBracket), AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine); + } + } + } } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs index 7939374ab92c5..41f463b88acc9 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using System.Composition; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.BraceCompletion; @@ -20,7 +19,6 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -28,14 +26,8 @@ namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion { [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class CurlyBraceCompletionService : AbstractBraceCompletionService + internal class CurlyBraceCompletionService : AbstractCurlyBraceOrBracketCompletionService { - /// - /// Annotation used to find the closing brace location after formatting changes are applied. - /// The closing brace location is then used as the caret location. - /// - private static readonly SyntaxAnnotation s_closingBraceSyntaxAnnotation = new(nameof(s_closingBraceSyntaxAnnotation)); - [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CurlyBraceCompletionService() @@ -49,156 +41,6 @@ public CurlyBraceCompletionService() public override Task AllowOverTypeAsync(BraceCompletionContext context, CancellationToken cancellationToken) => AllowOverTypeInUserCodeWithValidClosingTokenAsync(context, cancellationToken); - public override async Task GetTextChangesAfterCompletionAsync(BraceCompletionContext context, IndentationOptions options, CancellationToken cancellationToken) - { - // After the closing brace is completed we need to format the span from the opening point to the closing point. - // E.g. when the user triggers completion for an if statement ($$ is the caret location) we insert braces to get - // if (true){$$} - // We then need to format this to - // if (true) { $$} - - if (!options.AutoFormattingOptions.FormatOnCloseBrace) - { - return null; - } - - var (formattingChanges, finalCurlyBraceEnd) = await FormatTrackingSpanAsync( - context.Document, - context.OpeningPoint, - context.ClosingPoint, - // We're not trying to format the indented block here, so no need to pass in additional rules. - braceFormattingIndentationRules: ImmutableArray.Empty, - options, - cancellationToken).ConfigureAwait(false); - - if (formattingChanges.IsEmpty) - { - return null; - } - - // The caret location should be at the start of the closing brace character. - var originalText = await context.Document.GetTextAsync(cancellationToken).ConfigureAwait(false); - var formattedText = originalText.WithChanges(formattingChanges); - var caretLocation = formattedText.Lines.GetLinePosition(finalCurlyBraceEnd - 1); - - return new BraceCompletionResult(formattingChanges, caretLocation); - } - - public override async Task GetTextChangeAfterReturnAsync( - BraceCompletionContext context, - IndentationOptions options, - CancellationToken cancellationToken) - { - var document = context.Document; - var closingPoint = context.ClosingPoint; - var openingPoint = context.OpeningPoint; - var originalDocumentText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - - // check whether shape of the braces are what we support - // shape must be either "{|}" or "{ }". | is where caret is. otherwise, we don't do any special behavior - if (!ContainsOnlyWhitespace(originalDocumentText, openingPoint, closingPoint)) - { - return null; - } - - var openingPointLine = originalDocumentText.Lines.GetLineFromPosition(openingPoint).LineNumber; - var closingPointLine = originalDocumentText.Lines.GetLineFromPosition(closingPoint).LineNumber; - - // If there are already multiple empty lines between the braces, don't do anything. - // We need to allow a single empty line between the braces to account for razor scenarios where they insert a line. - if (closingPointLine - openingPointLine > 2) - { - return null; - } - - // If there is not already an empty line inserted between the braces, insert one. - TextChange? newLineEdit = null; - var textToFormat = originalDocumentText; - if (closingPointLine - openingPointLine == 1) - { - var newLineString = options.FormattingOptions.NewLine; - newLineEdit = new TextChange(new TextSpan(closingPoint - 1, 0), newLineString); - textToFormat = originalDocumentText.WithChanges(newLineEdit.Value); - - // Modify the closing point location to adjust for the newly inserted line. - closingPoint += newLineString.Length; - } - - var braceFormattingIndentationRules = ImmutableArray.Create( - BraceCompletionFormattingRule.ForIndentStyle(options.AutoFormattingOptions.IndentStyle)); - - // Format the text that contains the newly inserted line. - var (formattingChanges, newClosingPoint) = await FormatTrackingSpanAsync( - document.WithText(textToFormat), - openingPoint, - closingPoint, - braceFormattingIndentationRules, - options, - cancellationToken).ConfigureAwait(false); - - closingPoint = newClosingPoint; - var formattedText = textToFormat.WithChanges(formattingChanges); - - // Get the empty line between the curly braces. - var desiredCaretLine = GetLineBetweenCurlys(closingPoint, formattedText); - Debug.Assert(desiredCaretLine.GetFirstNonWhitespacePosition() == null, "the line between the formatted braces is not empty"); - - // Set the caret position to the properly indented column in the desired line. - var newDocument = document.WithText(formattedText); - var newDocumentText = await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); - var caretPosition = GetIndentedLinePosition(newDocument, newDocumentText, desiredCaretLine.LineNumber, cancellationToken); - - // The new line edit is calculated against the original text, d0, to get text d1. - // The formatting edits are calculated against d1 to get text d2. - // Merge the formatting and new line edits into a set of whitespace only text edits that all apply to d0. - var overallChanges = newLineEdit != null ? GetMergedChanges(newLineEdit.Value, formattingChanges, formattedText) : formattingChanges; - return new BraceCompletionResult(overallChanges, caretPosition); - - static TextLine GetLineBetweenCurlys(int closingPosition, SourceText text) - { - var closingBraceLineNumber = text.Lines.GetLineFromPosition(closingPosition - 1).LineNumber; - return text.Lines[closingBraceLineNumber - 1]; - } - - static LinePosition GetIndentedLinePosition(Document document, SourceText sourceText, int lineNumber, CancellationToken cancellationToken) - { - var indentationService = document.GetRequiredLanguageService(); - var indentation = indentationService.GetIndentation(document, lineNumber, cancellationToken); - - var baseLinePosition = sourceText.Lines.GetLinePosition(indentation.BasePosition); - var offsetOfBacePosition = baseLinePosition.Character; - var totalOffset = offsetOfBacePosition + indentation.Offset; - var indentedLinePosition = new LinePosition(lineNumber, totalOffset); - return indentedLinePosition; - } - - static ImmutableArray GetMergedChanges(TextChange newLineEdit, ImmutableArray formattingChanges, SourceText formattedText) - { - var newRanges = TextChangeRangeExtensions.Merge( - ImmutableArray.Create(newLineEdit.ToTextChangeRange()), - formattingChanges.SelectAsArray(f => f.ToTextChangeRange())); - - using var _ = ArrayBuilder.GetInstance(out var mergedChanges); - var amountToShift = 0; - foreach (var newRange in newRanges) - { - var newTextChangeSpan = newRange.Span; - // Get the text to put in the text change by looking at the span in the formatted text. - // As the new range start is relative to the original text, we need to adjust it assuming the previous changes were applied - // to get the correct start location in the formatted text. - // E.g. with changes - // 1. Insert "hello" at 2 - // 2. Insert "goodbye" at 3 - // "goodbye" is after "hello" at location 3 + 5 (length of "hello") in the new text. - var newTextChangeText = formattedText.GetSubText(new TextSpan(newRange.Span.Start + amountToShift, newRange.NewLength)).ToString(); - amountToShift += (newRange.NewLength - newRange.Span.Length); - mergedChanges.Add(new TextChange(newTextChangeSpan, newTextChangeText)); - } - - return mergedChanges.ToImmutable(); - } - } - public override async Task CanProvideBraceCompletionAsync(char brace, int openingPosition, Document document, CancellationToken cancellationToken) { // Only potentially valid for curly brace completion if not in an interpolation brace completion context. @@ -216,46 +58,8 @@ protected override bool IsValidOpeningBraceToken(SyntaxToken token) protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CloseBraceToken); - private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, int closingBraceEndPoint) - { - // Set the start point to the character after the opening brace. - var start = openingPosition + 1; - // Set the end point to the closing brace start character position. - var end = closingBraceEndPoint - 1; - - for (var i = start; i < end; i++) - { - if (!char.IsWhiteSpace(text[i])) - { - return false; - } - } - - return true; - } - - /// - /// Formats the span between the opening and closing points, options permitting. - /// Returns the text changes that should be applied to the input document to - /// get the formatted text and the end of the close curly brace in the formatted text. - /// - private static async Task<(ImmutableArray textChanges, int finalCurlyBraceEnd)> FormatTrackingSpanAsync( - Document document, - int openingPoint, - int closingPoint, - ImmutableArray braceFormattingIndentationRules, - IndentationOptions options, - CancellationToken cancellationToken) + protected override int AdjustFormattingEndPoint(SourceText text, SyntaxNode root, int startPoint, int endPoint) { - // Annotate the original closing brace so we can find it after formatting. - document = await GetDocumentWithAnnotatedClosingBraceAsync(document, closingPoint, cancellationToken).ConfigureAwait(false); - - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - var startPoint = openingPoint; - var endPoint = closingPoint; - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // 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[] {}` @@ -265,7 +69,7 @@ private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, if (text.Lines.GetLineFromPosition(startPoint) == text.Lines.GetLineFromPosition(endPoint)) { var startToken = root.FindToken(startPoint, findInsideTrivia: true); - if (startToken.IsKind(SyntaxKind.OpenBraceToken) && + if (IsValidOpeningBraceToken(startToken) && (startToken.Parent?.IsInitializerForArrayOrCollectionCreationExpression() == true || startToken.Parent is AnonymousObjectCreationExpressionSyntax)) { @@ -274,50 +78,13 @@ private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, } } - if (options.AutoFormattingOptions.IndentStyle == FormattingOptions.IndentStyle.Smart) - { - // Set the formatting start point to be the beginning of the first word to the left - // of the opening brace location. - // skip whitespace - while (startPoint >= 0 && char.IsWhiteSpace(text[startPoint])) - { - startPoint--; - } - - // skip tokens in the first word to the left. - startPoint--; - while (startPoint >= 0 && !char.IsWhiteSpace(text[startPoint])) - { - startPoint--; - } - } - - var spanToFormat = TextSpan.FromBounds(Math.Max(startPoint, 0), endPoint); - var rules = document.GetFormattingRules(spanToFormat, braceFormattingIndentationRules); - var services = document.Project.Solution.Workspace.Services; - var result = Formatter.GetFormattingResult( - root, SpecializedCollections.SingletonEnumerable(spanToFormat), services, options.FormattingOptions, rules, cancellationToken); - if (result == null) - { - return (ImmutableArray.Empty, closingPoint); - } - - var newRoot = result.GetFormattedRoot(cancellationToken); - var newClosingPoint = newRoot.GetAnnotatedTokens(s_closingBraceSyntaxAnnotation).Single().SpanStart + 1; - - var textChanges = result.GetTextChanges(cancellationToken).ToImmutableArray(); - return (textChanges, newClosingPoint); - - static async Task GetDocumentWithAnnotatedClosingBraceAsync(Document document, int closingBraceEndPoint, CancellationToken cancellationToken) - { - var originalRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var closeBraceToken = originalRoot.FindToken(closingBraceEndPoint - 1); - Debug.Assert(closeBraceToken.IsKind(SyntaxKind.CloseBraceToken)); + return endPoint; + } - var newCloseBraceToken = closeBraceToken.WithAdditionalAnnotations(s_closingBraceSyntaxAnnotation); - var root = originalRoot.ReplaceToken(closeBraceToken, newCloseBraceToken); - return document.WithSyntaxRoot(root); - } + protected override ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options) + { + var indentStyle = options.AutoFormattingOptions.IndentStyle; + return ImmutableArray.Create(BraceCompletionFormattingRule.ForIndentStyle(indentStyle)); } private sealed class BraceCompletionFormattingRule : BaseFormattingRule @@ -486,7 +253,7 @@ public override void AddAlignTokensOperations(List list, S if (_indentStyle == FormattingOptions.IndentStyle.Block) { var bracePair = node.GetBracePair(); - if (bracePair.IsValidBracePair()) + 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. diff --git a/src/Features/CSharp/Portable/Formatting/TypingFormattingRule.cs b/src/Features/CSharp/Portable/Formatting/TypingFormattingRule.cs index 811799d5ed05f..f9e6ff90ed455 100644 --- a/src/Features/CSharp/Portable/Formatting/TypingFormattingRule.cs +++ b/src/Features/CSharp/Portable/Formatting/TypingFormattingRule.cs @@ -26,7 +26,7 @@ public override void AddSuppressOperations(List list, SyntaxN private static bool TryAddSuppressionOnMissingCloseBraceCase(List list, SyntaxNode node) { var bracePair = node.GetBracePair(); - if (!bracePair.IsValidBracePair()) + if (!bracePair.IsValidBracketOrBracePair()) { return false; } diff --git a/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpAutomaticBraceCompletion.cs b/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpAutomaticBraceCompletion.cs index 85a6345d1b2fd..326429ff2813b 100644 --- a/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpAutomaticBraceCompletion.cs +++ b/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpAutomaticBraceCompletion.cs @@ -280,7 +280,7 @@ class C { VisualStudio.Workspace.SetTriggerCompletionInArgumentLists(showCompletionInArgumentLists); VisualStudio.Editor.SendKeys("int ["); - VisualStudio.Editor.Verify.CurrentLineText("int [$$]", assertCaretPosition: true); + VisualStudio.Editor.Verify.CurrentLineText("int[$$]", assertCaretPosition: true); } [WpfTheory, CombinatorialData, Trait(Traits.Feature, Traits.Features.AutomaticCompletion)] @@ -294,7 +294,7 @@ class C { VisualStudio.Workspace.SetTriggerCompletionInArgumentLists(showCompletionInArgumentLists); VisualStudio.Editor.SendKeys("int [", ']'); - VisualStudio.Editor.Verify.CurrentLineText("int []$$", assertCaretPosition: true); + VisualStudio.Editor.Verify.CurrentLineText(" int[]$$ ", assertCaretPosition: true, trimWhitespace: false); } [WpfTheory, CombinatorialData, Trait(Traits.Feature, Traits.Features.AutomaticCompletion)] diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs index f107931667ad8..301c54d8bf83a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs @@ -942,7 +942,7 @@ public static (SyntaxToken openParen, SyntaxToken closeParen) GetParentheses(thi } } - public static (SyntaxToken openBracket, SyntaxToken closeBracket) GetBrackets(this SyntaxNode node) + public static (SyntaxToken openBracket, SyntaxToken closeBracket) GetBrackets(this SyntaxNode? node) { switch (node) { @@ -951,6 +951,7 @@ public static (SyntaxToken openBracket, SyntaxToken closeBracket) GetBrackets(th case ImplicitArrayCreationExpressionSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); case AttributeListSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); case BracketedParameterListSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); + case ListPatternSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); default: return default; } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs index 31d9def066e6a..3986902727010 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs @@ -48,17 +48,29 @@ public static string ContentBeforeLastNewLine(this IEnumerable tri public static (SyntaxToken openBrace, SyntaxToken closeBrace) GetBracePair(this SyntaxNode? node) => node.GetBraces(); - public static bool IsValidBracePair(this (SyntaxToken openBrace, SyntaxToken closeBrace) bracePair) + public static (SyntaxToken openBracket, SyntaxToken closeBracket) GetBracketPair(this SyntaxNode? node) + => node.GetBrackets(); + + public static bool IsValidBracketOrBracePair(this (SyntaxToken openBracketOrBrace, SyntaxToken closeBracketOrBrace) bracketOrBracePair) { - if (bracePair.openBrace.IsKind(SyntaxKind.None) || - bracePair.openBrace.IsMissing || - bracePair.closeBrace.IsKind(SyntaxKind.None)) + if (bracketOrBracePair.openBracketOrBrace.IsKind(SyntaxKind.None) || + bracketOrBracePair.openBracketOrBrace.IsMissing || + bracketOrBracePair.closeBracketOrBrace.IsKind(SyntaxKind.None)) { return false; } - // don't check whether token is actually braces as long as it is not none. - return true; + if (bracketOrBracePair.openBracketOrBrace.IsKind(SyntaxKind.OpenBraceToken)) + { + return bracketOrBracePair.closeBracketOrBrace.IsKind(SyntaxKind.CloseBraceToken); + } + + if (bracketOrBracePair.openBracketOrBrace.IsKind(SyntaxKind.OpenBracketToken)) + { + return bracketOrBracePair.closeBracketOrBrace.IsKind(SyntaxKind.CloseBracketToken); + } + + return false; } public static bool IsOpenParenInParameterListOfAConversionOperatorDeclaration(this SyntaxToken token) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/BaseFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/BaseFormattingRule.cs index 3d8136fbabfd5..049fd7788603a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/BaseFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/BaseFormattingRule.cs @@ -161,7 +161,7 @@ protected static AdjustSpacesOperation CreateAdjustSpacesOperation(int space, Ad protected static void AddBraceSuppressOperations(List list, SyntaxNode node) { var bracePair = node.GetBracePair(); - if (!bracePair.IsValidBracePair()) + if (!bracePair.IsValidBracketOrBracePair()) { return; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs index 54a56ece929ff..050521624913f 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs @@ -50,6 +50,8 @@ public override void AddIndentBlockOperations(List list, S AddBlockIndentationOperation(list, node); + AddBracketIndentationOperation(list, node); + AddLabelIndentationOperation(list, node); AddSwitchIndentationOperation(list, node); @@ -212,7 +214,7 @@ private void AddBlockIndentationOperation(List list, Synta var bracePair = node.GetBracePair(); // don't put block indentation operation if the block only contains label statement - if (!bracePair.IsValidBracePair()) + if (!bracePair.IsValidBracketOrBracePair()) { return; } @@ -244,6 +246,24 @@ private void AddBlockIndentationOperation(List list, Synta AddIndentBlockOperation(list, bracePair.openBrace.GetNextToken(includeZeroWidth: true), bracePair.closeBrace.GetPreviousToken(includeZeroWidth: true)); } + private static void AddBracketIndentationOperation(List list, SyntaxNode node) + { + var bracketPair = node.GetBracketPair(); + + if (!bracketPair.IsValidBracketOrBracePair()) + { + return; + } + + if (node.IsKind(SyntaxKind.ListPattern) && node.Parent != null) + { + // Brackets in list patterns are formatted like blocks, so align close bracket with open bracket + AddAlignmentBlockOperationRelativeToFirstTokenOnBaseTokenLine(list, bracketPair); + + AddIndentBlockOperation(list, bracketPair.openBracket.GetNextToken(includeZeroWidth: true), bracketPair.closeBracket.GetPreviousToken(includeZeroWidth: true)); + } + } + private static void AddAlignmentBlockOperationRelativeToFirstTokenOnBaseTokenLine(List list, (SyntaxToken openBrace, SyntaxToken closeBrace) bracePair) { var option = IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentUserSettingsFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentUserSettingsFormattingRule.cs index cd57f7e65608a..4c85966d019b6 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentUserSettingsFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentUserSettingsFormattingRule.cs @@ -44,7 +44,7 @@ public override void AddIndentBlockOperations(List list, S var bracePair = node.GetBracePair(); // don't put block indentation operation if the block only contains lambda expression body block - if (node.IsLambdaBodyBlock() || !bracePair.IsValidBracePair()) + if (node.IsLambdaBodyBlock() || !bracePair.IsValidBracketOrBracePair()) { return; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/WrappingFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/WrappingFormattingRule.cs index f48b4724647e3..790927d6e3cfd 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/WrappingFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/WrappingFormattingRule.cs @@ -136,7 +136,7 @@ private static void RemoveSuppressOperationForStatementMethodDeclaration(List list, SyntaxNode node) { var bracePair = GetBracePair(node); - if (!bracePair.IsValidBracePair()) + if (!bracePair.IsValidBracketOrBracePair()) { return; }