示例#1
0
        public override bool TryFormatOnType(Position position, FormattingContext context, out TextEdit[] edits)
        {
            if (position is null)
            {
                throw new ArgumentNullException(nameof(position));
            }

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

            if (!context.Options.TryGetValue(LanguageServerConstants.ExpectsCursorPlaceholderKey, out var value) || !value.IsBool || !value.Bool)
            {
                // Temporary:
                // no-op if cursor placeholder isn't supported. This means the request isn't coming from VS.
                edits = null;
                return(false);
            }

            var syntaxTree = context.CodeDocument.GetSyntaxTree();

            var absoluteIndex = position.GetAbsoluteIndex(context.SourceText);
            var change        = new SourceChange(absoluteIndex, 0, string.Empty);
            var owner         = syntaxTree.Root.LocateOwner(change);

            if (!IsAtEnterRuleLocation(context, owner))
            {
                edits = null;
                return(false);
            }

            // We're currently at:
            // <someTag>
            // |</someTag>

            context.SourceText.GetLineAndOffset(owner.SpanStart, out var lineNumber, out _);

            var existingIndentation        = context.Indentations[lineNumber].ExistingIndentation;
            var existingIndentationString  = context.GetIndentationString(existingIndentation);
            var increasedIndentationString = context.GetIndentationLevelString(indentationLevel: 1);
            var innerIndentationString     = string.Concat(increasedIndentationString, existingIndentationString);

            // We mark start position at the beginning of the line in order to remove any pre-existing whitespace.
            var startPosition = new Position(position.Line, 0);
            var edit          = new TextEdit()
            {
                NewText = $"{innerIndentationString}{LanguageServerConstants.CursorPlaceholderString}{Environment.NewLine}{existingIndentationString}",
                Range   = new Range(startPosition, position)
            };

            edits = new[] { edit };
            return(true);
        }
        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);
        }
示例#3
0
        private List <TextChange> AdjustRazorIndentation(FormattingContext context)
        {
            // Assume HTML formatter has already run at this point and HTML is relatively indented correctly.
            // But HTML doesn't know about Razor blocks.
            // Our goal here is to indent each line according to the surrounding Razor blocks.
            var sourceText   = context.SourceText;
            var editsToApply = new List <TextChange>();

            for (var i = 0; i < sourceText.Lines.Count; i++)
            {
                var line = sourceText.Lines[i];
                if (line.Span.Length == 0)
                {
                    // Empty line.
                    continue;
                }

                if (context.Indentations[i].StartsInCSharpContext)
                {
                    continue;
                }

                var razorDesiredIndentationLevel = context.Indentations[i].RazorIndentationLevel;
                if (razorDesiredIndentationLevel == 0)
                {
                    // This line isn't under any Razor specific constructs. Trust the HTML formatter.
                    continue;
                }

                var htmlDesiredIndentationLevel = context.Indentations[i].HtmlIndentationLevel;
                if (htmlDesiredIndentationLevel == 0 && !IsPartOfHtmlTag(context, context.Indentations[i].FirstSpan.Span.Start))
                {
                    // This line is under some Razor specific constructs but not under any HTML tag.
                    // E.g,
                    // @{
                    //          @* comment *@ <----
                    // }
                    //
                    // In this case, the HTML formatter wouldn't touch it but we should format it correctly.
                    // So, let's use our syntax understanding to rewrite the indentation.
                    // Note: This case doesn't apply for HTML tags (HTML formatter will touch it even if it is in the root).
                    // Hence the second part of the if condition.
                    //
                    var desiredIndentationLevel  = context.Indentations[i].IndentationLevel;
                    var desiredIndentationString = context.GetIndentationLevelString(desiredIndentationLevel);
                    var spanToReplace            = new TextSpan(line.Start, context.Indentations[i].ExistingIndentation);
                    var change = new TextChange(spanToReplace, desiredIndentationString);
                    editsToApply.Add(change);
                }
                else
                {
                    // This line is under some Razor specific constructs and HTML tags.
                    // E.g,
                    // @{
                    //    <div class="foo"
                    //         id="oof">  <----
                    //    </div>
                    // }
                    //
                    // In this case, the HTML formatter would've formatted it correctly. Let's not use our syntax understanding.
                    // Instead, we should just add to the existing indentation.
                    //
                    var razorDesiredIndentationString = context.GetIndentationLevelString(razorDesiredIndentationLevel);
                    var existingIndentationString     = context.GetIndentationString(context.Indentations[i].ExistingIndentationSize);
                    var desiredIndentationString      = existingIndentationString + razorDesiredIndentationString;
                    var spanToReplace = new TextSpan(line.Start, context.Indentations[i].ExistingIndentation);
                    var change        = new TextChange(spanToReplace, desiredIndentationString);
                    editsToApply.Add(change);
                }
            }

            return(editsToApply);
        }
        private static List <TextChange> AdjustRazorIndentation(FormattingContext context)
        {
            // Assume HTML formatter has already run at this point and HTML is relatively indented correctly.
            // But HTML doesn't know about Razor blocks.
            // Our goal here is to indent each line according to the surrounding Razor blocks.
            var sourceText   = context.SourceText;
            var editsToApply = new List <TextChange>();
            var indentations = context.GetIndentations();

            for (var i = 0; i < sourceText.Lines.Count; i++)
            {
                var line = sourceText.Lines[i];
                if (line.Span.Length == 0)
                {
                    // Empty line.
                    continue;
                }

                if (indentations[i].StartsInCSharpContext)
                {
                    // Normally we don't do HTML things in C# contexts but there is one
                    // edge case when including render fragments in a C# code block, eg:
                    //
                    // @code {
                    //      void Foo()
                    //      {
                    //          Render(@<SurveyPrompt />);
                    //      {
                    // }
                    //
                    // This is popular in some libraries, like bUnit. The issue here is that
                    // the HTML formatter sees ~~~~~<SurveyPrompt /> and puts a newline before
                    // the tag, but obviously that breaks things.
                    //
                    // It's straight forward enough to just check for this situation and special case
                    // it by removing the newline again.

                    // There needs to be at least one more line, and the current line needs to end with
                    // an @ sign, and have an open angle bracket at the start of the next line.
                    if (sourceText.Lines.Count >= i + 1 &&
                        line.Text?.Length > 1 &&
                        line.Text?[line.End - 1] == '@')
                    {
                        var nextLine  = sourceText.Lines[i + 1];
                        var firstChar = nextLine.GetFirstNonWhitespaceOffset().GetValueOrDefault();

                        // When the HTML formatter inserts the newline in this scenario, it doesn't
                        // indent the component tag, so we use that as another signal that this is
                        // the scenario we think it is.
                        if (firstChar == 0 &&
                            nextLine.Text?[nextLine.Start] == '<')
                        {
                            var lineBreakLength = line.EndIncludingLineBreak - line.End;
                            var spanToReplace   = new TextSpan(line.End, lineBreakLength);
                            var change          = new TextChange(spanToReplace, string.Empty);
                            editsToApply.Add(change);

                            // Skip the next line because we've essentially just removed it.
                            i++;
                        }
                    }

                    continue;
                }

                var razorDesiredIndentationLevel = indentations[i].RazorIndentationLevel;
                if (razorDesiredIndentationLevel == 0)
                {
                    // This line isn't under any Razor specific constructs. Trust the HTML formatter.
                    continue;
                }

                var htmlDesiredIndentationLevel = indentations[i].HtmlIndentationLevel;
                if (htmlDesiredIndentationLevel == 0 && !IsPartOfHtmlTag(context, indentations[i].FirstSpan.Span.Start))
                {
                    // This line is under some Razor specific constructs but not under any HTML tag.
                    // E.g,
                    // @{
                    //          @* comment *@ <----
                    // }
                    //
                    // In this case, the HTML formatter wouldn't touch it but we should format it correctly.
                    // So, let's use our syntax understanding to rewrite the indentation.
                    // Note: This case doesn't apply for HTML tags (HTML formatter will touch it even if it is in the root).
                    // Hence the second part of the if condition.
                    //
                    var desiredIndentationLevel  = indentations[i].IndentationLevel;
                    var desiredIndentationString = context.GetIndentationLevelString(desiredIndentationLevel);
                    var spanToReplace            = new TextSpan(line.Start, indentations[i].ExistingIndentation);
                    var change = new TextChange(spanToReplace, desiredIndentationString);
                    editsToApply.Add(change);
                }
                else
                {
                    // This line is under some Razor specific constructs and HTML tags.
                    // E.g,
                    // @{
                    //    <div class="foo"
                    //         id="oof">  <----
                    //    </div>
                    // }
                    //
                    // In this case, the HTML formatter would've formatted it correctly. Let's not use our syntax understanding.
                    // Instead, we should just add to the existing indentation.
                    //
                    var razorDesiredIndentationString = context.GetIndentationLevelString(razorDesiredIndentationLevel);
                    var existingIndentationString     = context.GetIndentationString(indentations[i].ExistingIndentationSize);
                    var desiredIndentationString      = existingIndentationString + razorDesiredIndentationString;
                    var spanToReplace = new TextSpan(line.Start, indentations[i].ExistingIndentation);
                    var change        = new TextChange(spanToReplace, desiredIndentationString);
                    editsToApply.Add(change);
                }
            }

            return(editsToApply);
        }
        protected 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, allowImplicitStatements: true))
                {
                    // 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++)
            {
                if (context.Indentations[i].EmptyOrWhitespaceLine)
                {
                    // We should remove whitespace on empty lines.
                    newIndentations[i] = 0;
                    continue;
                }

                var line      = context.SourceText.Lines[i];
                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 minCSharpIndentation = context.GetIndentationOffsetForLevel(context.Indentations[i].MinCSharpIndentLevel);
                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(context.Indentations[i].IndentationLevel);
                if (!context.Indentations[i].StartsInCSharpContext)
                {
                    // 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.
                        razorDesiredIndentation = context.Indentations[i].ExistingIndentation;
                    }
                }
                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   = context.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);
        }