From ac8185a8bf55982a0c3e1bdb81db0bad2bc26486 Mon Sep 17 00:00:00 2001 From: tmat Date: Thu, 7 Apr 2022 14:51:23 -0700 Subject: [PATCH] Move IFormattingInteractionService to Editor Features and add inferred indentation detection to its implementation. Move GetFormattingChangesOnTypedCharacterAsync, GetFormattingChangesOnPasteAsync to ISyntaxFormattingService - these do not depend on the editor. --- .../CSharpFormattingInteractionService.cs | 132 ++++++ .../AutomaticBraceCompletionTests.cs | 1 - ...STypeScriptFormattingInteractionService.cs | 8 +- .../Formatting/FormatCommandHandler.Paste.cs | 4 +- .../Core/Formatting/FormatCommandHandler.cs | 17 +- .../IFormattingInteractionService.cs | 10 +- .../AsyncCompletion/CommitManager.cs | 6 +- .../AsyncCompletion/CommitManagerProvider.cs | 2 +- ...gInteractionService.PasteFormattingRule.cs | 32 -- .../CSharpFormattingInteractionService.cs | 431 ------------------ .../AbstractFormatDocumentHandlerBase.cs | 19 +- .../Formatting/FormatDocumentOnTypeHandler.cs | 36 +- .../Editor/FSharpEditorFormattingService.cs | 26 +- ...RazorCSharpFormattingInteractionService.cs | 21 +- .../CSharpSyntaxFormattingService.cs | 332 +++++++++++++- .../Formatting/TypingFormattingRule.cs | 0 .../Formatting/ISyntaxFormattingService.cs | 8 + .../VisualBasicSyntaxFormattingService.vb | 20 +- 18 files changed, 559 insertions(+), 546 deletions(-) create mode 100644 src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs rename src/{Features/Core/Portable => EditorFeatures/Core}/Formatting/IFormattingInteractionService.cs (80%) delete mode 100644 src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.PasteFormattingRule.cs delete mode 100644 src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.cs rename src/{Features => Workspaces}/CSharp/Portable/Formatting/TypingFormattingRule.cs (100%) diff --git a/src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs b/src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs new file mode 100644 index 0000000000000..670c44637a909 --- /dev/null +++ b/src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs @@ -0,0 +1,132 @@ +// 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.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Indentation; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text.Editor; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Formatting +{ + [ExportLanguageService(typeof(IFormattingInteractionService), LanguageNames.CSharp), Shared] + internal partial class CSharpFormattingInteractionService : IFormattingInteractionService + { + // All the characters that might potentially trigger formatting when typed + private static readonly char[] _supportedChars = ";{}#nte:)".ToCharArray(); + + private readonly IIndentationManagerService _indentationManager; + private readonly IGlobalOptionService _globalOptions; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFormattingInteractionService(IIndentationManagerService indentationManager, IGlobalOptionService globalOptions) + { + _indentationManager = indentationManager; + _globalOptions = globalOptions; + } + + public bool SupportsFormatDocument => true; + public bool SupportsFormatOnPaste => true; + public bool SupportsFormatSelection => true; + public bool SupportsFormatOnReturn => false; + + public bool SupportsFormattingOnTypedCharacter(Document document, char ch) + { + var isSmartIndent = _globalOptions.GetOption(IndentationOptionsStorage.SmartIndent, LanguageNames.CSharp) == FormattingOptions2.IndentStyle.Smart; + + // We consider the proper placement of a close curly or open curly when it is typed at + // the start of the line to be a smart-indentation operation. As such, even if "format + // on typing" is off, if "smart indent" is on, we'll still format this. (However, we + // won't touch anything else in the block this close curly belongs to.). + // + // See extended comment in GetFormattingChangesAsync for more details on this. + if (isSmartIndent && ch is '{' or '}') + { + return true; + } + + var options = _globalOptions.GetAutoFormattingOptions(LanguageNames.CSharp); + + // If format-on-typing is not on, then we don't support formatting on any other characters. + var autoFormattingOnTyping = options.FormatOnTyping; + if (!autoFormattingOnTyping) + { + return false; + } + + if (ch == '}' && !options.FormatOnCloseBrace) + { + return false; + } + + if (ch == ';' && !options.FormatOnSemicolon) + { + return false; + } + + // don't auto format after these keys if smart indenting is not on. + if (ch is '#' or 'n' && !isSmartIndent) + { + return false; + } + + return _supportedChars.Contains(ch); + } + + public async Task> GetFormattingChangesAsync( + Document document, + TextSpan? textSpan, + CancellationToken cancellationToken) + { + var options = await _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: true, cancellationToken).ConfigureAwait(false); + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var span = textSpan ?? new TextSpan(0, root.FullSpan.Length); + var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(root, span); + + var services = document.Project.Solution.Workspace.Services; + return Formatter.GetFormattedTextChanges(root, SpecializedCollections.SingletonEnumerable(formattingSpan), services, options, cancellationToken).ToImmutableArray(); + } + + public async Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + var options = await _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: true, cancellationToken).ConfigureAwait(false); + return await service.GetFormattingChangesOnPasteAsync(document, textSpan, options, cancellationToken).ConfigureAwait(false); + } + + Task> IFormattingInteractionService.GetFormattingChangesOnReturnAsync( + Document document, int caretPosition, CancellationToken cancellationToken) + => SpecializedTasks.EmptyImmutableArray(); + + public async Task> GetFormattingChangesAsync(Document document, char typedChar, int position, CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + + if (await service.ShouldFormatOnTypedCharacterAsync(document, typedChar, position, cancellationToken).ConfigureAwait(false)) + { + var formattingOptions = await _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: false, cancellationToken).ConfigureAwait(false); + var autoFormattingOptions = _globalOptions.GetAutoFormattingOptions(LanguageNames.CSharp); + var indentStyle = _globalOptions.GetOption(IndentationOptionsStorage.SmartIndent, LanguageNames.CSharp); + var indentationOptions = new IndentationOptions(formattingOptions, autoFormattingOptions, indentStyle); + + return await service.GetFormattingChangesOnTypedCharacterAsync(document, position, indentationOptions, cancellationToken).ConfigureAwait(false); + } + + return ImmutableArray.Empty; + } + } +} diff --git a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs index 1d4715627b4f5..96153086229fa 100644 --- a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs +++ b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticBraceCompletionTests.cs @@ -1117,7 +1117,6 @@ public void X() } }"; - using var session = CreateSession(code); Assert.NotNull(session); diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs index d3e7c39e92529..392be55a0dc28 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs @@ -32,16 +32,16 @@ public VSTypeScriptFormattingInteractionService(IVSTypeScriptFormattingInteracti public bool SupportsFormatOnPaste => _implementation.SupportsFormatOnPaste; public bool SupportsFormatOnReturn => _implementation.SupportsFormatOnReturn; - public bool SupportsFormattingOnTypedCharacter(Document document, AutoFormattingOptions options, FormattingOptions2.IndentStyle indentStyle, char ch) + public bool SupportsFormattingOnTypedCharacter(Document document, char ch) => _implementation.SupportsFormattingOnTypedCharacter(document, ch); - public Task> GetFormattingChangesAsync(Document document, TextSpan? textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) + public Task> GetFormattingChangesAsync(Document document, TextSpan? textSpan, CancellationToken cancellationToken) => _implementation.GetFormattingChangesAsync(document, textSpan, documentOptions: null, cancellationToken); - public Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) + public Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken) => _implementation.GetFormattingChangesOnPasteAsync(document, textSpan, documentOptions: null, cancellationToken); - public Task> GetFormattingChangesAsync(Document document, char typedChar, int position, IndentationOptions options, CancellationToken cancellationToken) + public Task> GetFormattingChangesAsync(Document document, char typedChar, int position, CancellationToken cancellationToken) => _implementation.GetFormattingChangesAsync(document, typedChar, position, documentOptions: null, cancellationToken); public Task> GetFormattingChangesOnReturnAsync(Document document, int position, CancellationToken cancellationToken) diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs index 3bb866cb6e7bb..e354705be24d0 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs @@ -87,10 +87,8 @@ private void ExecuteCommandWorker(PasteCommandArgs args, SnapshotPoint? caretPos var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive); var span = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot).Span.ToTextSpan(); - var formattingOptions = _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: false, cancellationToken).WaitAndGetResult(cancellationToken); - var changes = formattingService.GetFormattingChangesOnPasteAsync( - document, span, formattingOptions, cancellationToken).WaitAndGetResult(cancellationToken); + var changes = formattingService.GetFormattingChangesOnPasteAsync(document, span, cancellationToken).WaitAndGetResult(cancellationToken); if (changes.IsEmpty) { return; diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs index d710e3e165c0c..8f1f2991f8719 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs @@ -10,10 +10,8 @@ using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -45,7 +43,6 @@ internal partial class FormatCommandHandler : { private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; - private readonly IIndentationManagerService _indentationManager; private readonly IGlobalOptionService _globalOptions; public string DisplayName => EditorFeaturesResources.Automatic_Formatting; @@ -55,12 +52,10 @@ internal partial class FormatCommandHandler : public FormatCommandHandler( ITextUndoHistoryRegistry undoHistoryRegistry, IEditorOperationsFactoryService editorOperationsFactoryService, - IIndentationManagerService indentationManager, IGlobalOptionService globalOptions) { _undoHistoryRegistry = undoHistoryRegistry; _editorOperationsFactoryService = editorOperationsFactoryService; - _indentationManager = indentationManager; _globalOptions = globalOptions; } @@ -71,8 +66,7 @@ private void Format(ITextView textView, Document document, TextSpan? selectionOp using (Logger.LogBlock(FunctionId.CommandHandler_FormatCommand, KeyValueLogMessage.Create(LogType.UserAction, m => m["Span"] = selectionOpt?.Length ?? -1), cancellationToken)) using (var transaction = CreateEditTransaction(textView, EditorFeaturesResources.Formatting)) { - var formattingOptions = _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: true, cancellationToken).WaitAndGetResult(cancellationToken); - var changes = formattingService.GetFormattingChangesAsync(document, selectionOpt, formattingOptions, cancellationToken).WaitAndGetResult(cancellationToken); + var changes = formattingService.GetFormattingChangesAsync(document, selectionOpt, cancellationToken).WaitAndGetResult(cancellationToken); if (changes.IsEmpty) { return; @@ -170,18 +164,13 @@ private void ExecuteReturnOrTypeCommandWorker(EditorCommandArgs args, Cancellati } else if (args is TypeCharCommandArgs typeCharArgs) { - var autoFormattingOptions = _globalOptions.GetAutoFormattingOptions(document.Project.Language); - var indentStyle = _globalOptions.GetOption(IndentationOptionsStorage.SmartIndent, document.Project.Language); - if (!service.SupportsFormattingOnTypedCharacter(document, autoFormattingOptions, indentStyle, typeCharArgs.TypedChar)) + if (!service.SupportsFormattingOnTypedCharacter(document, typeCharArgs.TypedChar)) { return; } - var formattingOptions = _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: false, cancellationToken).WaitAndGetResult(cancellationToken); - var indentationOptions = new IndentationOptions(formattingOptions, autoFormattingOptions, indentStyle); - textChanges = service.GetFormattingChangesAsync( - document, typeCharArgs.TypedChar, caretPosition.Value, indentationOptions, cancellationToken).WaitAndGetResult(cancellationToken); + document, typeCharArgs.TypedChar, caretPosition.Value, cancellationToken).WaitAndGetResult(cancellationToken); } else { diff --git a/src/Features/Core/Portable/Formatting/IFormattingInteractionService.cs b/src/EditorFeatures/Core/Formatting/IFormattingInteractionService.cs similarity index 80% rename from src/Features/Core/Portable/Formatting/IFormattingInteractionService.cs rename to src/EditorFeatures/Core/Formatting/IFormattingInteractionService.cs index aadd733e7ce59..e562d3dbac03c 100644 --- a/src/Features/Core/Portable/Formatting/IFormattingInteractionService.cs +++ b/src/EditorFeatures/Core/Formatting/IFormattingInteractionService.cs @@ -6,8 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Indentation; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Formatting @@ -23,25 +21,25 @@ internal interface IFormattingInteractionService : ILanguageService /// True if this service would like to format the document based on the user typing the /// provided character. /// - bool SupportsFormattingOnTypedCharacter(Document document, AutoFormattingOptions options, FormattingOptions2.IndentStyle indentStyle, char ch); + bool SupportsFormattingOnTypedCharacter(Document document, char ch); /// /// Returns the text changes necessary to format the document. If is provided, /// only the text changes necessary to format that span are needed. /// - Task> GetFormattingChangesAsync(Document document, TextSpan? textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken); + Task> GetFormattingChangesAsync(Document document, TextSpan? textSpan, CancellationToken cancellationToken); /// /// Returns the text changes necessary to format the document on paste operation. /// - Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken); + Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken); /// /// Returns the text changes necessary to format the document after the user enters a /// character. The position provided is the position of the caret in the document after /// the character been inserted into the document. /// - Task> GetFormattingChangesAsync(Document document, char typedChar, int position, IndentationOptions options, CancellationToken cancellationToken); + Task> GetFormattingChangesAsync(Document document, char typedChar, int position, CancellationToken cancellationToken); /// /// Returns the text changes necessary to format the document after the user enters a Return diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs index eb556cb73f5af..037b1f4a82e31 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs @@ -36,7 +36,6 @@ internal sealed class CommitManager : IAsyncCompletionCommitManager private readonly RecentItemsManager _recentItemsManager; private readonly ITextView _textView; - private readonly IIndentationManagerService _indentationManager; private readonly IGlobalOptionService _globalOptions; private readonly IThreadingContext _threadingContext; @@ -59,7 +58,6 @@ public IEnumerable PotentialCommitCharacters internal CommitManager( ITextView textView, RecentItemsManager recentItemsManager, - IIndentationManagerService indentationManager, IGlobalOptionService globalOptions, IThreadingContext threadingContext) { @@ -67,7 +65,6 @@ internal CommitManager( _threadingContext = threadingContext; _recentItemsManager = recentItemsManager; _textView = textView; - _indentationManager = indentationManager; } /// @@ -290,10 +287,9 @@ private AsyncCompletionData.CommitResult Commit( if (currentDocument != null && formattingService != null) { - var formattingOptions = _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: true, cancellationToken).WaitAndGetResult(cancellationToken); var spanToFormat = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); var changes = formattingService.GetFormattingChangesAsync( - currentDocument, spanToFormat.Span.ToTextSpan(), formattingOptions, cancellationToken).WaitAndGetResult(cancellationToken); + currentDocument, spanToFormat.Span.ToTextSpan(), cancellationToken).WaitAndGetResult(cancellationToken); currentDocument.Project.Solution.Workspace.ApplyTextChanges(currentDocument.Id, changes, cancellationToken); } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs index 2a9223918c9ee..98484e9e6963a 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs @@ -45,7 +45,7 @@ public CommitManagerProvider( return null; } - return new CommitManager(textView, _recentItemsManager, _indentationManager, _globalOptions, _threadingContext); + return new CommitManager(textView, _recentItemsManager, _globalOptions, _threadingContext); } } } diff --git a/src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.PasteFormattingRule.cs b/src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.PasteFormattingRule.cs deleted file mode 100644 index eb1763381af8a..0000000000000 --- a/src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.PasteFormattingRule.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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 Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Formatting.Rules; - -namespace Microsoft.CodeAnalysis.CSharp.Formatting -{ - internal partial class CSharpFormattingInteractionService : IFormattingInteractionService - { - internal class PasteFormattingRule : AbstractFormattingRule - { - public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) - { - if (currentToken.Parent != null) - { - var currentTokenParentParent = currentToken.Parent.Parent; - if (currentToken.Kind() == SyntaxKind.OpenBraceToken && currentTokenParentParent != null && - (currentTokenParentParent.Kind() == SyntaxKind.SimpleLambdaExpression || - currentTokenParentParent.Kind() == SyntaxKind.ParenthesizedLambdaExpression || - currentTokenParentParent.Kind() == SyntaxKind.AnonymousMethodExpression)) - { - return FormattingOperations.CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines); - } - } - - return nextOperation.Invoke(in previousToken, in currentToken); - } - } - } -} diff --git a/src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.cs b/src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.cs deleted file mode 100644 index 325136f94c06c..0000000000000 --- a/src/Features/CSharp/Portable/Formatting/CSharpFormattingInteractionService.cs +++ /dev/null @@ -1,431 +0,0 @@ -// 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.Generic; -using System.Collections.Immutable; -using System.Composition; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.BraceCompletion; -using Microsoft.CodeAnalysis.CSharp.Extensions; -using Microsoft.CodeAnalysis.CSharp.Indentation; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.CSharp.Utilities; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Formatting.Rules; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Indentation; -using Microsoft.CodeAnalysis.LanguageServices; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.CSharp.Formatting -{ - [ExportLanguageService(typeof(IFormattingInteractionService), LanguageNames.CSharp), Shared] - internal partial class CSharpFormattingInteractionService : IFormattingInteractionService - { - // All the characters that might potentially trigger formatting when typed - private readonly char[] _supportedChars = ";{}#nte:)".ToCharArray(); - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFormattingInteractionService() - { - } - - public bool SupportsFormatDocument => true; - public bool SupportsFormatOnPaste => true; - public bool SupportsFormatSelection => true; - public bool SupportsFormatOnReturn => false; - - public bool SupportsFormattingOnTypedCharacter(Document document, AutoFormattingOptions options, FormattingOptions2.IndentStyle indentStyle, char ch) - { - var smartIndentOn = indentStyle == FormattingOptions2.IndentStyle.Smart; - - // We consider the proper placement of a close curly or open curly when it is typed at - // the start of the line to be a smart-indentation operation. As such, even if "format - // on typing" is off, if "smart indent" is on, we'll still format this. (However, we - // won't touch anything else in the block this close curly belongs to.). - // - // See extended comment in GetFormattingChangesAsync for more details on this. - if (smartIndentOn) - { - if (ch is '{' or '}') - { - return true; - } - } - - // If format-on-typing is not on, then we don't support formatting on any other characters. - var autoFormattingOnTyping = options.FormatOnTyping; - if (!autoFormattingOnTyping) - { - return false; - } - - if (ch == '}' && !options.FormatOnCloseBrace) - { - return false; - } - - if (ch == ';' && !options.FormatOnSemicolon) - { - return false; - } - - // don't auto format after these keys if smart indenting is not on. - if ((ch == '#' || ch == 'n') && !smartIndentOn) - { - return false; - } - - return _supportedChars.Contains(ch); - } - - public async Task> GetFormattingChangesAsync( - Document document, - TextSpan? textSpan, - SyntaxFormattingOptions options, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var span = textSpan ?? new TextSpan(0, root.FullSpan.Length); - var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(root, span); - - var services = document.Project.Solution.Workspace.Services; - return Formatter.GetFormattedTextChanges(root, SpecializedCollections.SingletonEnumerable(formattingSpan), services, options, cancellationToken).ToImmutableArray(); - } - - public async Task> GetFormattingChangesOnPasteAsync( - Document document, - TextSpan textSpan, - SyntaxFormattingOptions options, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(root, textSpan); - var service = document.GetRequiredLanguageService(); - - var rules = new List() { new PasteFormattingRule() }; - rules.AddRange(service.GetDefaultFormattingRules()); - - var result = service.GetFormattingResult(root, SpecializedCollections.SingletonEnumerable(formattingSpan), options, rules, cancellationToken); - return result.GetTextChanges(cancellationToken).ToImmutableArray(); - } - - private static ImmutableArray GetFormattingRules(Document document, int position, SyntaxToken tokenBeforeCaret) - { - var workspace = document.Project.Solution.Workspace; - var formattingRuleFactory = workspace.Services.GetRequiredService(); - return ImmutableArray.Create(formattingRuleFactory.CreateRule(document, position)) - .AddRange(GetTypingRules(tokenBeforeCaret)) - .AddRange(Formatter.GetDefaultFormattingRules(document)); - } - - Task> IFormattingInteractionService.GetFormattingChangesOnReturnAsync( - Document document, int caretPosition, CancellationToken cancellationToken) - => SpecializedTasks.EmptyImmutableArray(); - - private static async Task TokenShouldNotFormatOnTypeCharAsync( - SyntaxToken token, CancellationToken cancellationToken) - { - // 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, - IndentationOptions options, - CancellationToken cancellationToken) - { - // first, find the token user just typed. - var token = await GetTokenBeforeTheCaretAsync(document, caretPosition, cancellationToken).ConfigureAwait(false); - if (token.IsMissing || - !ValidSingleOrMultiCharactersTokenKind(typedChar, token.Kind()) || - token.IsKind(SyntaxKind.EndOfFileToken, SyntaxKind.None)) - { - return ImmutableArray.Empty; - } - - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var formattingRules = GetFormattingRules(document, caretPosition, token); - - var service = document.GetLanguageService(); - if (service != null && service.IsInNonUserCode(token.SyntaxTree, caretPosition, cancellationToken)) - { - return ImmutableArray.Empty; - } - - var shouldNotFormat = await TokenShouldNotFormatOnTypeCharAsync(token, cancellationToken).ConfigureAwait(false); - if (shouldNotFormat) - { - return ImmutableArray.Empty; - } - - // Do not attempt to format on open/close brace if autoformat on close brace feature is - // off, instead just smart indent. - // - // We want this behavior because it's totally reasonable for a user to want to not have - // on automatic formatting because they feel it is too aggressive. However, by default, - // if you have smart-indentation on and are just hitting enter, you'll common have the - // caret placed one indent higher than your current construct. For example, if you have: - // - // if (true) - // $ <-- smart indent will have placed the caret here here. - // - // This is acceptable given that the user may want to just write a simple statement there. - // However, if they start writing `{`, then things should snap over to be: - // - // if (true) - // { - // - // Importantly, this is just an indentation change, no actual 'formatting' is done. We do - // the same with close brace. If you have: - // - // if (...) - // { - // bad . ly ( for (mmated+code) ) ; - // $ <-- smart indent will have placed the care here. - // - // If the user hits `}` then we will properly smart indent the `}` to match the `{`. - // However, we won't touch any of the other code in that block, unlike if we were - // formatting. - var onlySmartIndent = - (token.IsKind(SyntaxKind.CloseBraceToken) && OnlySmartIndentCloseBrace(options.AutoFormattingOptions)) || - (token.IsKind(SyntaxKind.OpenBraceToken) && OnlySmartIndentOpenBrace(options.AutoFormattingOptions)); - - if (onlySmartIndent) - { - // if we're only doing smart indent, then ignore all edits to this token that occur before - // the span of the token. They're irrelevant and may screw up other code the user doesn't - // want touched. - var tokenEdits = await FormatTokenAsync(document, options, token, formattingRules, cancellationToken).ConfigureAwait(false); - return tokenEdits.Where(t => t.Span.Start >= token.FullSpan.Start).ToImmutableArray(); - } - - // if formatting range fails, do format token one at least - var changes = await FormatRangeAsync(document, options, token, formattingRules, cancellationToken).ConfigureAwait(false); - if (changes.Length > 0) - { - return changes; - } - - return (await FormatTokenAsync(document, options, token, formattingRules, cancellationToken).ConfigureAwait(false)).ToImmutableArray(); - } - - private static bool OnlySmartIndentCloseBrace(in AutoFormattingOptions options) - { - // User does not want auto-formatting (either in general, or for close braces in - // specific). So we only smart indent close braces when typed. - return !options.FormatOnCloseBrace || !options.FormatOnTyping; - } - - private static bool OnlySmartIndentOpenBrace(in AutoFormattingOptions options) - { - // User does not want auto-formatting . So we only smart indent open braces when typed. - // Note: there is no specific option for controlling formatting on open brace. So we - // don't have the symmetry with OnlySmartIndentCloseBrace. - return !options.FormatOnTyping; - } - - private static async Task GetTokenBeforeTheCaretAsync(Document document, int caretPosition, CancellationToken cancellationToken) - { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - - var position = Math.Max(0, caretPosition - 1); - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(position, findInsideTrivia: true); - return token; - } - - private static async Task> FormatTokenAsync( - Document document, IndentationOptions options, SyntaxToken token, ImmutableArray formattingRules, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var formatter = CreateSmartTokenFormatter(options, formattingRules, root); - var changes = await formatter.FormatTokenAsync(token, cancellationToken).ConfigureAwait(false); - return changes; - } - - private static ISmartTokenFormatter CreateSmartTokenFormatter(IndentationOptions options, ImmutableArray formattingRules, SyntaxNode root) - => new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)root); - - private static async Task> FormatRangeAsync( - Document document, - IndentationOptions options, - SyntaxToken endToken, - ImmutableArray formattingRules, - CancellationToken cancellationToken) - { - if (!IsEndToken(endToken)) - { - return ImmutableArray.Empty; - } - - var tokenRange = FormattingRangeHelper.FindAppropriateRange(endToken); - if (tokenRange == null || tokenRange.Value.Item1.Equals(tokenRange.Value.Item2)) - { - return ImmutableArray.Empty; - } - - if (IsInvalidTokenKind(tokenRange.Value.Item1) || IsInvalidTokenKind(tokenRange.Value.Item2)) - { - return ImmutableArray.Empty; - } - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var formatter = new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)root); - - var changes = formatter.FormatRange(tokenRange.Value.Item1, tokenRange.Value.Item2, cancellationToken); - return changes.ToImmutableArray(); - } - - private static IEnumerable GetTypingRules(SyntaxToken tokenBeforeCaret) - { - // Typing introduces several challenges around formatting. - // Historically we've shipped several triggers that cause formatting to happen directly while typing. - // These include formatting of blocks when '}' is typed, formatting of statements when a ';' is typed, formatting of ```case```s when ':' typed, and many other cases. - // However, formatting during typing can potentially cause problems. This is because the surrounding code may not be complete, - // or may otherwise have syntax errors, and thus edits could have unintended consequences. - // - // Because of this, we introduce an extra rule into the set of formatting rules whose purpose is to actually make formatting *more* - // conservative and *less* willing willing to make edits to the tree. - // The primary effect this rule has is to assume that more code is on a single line (and thus should stay that way) - // despite what the tree actually looks like. - // - // It's ok that this is only during formatting that is caused by an edit because that formatting happens - // implicitly and thus has to be more careful, whereas an explicit format-document call only happens on-demand - // and can be more aggressive about what it's doing. - // - // - // For example, say you have the following code. - // - // ```c# - // class C - // { - // int P { get { return - // } - // ``` - // - // Hitting ';' after 'return' should ideally only affect the 'return statement' and change it to: - // - // ```c# - // class C - // { - // int P { get { return; - // } - // ``` - // - // During a normal format-document call, this is not what would happen. - // Specifically, because the parser will consume the '}' into the accessor, - // it will think the accessor spans multiple lines, and thus should not stay on a single line. This will produce: - // - // ```c# - // class C - // { - // int P - // { - // get - // { - // return; - // } - // ``` - // - // Because it's ok for this to format in that fashion if format-document is invoked, - // but should not happen during typing, we insert a specialized rule *only* during typing to try to control this. - // During normal formatting we add 'keep on single line' suppression rules for blocks we find that are on a single line. - // But that won't work since this span is not on a single line: - // - // ```c# - // class C - // { - // int P { get [|{ return; - // }|] - // ``` - // - // So, during typing, if we see any parent block is incomplete, we'll assume that - // all our parent blocks are incomplete and we will place the suppression span like so: - // - // ```c# - // class C - // { - // int P { get [|{ return;|] - // } - // ``` - // - // This will have the desired effect of keeping these tokens on the same line, but only during typing scenarios. - if (tokenBeforeCaret.Kind() is SyntaxKind.CloseBraceToken or - SyntaxKind.EndOfFileToken) - { - return SpecializedCollections.EmptyEnumerable(); - } - - return SpecializedCollections.SingletonEnumerable(TypingFormattingRule.Instance); - } - - private static bool IsEndToken(SyntaxToken endToken) - { - if (endToken.IsKind(SyntaxKind.OpenBraceToken)) - { - return false; - } - - return true; - } - - // We'll autoformat on n, t, e, only if they are the last character of the below - // keywords. - private static bool ValidSingleOrMultiCharactersTokenKind(char typedChar, SyntaxKind kind) - => typedChar switch - { - 'n' => kind is SyntaxKind.RegionKeyword or SyntaxKind.EndRegionKeyword, - 't' => kind == SyntaxKind.SelectKeyword, - 'e' => kind == SyntaxKind.WhereKeyword, - _ => true, - }; - - private static bool IsInvalidTokenKind(SyntaxToken token) - { - // invalid token to be formatted - return token.IsKind(SyntaxKind.None) || - token.IsKind(SyntaxKind.EndOfDirectiveToken) || - token.IsKind(SyntaxKind.EndOfFileToken); - } - } -} diff --git a/src/Features/LanguageServer/Protocol/Handler/Formatting/AbstractFormatDocumentHandlerBase.cs b/src/Features/LanguageServer/Protocol/Handler/Formatting/AbstractFormatDocumentHandlerBase.cs index 602e0e05bd027..7f928efd6ce61 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Formatting/AbstractFormatDocumentHandlerBase.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Formatting/AbstractFormatDocumentHandlerBase.cs @@ -7,9 +7,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; @@ -31,22 +33,19 @@ internal abstract class AbstractFormatDocumentHandlerBase(); - - var formattingService = document.Project.LanguageServices.GetRequiredService(); var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - TextSpan? textSpan = null; - if (range != null) - { - textSpan = ProtocolConversions.RangeToTextSpan(range, text); - } + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var formattingSpan = (range != null) ? ProtocolConversions.RangeToTextSpan(range, text) : new TextSpan(0, root.FullSpan.Length); // We should use the options passed in by LSP instead of the document's options. var formattingOptions = await ProtocolConversions.GetFormattingOptionsAsync(options, document, cancellationToken).ConfigureAwait(false); - var textChanges = await formattingService.GetFormattingChangesAsync(document, textSpan, formattingOptions, cancellationToken).ConfigureAwait(false); - edits.AddRange(textChanges.Select(change => ProtocolConversions.TextChangeToTextEdit(change, text))); + var services = document.Project.Solution.Workspace.Services; + var textChanges = Formatter.GetFormattedTextChanges(root, SpecializedCollections.SingletonEnumerable(formattingSpan), services, formattingOptions, rules: null, cancellationToken); + var edits = new ArrayBuilder(); + edits.AddRange(textChanges.Select(change => ProtocolConversions.TextChangeToTextEdit(change, text))); return edits.ToArrayAndFree(); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs index 88970caf249f1..6d86b732f721c 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; @@ -47,38 +48,33 @@ public FormatDocumentOnTypeHandler(IGlobalOptionService globalOptions) if (document == null) return null; - var edits = new ArrayBuilder(); - - var formattingService = document.Project.LanguageServices.GetRequiredService(); var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(request.Character)) + if (string.IsNullOrEmpty(request.Character) || SyntaxFacts.IsNewLine(request.Character[0])) { - return edits.ToArrayAndFree(); + return Array.Empty(); } - IList? textChanges; - if (SyntaxFacts.IsNewLine(request.Character[0])) + var formattingService = document.Project.LanguageServices.GetRequiredService(); + + if (!await formattingService.ShouldFormatOnTypedCharacterAsync(document, request.Character[0], position, cancellationToken).ConfigureAwait(false)) { - textChanges = await formattingService.GetFormattingChangesOnReturnAsync( - document, position, cancellationToken).ConfigureAwait(false); + return Array.Empty(); } - else - { - // We should use the options passed in by LSP instead of the document's options. - var formattingOptions = await ProtocolConversions.GetFormattingOptionsAsync(request.Options, document, cancellationToken).ConfigureAwait(false); - var indentationOptions = new IndentationOptions(formattingOptions, _globalOptions.GetAutoFormattingOptions(document.Project.Language)); - textChanges = await formattingService.GetFormattingChangesAsync( - document, request.Character[0], position, indentationOptions, cancellationToken).ConfigureAwait(false); - } + // We should use the options passed in by LSP instead of the document's options. + var formattingOptions = await ProtocolConversions.GetFormattingOptionsAsync(request.Options, document, cancellationToken).ConfigureAwait(false); + var indentationOptions = new IndentationOptions(formattingOptions, _globalOptions.GetAutoFormattingOptions(document.Project.Language)); - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - if (textChanges != null) + var textChanges = await formattingService.GetFormattingChangesOnTypedCharacterAsync(document, position, indentationOptions, cancellationToken).ConfigureAwait(false); + if (textChanges.IsEmpty) { - edits.AddRange(textChanges.Select(change => ProtocolConversions.TextChangeToTextEdit(change, text))); + return Array.Empty(); } + var edits = new ArrayBuilder(); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + edits.AddRange(textChanges.Select(change => ProtocolConversions.TextChangeToTextEdit(change, text))); return edits.ToArrayAndFree(); } } diff --git a/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpEditorFormattingService.cs b/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpEditorFormattingService.cs index f49d3e1ec005f..12bac3e279c8a 100644 --- a/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpEditorFormattingService.cs +++ b/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpEditorFormattingService.cs @@ -22,12 +22,14 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.FSharp.Internal.Editor internal class FSharpEditorFormattingService : IFormattingInteractionService { private readonly IFSharpEditorFormattingService _service; + private readonly IGlobalOptionService _globalOptions; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public FSharpEditorFormattingService(IFSharpEditorFormattingService service) + public FSharpEditorFormattingService(IFSharpEditorFormattingService service, IGlobalOptionService globalOptions) { _service = service; + _globalOptions = globalOptions; } public bool SupportsFormatDocument => _service.SupportsFormatDocument; @@ -58,26 +60,34 @@ public Task> GetFormattingChangesOnPasteAsync(Document documen return _service.GetFormattingChangesOnReturnAsync(document, position, cancellationToken); } - public bool SupportsFormattingOnTypedCharacter(Document document, AutoFormattingOptions options, FormattingOptions2.IndentStyle indentStyle, char ch) + public bool SupportsFormattingOnTypedCharacter(Document document, char ch) { - return _service is IFSharpEditorFormattingServiceWithOptions serviceWithOptions ? - serviceWithOptions.SupportsFormattingOnTypedCharacter(document, new AutoFormattingOptionsWrapper(options, indentStyle), ch) : - _service.SupportsFormattingOnTypedCharacter(document, ch); + if (_service is IFSharpEditorFormattingServiceWithOptions serviceWithOptions) + { + var indentStyle = _globalOptions.GetOption(IndentationOptionsStorage.SmartIndent, LanguageNames.FSharp); + var options = _globalOptions.GetAutoFormattingOptions(LanguageNames.FSharp); + + return serviceWithOptions.SupportsFormattingOnTypedCharacter(document, new AutoFormattingOptionsWrapper(options, indentStyle), ch); + } + else + { + return _service.SupportsFormattingOnTypedCharacter(document, ch); + } } - async Task> IFormattingInteractionService.GetFormattingChangesAsync(Document document, TextSpan? textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) + async Task> IFormattingInteractionService.GetFormattingChangesAsync(Document document, TextSpan? textSpan, CancellationToken cancellationToken) { var changes = await GetFormattingChangesAsync(document, textSpan, cancellationToken).ConfigureAwait(false); return changes?.ToImmutableArray() ?? ImmutableArray.Empty; } - async Task> IFormattingInteractionService.GetFormattingChangesAsync(Document document, char typedChar, int position, IndentationOptions options, CancellationToken cancellationToken) + async Task> IFormattingInteractionService.GetFormattingChangesAsync(Document document, char typedChar, int position, CancellationToken cancellationToken) { var changes = await GetFormattingChangesAsync(document, typedChar, position, cancellationToken).ConfigureAwait(false); return changes?.ToImmutableArray() ?? ImmutableArray.Empty; } - async Task> IFormattingInteractionService.GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) + async Task> IFormattingInteractionService.GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken) { var changes = await GetFormattingChangesOnPasteAsync(document, textSpan, cancellationToken).ConfigureAwait(false); return changes?.ToImmutableArray() ?? ImmutableArray.Empty; diff --git a/src/Tools/ExternalAccess/Razor/RazorCSharpFormattingInteractionService.cs b/src/Tools/ExternalAccess/Razor/RazorCSharpFormattingInteractionService.cs index 2a9d91245f4fa..6d20e859fffed 100644 --- a/src/Tools/ExternalAccess/Razor/RazorCSharpFormattingInteractionService.cs +++ b/src/Tools/ExternalAccess/Razor/RazorCSharpFormattingInteractionService.cs @@ -29,7 +29,7 @@ internal static class RazorCSharpFormattingInteractionService /// the character been inserted into the document. /// [Obsolete("Use the other overload")] - public static Task> GetFormattingChangesAsync( + public static async Task> GetFormattingChangesAsync( Document document, char typedChar, int position, @@ -37,7 +37,13 @@ public static Task> GetFormattingChangesAsync( CancellationToken cancellationToken) { Contract.ThrowIfFalse(document.Project.Language is LanguageNames.CSharp); - var formattingService = document.GetRequiredLanguageService(); + var formattingService = document.GetRequiredLanguageService(); + + if (!await formattingService.ShouldFormatOnTypedCharacterAsync(document, typedChar, position, cancellationToken).ConfigureAwait(false)) + { + return ImmutableArray.Empty; + } + var services = document.Project.Solution.Workspace.Services; var globalOptions = document.Project.Solution.Workspace.Services.GetRequiredService(); @@ -46,7 +52,7 @@ public static Task> GetFormattingChangesAsync( SyntaxFormattingOptions.Create(documentOptions, services, document.Project.Language), globalOptions.GlobalOptions.GetAutoFormattingOptions(document.Project.Language)); - return formattingService.GetFormattingChangesAsync(document, typedChar, position, indentationOptions, cancellationToken); + return await formattingService.GetFormattingChangesOnTypedCharacterAsync(document, position, indentationOptions, cancellationToken).ConfigureAwait(false); } /// @@ -64,12 +70,17 @@ public static async Task> GetFormattingChangesAsync( CancellationToken cancellationToken) { Contract.ThrowIfFalse(document.Project.Language is LanguageNames.CSharp); - var formattingService = document.GetRequiredLanguageService(); + var formattingService = document.GetRequiredLanguageService(); + + if (!await formattingService.ShouldFormatOnTypedCharacterAsync(document, typedChar, position, cancellationToken).ConfigureAwait(false)) + { + return ImmutableArray.Empty; + } var formattingOptions = GetFormattingOptions(indentationOptions); var roslynIndentationOptions = new IndentationOptions(formattingOptions, autoFormattingOptions.UnderlyingObject, (FormattingOptions2.IndentStyle)indentStyle); - return await formattingService.GetFormattingChangesAsync(document, typedChar, position, roslynIndentationOptions, cancellationToken).ConfigureAwait(false); + return await formattingService.GetFormattingChangesOnTypedCharacterAsync(document, position, roslynIndentationOptions, cancellationToken).ConfigureAwait(false); } public static IList GetFormattedTextChanges( diff --git a/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs b/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs index fe0a926fce18a..cb8952e0d8cd3 100644 --- a/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs +++ b/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs @@ -3,19 +3,349 @@ // 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Indentation; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Indentation; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Formatting { [ExportLanguageService(typeof(ISyntaxFormattingService), LanguageNames.CSharp), Shared] - internal class CSharpSyntaxFormattingService : CSharpSyntaxFormatting, ISyntaxFormattingService + internal sealed class CSharpSyntaxFormattingService : CSharpSyntaxFormatting, ISyntaxFormattingService { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CSharpSyntaxFormattingService() { } + + public async Task ShouldFormatOnTypedCharacterAsync( + Document document, + char typedChar, + int caretPosition, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + // first, find the token user just typed. + var token = root.FindToken(Math.Max(0, caretPosition - 1), findInsideTrivia: true); + if (token.IsMissing || + !ValidSingleOrMultiCharactersTokenKind(typedChar, token.Kind()) || + token.IsKind(SyntaxKind.EndOfFileToken, SyntaxKind.None) || + root.SyntaxTree.IsInNonUserCode(caretPosition, cancellationToken)) + { + return false; + } + + // 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 false; + } + + // 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 false; + } + + // 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 root.SyntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + if (!token.IsFirstTokenOnLine(text)) + { + return false; + } + } + + return true; + } + + public async Task> GetFormattingChangesOnTypedCharacterAsync( + Document document, + int caretPosition, + IndentationOptions indentationOptions, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(Math.Max(0, caretPosition - 1), findInsideTrivia: true); + var formattingRules = GetFormattingRules(document, caretPosition, token); + + // Do not attempt to format on open/close brace if autoformat on close brace feature is + // off, instead just smart indent. + // + // We want this behavior because it's totally reasonable for a user to want to not have + // on automatic formatting because they feel it is too aggressive. However, by default, + // if you have smart-indentation on and are just hitting enter, you'll common have the + // caret placed one indent higher than your current construct. For example, if you have: + // + // if (true) + // $ <-- smart indent will have placed the caret here here. + // + // This is acceptable given that the user may want to just write a simple statement there. + // However, if they start writing `{`, then things should snap over to be: + // + // if (true) + // { + // + // Importantly, this is just an indentation change, no actual 'formatting' is done. We do + // the same with close brace. If you have: + // + // if (...) + // { + // bad . ly ( for (mmated+code) ) ; + // $ <-- smart indent will have placed the care here. + // + // If the user hits `}` then we will properly smart indent the `}` to match the `{`. + // However, we won't touch any of the other code in that block, unlike if we were + // formatting. + var onlySmartIndent = + (token.IsKind(SyntaxKind.CloseBraceToken) && OnlySmartIndentCloseBrace(indentationOptions.AutoFormattingOptions)) || + (token.IsKind(SyntaxKind.OpenBraceToken) && OnlySmartIndentOpenBrace(indentationOptions.AutoFormattingOptions)); + + if (onlySmartIndent) + { + // if we're only doing smart indent, then ignore all edits to this token that occur before + // the span of the token. They're irrelevant and may screw up other code the user doesn't + // want touched. + var tokenEdits = await FormatTokenAsync(root, indentationOptions, token, formattingRules, cancellationToken).ConfigureAwait(false); + return tokenEdits.Where(t => t.Span.Start >= token.FullSpan.Start).ToImmutableArray(); + } + + // if formatting range fails, do format token one at least + var changes = FormatRange(root, indentationOptions, token, formattingRules, cancellationToken); + if (changes.Length > 0) + { + return changes; + } + + return (await FormatTokenAsync(root, indentationOptions, token, formattingRules, cancellationToken).ConfigureAwait(false)).ToImmutableArray(); + } + + private static bool OnlySmartIndentCloseBrace(in AutoFormattingOptions options) + { + // User does not want auto-formatting (either in general, or for close braces in + // specific). So we only smart indent close braces when typed. + return !options.FormatOnCloseBrace || !options.FormatOnTyping; + } + + private static bool OnlySmartIndentOpenBrace(in AutoFormattingOptions options) + { + // User does not want auto-formatting . So we only smart indent open braces when typed. + // Note: there is no specific option for controlling formatting on open brace. So we + // don't have the symmetry with OnlySmartIndentCloseBrace. + return !options.FormatOnTyping; + } + + private static async Task> FormatTokenAsync( + SyntaxNode root, IndentationOptions options, SyntaxToken token, ImmutableArray formattingRules, CancellationToken cancellationToken) + { + var formatter = new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)root); + return await formatter.FormatTokenAsync(token, cancellationToken).ConfigureAwait(false); + } + + private static ImmutableArray FormatRange( + SyntaxNode root, + IndentationOptions options, + SyntaxToken endToken, + ImmutableArray formattingRules, + CancellationToken cancellationToken) + { + if (!IsEndToken(endToken)) + { + return ImmutableArray.Empty; + } + + var tokenRange = FormattingRangeHelper.FindAppropriateRange(endToken); + if (tokenRange == null || tokenRange.Value.Item1.Equals(tokenRange.Value.Item2)) + { + return ImmutableArray.Empty; + } + + if (IsInvalidTokenKind(tokenRange.Value.Item1) || IsInvalidTokenKind(tokenRange.Value.Item2)) + { + return ImmutableArray.Empty; + } + + var formatter = new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)root); + + var changes = formatter.FormatRange(tokenRange.Value.Item1, tokenRange.Value.Item2, cancellationToken); + return changes.ToImmutableArray(); + } + + private static IEnumerable GetTypingRules(SyntaxToken tokenBeforeCaret) + { + // Typing introduces several challenges around formatting. + // Historically we've shipped several triggers that cause formatting to happen directly while typing. + // These include formatting of blocks when '}' is typed, formatting of statements when a ';' is typed, formatting of ```case```s when ':' typed, and many other cases. + // However, formatting during typing can potentially cause problems. This is because the surrounding code may not be complete, + // or may otherwise have syntax errors, and thus edits could have unintended consequences. + // + // Because of this, we introduce an extra rule into the set of formatting rules whose purpose is to actually make formatting *more* + // conservative and *less* willing willing to make edits to the tree. + // The primary effect this rule has is to assume that more code is on a single line (and thus should stay that way) + // despite what the tree actually looks like. + // + // It's ok that this is only during formatting that is caused by an edit because that formatting happens + // implicitly and thus has to be more careful, whereas an explicit format-document call only happens on-demand + // and can be more aggressive about what it's doing. + // + // + // For example, say you have the following code. + // + // ```c# + // class C + // { + // int P { get { return + // } + // ``` + // + // Hitting ';' after 'return' should ideally only affect the 'return statement' and change it to: + // + // ```c# + // class C + // { + // int P { get { return; + // } + // ``` + // + // During a normal format-document call, this is not what would happen. + // Specifically, because the parser will consume the '}' into the accessor, + // it will think the accessor spans multiple lines, and thus should not stay on a single line. This will produce: + // + // ```c# + // class C + // { + // int P + // { + // get + // { + // return; + // } + // ``` + // + // Because it's ok for this to format in that fashion if format-document is invoked, + // but should not happen during typing, we insert a specialized rule *only* during typing to try to control this. + // During normal formatting we add 'keep on single line' suppression rules for blocks we find that are on a single line. + // But that won't work since this span is not on a single line: + // + // ```c# + // class C + // { + // int P { get [|{ return; + // }|] + // ``` + // + // So, during typing, if we see any parent block is incomplete, we'll assume that + // all our parent blocks are incomplete and we will place the suppression span like so: + // + // ```c# + // class C + // { + // int P { get [|{ return;|] + // } + // ``` + // + // This will have the desired effect of keeping these tokens on the same line, but only during typing scenarios. + if (tokenBeforeCaret.Kind() is SyntaxKind.CloseBraceToken or + SyntaxKind.EndOfFileToken) + { + return SpecializedCollections.EmptyEnumerable(); + } + + return SpecializedCollections.SingletonEnumerable(TypingFormattingRule.Instance); + } + + private static bool IsEndToken(SyntaxToken endToken) + { + if (endToken.IsKind(SyntaxKind.OpenBraceToken)) + { + return false; + } + + return true; + } + + // We'll autoformat on n, t, e, only if they are the last character of the below + // keywords. + private static bool ValidSingleOrMultiCharactersTokenKind(char typedChar, SyntaxKind kind) + => typedChar switch + { + 'n' => kind is SyntaxKind.RegionKeyword or SyntaxKind.EndRegionKeyword, + 't' => kind == SyntaxKind.SelectKeyword, + 'e' => kind == SyntaxKind.WhereKeyword, + _ => true, + }; + + private static bool IsInvalidTokenKind(SyntaxToken token) + { + // invalid token to be formatted + return token.IsKind(SyntaxKind.None) || + token.IsKind(SyntaxKind.EndOfDirectiveToken) || + token.IsKind(SyntaxKind.EndOfFileToken); + } + + private static ImmutableArray GetFormattingRules(Document document, int position, SyntaxToken tokenBeforeCaret) + { + var workspace = document.Project.Solution.Workspace; + var formattingRuleFactory = workspace.Services.GetRequiredService(); + return ImmutableArray.Create(formattingRuleFactory.CreateRule(document, position)) + .AddRange(GetTypingRules(tokenBeforeCaret)) + .AddRange(Formatter.GetDefaultFormattingRules(document)); + } + + public async Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(root, textSpan); + var service = document.GetRequiredLanguageService(); + + var rules = new List() { new PasteFormattingRule() }; + rules.AddRange(service.GetDefaultFormattingRules()); + + var result = service.GetFormattingResult(root, SpecializedCollections.SingletonEnumerable(formattingSpan), options, rules, cancellationToken); + return result.GetTextChanges(cancellationToken).ToImmutableArray(); + } + + internal sealed class PasteFormattingRule : AbstractFormattingRule + { + public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + if (currentToken.Parent != null) + { + var currentTokenParentParent = currentToken.Parent.Parent; + if (currentToken.Kind() == SyntaxKind.OpenBraceToken && currentTokenParentParent != null && + (currentTokenParentParent.Kind() == SyntaxKind.SimpleLambdaExpression || + currentTokenParentParent.Kind() == SyntaxKind.ParenthesizedLambdaExpression || + currentTokenParentParent.Kind() == SyntaxKind.AnonymousMethodExpression)) + { + return FormattingOperations.CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines); + } + } + + return nextOperation.Invoke(in previousToken, in currentToken); + } + } } } diff --git a/src/Features/CSharp/Portable/Formatting/TypingFormattingRule.cs b/src/Workspaces/CSharp/Portable/Formatting/TypingFormattingRule.cs similarity index 100% rename from src/Features/CSharp/Portable/Formatting/TypingFormattingRule.cs rename to src/Workspaces/CSharp/Portable/Formatting/TypingFormattingRule.cs diff --git a/src/Workspaces/Core/Portable/Formatting/ISyntaxFormattingService.cs b/src/Workspaces/Core/Portable/Formatting/ISyntaxFormattingService.cs index 5686bda7a06e3..684ed3d40e4f1 100644 --- a/src/Workspaces/Core/Portable/Formatting/ISyntaxFormattingService.cs +++ b/src/Workspaces/Core/Portable/Formatting/ISyntaxFormattingService.cs @@ -2,11 +2,19 @@ // 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.Threading; +using System.Threading.Tasks; +using System.Collections.Immutable; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Indentation; namespace Microsoft.CodeAnalysis.Formatting { internal interface ISyntaxFormattingService : ISyntaxFormatting, ILanguageService { + Task ShouldFormatOnTypedCharacterAsync(Document document, char typedChar, int caretPosition, CancellationToken cancellationToken); + Task> GetFormattingChangesOnTypedCharacterAsync(Document document, int caretPosition, IndentationOptions indentationOptions, CancellationToken cancellationToken); + Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken); } } diff --git a/src/Workspaces/VisualBasic/Portable/Formatting/VisualBasicSyntaxFormattingService.vb b/src/Workspaces/VisualBasic/Portable/Formatting/VisualBasicSyntaxFormattingService.vb index 1bb32f2a16542..bd8f025bef48f 100644 --- a/src/Workspaces/VisualBasic/Portable/Formatting/VisualBasicSyntaxFormattingService.vb +++ b/src/Workspaces/VisualBasic/Portable/Formatting/VisualBasicSyntaxFormattingService.vb @@ -3,14 +3,12 @@ ' See the LICENSE file in the project root for more information. Imports System.Collections.Immutable +Imports System.Composition Imports System.Threading Imports Microsoft.CodeAnalysis.Formatting -Imports Microsoft.CodeAnalysis.Formatting.Rules -Imports Microsoft.CodeAnalysis.Shared.Collections -Imports Microsoft.CodeAnalysis.Text -Imports Microsoft.CodeAnalysis.Diagnostics -Imports System.Composition Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.Indentation +Imports Microsoft.CodeAnalysis.Text Namespace Microsoft.CodeAnalysis.VisualBasic.Formatting @@ -22,5 +20,17 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Formatting Public Sub New() End Sub + + Public Function ShouldFormatOnTypedCharacterAsync(document As Document, typedChar As Char, caretPosition As Integer, cancellationToken As CancellationToken) As Task(Of Boolean) Implements ISyntaxFormattingService.ShouldFormatOnTypedCharacterAsync + Return Task.FromResult(False) + End Function + + Public Function GetFormattingChangesOnTypedCharacterAsync(document As Document, caretPosition As Integer, indentationOptions As IndentationOptions, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of TextChange)) Implements ISyntaxFormattingService.GetFormattingChangesOnTypedCharacterAsync + Throw ExceptionUtilities.Unreachable + End Function + + Public Function GetFormattingChangesOnPasteAsync(document As Document, textSpan As TextSpan, options As SyntaxFormattingOptions, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of TextChange)) Implements ISyntaxFormattingService.GetFormattingChangesOnPasteAsync + Throw ExceptionUtilities.Unreachable + End Function End Class End Namespace