diff --git a/src/EditorFeatures/CSharp/Formatting/CSharpEditorFormattingService.cs b/src/EditorFeatures/CSharp/Formatting/CSharpEditorFormattingService.cs index 5adf6dfeb084d987635e97b7d05921ac64231957..c4bea6f0d11f77395edcae232ae5cb1f0a1d9cd1 100644 --- a/src/EditorFeatures/CSharp/Formatting/CSharpEditorFormattingService.cs +++ b/src/EditorFeatures/CSharp/Formatting/CSharpEditorFormattingService.cs @@ -138,10 +138,37 @@ private static bool TokenShouldNotFormatOnReturn(SyntaxToken token) return !token.IsKind(SyntaxKind.CloseParenToken) || !token.Parent.IsKind(SyntaxKind.UsingStatement); } - private static bool TokenShouldNotFormatOnTypeChar(SyntaxToken token) + private static async Task TokenShouldNotFormatOnTypeCharAsync( + SyntaxToken token, CancellationToken cancellationToken) { - return (token.IsKind(SyntaxKind.CloseParenToken) && !token.Parent.IsKind(SyntaxKind.UsingStatement)) || - (token.IsKind(SyntaxKind.ColonToken) && !(token.Parent.IsKind(SyntaxKind.LabeledStatement) || token.Parent is SwitchLabelSyntax)); + // If the token is a ) we only want to format if it's the close paren + // of a using statement. That way if we have nested usings, the inner + // using will align with the outer one when the user types the close paren. + if (token.IsKind(SyntaxKind.CloseParenToken) && !token.Parent.IsKind(SyntaxKind.UsingStatement)) + { + return true; + } + + // If the token is a : we only want to format if it's a labeled statement + // or case. When the colon is typed we'll want ot immediately have those + // statements snap to their appropriate indentation level. + if (token.IsKind(SyntaxKind.ColonToken) && !(token.Parent.IsKind(SyntaxKind.LabeledStatement) || token.Parent is SwitchLabelSyntax)) + { + return true; + } + + // Only format an { if it is the first token on a line. We don't want to + // mess with it if it's inside a line. + if (token.IsKind(SyntaxKind.OpenBraceToken)) + { + var text = await token.SyntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + if (!token.IsFirstTokenOnLine(text)) + { + return true; + } + } + + return false; } public async Task> GetFormattingChangesAsync(Document document, char typedChar, int caretPosition, CancellationToken cancellationToken) @@ -149,7 +176,7 @@ public async Task> GetFormattingChangesAsync(Document document var formattingRules = this.GetFormattingRules(document, caretPosition); // first, find the token user just typed. - SyntaxToken token = await GetTokenBeforeTheCaretAsync(document, caretPosition, cancellationToken).ConfigureAwait(false); + var token = await GetTokenBeforeTheCaretAsync(document, caretPosition, cancellationToken).ConfigureAwait(false); if (token.IsMissing || !ValidSingleOrMultiCharactersTokenKind(typedChar, token.Kind()) || @@ -164,10 +191,8 @@ public async Task> GetFormattingChangesAsync(Document document return null; } - // Check to see if any of the below. If not, bail. - // case 1: The token is ')' and the parent is an using statement. - // case 2: The token is ':' and the parent is either labelled statement or case switch or default switch - if (TokenShouldNotFormatOnTypeChar(token)) + var shouldNotFormat = await TokenShouldNotFormatOnTypeCharAsync(token, cancellationToken).ConfigureAwait(false); + if (shouldNotFormat) { return null; } diff --git a/src/EditorFeatures/CSharp/Formatting/Indentation/SmartTokenFormatter.cs b/src/EditorFeatures/CSharp/Formatting/Indentation/SmartTokenFormatter.cs index 703f68bc4416ed9215928477a2beed0152253c90..d906cf6b5ddaf06d5a2097a77557ab5f5baac60e 100644 --- a/src/EditorFeatures/CSharp/Formatting/Indentation/SmartTokenFormatter.cs +++ b/src/EditorFeatures/CSharp/Formatting/Indentation/SmartTokenFormatter.cs @@ -68,7 +68,8 @@ private bool CloseBraceOfTryOrDoBlock(SyntaxToken endToken) (endToken.Parent.IsParentKind(SyntaxKind.TryStatement) || endToken.Parent.IsParentKind(SyntaxKind.DoStatement)); } - public Task> FormatTokenAsync(Workspace workspace, SyntaxToken token, CancellationToken cancellationToken) + public async Task> FormatTokenAsync( + Workspace workspace, SyntaxToken token, CancellationToken cancellationToken) { Contract.ThrowIfTrue(token.Kind() == SyntaxKind.None || token.Kind() == SyntaxKind.EndOfFileToken); @@ -77,7 +78,7 @@ public Task> FormatTokenAsync(Workspace workspace, SyntaxToken if (previousToken.Kind() == SyntaxKind.None) { // no previous token. nothing to format - return Task.FromResult(SpecializedCollections.EmptyList()); + return SpecializedCollections.EmptyList(); } // This is a heuristic to prevent brace completion from breaking user expectation/muscle memory in common scenarios (see Devdiv:823958). @@ -100,12 +101,19 @@ public Task> FormatTokenAsync(Workspace workspace, SyntaxToken var smartTokenformattingRules = (new SmartTokenFormattingRule()).Concat(_formattingRules); var adjustedStartPosition = previousToken.SpanStart; var indentStyle = _optionSet.GetOption(FormattingOptions.SmartIndent, LanguageNames.CSharp); - if (token.IsKind(SyntaxKind.OpenBraceToken) && token.IsFirstTokenOnLine(token.SyntaxTree.GetText()) && indentStyle != FormattingOptions.IndentStyle.Smart) + if (token.IsKind(SyntaxKind.OpenBraceToken) && + indentStyle != FormattingOptions.IndentStyle.Smart) { - adjustedStartPosition = token.SpanStart; + var text = await token.SyntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + if (token.IsFirstTokenOnLine(text)) + { + adjustedStartPosition = token.SpanStart; + } } - return Formatter.GetFormattedTextChangesAsync(_root, new TextSpan[] { TextSpan.FromBounds(adjustedStartPosition, adjustedEndPosition) }, workspace, _optionSet, smartTokenformattingRules, cancellationToken); + return await Formatter.GetFormattedTextChangesAsync(_root, + new TextSpan[] { TextSpan.FromBounds(adjustedStartPosition, adjustedEndPosition) }, + workspace, _optionSet, smartTokenformattingRules, cancellationToken).ConfigureAwait(false); } private class NoLineChangeFormattingRule : AbstractFormattingRule diff --git a/src/EditorFeatures/CSharpTest/Formatting/FormattingEngineTests.cs b/src/EditorFeatures/CSharpTest/Formatting/FormattingEngineTests.cs index 3eeed282793b99f8ef1947e9af39e08cd6009af5..92402c12cce53c957adf348a6528472d8641beeb 100644 --- a/src/EditorFeatures/CSharpTest/Formatting/FormattingEngineTests.cs +++ b/src/EditorFeatures/CSharpTest/Formatting/FormattingEngineTests.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Editor.Commands; using Microsoft.CodeAnalysis.Editor.Implementation.Formatting; +using Microsoft.CodeAnalysis.Editor.Options; using Microsoft.CodeAnalysis.Editor.Shared.Options; using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities; using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; @@ -162,7 +163,7 @@ static void Main(string[] args) [WorkItem(977133, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/977133")] [WpfFact, Trait(Traits.Feature, Traits.Features.Formatting)] - public async Task DoNotFormatRangeButFormatTokenOnOpenBrace() + public async Task DoNotFormatRangeOrFormatTokenOnOpenBraceOnSameLine() { var code = @"class C { @@ -175,7 +176,30 @@ public void M() { public void M() { - if (true) { + if (true) { + } +}"; + await AssertFormatAfterTypeCharAsync(code, expected); + } + + [WorkItem(14491, "https://github.com/dotnet/roslyn/pull/14491")] + [WpfFact, Trait(Traits.Feature, Traits.Features.Formatting)] + public async Task DoNotFormatRangeButFormatTokenOnOpenBraceOnNextLine() + { + var code = @"class C +{ + public void M() + { + if (true) + {$$ + } +}"; + var expected = @"class C +{ + public void M() + { + if (true) + { } }"; await AssertFormatAfterTypeCharAsync(code, expected); @@ -1142,6 +1166,64 @@ class C await AssertFormatAfterTypeCharAsync(code, expected, optionSet); } + [WpfFact, WorkItem(4435, "https://github.com/dotnet/roslyn/issues/4435")] + [Trait(Traits.Feature, Traits.Features.SmartTokenFormatting)] + public async Task OpenCurlyNotFormattedIfNotAtStartOfLine() + { + var code = +@" +class C +{ + public int P {$$ +} +"; + + var expected = +@" +class C +{ + public int P { +} +"; + + var optionSet = new Dictionary + { + { new OptionKey(BraceCompletionOptions.EnableBraceCompletion, LanguageNames.CSharp), false } + }; + + await AssertFormatAfterTypeCharAsync(code, expected); + } + + [WpfFact, WorkItem(4435, "https://github.com/dotnet/roslyn/issues/4435")] + [Trait(Traits.Feature, Traits.Features.SmartTokenFormatting)] + public async Task OpenCurlyFormattedIfAtStartOfLine() + { + var code = +@" +class C +{ + public int P + {$$ +} +"; + + var expected = +@" +class C +{ + public int P + { +} +"; + + var optionSet = new Dictionary + { + { new OptionKey(BraceCompletionOptions.EnableBraceCompletion, LanguageNames.CSharp), false } + }; + + await AssertFormatAfterTypeCharAsync(code, expected); + } + [WpfFact, Trait(Traits.Feature, Traits.Features.Formatting)] public async Task DoNotFormatIncompleteBlockOnSingleLineIfNotTypingCloseCurly1() {