private static async Task <TextEdit[]> GetFormattedCSharpEditsAsync(
            RazorCodeDocument codeDocument,
            char typedChar,
            int position,
            bool insertSpaces,
            int tabSize)
        {
            var generatedCode    = codeDocument.GetCSharpDocument().GeneratedCode;
            var csharpSourceText = SourceText.From(generatedCode);
            var document         = GenerateRoslynCSharpDocument(csharpSourceText);
            var documentOptions  = await GetDocumentOptionsAsync(document, insertSpaces, tabSize);

            var formattingChanges = await RazorCSharpFormattingInteractionService.GetFormattingChangesAsync(
                document, typedChar, position, documentOptions, CancellationToken.None).ConfigureAwait(false);

            var textEdits = formattingChanges.Select(change => change.AsTextEdit(csharpSourceText)).ToArray();

            return(textEdits);

            Document GenerateRoslynCSharpDocument(SourceText csharpSourceText)
            {
                var workspace = TestWorkspace.Create();
                var project   = workspace.CurrentSolution.AddProject("TestProject", "TestAssembly", LanguageNames.CSharp);
                var document  = project.AddDocument("TestDocument", csharpSourceText);

                return(document);
            }

            async Task <DocumentOptionSet> GetDocumentOptionsAsync(Document document, bool insertSpaces, int tabSize)
            {
                var documentOptions = await document.GetOptionsAsync().ConfigureAwait(false);

                documentOptions = documentOptions.WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.TabSize, tabSize)
                                  .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.IndentationSize, tabSize)
                                  .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.UseTabs, !insertSpaces);
                return(documentOptions);
            }
        }
        public async override Task <FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken)
        {
            if (!context.IsFormatOnType || result.Kind != RazorLanguageKind.CSharp)
            {
                // We don't want to handle regular formatting or non-C# on type formatting here.
                return(result);
            }

            // Normalize and re-map the C# edits.
            var codeDocument = context.CodeDocument;
            var csharpText   = codeDocument.GetCSharpSourceText();

            var textEdits = result.Edits;

            if (textEdits.Length == 0)
            {
                if (!DocumentMappingService.TryMapToProjectedDocumentPosition(codeDocument, context.HostDocumentIndex, out _, out var projectedIndex))
                {
                    _logger.LogWarning($"Failed to map to projected position for document {context.Uri}.");
                    return(result);
                }

                // Ask C# for formatting changes.
                var indentationOptions = new RazorIndentationOptions(
                    UseTabs: !context.Options.InsertSpaces,
                    TabSize: context.Options.TabSize,
                    IndentationSize: context.Options.TabSize);
                var autoFormattingOptions = new RazorAutoFormattingOptions(
                    formatOnReturn: true, formatOnTyping: true, formatOnSemicolon: true, formatOnCloseBrace: true);

                var formattingChanges = await RazorCSharpFormattingInteractionService.GetFormattingChangesAsync(
                    context.CSharpWorkspaceDocument,
                    typedChar : context.TriggerCharacter,
                    projectedIndex,
                    indentationOptions,
                    autoFormattingOptions,
                    indentStyle : CodeAnalysis.Formatting.FormattingOptions.IndentStyle.Smart,
                    cancellationToken).ConfigureAwait(false);

                if (formattingChanges.IsEmpty)
                {
                    _logger.LogInformation("Received no results.");
                    return(result);
                }

                textEdits = formattingChanges.Select(change => change.AsTextEdit(csharpText)).ToArray();
                _logger.LogInformation($"Received {textEdits.Length} results from C#.");
            }

            var normalizedEdits = NormalizeTextEdits(csharpText, textEdits, out var originalTextWithChanges);
            var mappedEdits     = RemapTextEdits(codeDocument, normalizedEdits, result.Kind);
            var filteredEdits   = FilterCSharpTextEdits(context, mappedEdits);

            if (filteredEdits.Length == 0)
            {
                // There are no CSharp edits for us to apply. No op.
                return(new FormattingResult(filteredEdits));
            }

            // Find the lines that were affected by these edits.
            var originalText = codeDocument.GetSourceText();
            var changes      = filteredEdits.Select(e => e.AsTextChange(originalText));

            // Apply the format on type edits sent over by the client.
            var formattedText  = ApplyChangesAndTrackChange(originalText, changes, out _, out var spanAfterFormatting);
            var changedContext = await context.WithTextAsync(formattedText);

            var rangeAfterFormatting = spanAfterFormatting.AsRange(formattedText);

            cancellationToken.ThrowIfCancellationRequested();

            // We make an optimistic attempt at fixing corner cases.
            var cleanupChanges = CleanupDocument(changedContext, rangeAfterFormatting);
            var cleanedText    = formattedText.WithChanges(cleanupChanges);

            changedContext = await changedContext.WithTextAsync(cleanedText);

            cancellationToken.ThrowIfCancellationRequested();

            // At this point we should have applied all edits that adds/removes newlines.
            // Let's now ensure the indentation of each of those lines is correct.

            // We only want to adjust the range that was affected.
            // We need to take into account the lines affected by formatting as well as cleanup.
            var lineDelta = LineDelta(formattedText, cleanupChanges, out var firstPosition, out var lastPosition);

            // Okay hear me out, I know this looks lazy, but it totally makes sense.
            // This method is called with edits that the C# formatter wants to make, and from those edits we work out which
            // other edits to apply etc. Fine, all good so far. BUT its totally possible that the user typed a closing brace
            // in the same position as the C# formatter thought it should be, on the line _after_ the code that the C# formatter
            // reformatted.
            //
            // For example, given:
            // if (true){
            //     }
            //
            // If the C# formatter is happy with the placement of that close brace then this method will get two edits:
            //  * On line 1 to indent the if by 4 spaces
            //  * On line 1 to add a newline and 4 spaces in front of the opening brace
            //
            // We'll happy format lines 1 and 2, and ignore the closing brace altogether. So, by looking one line further
            // we won't have that problem.
            if (rangeAfterFormatting.End.Line + lineDelta < cleanedText.Lines.Count)
            {
                lineDelta++;
            }

            // Now we know how many lines were affected by the cleanup and formatting, but we don't know where those lines are. For example, given:
            //
            // @if (true)
            // {
            //      }
            // else
            // {
            // $$}
            //
            // When typing that close brace, the changes would fix the previous close brace, but the line delta would be 0, so
            // we'd format line 6 and call it a day, even though the formatter made an edit on line 3. To fix this we use the
            // first and last position of edits made above, and make sure our range encompasses them as well. For convenience
            // we calculate these positions in the LineDelta method called above.
            // This is essentially: rangeToAdjust = new Range(Math.Min(firstFormattingEdit, userEdit), Math.Max(lastFormattingEdit, userEdit))
            var start = rangeAfterFormatting.Start;

            if (firstPosition is not null && firstPosition < start)
            {
                start = firstPosition;
            }

            var end = new Position(rangeAfterFormatting.End.Line + lineDelta, 0);

            if (lastPosition is not null && lastPosition < start)
            {
                end = lastPosition;
            }

            var rangeToAdjust = new Range(start, end);

            Debug.Assert(rangeToAdjust.End.IsValid(cleanedText), "Invalid range. This is unexpected.");

            var indentationChanges = await AdjustIndentationAsync(changedContext, cancellationToken, rangeToAdjust);

            if (indentationChanges.Count > 0)
            {
                // Apply the edits that modify indentation.
                cleanedText = cleanedText.WithChanges(indentationChanges);
            }

            // Now that we have made all the necessary changes to the document. Let's diff the original vs final version and return the diff.
            var finalChanges = cleanedText.GetTextChanges(originalText);
            var finalEdits   = finalChanges.Select(f => f.AsTextEdit(originalText)).ToArray();

            if (context.AutomaticallyAddUsings)
            {
                // Because we need to parse the C# code twice for this operation, lets do a quick check to see if its even necessary
                if (textEdits.Any(e => e.NewText.IndexOf("using") != -1))
                {
                    finalEdits = await AddUsingStatementEditsAsync(codeDocument, finalEdits, csharpText, originalTextWithChanges, cancellationToken);
                }
            }

            return(new FormattingResult(finalEdits));
        }