public DefaultRazorFormattingService( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, ILanguageServer server, IOptionsMonitor <RazorLSPOptions> optionsMonitor, ILoggerFactory loggerFactory) { if (documentMappingService is null) { throw new ArgumentNullException(nameof(documentMappingService)); } if (filePathNormalizer is null) { throw new ArgumentNullException(nameof(filePathNormalizer)); } if (server is null) { throw new ArgumentNullException(nameof(server)); } if (optionsMonitor is null) { throw new ArgumentNullException(nameof(optionsMonitor)); } if (loggerFactory is null) { throw new ArgumentNullException(nameof(loggerFactory)); } _server = server; _csharpFormatter = new CSharpFormatter(documentMappingService, server, filePathNormalizer); _htmlFormatter = new HtmlFormatter(server, filePathNormalizer); _optionsMonitor = optionsMonitor; _logger = loggerFactory.CreateLogger <DefaultRazorFormattingService>(); }
private async Task <List <TextEdit> > FormatCSharpAsync(FormattingContext context, CancellationToken cancellationToken) { var sourceText = context.SourceText; var csharpEdits = new List <TextEdit>(); foreach (var mapping in context.CodeDocument.GetCSharpDocument().SourceMappings) { var span = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length); if (!ShouldFormat(context, span, allowImplicitStatements: true)) { // We don't want to format this range. continue; } // These should already be remapped. var range = span.AsRange(sourceText); var edits = await CSharpFormatter.FormatAsync(context, range, cancellationToken); csharpEdits.AddRange(edits.Where(e => range.Contains(e.Range))); } return(csharpEdits); }
public FormattingPassBase( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, ILanguageServer server) { if (documentMappingService is null) { throw new ArgumentNullException(nameof(documentMappingService)); } if (filePathNormalizer is null) { throw new ArgumentNullException(nameof(filePathNormalizer)); } if (server is null) { throw new ArgumentNullException(nameof(server)); } _documentMappingService = documentMappingService; CSharpFormatter = new CSharpFormatter(documentMappingService, server, filePathNormalizer); HtmlFormatter = new HtmlFormatter(server, filePathNormalizer); }
protected CSharpFormattingPassBase(RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, ClientNotifierServiceBase server) : base(documentMappingService, filePathNormalizer, server) { CSharpFormatter = new CSharpFormatter(documentMappingService, server, filePathNormalizer); }
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); var mappingRange = mappingSpan.AsRange(context.SourceText); 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; 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; 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. razorDesiredIndentation = context.Indentations[i].ExistingIndentation; } } 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 List <TextChange> AdjustIndentation(FormattingContext context, CancellationToken cancellationToken, Range range = null) { // In this method, the goal is to make final adjustments to the indentation of each line. // We will take into account the following, // 1. The indentation due to nested C# structures // 2. The indentation due to Razor and HTML constructs var text = context.SourceText; range ??= TextSpan.FromBounds(0, text.Length).AsRange(text); // First, let's build an understanding of the desired C# indentation at the beginning and end of each source mapping. var sourceMappingIndentations = new SortedDictionary <int, int>(); foreach (var mapping in context.CodeDocument.GetCSharpDocument().SourceMappings) { var mappingSpan = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length); var mappingRange = mappingSpan.AsRange(context.SourceText); if (!ShouldFormat(context, mappingRange.Start)) { // We don't care about this range as this can potentially lead to incorrect scopes. continue; } var startIndentation = CSharpFormatter.GetCSharpIndentation(context, mapping.GeneratedSpan.AbsoluteIndex, cancellationToken); sourceMappingIndentations[mapping.OriginalSpan.AbsoluteIndex] = startIndentation; var endIndentation = CSharpFormatter.GetCSharpIndentation(context, mapping.GeneratedSpan.AbsoluteIndex + mapping.GeneratedSpan.Length + 1, cancellationToken); sourceMappingIndentations[mapping.OriginalSpan.AbsoluteIndex + mapping.OriginalSpan.Length + 1] = endIndentation; } var sourceMappingIndentationScopes = sourceMappingIndentations.Keys.ToArray(); // Now, let's combine the C# desired indentation with the Razor and HTML indentation for each line. var newIndentations = new Dictionary <int, int>(); for (var i = range.Start.Line; i <= range.End.Line; i++) { var line = context.SourceText.Lines[i]; if (line.Span.Length == 0) { // We don't want to indent empty lines. continue; } var lineStart = line.Start; int csharpDesiredIndentation; if (DocumentMappingService.TryMapToProjectedDocumentPosition(context.CodeDocument, lineStart, out _, out var projectedLineStart)) { // We were able to map this line to C# directly. csharpDesiredIndentation = CSharpFormatter.GetCSharpIndentation(context, projectedLineStart, cancellationToken); } else { // Couldn't remap. This is probably a non-C# location. // Use SourceMapping indentations to locate the C# scope of this line. // E.g, // // @if (true) { // <div> // |</div> // } // // We can't find a direct mapping at |, but we can infer its base indentation from the // indentation of the latest source mapping prior to this line. // We use binary search to find that spot. var index = Array.BinarySearch(sourceMappingIndentationScopes, lineStart); if (index < 0) { // Couldn't find the exact value. Find the index of the element to the left of the searched value. index = (~index) - 1; } // This will now be set to the same value as the end of the closest source mapping. csharpDesiredIndentation = index < 0 ? 0 : sourceMappingIndentations[sourceMappingIndentationScopes[index]]; } // Now let's use that information to figure out the effective C# indentation. // This should be based on context. // For instance, lines inside @code/@functions block should be reduced one level // and lines inside @{} should be reduced by two levels. var csharpDesiredIndentLevel = context.GetIndentationLevelForOffset(csharpDesiredIndentation); var minCSharpIndentLevel = context.Indentations[i].MinCSharpIndentLevel; if (csharpDesiredIndentLevel < minCSharpIndentLevel) { // CSharp formatter doesn't want to indent this. Let's not touch it. continue; } var effectiveCSharpDesiredIndentationLevel = csharpDesiredIndentLevel - minCSharpIndentLevel; var razorDesiredIndentationLevel = context.Indentations[i].IndentationLevel; if (!context.Indentations[i].StartsInCSharpContext) { // This is a non-C# line. Given that the HTML formatter ran before this, we can assume // HTML is already correctly formatted. So we can use the existing indentation as is. razorDesiredIndentationLevel = context.GetIndentationLevelForOffset(context.Indentations[i].ExistingIndentation); } var effectiveDesiredIndentationLevel = razorDesiredIndentationLevel + effectiveCSharpDesiredIndentationLevel; // This will now contain the indentation we ultimately want to apply to this line. newIndentations[i] = effectiveDesiredIndentationLevel; } // Now that we have collected all the indentations for each line, let's convert them to text edits. var changes = new List <TextChange>(); foreach (var item in newIndentations) { var line = item.Key; var indentationLevel = item.Value; Debug.Assert(indentationLevel >= 0, "Negative indent level. This is unexpected."); var existingIndentationLength = context.Indentations[line].ExistingIndentation; var spanToReplace = new TextSpan(context.SourceText.Lines[line].Start, existingIndentationLength); var effectiveDesiredIndentation = context.GetIndentationLevelString(indentationLevel); changes.Add(new TextChange(spanToReplace, effectiveDesiredIndentation)); } return(changes); }
public async override Task <FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result) { if (context.IsFormatOnType) { // We don't want to handle OnTypeFormatting here. return(result); } Debug.Assert(result.Edits.Length == 0, "CodeBlockDirectiveFormatter should be invoked first."); var edits = RemapTextEdits(context.CodeDocument, result.Edits, result.Kind); // 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.Count - 1; i >= 0; i--) { var directive = nodes[i]; if (!(directive.Body is RazorDirectiveBodySyntax directiveBody)) { // This can't happen realistically. Just being defensive. continue; } var directiveRange = directive.GetRange(source); if (!directiveRange.OverlapsWith(context.Range)) { // This block isn't in the selected range. 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 csharprangeToFormat = innerCodeBlockRange.Overlap(context.Range); if (csharprangeToFormat != null) { var codeEdits = await CSharpFormatter.FormatAsync(context.CodeDocument, csharprangeToFormat, context.Uri, context.Options); changedText = ApplyCSharpEdits(context, innerCodeBlockRange, codeEdits, minCSharpIndentLevel: 2); } var newEdits = new List <TextEdit>(); FormatCodeBlockStart(context, changedText, directiveBody, innerCodeBlockNode, newEdits); FormatCodeBlockEnd(context, changedText, directiveBody, innerCodeBlockNode, newEdits); changedText = ApplyChanges(changedText, newEdits.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); } var normalizedEdits = NormalizeTextEdits(context.SourceText, allEdits.ToArray()); return(new FormattingResult(normalizedEdits)); }