diff --git a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs index e7ddd4f9fa6d5..9603885a7200f 100644 --- a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs +++ b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs @@ -3,16 +3,12 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using Microsoft.CodeAnalysis.BraceCompletion; using Microsoft.CodeAnalysis.CSharp.Formatting; using Microsoft.CodeAnalysis.Editor.UnitTests.AutomaticCompletion; using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Test.Utilities; -using Microsoft.VisualStudio.Text.Editor; using Roslyn.Test.Utilities; using Xunit; using static Microsoft.CodeAnalysis.BraceCompletion.AbstractBraceCompletionService; @@ -1665,6 +1661,36 @@ public void man() CheckReturn(session.Session, 12, expected); } + [WpfFact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1758005")] + public void NoFormattingAfterNewlineIfOptionsDisabled() + { + var code = @"namespace NS1 +$$"; + + var expected = @"namespace NS1 +{}"; + + var expectedAfterReturn = @"namespace NS1 +{ + +}"; + + // Those option ensures no additional formatting would happen around added braces, including indention of added newline + var globalOptions = new OptionsCollection(LanguageNames.CSharp) + { + { FormattingOptions2.SmartIndent, FormattingOptions2.IndentStyle.None }, + { AutoFormattingOptionsStorage.FormatOnCloseBrace, false }, + }; + + using var session = CreateSession(code, globalOptions); + Assert.NotNull(session); + + CheckStart(session.Session); + Assert.Equal(expected, session.Session.SubjectBuffer.CurrentSnapshot.GetText()); + + CheckReturn(session.Session, 0, expectedAfterReturn); + } + internal static Holder CreateSession(string code, OptionsCollection? globalOptions = null) { return CreateSession( diff --git a/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs index dfff16f1b4285..35c0c380c932a 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.BraceCompletion; -using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Indentation; @@ -25,7 +24,8 @@ internal abstract class AbstractCurlyBraceOrBracketCompletionService : AbstractC /// 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)); + private static readonly SyntaxAnnotation s_closingBraceFormatAnnotation = new(nameof(s_closingBraceFormatAnnotation)); + private static readonly SyntaxAnnotation s_closingBraceNewlineAnnotation = new(nameof(s_closingBraceNewlineAnnotation)); protected abstract ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options); @@ -115,18 +115,17 @@ private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, if (closingPointLine - openingPointLine == 1) { // Handling syntax tree directly to avoid parsing in potentially UI blocking code-path - var closingToken = document.Root.FindTokenOnLeftOfPosition(context.ClosingPoint); - var newLineString = options.FormattingOptions.NewLine; - newLineEdit = new TextChange(new TextSpan(closingToken.FullSpan.Start, 0), newLineString); + var closingToken = FindClosingBraceToken(document.Root, closingPoint); + var annotatedNewline = SyntaxFactory.EndOfLine(options.FormattingOptions.NewLine).WithAdditionalAnnotations(s_closingBraceNewlineAnnotation); + var newClosingToken = closingToken.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(annotatedNewline)); - var generator = document.LanguageServices.GetRequiredService(); - var endOfLine = generator.EndOfLine(newLineString); - - var rootToFormat = document.Root.ReplaceToken(closingToken, closingToken.WithPrependedLeadingTrivia(endOfLine)); + var rootToFormat = document.Root.ReplaceToken(closingToken, newClosingToken); + annotatedNewline = rootToFormat.GetAnnotatedTrivia(s_closingBraceNewlineAnnotation).Single(); document = document.WithChangedRoot(rootToFormat, cancellationToken); - // Modify the closing point location to adjust for the newly inserted line. - closingPoint += newLineString.Length; + // Calculate text change for adding a newline and adjust closing point location. + closingPoint = annotatedNewline.Token.Span.End; + newLineEdit = new TextChange(new TextSpan(annotatedNewline.SpanStart, 0), annotatedNewline.ToString()); } // Format the text that contains the newly inserted line. @@ -147,11 +146,7 @@ private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, // Set the caret position to the properly indented column in the desired line. var caretPosition = GetIndentedLinePosition(newDocument, newDocument.Text, desiredCaretLine.LineNumber, options, 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, newDocument.Text) : formattingChanges; - return new BraceCompletionResult(overallChanges, caretPosition); + return new BraceCompletionResult(GetMergedChanges(newLineEdit, formattingChanges, newDocument.Text), caretPosition); static TextLine GetLineBetweenCurlys(int closingPosition, SourceText text) { @@ -165,16 +160,27 @@ static LinePosition GetIndentedLinePosition(ParsedDocument document, SourceText var indentation = indentationService.GetIndentation(document, lineNumber, options, cancellationToken); var baseLinePosition = sourceText.Lines.GetLinePosition(indentation.BasePosition); - var offsetOfBacePosition = baseLinePosition.Character; - var totalOffset = offsetOfBacePosition + indentation.Offset; + var offsetOfBasePosition = baseLinePosition.Character; + var totalOffset = offsetOfBasePosition + indentation.Offset; var indentedLinePosition = new LinePosition(lineNumber, totalOffset); return indentedLinePosition; } - static ImmutableArray GetMergedChanges(TextChange newLineEdit, ImmutableArray formattingChanges, SourceText formattedText) + static ImmutableArray GetMergedChanges(TextChange? newLineEdit, ImmutableArray formattingChanges, SourceText formattedText) { + // 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. + if (!newLineEdit.HasValue) + return formattingChanges; + + // Depending on options, we might not get any formatting change. + // In this case, the newline edit is the only change. + if (formattingChanges.IsEmpty) + return ImmutableArray.Create(newLineEdit.Value); + var newRanges = TextChangeRangeExtensions.Merge( - ImmutableArray.Create(newLineEdit.ToTextChangeRange()), + ImmutableArray.Create(newLineEdit.Value.ToTextChangeRange()), formattingChanges.SelectAsArray(f => f.ToTextChangeRange())); using var _ = ArrayBuilder.GetInstance(out var mergedChanges); @@ -247,19 +253,24 @@ static ImmutableArray GetMergedChanges(TextChange newLineEdit, Immut } var newRoot = result.GetFormattedRoot(cancellationToken); - var newClosingPoint = newRoot.GetAnnotatedTokens(s_closingBraceSyntaxAnnotation).Single().SpanStart + 1; + var newClosingPoint = newRoot.GetAnnotatedTokens(s_closingBraceFormatAnnotation).Single().SpanStart + 1; var textChanges = result.GetTextChanges(cancellationToken).ToImmutableArray(); return (newRoot, textChanges, newClosingPoint); SyntaxNode GetSyntaxRootWithAnnotatedClosingBrace(SyntaxNode originalRoot, int closingBraceEndPoint) { - var closeBraceToken = originalRoot.FindToken(closingBraceEndPoint - 1); - Debug.Assert(IsValidClosingBraceToken(closeBraceToken)); - - var newCloseBraceToken = closeBraceToken.WithAdditionalAnnotations(s_closingBraceSyntaxAnnotation); + var closeBraceToken = FindClosingBraceToken(originalRoot, closingBraceEndPoint); + var newCloseBraceToken = closeBraceToken.WithAdditionalAnnotations(s_closingBraceFormatAnnotation); return originalRoot.ReplaceToken(closeBraceToken, newCloseBraceToken); } } + + private SyntaxToken FindClosingBraceToken(SyntaxNode root, int closingBraceEndPoint) + { + var closeBraceToken = root.FindToken(closingBraceEndPoint - 1); + Debug.Assert(IsValidClosingBraceToken(closeBraceToken)); + return closeBraceToken; + } } }