public abstract Task <FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken);
protected static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements) { // We should be called with the range of various C# SourceMappings. if (mappingSpan.Start == 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 = mappingSpan.Start; if (mappingSpan.Length > 0) { // Slightly ugly hack to get around the behavior of LocateOwner. // In some cases, using the start of a mapping doesn't work well // because LocateOwner returns the previous node due to it owning the edge. // So, if we can try to find the owner using a position that fully belongs to the current mapping. absoluteIndex = mappingSpan.Start + 1; } var change = new SourceChange(absoluteIndex, 0, string.Empty); var syntaxTree = context.CodeDocument.GetSyntaxTree(); var owner = syntaxTree.Root.LocateOwner(change); if (owner == null) { // Can't determine owner of this position. Optimistically allow formatting. return(true); } // special case: If we're formatting implicit statements, we want to treat the `@attribute` directive as one // so that the C# definition of the attribute is formatted as C# if (allowImplicitStatements && IsAttributeDirective()) { return(true); } if (IsInHtmlTag() || IsInDirectiveWithNoKind() || IsInSingleLineDirective() || IsImplicitOrExplicitExpression() || IsInSectionDirective() || (!allowImplicitStatements && IsImplicitStatementStart())) { 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 // if (owner.SpanStart == mappingSpan.Start && owner is CSharpStatementLiteralSyntax && owner.Parent is CSharpCodeBlockSyntax && owner.PreviousSpan() is CSharpTransitionSyntax) { return(true); } // Not an implicit statement. return(false); } 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 IsInDirectiveWithNoKind() { // E.g, (| is position) // // `@using |System; // return(owner.AncestorsAndSelf().Any( n => n is RazorDirectiveSyntax directive && directive.DirectiveDescriptor == null)); } bool IsAttributeDirective() { // E.g, (| is position) // // `@attribute |[System.Obsolete] // return(owner.AncestorsAndSelf().Any( n => n is RazorDirectiveSyntax directive && directive.DirectiveDescriptor != null && directive.DirectiveDescriptor.Kind == DirectiveKind.SingleLine && directive.DirectiveDescriptor.Directive.Equals(AttributeDirective.Directive.Directive, StringComparison.Ordinal))); } bool IsInSingleLineDirective() { // E.g, (| is position) // // `@inject |SomeType SomeName` - true // return(owner.AncestorsAndSelf().Any( n => n is RazorDirectiveSyntax directive && 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)); } bool IsInSectionDirective() { var directive = owner.FirstAncestorOrSelf <RazorDirectiveSyntax>(); if (directive != null && directive.DirectiveDescriptor.Directive == SectionDirective.Directive.Directive) { return(true); } return(false); } }
private void CleanupSourceMappingStart(FormattingContext context, Range sourceMappingRange, List <TextChange> changes) { // // We look through every source mapping that intersects with the affected range and // bring the first line to its own line and adjust its indentation, // // E.g, // // @{ public int x = 0; // } // // becomes, // // @{ // public int x = 0; // } // if (!ShouldFormat(context, sourceMappingRange.Start, allowImplicitStatements: false)) { // We don't want to run cleanup on this range. return; } // @{ // if (true) // { // <div></div>| // // |} // } // We want to return the length of the range marked by |...| // var text = context.SourceText; var sourceMappingSpan = sourceMappingRange.AsTextSpan(text); var whitespaceLength = text.GetFirstNonWhitespaceOffset(sourceMappingSpan); if (whitespaceLength == null) { // There was no content here. Skip. return; } var spanToReplace = new TextSpan(sourceMappingSpan.Start, whitespaceLength.Value); 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); }
public static FormattingContext Create( Uri 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 result = new FormattingContext() { Uri = uri, OriginalSnapshot = originalSnapshot, CodeDocument = codedocument, Range = range, Options = options, IsFormatOnType = isFormatOnType }; var source = codedocument.Source; var syntaxTree = codedocument.GetSyntaxTree(); var formattingSpans = syntaxTree.GetFormattingSpans(); var total = 0; var previousIndentationLevel = 0; for (var i = 0; i < source.Lines.Count; i++) { // Get first non-whitespace character position var lineLength = source.Lines.GetLineLength(i); var nonWsChar = 0; for (var j = 0; j < lineLength; j++) { var ch = source[total + j]; if (!char.IsWhiteSpace(ch) && !ParserHelpers.IsNewLine(ch)) { nonWsChar = j; break; } } // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan. if (TryGetFormattingSpan(total + nonWsChar, formattingSpans, out var span)) { result.Indentations[i] = new IndentationContext { Line = i, IndentationLevel = span.IndentationLevel, RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel, ExistingIndentation = nonWsChar, FirstSpan = span, }; previousIndentationLevel = span.IndentationLevel; } else { // Couldn't find a corresponding FormattingSpan. result.Indentations[i] = new IndentationContext { Line = i, IndentationLevel = -1, RelativeIndentationLevel = previousIndentationLevel, ExistingIndentation = nonWsChar, }; } total += lineLength; } return(result); }
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 lineStartMap = 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. 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 (context.Indentations[i].EmptyOrWhitespaceLine) { // We should remove whitespace on empty lines. newIndentations[i] = 0; continue; } var minCSharpIndentation = context.GetIndentationOffsetForLevel(context.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; } // This will now be set to the same value as the end of the closest source mapping. if (index < 0) { csharpDesiredIndentation = 0; } else { 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(context.Indentations[i].IndentationLevel); if (context.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 = context.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 = 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); }
private SourceText CleanupDocument(FormattingContext context, Range range = null) { // // We look through every source mapping that intersects with the affected range and // adjust the indentation of the first line, // // E.g, // // @{ public int x = 0; // } // // becomes, // // @{ // public int x = 0; // } // var text = context.SourceText; range ??= TextSpan.FromBounds(0, text.Length).AsRange(text); var csharpDocument = context.CodeDocument.GetCSharpDocument(); var changes = new List <TextChange>(); foreach (var mapping in csharpDocument.SourceMappings) { var mappingSpan = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length); var mappingRange = mappingSpan.AsRange(text); if (!range.LineOverlapsWith(mappingRange)) { // We don't care about this range. It didn't change. continue; } var mappingStartLineIndex = (int)mappingRange.Start.Line; if (context.Indentations[mappingStartLineIndex].StartsInCSharpContext) { // Doesn't need cleaning up. // For corner cases like (Range marked with |...|), // @{ // if (true} { <div></div>| }| // } // We want to leave it alone because tackling it here is really complicated. continue; } // @{ // if (true) // { // <div></div>| // // |} // } // We want to return the length of the range marked by |...| // var whitespaceLength = text.GetFirstNonWhitespaceOffset(mappingSpan); if (whitespaceLength == null) { // There was no content here. Skip. continue; } var spanToReplace = new TextSpan(mappingSpan.Start, whitespaceLength.Value); if (!context.TryGetIndentationLevel(spanToReplace.End, out var contentIndentLevel)) { // Can't find the correct indentation for this content. Leave it alone. continue; } // 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); } var changedText = text.WithChanges(changes); return(changedText); }
public virtual FormattingResult Execute(FormattingContext context, FormattingResult result) { return(result); }
private async Task <TextEdit[]> FormatCodeBlockDirectivesAsync(FormattingContext context) { // A code block directive is any extensible directive that can contain C# code. Here is how we represent it, // E.g, // // @code { public class Foo { } } // ^ ^ ----> Full code block directive range (Includes preceding whitespace) // ^ ^ ----> Directive range // ^ ^ ----> DirectiveBody range // ^ ^ ----> inner codeblock range // // In this method, we are going to do the following for each code block directive, // 1. Format the inner codeblock using the C# formatter // 2. Adjust the absolute indentation of the lines formatted by the C# formatter while maintaining the relative indentation // 3. Indent the start of the code block (@code {) correctly and move any succeeding code to a separate line // 4. Indent the end of the code block (}) correctly and move it to a separate line if necessary // 5. Once all the edits are applied, compute the diff for this particular code block and add it to the global list of edits // var source = context.CodeDocument.Source; var syntaxTree = context.CodeDocument.GetSyntaxTree(); var nodes = syntaxTree.GetCodeBlockDirectives(); var allEdits = new List <TextEdit>(); // Iterate in reverse so that the newline changes don't affect the next code block directive. for (var i = nodes.Length - 1; i >= 0; i--) { var directive = nodes[i]; if (!(directive.Body is RazorDirectiveBodySyntax directiveBody)) { // This can't happen realistically. Just being defensive. continue; } // Get the inner code block node that contains the actual code. var innerCodeBlockNode = directiveBody.CSharpCode.DescendantNodes().FirstOrDefault(n => n is CSharpCodeBlockSyntax); if (innerCodeBlockNode == null) { // Nothing to indent. continue; } if (innerCodeBlockNode.DescendantNodes().Any(n => n is MarkupBlockSyntax || n is CSharpTransitionSyntax || n is RazorCommentBlockSyntax)) { // We currently don't support formatting code block directives with Markup or other Razor constructs. continue; } var originalText = context.SourceText; var changedText = originalText; var innerCodeBlockRange = innerCodeBlockNode.GetRange(source); // Compute the range inside the code block that overlaps with the provided input range. var rangeToFormat = innerCodeBlockRange.Overlap(context.Range); if (rangeToFormat != null) { var codeEdits = await _csharpFormatter.FormatAsync(context.CodeDocument, rangeToFormat, context.Uri, context.Options); changedText = ApplyCSharpEdits(context, innerCodeBlockRange, codeEdits, minCSharpIndentLevel: 2); } var edits = new List <TextEdit>(); FormatCodeBlockStart(context, changedText, directiveBody, innerCodeBlockNode, edits); FormatCodeBlockEnd(context, changedText, directiveBody, innerCodeBlockNode, edits); changedText = ApplyChanges(changedText, edits.Select(e => e.AsTextChange(changedText))); // We've now applied all the edits we wanted to do. We now need to identify everything that changed in the given code block. // We need to include the preceding newline in our input range because we could have unindented the code block to achieve the correct indentation. // Without including the preceding newline, that edit would be lost. var fullCodeBlockDirectiveSpan = GetSpanIncludingPrecedingWhitespaceInLine(originalText, directive.Position, directive.EndPosition); var changes = Diff(originalText, changedText, fullCodeBlockDirectiveSpan); var transformedEdits = changes.Select(c => c.AsTextEdit(originalText)); allEdits.AddRange(transformedEdits); } return(allEdits.ToArray()); }
// // 'minCSharpIndentLevel' refers to the minimum level of how much the C# formatter would indent code. // @code/@functions blocks contain class members and so are typically indented by 2 levels. // @{} blocks are put inside method body which means they are typically indented by 3 levels. // private SourceText ApplyCSharpEdits(FormattingContext context, Range codeBlockRange, TextEdit[] edits, int minCSharpIndentLevel) { var originalText = context.SourceText; var originalCodeBlockSpan = codeBlockRange.AsTextSpan(originalText); // Sometimes the C# formatter edits outside the range we supply. Filter out those edits. var changes = edits.Select(e => e.AsTextChange(originalText)).Where(c => originalCodeBlockSpan.Contains(c.Span)).ToArray(); if (changes.Length == 0) { return(originalText); } // Apply the C# edits to the document. var changedText = originalText.WithChanges(changes); TrackChangeInSpan(originalText, originalCodeBlockSpan, changedText, out var changedCodeBlockSpan, out var changeEncompassingSpan); // We now have the changed document with C# edits. But it might be indented more/less than what we want depending on the context. // So, we want to bring each line to the right level of indentation based on where the block is in the document. // We also need to only do this for the lines that are part of the input range to respect range formatting. var desiredIndentationLevel = context.Indentations[(int)codeBlockRange.Start.Line].IndentationLevel + 1; var editsToApply = new List <TextChange>(); var inputSpan = context.Range.AsTextSpan(originalText); TrackChangeInSpan(originalText, inputSpan, changedText, out var changedInputSpan, out _); var changedInputRange = changedInputSpan.AsRange(changedText); for (var i = (int)changedInputRange.Start.Line; i <= changedInputRange.End.Line; i++) { var line = changedText.Lines[i]; if (line.Span.Length == 0) { // Empty line. C# formatter didn't remove it so we won't either. continue; } if (!changedCodeBlockSpan.Contains(line.Start)) { // Defensive check to make sure we're not handling lines that are not part of the current code block. continue; } var leadingWhitespace = line.GetLeadingWhitespace(); var minCSharpIndentLength = GetIndentationString(context, minCSharpIndentLevel).Length; if (leadingWhitespace.Length < minCSharpIndentLength) { // For whatever reason, the C# formatter decided to not indent this. Leave it as is. continue; } else { // At this point we assume the C# formatter has relatively indented this line to the correct level. // All we want to do at this point is to indent/unindent this line based on the absolute indentation of the block // and the minimum C# indent level. We don't need to worry about the actual existing indentation here because it doesn't matter. var effectiveDesiredIndentationLevel = desiredIndentationLevel - minCSharpIndentLevel; var effectiveDesiredIndentation = GetIndentationString(context, Math.Abs(effectiveDesiredIndentationLevel)); if (effectiveDesiredIndentationLevel < 0) { // This means that we need to unindent. var span = new TextSpan(line.Start, effectiveDesiredIndentation.Length); editsToApply.Add(new TextChange(span, string.Empty)); } else if (effectiveDesiredIndentationLevel > 0) { // This means that we need to indent. var span = new TextSpan(line.Start, 0); editsToApply.Add(new TextChange(span, effectiveDesiredIndentation)); } } } changedText = ApplyChanges(changedText, editsToApply); return(changedText); }
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); }
private static bool ShouldFormat(FormattingContext context, Position position) { // 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; 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 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)); } }
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 result = new FormattingContext() { Uri = uri, OriginalSnapshot = originalSnapshot, CodeDocument = codeDocument, Range = range, Options = options, IsFormatOnType = isFormatOnType }; var source = codeDocument.Source; var syntaxTree = codeDocument.GetSyntaxTree(); var formattingSpans = syntaxTree.GetFormattingSpans(); var indentations = new Dictionary <int, IndentationContext>(); var total = 0; var previousIndentationLevel = 0; for (var i = 0; i < source.Lines.Count; i++) { // Get first non-whitespace character position var lineLength = source.Lines.GetLineLength(i); var nonWsChar = 0; for (var j = 0; j < lineLength; j++) { var ch = source[total + j]; if (!char.IsWhiteSpace(ch) && !ParserHelpers.IsNewLine(ch)) { nonWsChar = j; break; } } // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan. if (TryGetFormattingSpan(total + nonWsChar, formattingSpans, out var span)) { indentations[i] = new IndentationContext { Line = i, IndentationLevel = span.IndentationLevel, RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel, ExistingIndentation = nonWsChar, FirstSpan = span, }; 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(total + nonWsChar, 0), new Language.Syntax.TextSpan(total + nonWsChar, 0), FormattingSpanKind.Markup, FormattingBlockKind.Markup, indentationLevel: 0, isInClassBody: false); indentations[i] = new IndentationContext { Line = i, IndentationLevel = 0, RelativeIndentationLevel = previousIndentationLevel, ExistingIndentation = nonWsChar, FirstSpan = placeholderSpan, }; } total += lineLength; } result.Indentations = indentations; return(result); }
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 normalizedEdits = NormalizeTextEdits(csharpText, result.Edits); 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(); return(new FormattingResult(finalEdits)); }
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)); } }
public virtual Task <FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { return(Task.FromResult(Execute(context, result))); }
private void FormatCodeBlockStart(FormattingContext context, SourceText changedText, RazorDirectiveBodySyntax directiveBody, SyntaxNode innerCodeBlock, List <TextEdit> edits) { var sourceText = context.SourceText; var originalBodySpan = TextSpan.FromBounds(directiveBody.Position, directiveBody.EndPosition); var originalBodyRange = originalBodySpan.AsRange(sourceText); if (context.Range.Start.Line > originalBodyRange.Start.Line) { return; } // First line is within the selected range. Let's try and format the start. TrackChangeInSpan(sourceText, originalBodySpan, changedText, out var changedBodySpan, out _); var changedBodyRange = changedBodySpan.AsRange(changedText); // First, make sure the first line is indented correctly. var firstLine = changedText.Lines[(int)changedBodyRange.Start.Line]; var desiredIndentationLevel = context.Indentations[firstLine.LineNumber].IndentationLevel; var desiredIndentation = GetIndentationString(context, desiredIndentationLevel); var firstNonWhitespaceOffset = firstLine.GetFirstNonWhitespaceOffset(); if (firstNonWhitespaceOffset.HasValue) { var edit = new TextEdit() { Range = new Range( new Position(firstLine.LineNumber, 0), new Position(firstLine.LineNumber, firstNonWhitespaceOffset.Value)), NewText = desiredIndentation }; edits.Add(edit); } // We should also move any code that comes after '{' down to its own line. var originalInnerCodeBlockSpan = TextSpan.FromBounds(innerCodeBlock.Position, innerCodeBlock.EndPosition); TrackChangeInSpan(sourceText, originalInnerCodeBlockSpan, changedText, out var changedInnerCodeBlockSpan, out _); var innerCodeBlockRange = changedInnerCodeBlockSpan.AsRange(changedText); var innerCodeBlockLine = changedText.Lines[(int)innerCodeBlockRange.Start.Line]; var textAfterBlockStart = innerCodeBlockLine.ToString().Substring(innerCodeBlock.Position - innerCodeBlockLine.Start); var isBlockStartOnSeparateLine = string.IsNullOrWhiteSpace(textAfterBlockStart); var innerCodeBlockIndentationLevel = desiredIndentationLevel + 1; var desiredInnerCodeBlockIndentation = GetIndentationString(context, innerCodeBlockIndentationLevel); var whitespaceAfterBlockStart = textAfterBlockStart.GetLeadingWhitespace(); if (!isBlockStartOnSeparateLine) { // If the first line contains code, add a newline at the beginning and indent it. var edit = new TextEdit() { Range = new Range( new Position(innerCodeBlockLine.LineNumber, innerCodeBlock.Position - innerCodeBlockLine.Start), new Position(innerCodeBlockLine.LineNumber, innerCodeBlock.Position + whitespaceAfterBlockStart.Length - innerCodeBlockLine.Start)), NewText = Environment.NewLine + desiredInnerCodeBlockIndentation }; edits.Add(edit); } else { // // The code inside the code block directive is on its own line. Ideally the C# formatter would have already taken care of it. // Except, the first line of the code block is not indented because of how our SourceMappings work. // E.g, // @code { // ... // } // Our source mapping for this code block only ranges between the { and }, exclusive. // If the C# formatter provides any edits that start from before the {, we won't be able to map it back and we will ignore it. // Unfortunately because of this, we partially lose some edits which would have indented the first line of the code block correctly. // So let's manually indent the first line here. // var innerCodeBlockText = changedText.GetSubTextString(changedInnerCodeBlockSpan); if (!string.IsNullOrWhiteSpace(innerCodeBlockText)) { var codeStart = innerCodeBlockText.GetFirstNonWhitespaceOffset() + changedInnerCodeBlockSpan.Start; if (codeStart.HasValue && codeStart != changedInnerCodeBlockSpan.End) { // If we got here, it means this is a non-empty code block. We can safely indent the first line. var codeStartLine = changedText.Lines.GetLineFromPosition(codeStart.Value); var existingCodeStartIndentation = codeStartLine.GetFirstNonWhitespaceOffset() ?? 0; var edit = new TextEdit() { Range = new Range( new Position(codeStartLine.LineNumber, 0), new Position(codeStartLine.LineNumber, existingCodeStartIndentation)), NewText = desiredInnerCodeBlockIndentation }; edits.Add(edit); } } } }
private static async Task <Dictionary <int, int> > GetCSharpIndentationCoreAsync(FormattingContext context, List <int> projectedDocumentLocations, CancellationToken cancellationToken) { // No point calling the C# formatting if we won't be interested in any of its work anyway if (projectedDocumentLocations.Count == 0) { return(new Dictionary <int, int>()); } var(indentationMap, syntaxTree) = InitializeIndentationData(context, projectedDocumentLocations, cancellationToken); var root = await syntaxTree.GetRootAsync(cancellationToken); root = AttachAnnotations(indentationMap, projectedDocumentLocations, root); // At this point, we have added all the necessary markers and attached annotations. // Let's invoke the C# formatter and hope for the best. var formattedRoot = CodeAnalysis.Formatting.Formatter.Format(root, context.CSharpWorkspace, cancellationToken: cancellationToken); var formattedText = formattedRoot.GetText(); var desiredIndentationMap = new Dictionary <int, int>(); // Assuming the C# formatter did the right thing, let's extract the indentation offset from // the line containing trivia and token that has our attached annotations. ExtractTriviaAnnotations(context, formattedRoot, formattedText, desiredIndentationMap); ExtractTokenAnnotations(context, formattedRoot, formattedText, indentationMap, desiredIndentationMap); return(desiredIndentationMap);
private static FormattingContext CreateFormattingContext(Uri uri, RazorCodeDocument codedocument, Range range, FormattingOptions options) { var result = new FormattingContext() { Uri = uri, CodeDocument = codedocument, Range = range, Options = options }; var source = codedocument.Source; var syntaxTree = codedocument.GetSyntaxTree(); var formattingSpans = syntaxTree.GetFormattingSpans(); var total = 0; var previousIndentationLevel = 0; for (var i = 0; i < source.Lines.Count; i++) { // Get first non-whitespace character position var lineLength = source.Lines.GetLineLength(i); var nonWsChar = 0; for (var j = 0; j < lineLength; j++) { var ch = source[total + j]; if (!char.IsWhiteSpace(ch) && !ParserHelpers.IsNewLine(ch)) { nonWsChar = j; break; } } // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan. if (TryGetFormattingSpan(total + nonWsChar, formattingSpans, out var span)) { result.Indentations[i] = new IndentationContext { Line = i, IndentationLevel = span.IndentationLevel, RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel, ExistingIndentation = nonWsChar, FirstSpan = span, }; previousIndentationLevel = span.IndentationLevel; } else { // Couldn't find a corresponding FormattingSpan. result.Indentations[i] = new IndentationContext { Line = i, IndentationLevel = -1, RelativeIndentationLevel = previousIndentationLevel, ExistingIndentation = nonWsChar, }; } total += lineLength; } return(result); }
public virtual Task <FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result) { return(Task.FromResult(Execute(context, result))); }
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); }
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); }
private TextEdit[] FilterCSharpTextEdits(FormattingContext context, TextEdit[] edits) { var filteredEdits = edits.Where(e => ShouldFormat(context, e.Range.Start, allowImplicitStatements: false)).ToArray(); return(filteredEdits); }
private static void CleanupSourceMappingStart(FormattingContext context, Range sourceMappingRange, List <TextChange> changes) { // // We look through every source mapping that intersects with the affected range and // bring the first line to its own line and adjust its indentation, // // E.g, // // @{ public int x = 0; // } // // becomes, // // @{ // public int x = 0; // } // var text = context.SourceText; var sourceMappingSpan = sourceMappingRange.AsTextSpan(text); if (!ShouldFormat(context, sourceMappingSpan, allowImplicitStatements: false)) { // We don't want to run cleanup on this range. return; } if (sourceMappingRange.Start.Character == 0) { // It already starts on a fresh new line which doesn't need cleanup. // E.g, (The mapping starts at | in the below case) // @{ // @: Some html // | var x = 123; // } // return; } // @{ // if (true) // { // <div></div>| // // |} // } // We want to return the length of the range marked by |...| // var whitespaceLength = text.GetFirstNonWhitespaceOffset(sourceMappingSpan, out var newLineCount); if (whitespaceLength == null) { // There was no content after the start of this mapping. Meaning it already is clean. // E.g, // @{| // ... // } return; } var spanToReplace = new TextSpan(sourceMappingSpan.Start, whitespaceLength.Value); 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. // Make sure to preserve the same number of blank lines as the original string had var replacement = PrependLines(context.GetIndentationLevelString(contentIndentLevel), context.NewLineString, Math.Max(newLineCount, 1)); // After the below change the above example should look like, // @{ // if (true) // { // <div></div> // } // } var change = new TextChange(spanToReplace, replacement); changes.Add(change); }
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 = SourceText.From(codeDocument.GetCSharpDocument().GeneratedCode); var normalizedEdits = NormalizeTextEdits(csharpText, result.Edits); 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 = originalText.WithChanges(changes); var changedContext = await context.WithTextAsync(formattedText); TrackEncompassingChange(originalText, changes, out _, out var spanAfterFormatting); 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 cleanupLineDelta = LineDelta(formattedText, cleanupChanges); var rangeToAdjust = new Range(rangeAfterFormatting.Start, new Position(rangeAfterFormatting.End.Line + cleanupLineDelta, 0)); Debug.Assert(rangeToAdjust.End.IsValid(cleanedText), "Invalid range. This is unexpected."); var indentationChanges = AdjustIndentation(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 = SourceTextDiffer.GetMinimalTextChanges(originalText, cleanedText, lineDiffOnly: false); var finalEdits = finalChanges.Select(f => f.AsTextEdit(originalText)).ToArray(); return(new FormattingResult(finalEdits)); }
private static void CleanupSourceMappingEnd(FormattingContext context, Range sourceMappingRange, List <TextChange> changes) { // // 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; if (!context.Indentations[mappingEndLineIndex].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 == 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); }
public abstract bool TryFormatOnType(Position position, FormattingContext context, out TextEdit[] edits);
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); }
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)); }