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); }