protected TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits, RazorLanguageKind projectedKind)
        {
            if (codeDocument is null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            if (projectedTextEdits is null)
            {
                throw new ArgumentNullException(nameof(projectedTextEdits));
            }

            if (projectedKind != RazorLanguageKind.CSharp)
            {
                // Non C# projections map directly to Razor. No need to remap.
                return(projectedTextEdits);
            }

            var edits = new List <TextEdit>();

            for (var i = 0; i < projectedTextEdits.Length; i++)
            {
                var projectedRange = projectedTextEdits[i].Range;
                if (codeDocument.IsUnsupported() ||
                    !DocumentMappingService.TryMapFromProjectedDocumentRange(codeDocument, projectedRange, out var originalRange))
                {
                    // Can't map range. Discard this edit.
                    continue;
                }

                var edit = new TextEdit()
                {
                    Range   = originalRange,
                    NewText = projectedTextEdits[i].NewText
                };

                edits.Add(edit);
            }

            return(edits.ToArray());
        }
        protected async Task <List <TextChange> > AdjustIndentationAsync(FormattingContext context, CancellationToken cancellationToken, Range?range = null)
        {
            // In this method, the goal is to make final adjustments to the indentation of each line.
            // We will take into account the following,
            // 1. The indentation due to nested C# structures
            // 2. The indentation due to Razor and HTML constructs

            var text = context.SourceText;

            range ??= TextSpan.FromBounds(0, text.Length).AsRange(text);

            // To help with figuring out the correct indentation, first we will need the indentation
            // that the C# formatter wants to apply in the following locations,
            // 1. The start and end of each of our source mappings
            // 2. The start of every line that starts in C# context

            // Due to perf concerns, we only want to invoke the real C# formatter once.
            // So, let's collect all the significant locations that we want to obtain the CSharpDesiredIndentations for.

            var significantLocations = new HashSet <int>();

            // First, collect all the locations at the beginning and end of each source mapping.
            var sourceMappingMap = new Dictionary <int, int>();

            foreach (var mapping in context.CodeDocument.GetCSharpDocument().SourceMappings)
            {
                var mappingSpan = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length);
                if (!ShouldFormat(context, mappingSpan, allowImplicitStatements: true))
                {
                    // We don't care about this range as this can potentially lead to incorrect scopes.
                    continue;
                }

                var originalStartLocation  = mapping.OriginalSpan.AbsoluteIndex;
                var projectedStartLocation = mapping.GeneratedSpan.AbsoluteIndex;
                sourceMappingMap[originalStartLocation] = projectedStartLocation;
                significantLocations.Add(projectedStartLocation);

                var originalEndLocation  = mapping.OriginalSpan.AbsoluteIndex + mapping.OriginalSpan.Length + 1;
                var projectedEndLocation = mapping.GeneratedSpan.AbsoluteIndex + mapping.GeneratedSpan.Length + 1;
                sourceMappingMap[originalEndLocation] = projectedEndLocation;
                significantLocations.Add(projectedEndLocation);
            }

            // Next, collect all the line starts that start in C# context
            var indentations = context.GetIndentations();
            var lineStartMap = new Dictionary <int, int>();

            for (var i = range.Start.Line; i <= range.End.Line; i++)
            {
                if (indentations[i].EmptyOrWhitespaceLine)
                {
                    // We should remove whitespace on empty lines.
                    continue;
                }

                var line      = context.SourceText.Lines[i];
                var lineStart = line.GetFirstNonWhitespacePosition() ?? line.Start;

                var lineStartSpan = new TextSpan(lineStart, 0);
                if (!ShouldFormat(context, lineStartSpan, allowImplicitStatements: true))
                {
                    // We don't care about this range as this can potentially lead to incorrect scopes.
                    continue;
                }

                if (DocumentMappingService.TryMapToProjectedDocumentPosition(context.CodeDocument, lineStart, out _, out var projectedLineStart))
                {
                    lineStartMap[lineStart] = projectedLineStart;
                    significantLocations.Add(projectedLineStart);
                }
            }

            // Now, invoke the C# formatter to obtain the CSharpDesiredIndentation for all significant locations.
            var significantLocationIndentation = await CSharpFormatter.GetCSharpIndentationAsync(context, significantLocations, cancellationToken);

            // Build source mapping indentation scopes.
            var sourceMappingIndentations = new SortedDictionary <int, int>();

            foreach (var originalLocation in sourceMappingMap.Keys)
            {
                var significantLocation = sourceMappingMap[originalLocation];
                if (!significantLocationIndentation.TryGetValue(significantLocation, out var indentation))
                {
                    // C# formatter didn't return an indentation for this. Skip.
                    continue;
                }

                sourceMappingIndentations[originalLocation] = indentation;
            }

            var sourceMappingIndentationScopes = sourceMappingIndentations.Keys.ToArray();

            // Build lineStart indentation map.
            var lineStartIndentations = new Dictionary <int, int>();

            foreach (var originalLocation in lineStartMap.Keys)
            {
                var significantLocation = lineStartMap[originalLocation];
                if (!significantLocationIndentation.TryGetValue(significantLocation, out var indentation))
                {
                    // C# formatter didn't return an indentation for this. Skip.
                    continue;
                }

                lineStartIndentations[originalLocation] = indentation;
            }

            // Now, let's combine the C# desired indentation with the Razor and HTML indentation for each line.
            var newIndentations = new Dictionary <int, int>();

            for (var i = range.Start.Line; i <= range.End.Line; i++)
            {
                if (indentations[i].EmptyOrWhitespaceLine)
                {
                    // We should remove whitespace on empty lines.
                    newIndentations[i] = 0;
                    continue;
                }

                var minCSharpIndentation = context.GetIndentationOffsetForLevel(indentations[i].MinCSharpIndentLevel);
                var line          = context.SourceText.Lines[i];
                var lineStart     = line.GetFirstNonWhitespacePosition() ?? line.Start;
                var lineStartSpan = new TextSpan(lineStart, 0);
                if (!ShouldFormat(context, lineStartSpan, allowImplicitStatements: true))
                {
                    // We don't care about this line as it lies in an area we don't want to format.
                    continue;
                }

                if (!lineStartIndentations.TryGetValue(lineStart, out var csharpDesiredIndentation))
                {
                    // Couldn't remap. This is probably a non-C# location.
                    // Use SourceMapping indentations to locate the C# scope of this line.
                    // E.g,
                    //
                    // @if (true) {
                    //   <div>
                    //  |</div>
                    // }
                    //
                    // We can't find a direct mapping at |, but we can infer its base indentation from the
                    // indentation of the latest source mapping prior to this line.
                    // We use binary search to find that spot.

                    var index = Array.BinarySearch(sourceMappingIndentationScopes, lineStart);
                    if (index < 0)
                    {
                        // Couldn't find the exact value. Find the index of the element to the left of the searched value.
                        index = (~index) - 1;
                    }

                    if (index < 0)
                    {
                        // If we _still_ couldn't find the right indentation, then it probably means that the text is
                        // before the first source mapping location, so we can just place it in the minimum spot (realistically
                        // at index 0 in the razor file, but we use minCSharpIndentation because we're adjusting based on the
                        // generated file here)
                        csharpDesiredIndentation = minCSharpIndentation;
                    }
                    else
                    {
                        // index will now be set to the same value as the end of the closest source mapping.
                        var absoluteIndex = sourceMappingIndentationScopes[index];
                        csharpDesiredIndentation = sourceMappingIndentations[absoluteIndex];

                        // This means we didn't find an exact match and so we used the indentation of the end of a previous mapping.
                        // So let's use the MinCSharpIndentation of that same location if possible.
                        if (context.TryGetFormattingSpan(absoluteIndex, out var span))
                        {
                            minCSharpIndentation = context.GetIndentationOffsetForLevel(span.MinCSharpIndentLevel);
                        }
                    }
                }

                // Now let's use that information to figure out the effective C# indentation.
                // This should be based on context.
                // For instance, lines inside @code/@functions block should be reduced one level
                // and lines inside @{} should be reduced by two levels.

                if (csharpDesiredIndentation < minCSharpIndentation)
                {
                    // CSharp formatter doesn't want to indent this. Let's not touch it.
                    continue;
                }

                var effectiveCSharpDesiredIndentation = csharpDesiredIndentation - minCSharpIndentation;
                var razorDesiredIndentation           = context.GetIndentationOffsetForLevel(indentations[i].IndentationLevel);
                if (indentations[i].StartsInHtmlContext)
                {
                    // This is a non-C# line.
                    if (context.IsFormatOnType)
                    {
                        // HTML formatter doesn't run in the case of format on type.
                        // Let's stick with our syntax understanding of HTML to figure out the desired indentation.
                    }
                    else
                    {
                        // Given that the HTML formatter ran before this, we can assume
                        // HTML is already correctly formatted. So we can use the existing indentation as is.
                        // We need to make sure to use the indentation size, as this will get passed to
                        // GetIndentationString eventually.
                        razorDesiredIndentation = indentations[i].ExistingIndentationSize;
                    }
                }

                var effectiveDesiredIndentation = razorDesiredIndentation + effectiveCSharpDesiredIndentation;

                // This will now contain the indentation we ultimately want to apply to this line.
                newIndentations[i] = effectiveDesiredIndentation;
            }

            // Now that we have collected all the indentations for each line, let's convert them to text edits.
            var changes = new List <TextChange>();

            foreach (var item in newIndentations)
            {
                var line        = item.Key;
                var indentation = item.Value;
                Debug.Assert(indentation >= 0, "Negative indentation. This is unexpected.");

                var existingIndentationLength   = indentations[line].ExistingIndentation;
                var spanToReplace               = new TextSpan(context.SourceText.Lines[line].Start, existingIndentationLength);
                var effectiveDesiredIndentation = context.GetIndentationString(indentation);
                changes.Add(new TextChange(spanToReplace, effectiveDesiredIndentation));
            }

            return(changes);
        }
        protected bool ShouldFormat(FormattingContext context, Position position, bool allowImplicitStatements)
        {
            // We should be called with start positions of various C# SourceMappings.
            if (position.Character == 0)
            {
                // The mapping starts at 0. It can't be anything special but pure C#. Let's format it.
                return(true);
            }

            var sourceText    = context.SourceText;
            var absoluteIndex = sourceText.Lines[(int)position.Line].Start + (int)position.Character;

            if (IsImplicitStatementStart() && !allowImplicitStatements)
            {
                return(false);
            }

            var syntaxTree = context.CodeDocument.GetSyntaxTree();
            var change     = new SourceChange(absoluteIndex, 0, string.Empty);
            var owner      = syntaxTree.Root.LocateOwner(change);

            if (owner == null)
            {
                // Can't determine owner of this position. Optimistically allow formatting.
                return(true);
            }

            if (IsInHtmlTag() ||
                IsInSingleLineDirective() ||
                IsImplicitOrExplicitExpression())
            {
                return(false);
            }

            return(true);

            bool IsImplicitStatementStart()
            {
                // We will return true if the position points to the start of the C# portion of an implicit statement.
                // `@|for(...)` - true
                // `@|if(...)` - true
                // `@{|...` - false
                // `@code {|...` - false
                //

                var previousCharIndex = absoluteIndex - 1;
                var previousChar      = sourceText[previousCharIndex];

                if (previousChar != '@')
                {
                    // Not an implicit statement.
                    return(false);
                }

                // This is an implicit statement if the previous '@' is not C# (meaning it shouldn't have a projected mapping).
                return(!DocumentMappingService.TryMapToProjectedDocumentPosition(context.CodeDocument, previousCharIndex, out _, out _));
            }

            bool IsInHtmlTag()
            {
                // E.g, (| is position)
                //
                // `<p csharpattr="|Variable">` - true
                //
                return(owner.AncestorsAndSelf().Any(
                           n => n is MarkupStartTagSyntax || n is MarkupTagHelperStartTagSyntax || n is MarkupEndTagSyntax || n is MarkupTagHelperEndTagSyntax));
            }

            bool IsInSingleLineDirective()
            {
                // E.g, (| is position)
                //
                // `@inject |SomeType SomeName` - true
                //
                // Note: @using directives don't have a descriptor associated with them, hence the extra null check.
                //
                return(owner.AncestorsAndSelf().Any(
                           n => n is RazorDirectiveSyntax directive && (directive.DirectiveDescriptor == null || directive.DirectiveDescriptor.Kind == DirectiveKind.SingleLine)));
            }

            bool IsImplicitOrExplicitExpression()
            {
                // E.g, (| is position)
                //
                // `@|foo` - true
                // `@(|foo)` - true
                //
                return(owner.AncestorsAndSelf().Any(n => n is CSharpImplicitExpressionSyntax || n is CSharpExplicitExpressionSyntax));
            }
        }
Exemplo n.º 4
0
        private List <TextChange> AdjustIndentation(FormattingContext context, CancellationToken cancellationToken, Range range = null)
        {
            // In this method, the goal is to make final adjustments to the indentation of each line.
            // We will take into account the following,
            // 1. The indentation due to nested C# structures
            // 2. The indentation due to Razor and HTML constructs

            var text = context.SourceText;

            range ??= TextSpan.FromBounds(0, text.Length).AsRange(text);

            // First, let's build an understanding of the desired C# indentation at the beginning and end of each source mapping.
            var sourceMappingIndentations = new SortedDictionary <int, int>();

            foreach (var mapping in context.CodeDocument.GetCSharpDocument().SourceMappings)
            {
                var mappingSpan  = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length);
                var mappingRange = mappingSpan.AsRange(context.SourceText);
                if (!ShouldFormat(context, mappingRange.Start))
                {
                    // We don't care about this range as this can potentially lead to incorrect scopes.
                    continue;
                }

                var startIndentation = CSharpFormatter.GetCSharpIndentation(context, mapping.GeneratedSpan.AbsoluteIndex, cancellationToken);
                sourceMappingIndentations[mapping.OriginalSpan.AbsoluteIndex] = startIndentation;

                var endIndentation = CSharpFormatter.GetCSharpIndentation(context, mapping.GeneratedSpan.AbsoluteIndex + mapping.GeneratedSpan.Length + 1, cancellationToken);
                sourceMappingIndentations[mapping.OriginalSpan.AbsoluteIndex + mapping.OriginalSpan.Length + 1] = endIndentation;
            }

            var sourceMappingIndentationScopes = sourceMappingIndentations.Keys.ToArray();

            // Now, let's combine the C# desired indentation with the Razor and HTML indentation for each line.
            var newIndentations = new Dictionary <int, int>();

            for (var i = range.Start.Line; i <= range.End.Line; i++)
            {
                var line = context.SourceText.Lines[i];
                if (line.Span.Length == 0)
                {
                    // We don't want to indent empty lines.
                    continue;
                }

                var lineStart = line.Start;
                int csharpDesiredIndentation;
                if (DocumentMappingService.TryMapToProjectedDocumentPosition(context.CodeDocument, lineStart, out _, out var projectedLineStart))
                {
                    // We were able to map this line to C# directly.
                    csharpDesiredIndentation = CSharpFormatter.GetCSharpIndentation(context, projectedLineStart, cancellationToken);
                }
                else
                {
                    // Couldn't remap. This is probably a non-C# location.
                    // Use SourceMapping indentations to locate the C# scope of this line.
                    // E.g,
                    //
                    // @if (true) {
                    //   <div>
                    //  |</div>
                    // }
                    //
                    // We can't find a direct mapping at |, but we can infer its base indentation from the
                    // indentation of the latest source mapping prior to this line.
                    // We use binary search to find that spot.

                    var index = Array.BinarySearch(sourceMappingIndentationScopes, lineStart);
                    if (index < 0)
                    {
                        // Couldn't find the exact value. Find the index of the element to the left of the searched value.
                        index = (~index) - 1;
                    }

                    // This will now be set to the same value as the end of the closest source mapping.
                    csharpDesiredIndentation = index < 0 ? 0 : sourceMappingIndentations[sourceMappingIndentationScopes[index]];
                }

                // Now let's use that information to figure out the effective C# indentation.
                // This should be based on context.
                // For instance, lines inside @code/@functions block should be reduced one level
                // and lines inside @{} should be reduced by two levels.

                var csharpDesiredIndentLevel = context.GetIndentationLevelForOffset(csharpDesiredIndentation);
                var minCSharpIndentLevel     = context.Indentations[i].MinCSharpIndentLevel;
                if (csharpDesiredIndentLevel < minCSharpIndentLevel)
                {
                    // CSharp formatter doesn't want to indent this. Let's not touch it.
                    continue;
                }

                var effectiveCSharpDesiredIndentationLevel = csharpDesiredIndentLevel - minCSharpIndentLevel;
                var razorDesiredIndentationLevel           = context.Indentations[i].IndentationLevel;
                if (!context.Indentations[i].StartsInCSharpContext)
                {
                    // This is a non-C# line. Given that the HTML formatter ran before this, we can assume
                    // HTML is already correctly formatted. So we can use the existing indentation as is.
                    razorDesiredIndentationLevel = context.GetIndentationLevelForOffset(context.Indentations[i].ExistingIndentation);
                }
                var effectiveDesiredIndentationLevel = razorDesiredIndentationLevel + effectiveCSharpDesiredIndentationLevel;

                // This will now contain the indentation we ultimately want to apply to this line.
                newIndentations[i] = effectiveDesiredIndentationLevel;
            }

            // Now that we have collected all the indentations for each line, let's convert them to text edits.
            var changes = new List <TextChange>();

            foreach (var item in newIndentations)
            {
                var line             = item.Key;
                var indentationLevel = item.Value;
                Debug.Assert(indentationLevel >= 0, "Negative indent level. This is unexpected.");

                var existingIndentationLength   = context.Indentations[line].ExistingIndentation;
                var spanToReplace               = new TextSpan(context.SourceText.Lines[line].Start, existingIndentationLength);
                var effectiveDesiredIndentation = context.GetIndentationLevelString(indentationLevel);
                changes.Add(new TextChange(spanToReplace, effectiveDesiredIndentation));
            }

            return(changes);
        }
        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));
        }