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