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); }
public static FormattingContext Create( DocumentUri uri, DocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, FormattingOptions options, AdhocWorkspaceFactory workspaceFactory, bool isFormatOnType = false) { if (uri is null) { throw new ArgumentNullException(nameof(uri)); } if (originalSnapshot is null) { throw new ArgumentNullException(nameof(originalSnapshot)); } if (codeDocument is null) { throw new ArgumentNullException(nameof(codeDocument)); } if (options is null) { throw new ArgumentNullException(nameof(options)); } if (workspaceFactory is null) { throw new ArgumentNullException(nameof(workspaceFactory)); } var syntaxTree = codeDocument.GetSyntaxTree(); var formattingSpans = syntaxTree.GetFormattingSpans(); var result = new FormattingContext(workspaceFactory) { Uri = uri, OriginalSnapshot = originalSnapshot, CodeDocument = codeDocument, Options = options, IsFormatOnType = isFormatOnType, FormattingSpans = formattingSpans }; var sourceText = codeDocument.GetSourceText(); var indentations = new Dictionary <int, IndentationContext>(); var previousIndentationLevel = 0; for (var i = 0; i < sourceText.Lines.Count; i++) { // Get first non-whitespace character position var nonWsPos = sourceText.Lines[i].GetFirstNonWhitespacePosition(); var existingIndentation = (nonWsPos ?? sourceText.Lines[i].End) - sourceText.Lines[i].Start; // The existingIndentation above is measured in characters, and is used to create text edits // The below is measured in columns, so takes into account tab size. This is useful for creating // new indentation strings var existingIndentationSize = sourceText.Lines[i].GetIndentationSize(options.TabSize); var emptyOrWhitespaceLine = false; if (nonWsPos == null) { emptyOrWhitespaceLine = true; nonWsPos = sourceText.Lines[i].Start; } // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan. if (result.TryGetFormattingSpan(nonWsPos.Value, out var span)) { indentations[i] = new IndentationContext(firstSpan: span) { Line = i, RazorIndentationLevel = span.RazorIndentationLevel, HtmlIndentationLevel = span.HtmlIndentationLevel, RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel, ExistingIndentation = existingIndentation, ExistingIndentationSize = existingIndentationSize, EmptyOrWhitespaceLine = emptyOrWhitespaceLine, }; previousIndentationLevel = span.IndentationLevel; } else { // Couldn't find a corresponding FormattingSpan. Happens if it is a 0 length line. // Let's create a 0 length span to represent this and default it to HTML. var placeholderSpan = new FormattingSpan( new Language.Syntax.TextSpan(nonWsPos.Value, 0), new Language.Syntax.TextSpan(nonWsPos.Value, 0), FormattingSpanKind.Markup, FormattingBlockKind.Markup, razorIndentationLevel: 0, htmlIndentationLevel: 0, isInClassBody: false, componentLambdaNestingLevel: 0); indentations[i] = new IndentationContext(firstSpan: placeholderSpan) { Line = i, RazorIndentationLevel = 0, HtmlIndentationLevel = 0, RelativeIndentationLevel = previousIndentationLevel, ExistingIndentation = existingIndentation, ExistingIndentationSize = existingIndentation, EmptyOrWhitespaceLine = emptyOrWhitespaceLine, }; } } result.Indentations = indentations; return(result); }
public static FormattingContext Create( DocumentUri uri, DocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, FormattingOptions options, Range range = null, bool isFormatOnType = false) { if (uri is null) { throw new ArgumentNullException(nameof(uri)); } if (originalSnapshot is null) { throw new ArgumentNullException(nameof(originalSnapshot)); } if (codeDocument is null) { throw new ArgumentNullException(nameof(codeDocument)); } if (options is null) { throw new ArgumentNullException(nameof(options)); } var text = codeDocument.GetSourceText(); range ??= TextSpan.FromBounds(0, text.Length).AsRange(text); var syntaxTree = codeDocument.GetSyntaxTree(); var formattingSpans = syntaxTree.GetFormattingSpans(); var result = new FormattingContext() { Uri = uri, OriginalSnapshot = originalSnapshot, CodeDocument = codeDocument, Range = range, Options = options, IsFormatOnType = isFormatOnType, FormattingSpans = formattingSpans }; var sourceText = codeDocument.GetSourceText(); var indentations = new Dictionary <int, IndentationContext>(); var previousIndentationLevel = 0; for (var i = 0; i < sourceText.Lines.Count; i++) { // Get first non-whitespace character position var nonWsPos = sourceText.Lines[i].GetFirstNonWhitespacePosition(); var existingIndentation = (nonWsPos ?? sourceText.Lines[i].End) - sourceText.Lines[i].Start; var emptyOrWhitespaceLine = false; if (nonWsPos == null) { emptyOrWhitespaceLine = true; nonWsPos = sourceText.Lines[i].Start; } // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan. if (result.TryGetFormattingSpan(nonWsPos.Value, out var span)) { indentations[i] = new IndentationContext { Line = i, RazorIndentationLevel = span.RazorIndentationLevel, HtmlIndentationLevel = span.HtmlIndentationLevel, RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel, ExistingIndentation = existingIndentation, FirstSpan = span, EmptyOrWhitespaceLine = emptyOrWhitespaceLine, }; previousIndentationLevel = span.IndentationLevel; } else { // Couldn't find a corresponding FormattingSpan. Happens if it is a 0 length line. // Let's create a 0 length span to represent this and default it to HTML. var placeholderSpan = new FormattingSpan( new Language.Syntax.TextSpan(nonWsPos.Value, 0), new Language.Syntax.TextSpan(nonWsPos.Value, 0), FormattingSpanKind.Markup, FormattingBlockKind.Markup, razorIndentationLevel: 0, htmlIndentationLevel: 0, isInClassBody: false, componentLambdaNestingLevel: 0); indentations[i] = new IndentationContext { Line = i, RazorIndentationLevel = 0, HtmlIndentationLevel = 0, RelativeIndentationLevel = previousIndentationLevel, ExistingIndentation = existingIndentation, FirstSpan = placeholderSpan, EmptyOrWhitespaceLine = emptyOrWhitespaceLine, }; } } result.Indentations = indentations; return(result); }