File: Formatting\CoreFormatterTestsBase.cs
Web Access
Project: src\src\EditorFeatures\TestUtilities\Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj (Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities)
// 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.
 
#nullable disable
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Implementation.SmartIndent;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Text.Projection;
using Moq;
using Roslyn.Test.EditorUtilities;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.Editor.UnitTests.Formatting;
 
public abstract class CoreFormatterTestsBase
{
    private static readonly TestComposition s_composition = EditorTestCompositions.EditorFeatures.AddParts(typeof(TestFormattingRuleFactoryServiceFactory));
 
    private readonly ITestOutputHelper _output;
 
    protected CoreFormatterTestsBase(ITestOutputHelper output)
        => _output = output;
 
    protected abstract string GetLanguageName();
    protected abstract SyntaxNode ParseCompilationUnit(string expected);
 
    internal static void TestIndentation(
        int point, int? expectedIndentation, ITextView textView, EditorTestHostDocument subjectDocument, EditorOptionsService editorOptionsService)
    {
        var textUndoHistory = new Mock<ITextUndoHistoryRegistry>();
        var editorOperationsFactory = new Mock<IEditorOperationsFactoryService>();
        var editorOperations = new Mock<IEditorOperations>();
        editorOperationsFactory.Setup(x => x.GetEditorOperations(textView)).Returns(editorOperations.Object);
 
        var snapshot = subjectDocument.GetTextBuffer().CurrentSnapshot;
        var indentationLineFromBuffer = snapshot.GetLineFromPosition(point);
 
        var provider = new SmartIndent(textView, editorOptionsService);
        var actualIndentation = provider.GetDesiredIndentation(indentationLineFromBuffer);
 
        Assert.Equal(expectedIndentation, actualIndentation.Value);
    }
 
    internal void TestIndentation(
        EditorTestWorkspace workspace,
        int indentationLine,
        int? expectedIndentation,
        FormattingOptions2.IndentStyle indentStyle,
        bool useTabs)
    {
        var language = GetLanguageName();
 
        var editorOptionsFactory = workspace.GetService<IEditorOptionsFactoryService>();
        var document = workspace.Documents.First();
        var textBuffer = document.GetTextBuffer();
        var editorOptions = editorOptionsFactory.GetOptions(textBuffer);
 
        editorOptions.SetOptionValue(DefaultOptions.IndentStyleId, indentStyle.ToEditorIndentStyle());
        editorOptions.SetOptionValue(DefaultOptions.ConvertTabsToSpacesOptionId, !useTabs);
 
        // Remove once https://github.com/dotnet/roslyn/issues/62204 is fixed:
        workspace.GlobalOptions.SetGlobalOption(IndentationOptionsStorage.SmartIndent, document.Project.Language, indentStyle);
 
        var snapshot = textBuffer.CurrentSnapshot;
        var bufferGraph = new Mock<IBufferGraph>(MockBehavior.Strict);
        bufferGraph.Setup(x => x.MapUpToSnapshot(It.IsAny<SnapshotPoint>(),
                                                 It.IsAny<PointTrackingMode>(),
                                                 It.IsAny<PositionAffinity>(),
                                                 It.IsAny<ITextSnapshot>()))
            .Returns<SnapshotPoint, PointTrackingMode, PositionAffinity, ITextSnapshot>((p, m, a, s) =>
            {
                if (workspace.Services.GetService<IHostDependentFormattingRuleFactoryService>() is TestFormattingRuleFactoryServiceFactory.Factory factory && factory.BaseIndentation != 0 && factory.TextSpan.Contains(p.Position))
                {
                    var line = p.GetContainingLine();
                    var projectedOffset = line.GetFirstNonWhitespaceOffset().Value - factory.BaseIndentation;
                    return new SnapshotPoint(p.Snapshot, p.Position - projectedOffset);
                }
 
                return p;
            });
 
        var projectionBuffer = new Mock<ITextBuffer>(MockBehavior.Strict);
        projectionBuffer.Setup(x => x.ContentType.DisplayName).Returns("None");
 
        var textView = new Mock<ITextView>(MockBehavior.Strict);
        textView.Setup(x => x.Options).Returns(TestEditorOptions.Instance);
        textView.Setup(x => x.BufferGraph).Returns(bufferGraph.Object);
        textView.SetupGet(x => x.TextSnapshot.TextBuffer).Returns(projectionBuffer.Object);
 
        var provider = new SmartIndent(
            textView.Object,
            workspace.GetService<EditorOptionsService>());
 
        var indentationLineFromBuffer = snapshot.GetLineFromLineNumber(indentationLine);
        var actualIndentation = provider.GetDesiredIndentation(indentationLineFromBuffer);
 
        Assert.Equal(expectedIndentation, actualIndentation);
    }
 
    private protected void AssertFormatWithView(string expectedWithMarker, string codeWithMarker, OptionsCollection options = null)
    {
        AssertFormatWithView(expectedWithMarker, codeWithMarker, parseOptions: null, options);
    }
 
    private protected void AssertFormatWithView(string expectedWithMarker, string codeWithMarker, ParseOptions parseOptions, OptionsCollection options = null)
    {
        using var workspace = CreateWorkspace(codeWithMarker, parseOptions);
 
        workspace.SetAnalyzerFallbackAndGlobalOptions(options);
 
        // set up caret position
        var testDocument = workspace.Documents.Single();
        var view = testDocument.GetTextView();
        view.Caret.MoveTo(new SnapshotPoint(view.TextSnapshot, testDocument.CursorPosition.Value));
 
        // get original buffer
        var buffer = workspace.Documents.First().GetTextBuffer();
 
        var commandHandler = workspace.GetService<FormatCommandHandler>();
 
        var commandArgs = new FormatDocumentCommandArgs(view, view.TextBuffer);
        commandHandler.ExecuteCommand(commandArgs, TestCommandExecutionContext.Create());
        MarkupTestFile.GetPosition(expectedWithMarker, out var expected, out int expectedPosition);
 
        Assert.Equal(expected, view.TextSnapshot.GetText());
 
        var caretPosition = view.Caret.Position.BufferPosition.Position;
        Assert.True(expectedPosition == caretPosition,
            string.Format("Caret positioned incorrectly. Should have been {0}, but was {1}.", expectedPosition, caretPosition));
    }
 
    private EditorTestWorkspace CreateWorkspace(string codeWithMarker, ParseOptions parseOptions = null)
        => this.GetLanguageName() == LanguageNames.CSharp
            ? EditorTestWorkspace.CreateCSharp(codeWithMarker, composition: s_composition, parseOptions: parseOptions)
            : EditorTestWorkspace.CreateVisualBasic(codeWithMarker, composition: s_composition, parseOptions: parseOptions);
 
    private static string ApplyResultAndGetFormattedText(ITextBuffer buffer, IList<TextChange> changes)
    {
        using (var edit = buffer.CreateEdit())
        {
            foreach (var change in changes)
            {
                edit.Replace(change.Span.ToSpan(), change.NewText);
            }
 
            edit.Apply();
        }
 
        return buffer.CurrentSnapshot.GetText();
    }
 
    private protected async Task AssertFormatAsync(string expected, string code, IEnumerable<TextSpan> spans, OptionsCollection options = null, int? baseIndentation = null)
    {
        using var workspace = CreateWorkspace(code);
        var hostdoc = workspace.Documents.First();
        var buffer = hostdoc.GetTextBuffer();
 
        var document = workspace.CurrentSolution.GetDocument(hostdoc.Id);
        var documentSyntax = await ParsedDocument.CreateAsync(document, CancellationToken.None).ConfigureAwait(false);
 
        // create new buffer with cloned content
        var clonedBuffer = EditorFactory.CreateBuffer(
            workspace.ExportProvider,
            buffer.ContentType,
            buffer.CurrentSnapshot.GetText());
 
        var formattingRuleProvider = workspace.Services.GetService<IHostDependentFormattingRuleFactoryService>();
        if (baseIndentation.HasValue)
        {
            var factory = (TestFormattingRuleFactoryServiceFactory.Factory)formattingRuleProvider;
            factory.BaseIndentation = baseIndentation.Value;
            factory.TextSpan = spans?.First() ?? documentSyntax.Root.FullSpan;
        }
 
        var formattingService = document.GetRequiredLanguageService<ISyntaxFormattingService>();
 
        var formattingOptions = (options != null)
            ? formattingService.GetFormattingOptions(options)
            : formattingService.DefaultOptions;
 
        ImmutableArray<AbstractFormattingRule> rules = [formattingRuleProvider.CreateRule(documentSyntax, 0), .. Formatter.GetDefaultFormattingRules(document)];
        AssertFormat(workspace, expected, formattingOptions, rules, clonedBuffer, documentSyntax.Root, spans);
 
        // format with node and transform
        AssertFormatWithTransformation(workspace, expected, formattingOptions, rules, documentSyntax.Root, spans);
    }
 
    internal void AssertFormatWithTransformation(Workspace workspace, string expected, SyntaxFormattingOptions options, ImmutableArray<AbstractFormattingRule> rules, SyntaxNode root, IEnumerable<TextSpan> spans)
    {
        var newRootNode = Formatter.Format(root, spans, workspace.Services.SolutionServices, options, rules, CancellationToken.None);
 
        Assert.Equal(expected, newRootNode.ToFullString());
 
        // test doesn't use parsing option. add one if needed later
        var newRootNodeFromString = ParseCompilationUnit(expected);
 
        // simple check to see whether two nodes are equivalent each other.
        Assert.True(newRootNodeFromString.IsEquivalentTo(newRootNode));
    }
 
    internal void AssertFormat(Workspace workspace, string expected, SyntaxFormattingOptions options, ImmutableArray<AbstractFormattingRule> rules, ITextBuffer clonedBuffer, SyntaxNode root, IEnumerable<TextSpan> spans)
    {
        var result = Formatter.GetFormattedTextChanges(root, spans, workspace.Services.SolutionServices, options, rules, CancellationToken.None);
        var actual = ApplyResultAndGetFormattedText(clonedBuffer, result);
 
        if (actual != expected)
        {
            _output.WriteLine(actual);
            AssertEx.EqualOrDiff(expected, actual);
        }
    }
 
    protected void AssertFormatWithPasteOrReturn(string expectedWithMarker, string codeWithMarker, bool allowDocumentChanges, bool isPaste = true)
    {
        using var workspace = CreateWorkspace(codeWithMarker);
        workspace.CanApplyChangeDocument = allowDocumentChanges;
 
        // set up caret position
        var testDocument = workspace.Documents.Single();
        var view = testDocument.GetTextView();
        view.Caret.MoveTo(new SnapshotPoint(view.TextSnapshot, testDocument.CursorPosition.Value));
 
        // get original buffer
        var buffer = workspace.Documents.First().GetTextBuffer();
 
        if (isPaste)
        {
            var commandHandler = workspace.GetService<FormatCommandHandler>();
            var commandArgs = new PasteCommandArgs(view, view.TextBuffer);
            commandHandler.ExecuteCommand(commandArgs, () => { }, TestCommandExecutionContext.Create());
        }
        else
        {
            // Return Key Command
            var commandHandler = workspace.GetService<FormatCommandHandler>();
            var commandArgs = new ReturnKeyCommandArgs(view, view.TextBuffer);
            commandHandler.ExecuteCommand(commandArgs, () => { }, TestCommandExecutionContext.Create());
        }
 
        MarkupTestFile.GetPosition(expectedWithMarker, out var expected, out int expectedPosition);
 
        Assert.Equal(expected, view.TextSnapshot.GetText());
 
        var caretPosition = view.Caret.Position.BufferPosition.Position;
        Assert.True(expectedPosition == caretPosition,
            string.Format("Caret positioned incorrectly. Should have been {0}, but was {1}.", expectedPosition, caretPosition));
    }
 
    private protected async Task AssertFormatWithBaseIndentAsync(string expected, string markupCode, int baseIndentation, OptionsCollection options = null)
    {
        TestFileMarkupParser.GetSpans(markupCode, out var code, out ImmutableArray<TextSpan> spans);
        await AssertFormatAsync(expected, code, spans, options, baseIndentation);
    }
 
    /// <summary>
    /// Asserts formatting on an arbitrary <see cref="SyntaxNode"/> that is not part of a <see cref="SyntaxTree"/>
    /// </summary>
    /// <param name="node">the <see cref="SyntaxNode"/> to format.</param>
    /// <remarks>uses an <see cref="AdhocWorkspace"/> for formatting context, since the <paramref name="node"/> is not associated with a <see cref="SyntaxTree"/> </remarks>
    protected static void AssertFormatOnArbitraryNode(SyntaxNode node, string expected)
    {
        using var workspace = new AdhocWorkspace();
        var formattingService = workspace.Services.GetLanguageServices(node.Language).GetRequiredService<ISyntaxFormattingService>();
        var options = formattingService.GetFormattingOptions(StructuredAnalyzerConfigOptions.Empty);
        var result = Formatter.Format(node, workspace.Services.SolutionServices, options, CancellationToken.None);
        var actual = result.GetText().ToString();
 
        Assert.Equal(expected, actual);
    }
}