Skip to content

Commit

Permalink
Global indentation options - take 2 (#60565)
Browse files Browse the repository at this point in the history
* Revert "Revert "Global indentation options (#59679)" (#60199)"

This reverts commit ab57ce8.

* 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.

* Test fix
  • Loading branch information
tmat authored Apr 9, 2022
1 parent 3b137f6 commit 0c7c0b3
Show file tree
Hide file tree
Showing 109 changed files with 1,374 additions and 1,304 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

#if CODE_STYLE
using OptionSet = Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions;
#else
using Microsoft.CodeAnalysis.Options;
#endif

namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace
{
using static ConvertNamespaceAnalysis;
Expand Down Expand Up @@ -60,7 +69,14 @@ protected override async Task FixAllAsync(
var diagnostic = diagnostics.First();

var namespaceDecl = (BaseNamespaceDeclarationSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken);
var converted = await ConvertAsync(document, namespaceDecl, cancellationToken).ConfigureAwait(false);

#if CODE_STYLE
var configOptions = document.Project.AnalyzerOptions.GetAnalyzerOptionSet(namespaceDecl.SyntaxTree, cancellationToken);
var options = CSharpSyntaxFormattingOptions.Create(configOptions);
#else
var options = await SyntaxFormattingOptions.FromDocumentAsync(document, cancellationToken).ConfigureAwait(false);
#endif
var converted = await ConvertAsync(document, namespaceDecl, options, cancellationToken).ConfigureAwait(false);

editor.ReplaceNode(
editor.OriginalRoot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,29 @@ namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace
{
internal static class ConvertNamespaceTransform
{
public static async Task<Document> ConvertAsync(Document document, BaseNamespaceDeclarationSyntax baseNamespace, CancellationToken cancellationToken)
public static async Task<Document> ConvertAsync(Document document, BaseNamespaceDeclarationSyntax baseNamespace, SyntaxFormattingOptions options, CancellationToken cancellationToken)
{
switch (baseNamespace)
{
case FileScopedNamespaceDeclarationSyntax fileScopedNamespace:
return await ConvertFileScopedNamespaceAsync(document, fileScopedNamespace, cancellationToken).ConfigureAwait(false);

case NamespaceDeclarationSyntax namespaceDeclaration:
var (doc, _) = await ConvertNamespaceDeclarationAsync(document, namespaceDeclaration, cancellationToken).ConfigureAwait(false);
var (doc, _) = await ConvertNamespaceDeclarationAsync(document, namespaceDeclaration, options, cancellationToken).ConfigureAwait(false);
return doc;

default:
throw ExceptionUtilities.UnexpectedValue(baseNamespace.Kind());
}
}

public static async Task<(Document document, TextSpan semicolonSpan)> ConvertNamespaceDeclarationAsync(Document document, NamespaceDeclarationSyntax namespaceDeclaration, CancellationToken cancellationToken)
public static async Task<(Document document, TextSpan semicolonSpan)> ConvertNamespaceDeclarationAsync(Document document, NamespaceDeclarationSyntax namespaceDeclaration, SyntaxFormattingOptions options, CancellationToken cancellationToken)
{
// First, determine how much indentation we had inside the original block namespace. We'll attempt to remove
// that much indentation from each applicable line after we conver the block namespace to a file scoped
// namespace.
var indentation = await GetIndentationAsync(document, namespaceDeclaration, cancellationToken).ConfigureAwait(false);

var indentation = await GetIndentationAsync(document, namespaceDeclaration, options, cancellationToken).ConfigureAwait(false);

// Next, actually replace the block namespace with the file scoped namespace.
var annotation = new SyntaxAnnotation();
Expand All @@ -77,7 +78,7 @@ public static async Task<Document> ConvertAsync(Document document, BaseNamespace
return (document.WithSyntaxRoot(updatedRoot), fileScopedNamespace.SemicolonToken.Span);
}

private static async Task<string?> GetIndentationAsync(Document document, NamespaceDeclarationSyntax namespaceDeclaration, CancellationToken cancellationToken)
private static async Task<string?> GetIndentationAsync(Document document, NamespaceDeclarationSyntax namespaceDeclaration, SyntaxFormattingOptions options, CancellationToken cancellationToken)
{
var indentationService = document.GetRequiredLanguageService<IIndentationService>();
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -87,19 +88,12 @@ public static async Task<Document> ConvertAsync(Document document, BaseNamespace
if (openBraceLine == closeBraceLine)
return null;

#if CODE_STYLE
var options = document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(namespaceDeclaration.SyntaxTree!);
#else
var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
#endif
var style = options.GetOption(FormattingOptions2.SmartIndent, document.Project.Language);

var indentation = indentationService.GetIndentation(document, openBraceLine + 1, (FormattingOptions.IndentStyle)style, cancellationToken);
// Auto-formatting options are not relevant since they only control behavior on typing.
var indentationOptions = new IndentationOptions(options, AutoFormattingOptions.Default);

var useTabs = options.GetOption(FormattingOptions2.UseTabs);
var tabSize = options.GetOption(FormattingOptions2.TabSize);
var indentation = indentationService.GetIndentation(document, openBraceLine + 1, indentationOptions, cancellationToken);

return indentation.GetIndentationString(sourceText, useTabs, tabSize);
return indentation.GetIndentationString(sourceText, options.UseTabs, options.TabSize);
}

private static async Task<(Document document, TextSpan semicolonSpan)> DedentNamespaceAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler,
if (!ConvertNamespaceAnalysis.CanOfferUseFileScoped(s_optionSet, root, namespaceDecl, forAnalyzer: true, LanguageVersion.CSharp10))
return default;

var (converted, semicolonSpan) = ConvertNamespaceTransform.ConvertNamespaceDeclarationAsync(document, namespaceDecl, cancellationToken).WaitAndGetResult(cancellationToken);
var formattingOptions = SyntaxFormattingOptions.FromDocumentAsync(document, cancellationToken).WaitAndGetResult(cancellationToken);
var (converted, semicolonSpan) = ConvertNamespaceTransform.ConvertNamespaceDeclarationAsync(document, namespaceDecl, formattingOptions, cancellationToken).WaitAndGetResult(cancellationToken);
var text = converted.GetTextSynchronously(cancellationToken);
return (text, semicolonSpan);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text.Operations;
Expand All @@ -20,16 +21,17 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.DocumentationComments
[Name(PredefinedCommandHandlerNames.DocumentationComments)]
[Order(After = PredefinedCommandHandlerNames.Rename)]
[Order(After = PredefinedCompletionNames.CompletionCommandHandler)]
internal class DocumentationCommentCommandHandler
internal sealed class DocumentationCommentCommandHandler
: AbstractDocumentationCommentCommandHandler
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public DocumentationCommentCommandHandler(
IUIThreadOperationExecutor uiThreadOperationExecutor,
ITextUndoHistoryRegistry undoHistoryRegistry,
IEditorOperationsFactoryService editorOperationsFactoryService)
: base(uiThreadOperationExecutor, undoHistoryRegistry, editorOperationsFactoryService)
IEditorOperationsFactoryService editorOperationsFactoryService,
IGlobalOptionService globalOptions)
: base(uiThreadOperationExecutor, undoHistoryRegistry, editorOperationsFactoryService, globalOptions)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ImmutableArray<TextChange>> 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<ImmutableArray<TextChange>> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken)
{
var service = document.GetRequiredLanguageService<ISyntaxFormattingService>();
var options = await _indentationManager.GetInferredFormattingOptionsAsync(document, explicitFormat: true, cancellationToken).ConfigureAwait(false);
return await service.GetFormattingChangesOnPasteAsync(document, textSpan, options, cancellationToken).ConfigureAwait(false);
}

Task<ImmutableArray<TextChange>> IFormattingInteractionService.GetFormattingChangesOnReturnAsync(
Document document, int caretPosition, CancellationToken cancellationToken)
=> SpecializedTasks.EmptyImmutableArray<TextChange>();

public async Task<ImmutableArray<TextChange>> GetFormattingChangesAsync(Document document, char typedChar, int position, CancellationToken cancellationToken)
{
var service = document.GetRequiredLanguageService<ISyntaxFormattingService>();

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<TextChange>.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ SyntaxKind.InterpolatedSingleLineRawStringStartToken or
return false;
}

var indentation = token.GetPreferredIndentation(document, cancellationToken);
var indentationOptions = _globalOptions.GetIndentationOptionsAsync(document, cancellationToken).WaitAndGetResult(cancellationToken);
var indentation = token.GetPreferredIndentation(document, indentationOptions, cancellationToken);

var newLine = document.Project.Solution.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
var newLine = indentationOptions.FormattingOptions.NewLine;

using var transaction = CaretPreservingEditTransaction.TryCreate(
CSharpEditorResources.Split_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
Expand Down Expand Up @@ -120,12 +121,12 @@ private bool SplitString(ITextView textView, ITextBuffer subjectBuffer, int posi
}

// TODO: read option from textView.Options (https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1412138)
var indentStyle = document.Project.Solution.Options.GetOption(FormattingOptions.SmartIndent, LanguageNames.CSharp);
var options = _globalOptions.GetIndentationOptionsAsync(document, cancellationToken).WaitAndGetResult(cancellationToken);

using var transaction = CaretPreservingEditTransaction.TryCreate(
CSharpEditorResources.Split_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService);

var splitter = StringSplitter.TryCreate(document, position, useTabs, tabSize, indentStyle, cancellationToken);
var splitter = StringSplitter.TryCreate(document, position, options, useTabs, tabSize, cancellationToken);
if (splitter?.TrySplit(out var newDocument, out var newPosition) != true)
{
return false;
Expand Down
Loading

0 comments on commit 0c7c0b3

Please sign in to comment.