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;
}