diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/RazorCodeDocumentExtensions.cs index f825ddbecb6..2066c58b4ae 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/RazorCodeDocumentExtensions.cs @@ -11,6 +11,8 @@ internal static class RazorCodeDocumentExtensions { private static readonly object UnsupportedKey = new object(); private static readonly object SourceTextKey = new object(); + private static readonly object CSharpSourceTextKey = new object(); + private static readonly object HtmlSourceTextKey = new object(); public static bool IsUnsupported(this RazorCodeDocument document) { @@ -59,5 +61,45 @@ public static SourceText GetSourceText(this RazorCodeDocument document) return (SourceText)sourceTextObj; } + + public static SourceText GetCSharpSourceText(this RazorCodeDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + var sourceTextObj = document.Items[CSharpSourceTextKey]; + if (sourceTextObj == null) + { + var csharpDocument = document.GetCSharpDocument(); + var sourceText = SourceText.From(csharpDocument.GeneratedCode); + document.Items[CSharpSourceTextKey] = sourceText; + + return sourceText; + } + + return (SourceText)sourceTextObj; + } + + public static SourceText GetHtmlSourceText(this RazorCodeDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + var sourceTextObj = document.Items[HtmlSourceTextKey]; + if (sourceTextObj == null) + { + var htmlDocument = document.GetHtmlDocument(); + var sourceText = SourceText.From(htmlDocument.GeneratedHtml); + document.Items[HtmlSourceTextKey] = sourceText; + + return sourceText; + } + + return (SourceText)sourceTextObj; + } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormatter.cs index 8f06e1a1b58..ae02a03f540 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormatter.cs @@ -4,13 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using OmniSharp.Extensions.LanguageServer.Protocol; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Formatting; using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -23,12 +22,12 @@ internal class CSharpFormatter private readonly RazorDocumentMappingService _documentMappingService; private readonly FilePathNormalizer _filePathNormalizer; private readonly IClientLanguageServer _server; - private readonly ProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor; + private readonly object _indentationService; + private readonly MethodInfo _getIndentationMethod; public CSharpFormatter( RazorDocumentMappingService documentMappingService, IClientLanguageServer languageServer, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor, FilePathNormalizer filePathNormalizer) { if (documentMappingService is null) @@ -41,11 +40,6 @@ public CSharpFormatter( throw new ArgumentNullException(nameof(languageServer)); } - if (projectSnapshotManagerAccessor is null) - { - throw new ArgumentNullException(nameof(projectSnapshotManagerAccessor)); - } - if (filePathNormalizer is null) { throw new ArgumentNullException(nameof(filePathNormalizer)); @@ -53,20 +47,41 @@ public CSharpFormatter( _documentMappingService = documentMappingService; _server = languageServer; - _projectSnapshotManagerAccessor = projectSnapshotManagerAccessor; _filePathNormalizer = filePathNormalizer; + + try + { + var type = typeof(CSharpFormattingOptions).Assembly.GetType("Microsoft.CodeAnalysis.CSharp.Indentation.CSharpIndentationService", throwOnError: true); + _indentationService = Activator.CreateInstance(type); + var indentationService = type.GetInterface("IIndentationService"); + _getIndentationMethod = indentationService.GetMethod("GetIndentation"); + } + catch (Exception ex) + { + throw new InvalidOperationException( + "Error occured when creating an instance of Roslyn's IIndentationService. Roslyn may have changed in an unexpected way.", + ex); + } } public async Task FormatAsync( - RazorCodeDocument codeDocument, - Range range, - DocumentUri uri, - FormattingOptions options, + FormattingContext context, + Range rangeToFormat, CancellationToken cancellationToken, bool formatOnClient = false) { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (rangeToFormat is null) + { + throw new ArgumentNullException(nameof(rangeToFormat)); + } + Range projectedRange = null; - if (range != null && !_documentMappingService.TryMapToProjectedDocumentRange(codeDocument, range, out projectedRange)) + if (rangeToFormat != null && !_documentMappingService.TryMapToProjectedDocumentRange(context.CodeDocument, rangeToFormat, out projectedRange)) { return Array.Empty(); } @@ -74,17 +89,64 @@ public async Task FormatAsync( TextEdit[] edits; if (formatOnClient) { - edits = await FormatOnClientAsync(codeDocument, projectedRange, uri, options, cancellationToken); + edits = await FormatOnClientAsync(context, projectedRange, cancellationToken); } else { - edits = await FormatOnServerAsync(codeDocument, projectedRange, uri, options, cancellationToken); + edits = await FormatOnServerAsync(context, projectedRange, cancellationToken); } - var mappedEdits = MapEditsToHostDocument(codeDocument, edits); + var mappedEdits = MapEditsToHostDocument(context.CodeDocument, edits); return mappedEdits; } + public int GetCSharpIndentation(FormattingContext context, int projectedDocumentIndex, CancellationToken cancellationToken) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Add a marker at the position where we need the indentation. + var changedText = context.CSharpSourceText; + var marker = $"{context.NewLineString}#line default{context.NewLineString}#line hidden{context.NewLineString}"; + changedText = changedText.WithChanges(new TextChange(TextSpan.FromBounds(projectedDocumentIndex, projectedDocumentIndex), marker)); + var changedDocument = context.CSharpWorkspaceDocument.WithText(changedText); + + // Get the line number at the position after the marker + var line = changedText.Lines.GetLinePosition(projectedDocumentIndex + marker.Length).Line; + + try + { + var result = _getIndentationMethod.Invoke( + _indentationService, + new object[] { changedDocument, line, CodeAnalysis.Formatting.FormattingOptions.IndentStyle.Smart, cancellationToken }); + + var baseProperty = result.GetType().GetProperty("BasePosition"); + var basePosition = (int)baseProperty.GetValue(result); + var offsetProperty = result.GetType().GetProperty("Offset"); + var offset = (int)offsetProperty.GetValue(result); + + var resultLine = changedText.Lines.GetLinePosition(basePosition); + var indentation = resultLine.Character + offset; + + // IIndentationService always returns offset as the number of spaces. + // So if the client uses tabs instead of spaces, we need to convert accordingly. + if (!context.Options.InsertSpaces) + { + indentation /= (int)context.Options.TabSize; + } + + return indentation; + } + catch (Exception ex) + { + throw new InvalidOperationException( + "Error occured when reflection invoking Roslyn's IIndentationService. Roslyn may have changed in an unexpected way.", + ex); + } + } + private TextEdit[] MapEditsToHostDocument(RazorCodeDocument codeDocument, TextEdit[] csharpEdits) { var actualEdits = new List(); @@ -104,18 +166,16 @@ private TextEdit[] MapEditsToHostDocument(RazorCodeDocument codeDocument, TextEd } private async Task FormatOnClientAsync( - RazorCodeDocument codeDocument, + FormattingContext context, Range projectedRange, - DocumentUri uri, - FormattingOptions options, CancellationToken cancellationToken) { var @params = new RazorDocumentRangeFormattingParams() { Kind = RazorLanguageKind.CSharp, ProjectedRange = projectedRange, - HostDocumentFilePath = _filePathNormalizer.Normalize(uri.GetAbsoluteOrUNCPath()), - Options = options + HostDocumentFilePath = _filePathNormalizer.Normalize(context.Uri.GetAbsoluteOrUNCPath()), + Options = context.Options }; var response = _server.SendRequest(LanguageServerConstants.RazorRangeFormattingEndpoint, @params); @@ -125,26 +185,19 @@ private async Task FormatOnClientAsync( } private async Task FormatOnServerAsync( - RazorCodeDocument codeDocument, + FormattingContext context, Range projectedRange, - DocumentUri uri, - FormattingOptions options, CancellationToken cancellationToken) { - var workspace = _projectSnapshotManagerAccessor.Instance.Workspace; - var csharpOptions = workspace.Options - .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.TabSize, LanguageNames.CSharp, (int)options.TabSize) - .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.UseTabs, LanguageNames.CSharp, !options.InsertSpaces); - - var csharpDocument = codeDocument.GetCSharpDocument(); - var syntaxTree = CSharpSyntaxTree.ParseText(csharpDocument.GeneratedCode); - var sourceText = SourceText.From(csharpDocument.GeneratedCode); - var root = await syntaxTree.GetRootAsync(); - var spanToFormat = projectedRange.AsTextSpan(sourceText); + var csharpSourceText = context.CodeDocument.GetCSharpSourceText(); + var spanToFormat = projectedRange.AsTextSpan(csharpSourceText); + var root = await context.CSharpWorkspaceDocument.GetSyntaxRootAsync(cancellationToken); + var workspace = context.CSharpWorkspace; - var changes = CodeAnalysis.Formatting.Formatter.GetFormattedTextChanges(root, spanToFormat, workspace, options: csharpOptions); + // Formatting options will already be set in the workspace. + var changes = CodeAnalysis.Formatting.Formatter.GetFormattedTextChanges(root, spanToFormat, workspace); - var edits = changes.Select(c => c.AsTextEdit(sourceText)).ToArray(); + var edits = changes.Select(c => c.AsTextEdit(csharpSourceText)).ToArray(); return edits; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormattingPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormattingPass.cs index de608818cf0..905785d3316 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormattingPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpFormattingPass.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -25,9 +27,8 @@ public CSharpFormattingPass( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, IClientLanguageServer server, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor, ILoggerFactory loggerFactory) - : base(documentMappingService, filePathNormalizer, server, projectSnapshotManagerAccessor) + : base(documentMappingService, filePathNormalizer, server) { if (loggerFactory is null) { @@ -72,23 +73,17 @@ public async override Task ExecuteAsync(FormattingContext cont cancellationToken.ThrowIfCancellationRequested(); - // Now, for each affected line in the edited version of the document, remove x amount of spaces - // at the front to account for extra indentation applied by the C# formatter. - // 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 indentationChanges = AdjustCSharpIndentation(changedContext, startLine: 0, endLine: changedText.Lines.Count - 1); + // We make an optimistic attempt at fixing corner cases. + changedText = CleanupDocument(changedContext); + changedContext = await changedContext.WithTextAsync(changedText); + var indentationChanges = AdjustIndentation(changedContext, cancellationToken); if (indentationChanges.Count > 0) { // Apply the edits that modify indentation. changedText = changedText.WithChanges(indentationChanges); - changedContext = await changedContext.WithTextAsync(changedText); } - // We make an optimistic attempt at fixing corner cases. - changedText = CleanupDocument(changedContext); - var finalChanges = SourceTextDiffer.GetMinimalTextChanges(originalText, changedText, lineDiffOnly: false); var finalEdits = finalChanges.Select(f => f.AsTextEdit(originalText)).ToArray(); @@ -110,13 +105,132 @@ private async Task> FormatCSharpAsync(FormattingContext context, } // These should already be remapped. - var edits = await CSharpFormatter.FormatAsync(context.CodeDocument, range, context.Uri, context.Options, cancellationToken); + var edits = await CSharpFormatter.FormatAsync(context, range, cancellationToken); csharpEdits.AddRange(edits.Where(e => range.Contains(e.Range))); } return csharpEdits; } + private List 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(); + 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(); + 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) { + //
+ // |
+ // } + // + // 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(); + 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. diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpOnTypeFormattingPass.cs index db364a31a5d..a4a977a1fcc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/CSharpOnTypeFormattingPass.cs @@ -22,9 +22,8 @@ public CSharpOnTypeFormattingPass( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, IClientLanguageServer server, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor, ILoggerFactory loggerFactory) - : base(documentMappingService, filePathNormalizer, server, projectSnapshotManagerAccessor) + : base(documentMappingService, filePathNormalizer, server) { if (loggerFactory is null) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs index a31e6b0f7c2..93f5d455da8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs @@ -58,7 +58,7 @@ public override async Task FormatAsync(DocumentUri uri, DocumentSnap } var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(); - var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, range); + using var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, range); var result = new FormattingResult(Array.Empty()); foreach (var pass in _formattingPasses) @@ -80,7 +80,7 @@ public override async Task ApplyFormattedEditsAsync(DocumentUri uri, } var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(); - var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, isFormatOnType: true); + using var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, isFormatOnType: true); var result = new FormattingResult(formattedEdits, kind); foreach (var pass in _formattingPasses) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContentValidationPass.cs index 72239e12df9..015a00f554e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContentValidationPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContentValidationPass.cs @@ -19,9 +19,8 @@ public FormattingContentValidationPass( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, IClientLanguageServer server, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor, ILoggerFactory loggerFactory) - : base(documentMappingService, filePathNormalizer, server, projectSnapshotManagerAccessor) + : base(documentMappingService, filePathNormalizer, server) { if (loggerFactory is null) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContext.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContext.cs index 76496dc840b..7f280063029 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingContext.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions.LanguageServer.Protocol; @@ -15,25 +16,53 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting { - internal class FormattingContext + internal class FormattingContext : IDisposable { - public DocumentUri Uri { get; set; } + private Document _csharpWorkspaceDocument; - public DocumentSnapshot OriginalSnapshot { get; set; } + public DocumentUri Uri { get; private set; } - public RazorCodeDocument CodeDocument { get; set; } + public DocumentSnapshot OriginalSnapshot { get; private set; } - public SourceText SourceText => CodeDocument?.GetSourceText(); + public RazorCodeDocument CodeDocument { get; private set; } - public FormattingOptions Options { get; set; } + public SourceText SourceText => CodeDocument.GetSourceText(); + + public SourceText CSharpSourceText => CodeDocument.GetCSharpSourceText(); + + public Document CSharpWorkspaceDocument + { + get + { + if (_csharpWorkspaceDocument == null) + { + var adhocWorkspace = new AdhocWorkspace(); + var csharpOptions = adhocWorkspace.Options + .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.TabSize, LanguageNames.CSharp, (int)Options.TabSize) + .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.IndentationSize, LanguageNames.CSharp, (int)Options.TabSize) + .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.UseTabs, LanguageNames.CSharp, !Options.InsertSpaces); + adhocWorkspace.TryApplyChanges(adhocWorkspace.CurrentSolution.WithOptions(csharpOptions)); + + var project = adhocWorkspace.AddProject("TestProject", LanguageNames.CSharp); + var csharpSourceText = CodeDocument.GetCSharpSourceText(); + _csharpWorkspaceDocument = adhocWorkspace.AddDocument(project.Id, "TestDocument", csharpSourceText); + } + + return _csharpWorkspaceDocument; + } + } + + public Workspace CSharpWorkspace => CSharpWorkspaceDocument.Project.Solution.Workspace; + + public FormattingOptions Options { get; private set; } public string NewLineString => Environment.NewLine; - public bool IsFormatOnType { get; set; } + public bool IsFormatOnType { get; private set; } - public Range Range { get; set; } + public Range Range { get; private set; } - public Dictionary Indentations { get; } = new Dictionary(); + public IReadOnlyDictionary Indentations { get; private set; } /// /// Generates a string of indentation based on a specific indentation level. For instance, inside of a C# method represents 1 indentation level. A method within a class would have indentaiton level of 2 by default etc. @@ -71,6 +100,21 @@ public string GetIndentationString(int indentation) } } + /// + /// Given an offset return the corresponding indent level. + /// + /// A value represents the number of spaces/tabs at the start of a line. + /// The corresponding indent level. + public int GetIndentationLevelForOffset(int offset) + { + if (Options.InsertSpaces) + { + offset /= (int)Options.TabSize; + } + + return offset; + } + public bool TryGetIndentationLevel(int position, out int indentationLevel) { var syntaxTree = CodeDocument.GetSyntaxTree(); @@ -85,6 +129,15 @@ public bool TryGetIndentationLevel(int position, out int indentationLevel) return false; } + public void Dispose() + { + if (_csharpWorkspaceDocument != null) + { + CSharpWorkspace.Dispose(); + _csharpWorkspaceDocument = null; + } + } + public async Task WithTextAsync(SourceText changedText) { if (changedText is null) @@ -110,14 +163,14 @@ public async Task WithTextAsync(SourceText changedText) var codeDocument = engine.ProcessDesignTime(changedSourceDocument, OriginalSnapshot.FileKind, importSources, OriginalSnapshot.Project.TagHelpers); - var newContext = Create(Uri, OriginalSnapshot, codeDocument, Options, Range); + var newContext = Create(Uri, OriginalSnapshot, codeDocument, Options, Range, IsFormatOnType); return newContext; } public static FormattingContext Create( DocumentUri uri, DocumentSnapshot originalSnapshot, - RazorCodeDocument codedocument, + RazorCodeDocument codeDocument, FormattingOptions options, Range range = null, bool isFormatOnType = false) @@ -132,9 +185,9 @@ public static FormattingContext Create( throw new ArgumentNullException(nameof(originalSnapshot)); } - if (codedocument is null) + if (codeDocument is null) { - throw new ArgumentNullException(nameof(codedocument)); + throw new ArgumentNullException(nameof(codeDocument)); } if (options is null) @@ -142,22 +195,23 @@ public static FormattingContext Create( throw new ArgumentNullException(nameof(options)); } - var text = codedocument.GetSourceText(); + var text = codeDocument.GetSourceText(); range ??= TextSpan.FromBounds(0, text.Length).AsRange(text); var result = new FormattingContext() { Uri = uri, OriginalSnapshot = originalSnapshot, - CodeDocument = codedocument, + CodeDocument = codeDocument, Range = range, Options = options, IsFormatOnType = isFormatOnType }; - var source = codedocument.Source; - var syntaxTree = codedocument.GetSyntaxTree(); + var source = codeDocument.Source; + var syntaxTree = codeDocument.GetSyntaxTree(); var formattingSpans = syntaxTree.GetFormattingSpans(); + var indentations = new Dictionary(); var total = 0; var previousIndentationLevel = 0; @@ -179,7 +233,7 @@ public static FormattingContext Create( // 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 + indentations[i] = new IndentationContext { Line = i, IndentationLevel = span.IndentationLevel, @@ -201,7 +255,7 @@ public static FormattingContext Create( indentationLevel: 0, isInClassBody: false); - result.Indentations[i] = new IndentationContext + indentations[i] = new IndentationContext { Line = i, IndentationLevel = 0, @@ -214,6 +268,8 @@ public static FormattingContext Create( total += lineLength; } + result.Indentations = indentations; + return result; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingDiagnosticValidationPass.cs index 23a859b18db..a8f2bb0531b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingDiagnosticValidationPass.cs @@ -22,9 +22,8 @@ public FormattingDiagnosticValidationPass( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, IClientLanguageServer server, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor, ILoggerFactory loggerFactory) - : base(documentMappingService, filePathNormalizer, server, projectSnapshotManagerAccessor) + : base(documentMappingService, filePathNormalizer, server) { if (loggerFactory is null) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingPassBase.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingPassBase.cs index bd756bf79cd..ebcfccf3ec1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingPassBase.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingPassBase.cs @@ -21,13 +21,10 @@ internal abstract class FormattingPassBase : IFormattingPass { protected static readonly int DefaultOrder = 1000; - private readonly RazorDocumentMappingService _documentMappingService; - public FormattingPassBase( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, - IClientLanguageServer server, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor) + IClientLanguageServer server) { if (documentMappingService is null) { @@ -44,18 +41,15 @@ public FormattingPassBase( throw new ArgumentNullException(nameof(server)); } - if (projectSnapshotManagerAccessor is null) - { - throw new ArgumentNullException(nameof(projectSnapshotManagerAccessor)); - } - - _documentMappingService = documentMappingService; - CSharpFormatter = new CSharpFormatter(documentMappingService, server, projectSnapshotManagerAccessor, filePathNormalizer); + DocumentMappingService = documentMappingService; + CSharpFormatter = new CSharpFormatter(documentMappingService, server, filePathNormalizer); HtmlFormatter = new HtmlFormatter(server, filePathNormalizer); } public virtual int Order => DefaultOrder; + protected RazorDocumentMappingService DocumentMappingService { get; } + protected CSharpFormatter CSharpFormatter { get; } protected HtmlFormatter HtmlFormatter { get; } @@ -93,7 +87,7 @@ protected TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] p { var projectedRange = projectedTextEdits[i].Range; if (codeDocument.IsUnsupported() || - !_documentMappingService.TryMapFromProjectedDocumentRange(codeDocument, projectedRange, out var originalRange)) + !DocumentMappingService.TryMapFromProjectedDocumentRange(codeDocument, projectedRange, out var originalRange)) { // Can't map range. Discard this edit. continue; @@ -243,7 +237,7 @@ protected static SourceText CleanupDocument(FormattingContext context, Range ran continue; } - var mappingStartLineIndex = (int)mappingRange.Start.Line; + var mappingStartLineIndex = mappingRange.Start.Line; if (context.Indentations[mappingStartLineIndex].StartsInCSharpContext) { // Doesn't need cleaning up. diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingVisitor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingVisitor.cs index cb403cf8449..fcdd7b57aeb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingVisitor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/FormattingVisitor.cs @@ -48,7 +48,10 @@ public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) // We need to generate a formatting span at this position. So insert a marker in its place. comment = (SyntaxToken)SyntaxFactory.Token(SyntaxKind.Marker, string.Empty).Green.CreateRed(razorCommentSyntax, razorCommentSyntax.StartCommentStar.EndPosition); } + + _currentIndentationLevel++; WriteSpan(comment, FormattingSpanKind.Comment); + _currentIndentationLevel--; WriteSpan(razorCommentSyntax.EndCommentStar, FormattingSpanKind.MetaCode); WriteSpan(razorCommentSyntax.EndCommentTransition, FormattingSpanKind.Transition); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs index 4ffcbe623f9..fc1ee50c01c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs @@ -4,12 +4,9 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using LSPFormattingOptions = OmniSharp.Extensions.LanguageServer.Protocol.Models.FormattingOptions; using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting @@ -38,18 +35,26 @@ public HtmlFormatter( } public async Task FormatAsync( - RazorCodeDocument codeDocument, - Range range, - DocumentUri uri, - LSPFormattingOptions options, + FormattingContext context, + Range rangeToFormat, CancellationToken cancellationToken) { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (rangeToFormat is null) + { + throw new ArgumentNullException(nameof(rangeToFormat)); + } + var @params = new RazorDocumentRangeFormattingParams() { Kind = RazorLanguageKind.Html, - ProjectedRange = range, - HostDocumentFilePath = _filePathNormalizer.Normalize(uri.GetAbsoluteOrUNCPath()), - Options = options + ProjectedRange = rangeToFormat, + HostDocumentFilePath = _filePathNormalizer.Normalize(context.Uri.GetAbsoluteOrUNCPath()), + Options = context.Options }; var response = _server.SendRequest(LanguageServerConstants.RazorRangeFormattingEndpoint, @params); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs index a046a97c2af..714f443e24d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs @@ -21,9 +21,8 @@ public HtmlFormattingPass( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, IClientLanguageServer server, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor, ILoggerFactory loggerFactory) - : base(documentMappingService, filePathNormalizer, server, projectSnapshotManagerAccessor) + : base(documentMappingService, filePathNormalizer, server) { if (loggerFactory is null) { @@ -46,20 +45,20 @@ public async override Task ExecuteAsync(FormattingContext cont var originalText = context.SourceText; - var htmlEdits = await HtmlFormatter.FormatAsync(context.CodeDocument, context.Range, context.Uri, context.Options, cancellationToken); + var htmlEdits = await HtmlFormatter.FormatAsync(context, context.Range, cancellationToken); var normalizedEdits = NormalizeTextEdits(originalText, htmlEdits); var mappedEdits = RemapTextEdits(context.CodeDocument, normalizedEdits, RazorLanguageKind.Html); var changes = mappedEdits.Select(e => e.AsTextChange(originalText)); - if (!changes.Any()) + + var changedText = originalText; + var changedContext = context; + if (changes.Any()) { - return result; + changedText = originalText.WithChanges(changes); + // Create a new formatting context for the changed razor document. + changedContext = await context.WithTextAsync(changedText); } - var changedText = originalText.WithChanges(changes); - - // Create a new formatting context for the changed razor document. - var changedContext = await context.WithTextAsync(changedText); - var indentationChanges = AdjustRazorIndentation(changedContext); if (indentationChanges.Count > 0) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/OnTypeFormattingStructureValidationPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/OnTypeFormattingStructureValidationPass.cs index 2c708c9acba..5dd5c46566d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/OnTypeFormattingStructureValidationPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/OnTypeFormattingStructureValidationPass.cs @@ -21,9 +21,8 @@ public OnTypeFormattingStructureValidationPass( RazorDocumentMappingService documentMappingService, FilePathNormalizer filePathNormalizer, IClientLanguageServer server, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor, ILoggerFactory loggerFactory) - : base(documentMappingService, filePathNormalizer, server, projectSnapshotManagerAccessor) + : base(documentMappingService, filePathNormalizer, server) { if (loggerFactory is null) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/CodeDirectiveFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/CodeDirectiveFormattingTest.cs index 8ce4f6f468a..9f9f89bf70d 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/CodeDirectiveFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/CodeDirectiveFormattingTest.cs @@ -133,8 +133,8 @@ void Method() { } @functions { public class Foo { -@* This is a Razor Comment *@ -void Method() { } + @* This is a Razor Comment *@ + void Method() { } } } "); @@ -155,7 +155,7 @@ public class Foo{ @functions { public class Foo { -@* This is a Razor Comment *@ + @* This is a Razor Comment *@ } } "); @@ -254,7 +254,7 @@ public class HelloWorld } @functions{ - + public class Bar {} } |", @@ -323,7 +323,7 @@ public class Foo { } "); } - [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/25475")] + [Fact] public async Task IndentsCodeBlockDirectiveStart() { await RunFormattingTestAsync( @@ -340,7 +340,7 @@ public class Foo { } "); } - [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/25475")] + [Fact] public async Task IndentsCodeBlockDirectiveEnd() { await RunFormattingTestAsync( @@ -371,6 +371,11 @@ public Foo() ""One"", ""two"", ""three"" }; + var str = @"" +This should +not +be indented. +""; } public int MyProperty { get { @@ -391,9 +396,14 @@ public class Foo public Foo() { var arr = new string[] { -""One"", ""two"", -""three"" - }; + ""One"", ""two"", + ""three"" + }; + var str = @"" +This should +not +be indented. +""; } public int MyProperty { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingContentValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingContentValidationPassTest.cs index 4cf1fca295a..5a0ae92f496 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingContentValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingContentValidationPassTest.cs @@ -120,8 +120,7 @@ private FormattingContentValidationPass GetPass(RazorCodeDocument codeDocument) var mappingService = new DefaultRazorDocumentMappingService(); var client = Mock.Of(); - var projectSnapshotManagerAccessor = Mock.Of(); - var pass = new FormattingContentValidationPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory); + var pass = new FormattingContentValidationPass(mappingService, FilePathNormalizer, client, LoggerFactory); pass.DebugAssertsEnabled = false; return pass; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingDiagnosticValidationPassTest.cs index 5832802da34..c15696671f6 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingDiagnosticValidationPassTest.cs @@ -127,8 +127,7 @@ private FormattingDiagnosticValidationPass GetPass(RazorCodeDocument codeDocumen var mappingService = new DefaultRazorDocumentMappingService(); var client = Mock.Of(); - var projectSnapshotManagerAccessor = Mock.Of(); - var pass = new FormattingDiagnosticValidationPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory); + var pass = new FormattingDiagnosticValidationPass(mappingService, FilePathNormalizer, client, LoggerFactory); pass.DebugAssertsEnabled = false; return pass; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingLanguageServerClient.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingLanguageServerClient.cs index 8187210f7fd..98358be2497 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingLanguageServerClient.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingLanguageServerClient.cs @@ -81,6 +81,7 @@ private RazorDocumentRangeFormattingResponse Format(RazorDocumentRangeFormatting var workspace = new AdhocWorkspace(); var cSharpOptions = workspace.Options .WithChangedOption(FormattingOptions.TabSize, LanguageNames.CSharp, (int)options.TabSize) + .WithChangedOption(FormattingOptions.IndentationSize, LanguageNames.CSharp, (int)options.TabSize) .WithChangedOption(FormattingOptions.UseTabs, LanguageNames.CSharp, !options.InsertSpaces); var codeDocument = _documents[@params.HostDocumentFilePath]; @@ -100,7 +101,7 @@ private RazorDocumentRangeFormattingResponse Format(RazorDocumentRangeFormatting var codeDocument = _documents[@params.HostDocumentFilePath]; var generatedHtml = codeDocument.GetHtmlDocument().GeneratedHtml; - var inputText = SourceText.From(generatedHtml); + generatedHtml = generatedHtml.Replace("\r", "", StringComparison.Ordinal).Replace("\n", "\r\n", StringComparison.Ordinal); // Get formatted baseline file var baselineInputFileName = Path.ChangeExtension(_baselineFileName, ".input.html"); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingTestBase.cs index c60811bfdef..9af1697b825 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/FormattingTestBase.cs @@ -73,7 +73,7 @@ protected async Task RunFormattingTestAsync(string input, string expected, int t #if GENERATE_BASELINES Assert.False(true, "GENERATE_BASELINES is set to true."); #else - Assert.Equal(expected, actual); + Assert.Equal(expected, actual, ignoreLineEndingDifferences: true); #endif } @@ -148,15 +148,14 @@ private RazorFormattingService CreateFormattingService(RazorCodeDocument codeDoc var client = new FormattingLanguageServerClient(TestProjectPath, FileName); client.AddCodeDocument(codeDocument); - var projectSnapshotManagerAccessor = Mock.Of(p => p.Instance.Workspace == new AdhocWorkspace()); var passes = new List() { - new HtmlFormattingPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory), - new CSharpFormattingPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory), - new CSharpOnTypeFormattingPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory), - new OnTypeFormattingStructureValidationPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory), - new FormattingDiagnosticValidationPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory), - new FormattingContentValidationPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory), + new HtmlFormattingPass(mappingService, FilePathNormalizer, client, LoggerFactory), + new CSharpFormattingPass(mappingService, FilePathNormalizer, client, LoggerFactory), + new CSharpOnTypeFormattingPass(mappingService, FilePathNormalizer, client, LoggerFactory), + new OnTypeFormattingStructureValidationPass(mappingService, FilePathNormalizer, client, LoggerFactory), + new FormattingDiagnosticValidationPass(mappingService, FilePathNormalizer, client, LoggerFactory), + new FormattingContentValidationPass(mappingService, FilePathNormalizer, client, LoggerFactory), }; return new DefaultRazorFormattingService(passes, LoggerFactory); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/HtmlFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/HtmlFormattingTest.cs index ae10e9133d9..01a7cb99fac 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/HtmlFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/HtmlFormattingTest.cs @@ -137,6 +137,45 @@ This is heavily nested } } +"); + } + + [Fact] + public async Task FormatsMixedRazorBlock() + { + await RunFormattingTestAsync( +input: @"|@page ""/test"" + +
Some Text
+ +@{ +

+ @if (true) { + var t = 1; +if (true) +{ +

@DateTime.Now
+ } + } +

+} +|", +expected: @"@page ""/test"" + +
Some Text
+ +@{ +

+ @if (true) + { + var t = 1; + if (true) + { +

@DateTime.Now
+ } + } +

+} "); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/OnTypeFormattingStructureValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/OnTypeFormattingStructureValidationPassTest.cs index 2eb1f2c985e..9cebe4708d8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/OnTypeFormattingStructureValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/OnTypeFormattingStructureValidationPassTest.cs @@ -239,8 +239,7 @@ private OnTypeFormattingStructureValidationPass GetPass(RazorCodeDocument codeDo var mappingService = new DefaultRazorDocumentMappingService(); var client = Mock.Of(); - var projectSnapshotManagerAccessor = Mock.Of(); - var pass = new OnTypeFormattingStructureValidationPass(mappingService, FilePathNormalizer, client, projectSnapshotManagerAccessor, LoggerFactory); + var pass = new OnTypeFormattingStructureValidationPass(mappingService, FilePathNormalizer, client, LoggerFactory); return pass; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.input.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.input.html index 1b380465303..9fcf437f0c0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.input.html +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.input.html @@ -8,6 +8,11 @@ ~~~~~~ ~~~~~~ ~~~~~~~ ~~ + ~~~ ~~~ ~ ~~ +~~~~ ~~~~~~ +~~~ +~~ ~~~~~~~~~ +~~ ~ ~~~~~~ ~~~ ~~~~~~~~~~ ~ ~~~ ~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.output.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.output.html index 1b380465303..9fcf437f0c0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.output.html +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/ComplexCodeBlockDirective.output.html @@ -8,6 +8,11 @@ ~~~~~~ ~~~~~~ ~~~~~~~ ~~ + ~~~ ~~~ ~ ~~ +~~~~ ~~~~~~ +~~~ +~~ ~~~~~~~~~ +~~ ~ ~~~~~~ ~~~ ~~~~~~~~~~ ~ ~~~ ~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveEnd.input.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveEnd.input.html new file mode 100644 index 00000000000..c66280585df --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveEnd.input.html @@ -0,0 +1,4 @@ + + ~~~~~~~~~~ ~ +~~~~~~ ~~~~~ ~~~~~ + ~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveEnd.output.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveEnd.output.html new file mode 100644 index 00000000000..6ce3818453b --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveEnd.output.html @@ -0,0 +1,3 @@ +~~~~~~~~~~ ~ +~~~~~~ ~~~~~ ~~~~~ +~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveStart.input.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveStart.input.html new file mode 100644 index 00000000000..39e8a639c5e --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveStart.input.html @@ -0,0 +1,4 @@ + +Hello World + ~~~~~~~~~~ ~~~~~~~ ~~~~~ ~~~~~ +~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveStart.output.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveStart.output.html new file mode 100644 index 00000000000..ee1f93c1c69 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/IndentsCodeBlockDirectiveStart.output.html @@ -0,0 +1,3 @@ +Hello World +~~~~~~~~~~ ~~~~~~~ ~~~~~ ~~~~~ +~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/MultipleCodeBlockDirectives2.input.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/MultipleCodeBlockDirectives2.input.html index 45999c85f8e..1b2dff4b5fe 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/MultipleCodeBlockDirectives2.input.html +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/CodeDirectiveFormattingTest/MultipleCodeBlockDirectives2.input.html @@ -6,6 +6,6 @@ ~ ~~~~~~~~~~~ - + ~~~~~~ ~~~~~ ~~~ ~~ ~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/HtmlFormattingTest/FormatsMixedRazorBlock.input.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/HtmlFormattingTest/FormatsMixedRazorBlock.input.html new file mode 100644 index 00000000000..ec0b6c69829 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/HtmlFormattingTest/FormatsMixedRazorBlock.input.html @@ -0,0 +1,15 @@ +~~~~~ ~~~~~~~ + +
Some Text
+ +~~ +

+ ~~~ ~~~~~~ ~ + ~~~ ~ ~ ~~ +~~ ~~~~~~ +~ +

~~~~~~~~~~~~~
+ ~ + ~ +

+~ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/HtmlFormattingTest/FormatsMixedRazorBlock.output.html b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/HtmlFormattingTest/FormatsMixedRazorBlock.output.html new file mode 100644 index 00000000000..b9863e193c8 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/TestFiles/HtmlFormattingTest/FormatsMixedRazorBlock.output.html @@ -0,0 +1,15 @@ +~~~~~ ~~~~~~~ + +
Some Text
+ +~~ +

+ ~~~ ~~~~~~ ~ + ~~~ ~ ~ ~~ + ~~ ~~~~~~ + ~ +

~~~~~~~~~~~~~
+ ~ + ~ +

+~