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 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); }
private static void CleanupSourceMappingEnd(FormattingContext context, Range sourceMappingRange, List <TextChange> changes, bool newLineWasAddedAtStart) { // // We look through every source mapping that intersects with the affected range and // bring the content after the last line to its own line and adjust its indentation, // // E.g, // // @{ // if (true) // { <div></div> // } // } // // becomes, // // @{ // if (true) // { // </div></div> // } // } // var text = context.SourceText; var sourceMappingSpan = sourceMappingRange.AsTextSpan(text); var mappingEndLineIndex = sourceMappingRange.End.Line; var indentations = context.GetIndentations(); var startsInCSharpContext = indentations[mappingEndLineIndex].StartsInCSharpContext; // If the span is on a single line, and we added a line, then end point is now on a line that does start in a C# context. if (!startsInCSharpContext && newLineWasAddedAtStart && sourceMappingRange.Start.Line == mappingEndLineIndex) { startsInCSharpContext = true; } if (!startsInCSharpContext) { // For corner cases like (Position marked with |), // It is already in a separate line. It doesn't need cleaning up. // @{ // if (true} // { // |<div></div> // } // } // return; } var endSpan = TextSpan.FromBounds(sourceMappingSpan.End, sourceMappingSpan.End); if (!ShouldFormat(context, endSpan, allowImplicitStatements: false)) { // We don't want to run cleanup on this range. return; } var contentStartOffset = text.Lines[mappingEndLineIndex].GetFirstNonWhitespaceOffset(sourceMappingRange.End.Character); if (contentStartOffset is null) { // There is no content after the end of this source mapping. No need to clean up. return; } var spanToReplace = new TextSpan(sourceMappingSpan.End, 0); if (!context.TryGetIndentationLevel(spanToReplace.End, out var contentIndentLevel)) { // Can't find the correct indentation for this content. Leave it alone. return; } // At this point, `contentIndentLevel` should contain the correct indentation level for `}` in the above example. var replacement = context.NewLineString + context.GetIndentationLevelString(contentIndentLevel); // After the below change the above example should look like, // @{ // if (true) // { // <div></div> // } // } var change = new TextChange(spanToReplace, replacement); changes.Add(change); }