// This method handles adjusting of indentation of Razor blocks after C# formatter has finished formatting the document. // For instance, lines inside @code/@functions block should be reduced one level // and lines inside @{} should be reduced by two levels. protected static List <TextChange> AdjustCSharpIndentation(FormattingContext context, int startLine, int endLine) { if (context is null) { throw new ArgumentNullException(nameof(context)); } var sourceText = context.SourceText; var editsToApply = new List <TextChange>(); for (var i = startLine; i <= endLine; i++) { if (!context.Indentations[i].StartsInCSharpContext) { // Not a CSharp line. Don't touch it. continue; } var line = sourceText.Lines[i]; if (line.Span.Length == 0) { // Empty line. C# formatter didn't remove it so we won't either. continue; } var leadingWhitespace = line.GetLeadingWhitespace(); var minCSharpIndentLevel = context.Indentations[i].MinCSharpIndentLevel; var minCSharpIndentLength = context.GetIndentationLevelString(minCSharpIndentLevel).Length; var desiredIndentationLevel = context.Indentations[i].IndentationLevel; 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 = context.GetIndentationLevelString(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)); } } } return(editsToApply); }
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 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); } return(editsToApply); }
private void FormatCodeBlockEnd(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.End.Line < originalBodyRange.End.Line) { return; } // Last line is within the selected range. Let's try and format the end. TrackChangeInSpan(sourceText, originalBodySpan, changedText, out var changedBodySpan, out _); var changedBodyRange = changedBodySpan.AsRange(changedText); var firstLine = changedText.Lines[(int)changedBodyRange.Start.Line]; var desiredIndentationLevel = context.Indentations[firstLine.LineNumber].IndentationLevel; var desiredIndentation = context.GetIndentationLevelString(desiredIndentationLevel); // we want to keep the close '}' on its own line. So bring it to the next line. var originalInnerCodeBlockSpan = TextSpan.FromBounds(innerCodeBlock.Position, innerCodeBlock.EndPosition); TrackChangeInSpan(sourceText, originalInnerCodeBlockSpan, changedText, out var changedInnerCodeBlockSpan, out _); var closeCurlyLocation = changedInnerCodeBlockSpan.End; var closeCurlyLine = changedText.Lines.GetLineFromPosition(closeCurlyLocation); var firstNonWhitespaceOffset = closeCurlyLine.GetFirstNonWhitespaceOffset() ?? 0; if (closeCurlyLine.Start + firstNonWhitespaceOffset != closeCurlyLocation) { // This means the '}' is on the same line as some C# code. // Bring it down to the next line and apply the desired indentation. var edit = new TextEdit() { Range = new Range( new Position(closeCurlyLine.LineNumber, closeCurlyLocation - closeCurlyLine.Start), new Position(closeCurlyLine.LineNumber, closeCurlyLocation - closeCurlyLine.Start)), NewText = Environment.NewLine + desiredIndentation }; edits.Add(edit); } else if (firstNonWhitespaceOffset != desiredIndentation.Length) { // This means the '}' is on its own line but is not indented correctly. Correct it. var edit = new TextEdit() { Range = new Range( new Position(closeCurlyLine.LineNumber, 0), new Position(closeCurlyLine.LineNumber, firstNonWhitespaceOffset)), NewText = desiredIndentation }; edits.Add(edit); } }
public override bool TryFormatOnType(Position position, FormattingContext context, out TextEdit[] edits) { if (position is null) { throw new ArgumentNullException(nameof(position)); } if (context is null) { throw new ArgumentNullException(nameof(context)); } if (!context.Options.TryGetValue(LanguageServerConstants.ExpectsCursorPlaceholderKey, out var value) || !value.IsBool || !value.Bool) { // Temporary: // no-op if cursor placeholder isn't supported. This means the request isn't coming from VS. edits = null; return(false); } var syntaxTree = context.CodeDocument.GetSyntaxTree(); var absoluteIndex = position.GetAbsoluteIndex(context.SourceText); var change = new SourceChange(absoluteIndex, 0, string.Empty); var owner = syntaxTree.Root.LocateOwner(change); if (!IsAtEnterRuleLocation(context, owner)) { edits = null; return(false); } // We're currently at: // <someTag> // |</someTag> context.SourceText.GetLineAndOffset(owner.SpanStart, out var lineNumber, out _); var existingIndentation = context.Indentations[lineNumber].ExistingIndentation; var existingIndentationString = context.GetIndentationString(existingIndentation); var increasedIndentationString = context.GetIndentationLevelString(indentationLevel: 1); var innerIndentationString = string.Concat(increasedIndentationString, existingIndentationString); // We mark start position at the beginning of the line in order to remove any pre-existing whitespace. var startPosition = new Position(position.Line, 0); var edit = new TextEdit() { NewText = $"{innerIndentationString}{LanguageServerConstants.CursorPlaceholderString}{Environment.NewLine}{existingIndentationString}", Range = new Range(startPosition, position) }; edits = new[] { edit }; return(true); }
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 = context.GetIndentationLevelString(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 = context.GetIndentationLevelString(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); } } } }
// // '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 = context.GetIndentationLevelString(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 = context.GetIndentationLevelString(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> 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); }
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); }
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); }
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); }
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); }
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 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); }