Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format list patterns #57568

Merged
merged 9 commits into from
Feb 18, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,16 @@ class C
{
void M(object o)
{
_ = o is $$
_ = o is$$
}
}
";
var expectedBeforeReturn = @"
class C
{
void M(object o)
{
_ = o is []
}
}
";
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1367,12 +1367,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)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
// 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole class is just a move from CurlyBraceCompletionService except where noted.

{
/// <summary>
/// Annotation used to find the closing brace location after formatting changes are applied.
/// The closing brace location is then used as the caret location.
/// </summary>
private static readonly SyntaxAnnotation s_closingBraceSyntaxAnnotation = new(nameof(s_closingBraceSyntaxAnnotation));

protected abstract ImmutableArray<AbstractFormattingRule> GetBraceFormattingIndentationRulesAfterReturn(DocumentOptionSet documentOptions);

public override async Task<BraceCompletionResult?> GetTextChangesAfterCompletionAsync(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken)
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
{
var documentOptions = await braceCompletionContext.Document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);

// 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) { $$}
var (formattingChanges, finalCurlyBraceEnd) = await FormatTrackingSpanAsync(
braceCompletionContext.Document,
braceCompletionContext.OpeningPoint,
braceCompletionContext.ClosingPoint,
shouldHonorAutoFormattingOnCloseBraceOption: true,
// We're not trying to format the indented block here, so no need to pass in additional rules.
braceFormattingIndentationRules: ImmutableArray<AbstractFormattingRule>.Empty,
documentOptions,
cancellationToken).ConfigureAwait(false);

if (formattingChanges.IsEmpty)
{
return null;
}

// The caret location should be at the start of the closing brace character.
var originalText = await braceCompletionContext.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 override async Task<BraceCompletionResult?> GetTextChangeAfterReturnAsync(
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
BraceCompletionContext context,
DocumentOptionSet documentOptions,
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 = documentOptions.GetOption(FormattingOptions2.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,
shouldHonorAutoFormattingOnCloseBraceOption: false,
braceFormattingIndentationRules: GetBraceFormattingIndentationRulesAfterReturn(documentOptions),
documentOptions,
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<IIndentationService>();
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<TextChange> GetMergedChanges(TextChange newLineEdit, ImmutableArray<TextChange> formattingChanges, SourceText formattedText)
{
var newRanges = TextChangeRangeExtensions.Merge(
ImmutableArray.Create(newLineEdit.ToTextChangeRange()),
formattingChanges.SelectAsArray(f => f.ToTextChangeRange()));

using var _ = ArrayBuilder<TextChange>.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();
}
}

/// <summary>
/// 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.
/// </summary>
private async Task<(ImmutableArray<TextChange> textChanges, int finalBraceEnd)> FormatTrackingSpanAsync(
Document document,
int openingPoint,
int closingPoint,
bool shouldHonorAutoFormattingOnCloseBraceOption,
ImmutableArray<AbstractFormattingRule> braceFormattingIndentationRules,
DocumentOptionSet documentOptions,
CancellationToken cancellationToken)
{
var option = document.Project.Solution.Options.GetOption(BraceCompletionOptions.AutoFormattingOnCloseBrace, document.Project.Language);
if (!option && shouldHonorAutoFormattingOnCloseBraceOption)
{
return (ImmutableArray<TextChange>.Empty, closingPoint);
}

// 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in CurlyBraceCompletionService.AdjustFormattingEndPoint was previously inline at this point


var style = documentOptions.GetOption(FormattingOptions.SmartIndent);
if (style == 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 result = Formatter.GetFormattingResult(
root, SpecializedCollections.SingletonEnumerable(spanToFormat), document.Project.Solution.Workspace, documentOptions, rules, cancellationToken);
if (result == null)
{
return (ImmutableArray<TextChange>.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<Document> 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));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to directly check for SyntaxKind.CloseBrace


var newCloseBraceToken = closeBraceToken.WithAdditionalAnnotations(s_closingBraceSyntaxAnnotation);
var root = originalRoot.ReplaceToken(closeBraceToken, newCloseBraceToken);
return document.WithSyntaxRoot(root);
}
}

protected virtual int AdjustFormattingEndPoint(SourceText text, SyntaxNode root, int startPoint, int endPoint)
=> endPoint;
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
// 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.Options;
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)]
Expand All @@ -30,5 +37,41 @@ public override Task<bool> 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 ImmutableArray<AbstractFormattingRule> GetBraceFormattingIndentationRulesAfterReturn(DocumentOptionSet documentOptions)
{
return ImmutableArray.Create(BraceCompletionFormattingRule.Instance);
}

private sealed class BraceCompletionFormattingRule : BaseFormattingRule
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
{
public static readonly AbstractFormattingRule Instance = new BraceCompletionFormattingRule();

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<AlignTokensOperation> list, SyntaxNode node, in NextAlignTokensOperationAction nextOperation)
{
base.AddAlignTokensOperations(list, node, in nextOperation);

var bracePair = node.GetBracketPair();
if (bracePair.IsValidBracePair() && 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 brack
AddAlignIndentationOfTokensToBaseTokenOperation(list, node, bracePair.openBrace,
SpecializedCollections.SingletonEnumerable(bracePair.closeBrace), AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine);
}
}
}
}
}
Loading