diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs index 56158e983b8..1ecf523acd0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -23,7 +24,52 @@ public async Task> ExecuteAsync(FormattingContext con if (changes.Length > 0) { + // There is a lot of uncertainty when we're dealing with edits that come from the Html formatter + // because we are not responsible for it. It could make all sorts of strange edits, and it could + // structure those edits is all sorts of ways. eg, it could have individual character edits, or + // it could have a single edit that replaces a whole section of text, or the whole document. + // Since the Html formatter doesn't understand Razor, and in fact doesn't even format the actual + // Razor document directly (all C# is replaced), we have to be selective about what edits we will + // actually use, but being selective is tricky because we might be missing some intentional edits + // that the formatter made. + // + // To solve this, and work around various issues due to the Html formatter seeing a much simpler + // document that we are actually dealing with, the first thing we do is take the changes it suggests + // and apply them to the document it saw, then use our own algorithm to produce a set of changes + // that more closely match what we want to get out of it. Specifically, we only want to see changes + // to whitespace, or Html, not changes that include C#. Fortunately since we encode all C# as tildes + // it means we can do a word-based diff, and all C# will essentially be equal to all other C#, so + // won't appear in the diff. + // + // So we end up with a set of changes that are only ever to whitespace, or legitimate Html (though + // in reality the formatter doesn't change that anyway). + + // Avoid computing a minimal diff if we don't need to. Slightly wasteful if we've come from one + // of the other overloads, but worth it if we haven't (and worth it for them to validate before + // doing the work to convert edits to changes). + if (changes.Any(static e => e.NewText?.Contains('~') ?? false)) + { + var htmlSourceText = context.CodeDocument.GetHtmlSourceText(); + context.Logger?.LogSourceText("HtmlSourceText", htmlSourceText); + var htmlWithChanges = htmlSourceText.WithChanges(changes); + + changes = SourceTextDiffer.GetMinimalTextChanges(htmlSourceText, htmlWithChanges, DiffKind.Word); + if (changes.Length == 0) + { + return []; + } + } + + // Now that the changes are on our terms, we can apply our own filtering without having to worry + // that we're missing something important. We could still, in theory, be missing something the Html + // formatter intentionally did, but we also know the Html formatter made its decisions without an + // awareness of Razor anyway, so it's not a reliable source. var filteredChanges = await FilterIncomingChangesAsync(context, changes, cancellationToken).ConfigureAwait(false); + if (filteredChanges.Length == 0) + { + return []; + } + changedText = changedText.WithChanges(filteredChanges); context.Logger?.LogSourceText("AfterHtmlFormatter", changedText); @@ -49,6 +95,7 @@ private async Task> FilterIncomingChangesAsync(Format var comment = node?.FirstAncestorOrSelf(); if (comment is not null && change.Span.Start > comment.SpanStart) { + context.Logger?.LogMessage($"Dropping change {change} because it's in a Razor comment"); continue; } @@ -75,6 +122,7 @@ private async Task> FilterIncomingChangesAsync(Format sourceText[change.Span.Start - 1] == '@' && sourceText[change.Span.Start] == '<') { + context.Logger?.LogMessage($"Dropping change {change} because it breaks a C# template"); continue; } @@ -110,6 +158,7 @@ private async Task> FilterIncomingChangesAsync(Format csharpSyntaxRoot.FindNode(new TextSpan(csharpIndex, 0), getInnermostNodeForTie: true) is { } csharpNode && csharpNode is CSharp.Syntax.LiteralExpressionSyntax or CSharp.Syntax.InterpolatedStringTextSyntax) { + context.Logger?.LogMessage($"Dropping change {change} because it breaks a C# string literal"); continue; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/DiffKind.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/DiffKind.cs index 594cec83f40..1308827c508 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/DiffKind.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/DiffKind.cs @@ -5,6 +5,19 @@ namespace Microsoft.CodeAnalysis.Razor.TextDifferencing; internal enum DiffKind : byte { + /// + /// Diff by character. + /// Char, + /// + /// Diff by line + /// Line, + /// + /// Diff by word + /// + /// + /// Word break characters are: whitespace, '/' and '"'. Contiguous word breaks are treated as a single word. + /// + Word, } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.LineDiffer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.LineDiffer.cs index db9d2fd947b..901a8b95568 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.LineDiffer.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.LineDiffer.cs @@ -1,115 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.TextDifferencing; internal partial class SourceTextDiffer { - private class LineDiffer : SourceTextDiffer + private sealed class LineDiffer(SourceText oldText, SourceText newText) + : TextSpanDiffer(oldText, newText) { - private readonly TextLineCollection _oldLines; - private readonly TextLineCollection _newLines; - - private char[] _oldLineBuffer; - private char[] _newLineBuffer; - private char[] _appendBuffer; - - protected override int OldSourceLength { get; } - protected override int NewSourceLength { get; } - - public LineDiffer(SourceText oldText, SourceText newText) - : base(oldText, newText) - { - _oldLineBuffer = RentArray(1024); - _newLineBuffer = RentArray(1024); - _appendBuffer = RentArray(1024); - - _oldLines = oldText.Lines; - _newLines = newText.Lines; - - OldSourceLength = _oldLines.Count; - NewSourceLength = _newLines.Count; - } - - public override void Dispose() + protected override ImmutableArray Tokenize(SourceText text) { - ReturnArray(_oldLineBuffer); - ReturnArray(_newLineBuffer); - ReturnArray(_appendBuffer); - } - - protected override bool SourceEqual(int oldSourceIndex, int newSourceIndex) - { - var oldLine = _oldLines[oldSourceIndex]; - var newLine = _newLines[newSourceIndex]; - - var oldSpan = oldLine.SpanIncludingLineBreak; - var newSpan = newLine.SpanIncludingLineBreak; - - if (oldSpan.Length != newSpan.Length) - { - return false; - } - - var length = oldSpan.Length; + using var builder = new PooledArrayBuilder(); - // Simple case: Both lines are empty. - if (length == 0) + foreach (var line in text.Lines) { - return true; - } - - // Copy the text into char arrays for comparison. Note: To avoid allocation, - // we try to reuse the same char buffers and only grow them when a longer - // line is encountered. - var oldChars = EnsureBuffer(ref _oldLineBuffer, length); - var newChars = EnsureBuffer(ref _newLineBuffer, length); - - OldText.CopyTo(oldSpan.Start, oldChars, 0, length); - NewText.CopyTo(newSpan.Start, newChars, 0, length); - - for (var i = 0; i < length; i++) - { - if (oldChars[i] != newChars[i]) - { - return false; - } - } - - return true; - } - - protected override int GetEditPosition(DiffEdit edit) - => _oldLines[edit.Position].Start; - - protected override int AppendEdit(DiffEdit edit, StringBuilder builder) - { - if (edit.Kind == DiffEditKind.Insert) - { - Assumes.NotNull(edit.NewTextPosition); - var newTextPosition = edit.NewTextPosition.GetValueOrDefault(); - - for (var i = 0; i < edit.Length; i++) - { - var newLine = _newLines[newTextPosition + i]; - - var newSpan = newLine.SpanIncludingLineBreak; - if (newSpan.Length > 0) - { - var buffer = EnsureBuffer(ref _appendBuffer, newSpan.Length); - NewText.CopyTo(newSpan.Start, buffer, 0, newSpan.Length); - - builder.Append(buffer, 0, newSpan.Length); - } - } - - return _oldLines[edit.Position].Start; + builder.Add(line.SpanIncludingLineBreak); } - return _oldLines[edit.Position + edit.Length - 1].EndIncludingLineBreak; + return builder.ToImmutableAndClear(); } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.TextSpanDiffer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.TextSpanDiffer.cs new file mode 100644 index 00000000000..b01bde5f5fc --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.TextSpanDiffer.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.TextDifferencing; + +internal partial class SourceTextDiffer +{ + private abstract class TextSpanDiffer : SourceTextDiffer + { + private readonly ImmutableArray _oldSpans = []; + private readonly ImmutableArray _newSpans = []; + + private char[] _oldBuffer; + private char[] _newBuffer; + private char[] _appendBuffer; + + protected override int OldSourceLength { get; } + protected override int NewSourceLength { get; } + + public TextSpanDiffer(SourceText oldText, SourceText newText) + : base(oldText, newText) + { + _oldBuffer = RentArray(1024); + _newBuffer = RentArray(1024); + _appendBuffer = RentArray(1024); + + if (oldText.Length > 0) + { + _oldSpans = Tokenize(oldText); + } + + if (newText.Length > 0) + { + _newSpans = Tokenize(newText); + } + + OldSourceLength = _oldSpans.Length; + NewSourceLength = _newSpans.Length; + } + + protected abstract ImmutableArray Tokenize(SourceText text); + + public override void Dispose() + { + ReturnArray(_oldBuffer); + ReturnArray(_newBuffer); + ReturnArray(_appendBuffer); + } + + protected override bool SourceEqual(int oldSourceIndex, int newSourceIndex) + { + var oldSpan = _oldSpans[oldSourceIndex]; + var newSpan = _newSpans[newSourceIndex]; + + if (oldSpan.Length != newSpan.Length) + { + return false; + } + + var length = oldSpan.Length; + + // Simple case: Both lines are empty. + if (length == 0) + { + return true; + } + + // Copy the text into char arrays for comparison. Note: To avoid allocation, + // we try to reuse the same char buffers and only grow them when a longer + // line is encountered. + var oldChars = EnsureBuffer(ref _oldBuffer, length); + var newChars = EnsureBuffer(ref _newBuffer, length); + + OldText.CopyTo(oldSpan.Start, oldChars, 0, length); + NewText.CopyTo(newSpan.Start, newChars, 0, length); + + for (var i = 0; i < length; i++) + { + if (oldChars[i] != newChars[i]) + { + return false; + } + } + + return true; + } + + protected override int GetEditPosition(DiffEdit edit) + => _oldSpans[edit.Position].Start; + + protected override int AppendEdit(DiffEdit edit, StringBuilder builder) + { + if (edit.Kind == DiffEditKind.Insert) + { + Assumes.NotNull(edit.NewTextPosition); + var newTextPosition = edit.NewTextPosition.GetValueOrDefault(); + + for (var i = 0; i < edit.Length; i++) + { + var newSpan = _newSpans[newTextPosition + i]; + + if (newSpan.Length > 0) + { + var buffer = EnsureBuffer(ref _appendBuffer, newSpan.Length); + NewText.CopyTo(newSpan.Start, buffer, 0, newSpan.Length); + + builder.Append(buffer, 0, newSpan.Length); + } + } + + return _oldSpans[edit.Position].Start; + } + + return _oldSpans[edit.Position + edit.Length - 1].End; + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.WordDiffer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.WordDiffer.cs new file mode 100644 index 00000000000..367dafc73ad --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.WordDiffer.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.TextDifferencing; + +internal partial class SourceTextDiffer +{ + private sealed class WordDiffer(SourceText oldText, SourceText newText) + : TextSpanDiffer(oldText, newText) + { + protected override ImmutableArray Tokenize(SourceText text) + { + using var builder = new PooledArrayBuilder(); + + var currentSpanStart = 0; + var currentClassification = Classify(text[0]); + + // This algorithm is simpler than a normal tokenizer might be because we want to keep contiguous + // whitespace characters in the same "word", and we don't really care about contiguous quotes + // or slashes, so we can keep it simple and just capture a "word" when the classification of + // the current character changes. + for (var index = 1; index < text.Length; index++) + { + var classification = Classify(text[index]); + if (classification != currentClassification) + { + // We've hit a word boundary, so store this and move on + builder.Add(TextSpan.FromBounds(currentSpanStart, index)); + currentSpanStart = index; + currentClassification = classification; + } + } + + // It's impossible for the loop to capture the last word + Debug.Assert(currentSpanStart < text.Length); + builder.Add(TextSpan.FromBounds(currentSpanStart, text.Length)); + + return builder.ToImmutableAndClear(); + } + + private static int Classify(char c) + { + // The type of classification doesn't matter as long as its unique and equatible + return c switch + { + '/' => 0, + '"' => 1, + _ when char.IsWhiteSpace(c) => 2, + _ => 3, + }; + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.cs index b1216936400..ad35039f4ad 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/SourceTextDiffer.cs @@ -13,16 +13,10 @@ namespace Microsoft.CodeAnalysis.Razor.TextDifferencing; -internal abstract partial class SourceTextDiffer : TextDiffer, IDisposable +internal abstract partial class SourceTextDiffer(SourceText oldText, SourceText newText) : TextDiffer, IDisposable { - protected readonly SourceText OldText; - protected readonly SourceText NewText; - - protected SourceTextDiffer(SourceText oldText, SourceText newText) - { - OldText = oldText ?? throw new ArgumentNullException(nameof(oldText)); - NewText = newText ?? throw new ArgumentNullException(nameof(newText)); - } + protected readonly SourceText OldText = oldText; + protected readonly SourceText NewText = newText; public abstract void Dispose(); @@ -30,21 +24,21 @@ protected SourceTextDiffer(SourceText oldText, SourceText newText) protected abstract int AppendEdit(DiffEdit edit, StringBuilder builder); /// - /// Rents a char array of at least from the shared array pool. + /// Rents a char array of at least from the shared array pool. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static char[] RentArray(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); /// - /// Returns a char array to the shared array pool. + /// Returns a char array to the shared array pool. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static void ReturnArray(char[] array, bool clearArray = false) => ArrayPool.Shared.Return(array, clearArray); /// - /// Ensures that references a char array of at least . + /// Ensures that references a char array of at least . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static char[] EnsureBuffer(ref char[] array, int minimumLength) @@ -114,9 +108,13 @@ public static ImmutableArray GetMinimalTextChanges(SourceText oldTex return newText.GetTextChangesArray(oldText); } - using SourceTextDiffer differ = kind == DiffKind.Line - ? new LineDiffer(oldText, newText) - : new CharDiffer(oldText, newText); + using SourceTextDiffer differ = kind switch + { + DiffKind.Line => new LineDiffer(oldText, newText), + DiffKind.Char => new CharDiffer(oldText, newText), + DiffKind.Word => new WordDiffer(oldText, newText), + _ => throw new ArgumentOutOfRangeException(nameof(kind)), + }; var edits = differ.ComputeDiff(); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionProviderTest.cs index 40cf81cf18c..def2a9572d3 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionProviderTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Immutable; +using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -17,6 +18,7 @@ using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; using Moq; using Xunit; using Xunit.Abstractions; @@ -123,16 +125,10 @@ public async Task ProvideAsync_RemapsAndFixesEdits() Assert.NotNull(action.Edit); Assert.True(action.Edit.TryGetTextDocumentEdits(out var documentEdits)); Assert.Equal(documentPath, documentEdits[0].TextDocument.DocumentUri.GetRequiredParsedUri().AbsolutePath); - // Edit should be converted to 2 edits, to remove the tags - Assert.Collection(documentEdits[0].Edits, - e => - { - Assert.Equal("", ((TextEdit)e).NewText); - }, - e => - { - Assert.Equal("", ((TextEdit)e).NewText); - }); + + var text = SourceText.From(contents); + var changed = text.WithChanges(documentEdits[0].Edits.Select(e => text.GetTextChange((TextEdit)e))); + Assert.Equal("Goo @(DateTime.Now) Bar", changed.ToString()); } private static RazorCodeActionContext CreateRazorCodeActionContext( diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionResolverTest.cs index 0855a7826aa..7c2a1f40ff2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/HtmlCodeActionResolverTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; @@ -13,6 +14,7 @@ using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; using Moq; using Xunit; using Xunit.Abstractions; @@ -85,15 +87,9 @@ public async Task ResolveAsync_RemapsAndFixesEdits() Assert.NotNull(action.Edit); Assert.True(action.Edit.TryGetTextDocumentEdits(out var documentEdits)); Assert.Equal(documentPath, documentEdits[0].TextDocument.DocumentUri.GetRequiredParsedUri().AbsolutePath); - // Edit should be converted to 2 edits, to remove the tags - Assert.Collection(documentEdits[0].Edits, - e => - { - Assert.Equal("", ((TextEdit)e).NewText); - }, - e => - { - Assert.Equal("", ((TextEdit)e).NewText); - }); + + var text = SourceText.From(contents); + var changed = text.WithChanges(documentEdits[0].Edits.Select(e => text.GetTextChange((TextEdit)e))); + Assert.Equal("Goo @(DateTime.Now) Bar", changed.ToString()); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs index 64f4e72b6bb..94cec39477a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs @@ -27,6 +27,41 @@ await RunFormattingTestAsync( expected: ""); } + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12416")] + public async Task MixedIndentation() + { + // This doesn't actually fail because the Html formatter in Web Tools doesn't produce "bad" edits + // like VS Code does, but thought I'd put it here just in case. Tests in FormattingLogTest validate + // the same scenario with VS Code edits. + + await RunFormattingTestAsync( + input: """ +
+ @switch (true) + { + case true: + @if (true) + { + } + break; + } +
+ """, + expected: """ +
+ @switch (true) + { + case true: + @if (true) + { + } + break; + } +
+ """); + } + [FormattingTestFact] public async Task RangeFormatOpenBrace() { diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TextDifferencing/SourceTextDifferTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TextDifferencing/SourceTextDifferTest.cs index 6f450c44d8a..eea586dd844 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TextDifferencing/SourceTextDifferTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TextDifferencing/SourceTextDifferTest.cs @@ -9,13 +9,8 @@ namespace Microsoft.CodeAnalysis.Razor.TextDifferencing; -public class SourceTextDifferTest : ToolingTestBase +public class SourceTextDifferTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { - public SourceTextDifferTest(ITestOutputHelper testOutput) - : base(testOutput) - { - } - [Theory] [InlineData("asdf", ";lkj")] [InlineData("asdf", ";asd")] @@ -37,18 +32,41 @@ public void GetMinimalTextChanges_ReturnsAccurateResults(string oldStr, string n var oldText = CreateSourceText(oldStr, fixLineEndings: false); var newText = CreateSourceText(newStr, fixLineEndings: false); - // Act 1 + // Act var characterChanges = SourceTextDiffer.GetMinimalTextChanges(oldText, newText, DiffKind.Char); - // Assert 1 + // Assert var changedText = oldText.WithChanges(characterChanges); Assert.Equal(newStr, changedText.ToString()); + } - // Act 2 - var lineChanges = SourceTextDiffer.GetMinimalTextChanges(oldText, newText, DiffKind.Char); + [Theory] + [InlineData("asdf", ";lkj")] + [InlineData("asdf", ";asd")] + [InlineData("", "")] + [InlineData("", "a")] + [InlineData("a", "b")] + [InlineData("a", "a")] + [InlineData("a", "")] + [InlineData("aabd", "a")] + [InlineData("trtrt4 5rtt()", "atbd")] + [InlineData(@"trtrt4\n5rtt()", "atb\nd")] + [InlineData("Hello\r\nWorld\r\n123", "Hola\r\nWorld\r\n\r\n1234")] + [InlineData("Hello\r\nWorld\r\n123", "Hola World 456")] + [InlineData("Hello\tWorld\t123", "Hola Earth 456")] + [InlineData("\t
", "
")] + [InlineData("\t
", "
")] + public void GetMinimalTextChanges_ReturnsAccurateResults_WordDiffer(string oldStr, string newStr) + { + // Arrange + var oldText = CreateSourceText(oldStr, fixLineEndings: false); + var newText = CreateSourceText(newStr, fixLineEndings: false); - // Assert 2 - changedText = oldText.WithChanges(lineChanges); + // Act + var wordChanges = SourceTextDiffer.GetMinimalTextChanges(oldText, newText, DiffKind.Word); + + // Assert + var changedText = oldText.WithChanges(wordChanges); Assert.Equal(newStr, changedText.ToString()); } @@ -82,6 +100,13 @@ public void GetMinimalTextChanges_ReturnsExpectedResults() // Assert 2 var change = Assert.Single(lineChanges); Assert.Equal(new TextChange(TextSpan.FromBounds(7, 17), " Hola!\r\n"), change); + + // Act 3 + var wordChanges = SourceTextDiffer.GetMinimalTextChanges(oldText, newText, DiffKind.Word); + + // Assert 3 + Assert.Collection(wordChanges, + change => Assert.Equal(new TextChange(TextSpan.FromBounds(9, 15), "Hola!"), change)); } [Fact] @@ -123,6 +148,16 @@ public void GetMinimalTextChanges_MultiLineChange_ReturnsExpectedResults() Assert.Collection(lineChanges, change => Assert.Equal(new TextChange(TextSpan.FromBounds(0, 7), "THESE\r\n"), change), change => Assert.Equal(new TextChange(TextSpan.FromBounds(12, 33), "MULTIPLE\r\nLINES\r\nOF\r\n"), change)); + + // Act 3 + var wordChanges = SourceTextDiffer.GetMinimalTextChanges(oldText, newText, DiffKind.Word); + + // Assert 3 + Assert.Collection(wordChanges, + change => Assert.Equal(new TextChange(TextSpan.FromBounds(0, 5), "THESE"), change), + change => Assert.Equal(new TextChange(TextSpan.FromBounds(12, 20), "MULTIPLE"), change), + change => Assert.Equal(new TextChange(TextSpan.FromBounds(22, 27), "LINES"), change), + change => Assert.Equal(new TextChange(TextSpan.FromBounds(29, 31), "OF"), change)); } private static SourceText CreateSourceText(string input, bool fixLineEndings = true) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs index a4a2ef3768a..b0addb46abb 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs @@ -48,6 +48,42 @@ public async Task UnexpectedFalseInIndentBlockOperation() await GetFormattingEditsAsync(document, htmlEdits, span: default, options.CodeBlockBraceOnNextLine, options.InsertSpaces, options.TabSize, options.ToRazorFormattingOptions().CSharpSyntaxFormattingOptions.AssumeNotNull()); } + [Fact] + [WorkItem("https://github.com/dotnet/razor/issues/12416")] + public Task MixedIndentation() + { + var contents = GetResource("InitialDocument.txt"); + var htmlChangesFile = GetResource("HtmlChanges.json"); + + return VerifyMixedIndentationAsync(contents, htmlChangesFile); + } + + [Fact] + [WorkItem("https://github.com/dotnet/razor/issues/12416")] + public Task RealWorldMixedIndentation() + { + var contents = GetResource("InitialDocument.txt"); + var htmlChangesFile = GetResource("HtmlChanges.json"); + + return VerifyMixedIndentationAsync(contents, htmlChangesFile); + } + + private async Task VerifyMixedIndentationAsync(string contents, string htmlChangesFile) + { + var document = CreateProjectAndRazorDocument(contents); + + var options = new TempRazorFormattingOptions(); + + var formattingService = (RazorFormattingService)OOPExportProvider.GetExportedValue(); + formattingService.GetTestAccessor().SetFormattingLoggerFactory(new TestFormattingLoggerFactory(TestOutputHelper)); + + var htmlChanges = JsonSerializer.Deserialize(htmlChangesFile, JsonHelpers.JsonSerializerOptions); + var sourceText = await document.GetTextAsync(); + var htmlEdits = htmlChanges.Select(c => sourceText.GetTextEdit(c.ToTextChange())).ToArray(); + + await GetFormattingEditsAsync(document, htmlEdits, span: default, options.CodeBlockBraceOnNextLine, options.InsertSpaces, options.TabSize, options.ToRazorFormattingOptions().CSharpSyntaxFormattingOptions.AssumeNotNull()); + } + private string GetResource(string name, [CallerMemberName] string? testName = null) { var baselineFileName = $@"TestFiles\FormattingLog\{testName}\{name}"; diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/MixedIndentation/HtmlChanges.json b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/MixedIndentation/HtmlChanges.json new file mode 100644 index 00000000000..958968df081 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/MixedIndentation/HtmlChanges.json @@ -0,0 +1 @@ +[{"span":{"start":7,"end":7,"length":0},"newText":" "},{"span":{"start":23,"end":23,"length":0},"newText":" "},{"span":{"start":42,"end":44,"length":2},"newText":" "},{"span":{"start":56,"end":63,"length":7},"newText":" "},{"span":{"start":70,"end":70,"length":0},"newText":"~\r\n"},{"span":{"start":82,"end":82,"length":0},"newText":" "}] \ No newline at end of file diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/MixedIndentation/InitialDocument.txt b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/MixedIndentation/InitialDocument.txt new file mode 100644 index 00000000000..b598265d66b --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/MixedIndentation/InitialDocument.txt @@ -0,0 +1,10 @@ +
+@switch (true) +{ + case true: + @if (true) + { + } + break; +} +
\ No newline at end of file diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/RealWorldMixedIndentation/HtmlChanges.json b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/RealWorldMixedIndentation/HtmlChanges.json new file mode 100644 index 00000000000..219021f6f4b --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/RealWorldMixedIndentation/HtmlChanges.json @@ -0,0 +1 @@ +[{"span":{"start":516,"end":516,"length":0},"newText":"\r\n"},{"span":{"start":517,"end":517,"length":0},"newText":" "},{"span":{"start":707,"end":711,"length":4},"newText":""},{"span":{"start":771,"end":775,"length":4},"newText":""},{"span":{"start":836,"end":840,"length":4},"newText":""},{"span":{"start":1051,"end":1055,"length":4},"newText":""},{"span":{"start":1423,"end":1423,"length":0},"newText":"\r\n"},{"span":{"start":1424,"end":1424,"length":0},"newText":" "},{"span":{"start":1506,"end":1510,"length":4},"newText":""},{"span":{"start":1883,"end":1883,"length":0},"newText":"\r\n "},{"span":{"start":1943,"end":1947,"length":4},"newText":""},{"span":{"start":2339,"end":2339,"length":0},"newText":"\r\n "},{"span":{"start":2412,"end":2416,"length":4},"newText":""},{"span":{"start":2813,"end":2817,"length":4},"newText":""},{"span":{"start":3266,"end":3267,"length":1},"newText":""},{"span":{"start":3279,"end":3282,"length":3},"newText":""},{"span":{"start":3311,"end":3316,"length":5},"newText":""},{"span":{"start":3328,"end":3331,"length":3},"newText":""},{"span":{"start":3361,"end":3366,"length":5},"newText":""},{"span":{"start":3382,"end":3385,"length":3},"newText":""},{"span":{"start":3432,"end":3437,"length":5},"newText":""},{"span":{"start":3453,"end":3456,"length":3},"newText":""},{"span":{"start":3459,"end":3464,"length":5},"newText":""},{"span":{"start":3480,"end":3487,"length":7},"newText":""},{"span":{"start":3512,"end":3521,"length":9},"newText":""},{"span":{"start":3541,"end":3544,"length":3},"newText":""},{"span":{"start":3574,"end":3586,"length":12},"newText":""},{"span":{"start":3696,"end":3708,"length":12},"newText":""},{"span":{"start":3716,"end":3720,"length":4},"newText":""},{"span":{"start":3736,"end":3740,"length":4},"newText":""},{"span":{"start":3745,"end":3749,"length":4},"newText":""},{"span":{"start":3765,"end":3769,"length":4},"newText":""},{"span":{"start":3856,"end":3859,"length":3},"newText":""},{"span":{"start":3875,"end":3880,"length":5},"newText":""},{"span":{"start":3883,"end":3894,"length":11},"newText":""},{"span":{"start":3910,"end":3911,"length":1},"newText":""},{"span":{"start":3936,"end":3943,"length":7},"newText":""},{"span":{"start":3963,"end":3968,"length":5},"newText":""},{"span":{"start":3995,"end":3997,"length":2},"newText":""},{"span":{"start":4017,"end":4027,"length":10},"newText":""},{"span":{"start":4090,"end":4096,"length":6},"newText":""},{"span":{"start":4112,"end":4118,"length":6},"newText":""},{"span":{"start":4128,"end":4138,"length":10},"newText":""},{"span":{"start":4154,"end":4156,"length":2},"newText":""},{"span":{"start":4203,"end":4209,"length":6},"newText":""},{"span":{"start":4225,"end":4231,"length":6},"newText":""},{"span":{"start":4234,"end":4236,"length":2},"newText":""},{"span":{"start":4252,"end":4266,"length":14},"newText":""},{"span":{"start":4291,"end":4292,"length":1},"newText":""},{"span":{"start":4312,"end":4327,"length":15},"newText":""},{"span":{"start":4356,"end":4372,"length":16},"newText":""},{"span":{"start":4510,"end":4526,"length":16},"newText":""},{"span":{"start":4534,"end":4546,"length":12},"newText":""},{"span":{"start":4565,"end":4568,"length":3},"newText":""},{"span":{"start":4584,"end":4589,"length":5},"newText":""},{"span":{"start":4594,"end":4597,"length":3},"newText":""},{"span":{"start":4613,"end":4618,"length":5},"newText":""},{"span":{"start":4717,"end":4720,"length":3},"newText":""},{"span":{"start":4736,"end":4741,"length":5},"newText":""},{"span":{"start":4744,"end":4755,"length":11},"newText":""},{"span":{"start":4771,"end":4772,"length":1},"newText":""},{"span":{"start":4797,"end":4804,"length":7},"newText":""},{"span":{"start":4824,"end":4829,"length":5},"newText":""},{"span":{"start":4864,"end":4867,"length":3},"newText":""},{"span":{"start":4887,"end":4896,"length":9},"newText":""},{"span":{"start":4901,"end":4907,"length":6},"newText":""},{"span":{"start":4931,"end":4937,"length":6},"newText":""},{"span":{"start":5017,"end":5017,"length":0},"newText":"\r\n "},{"span":{"start":5018,"end":5018,"length":0},"newText":" "},{"span":{"start":5036,"end":5048,"length":12},"newText":""},{"span":{"start":5156,"end":5168,"length":12},"newText":""},{"span":{"start":5174,"end":5177,"length":3},"newText":""},{"span":{"start":5197,"end":5206,"length":9},"newText":""},{"span":{"start":5212,"end":5219,"length":7},"newText":""},{"span":{"start":5235,"end":5240,"length":5},"newText":""},{"span":{"start":5248,"end":5251,"length":3},"newText":""},{"span":{"start":5267,"end":5272,"length":5},"newText":""},{"span":{"start":5277,"end":5280,"length":3},"newText":""},{"span":{"start":5296,"end":5301,"length":5},"newText":""},{"span":{"start":5326,"end":5329,"length":3},"newText":""},{"span":{"start":5349,"end":5354,"length":5},"newText":""},{"span":{"start":5384,"end":5387,"length":3},"newText":""},{"span":{"start":5407,"end":5412,"length":5},"newText":""},{"span":{"start":5417,"end":5420,"length":3},"newText":""},{"span":{"start":5444,"end":5449,"length":5},"newText":""},{"span":{"start":5524,"end":5526,"length":2},"newText":""},{"span":{"start":5550,"end":5556,"length":6},"newText":""},{"span":{"start":5559,"end":5569,"length":10},"newText":""},{"span":{"start":5593,"end":5595,"length":2},"newText":""},{"span":{"start":5684,"end":5690,"length":6},"newText":""},{"span":{"start":5718,"end":5724,"length":6},"newText":""},{"span":{"start":5773,"end":5774,"length":1},"newText":""},{"span":{"start":5798,"end":5809,"length":11},"newText":""},{"span":{"start":5815,"end":5820,"length":5},"newText":""},{"span":{"start":5844,"end":5847,"length":3},"newText":""},{"span":{"start":5850,"end":5855,"length":5},"newText":""},{"span":{"start":5879,"end":5882,"length":3},"newText":""},{"span":{"start":5959,"end":5963,"length":4},"newText":""},{"span":{"start":5987,"end":5991,"length":4},"newText":""},{"span":{"start":5994,"end":6006,"length":12},"newText":""},{"span":{"start":6148,"end":6160,"length":12},"newText":""},{"span":{"start":6210,"end":6214,"length":4},"newText":""},{"span":{"start":6238,"end":6246,"length":8},"newText":""},{"span":{"start":6252,"end":6260,"length":8},"newText":""},{"span":{"start":6311,"end":6319,"length":8},"newText":""},{"span":{"start":6392,"end":6399,"length":7},"newText":""},{"span":{"start":6423,"end":6424,"length":1},"newText":""},{"span":{"start":6427,"end":6434,"length":7},"newText":""},{"span":{"start":6458,"end":6463,"length":5},"newText":""},{"span":{"start":6551,"end":6554,"length":3},"newText":""},{"span":{"start":6582,"end":6591,"length":9},"newText":""},{"span":{"start":6639,"end":6645,"length":6},"newText":""},{"span":{"start":6669,"end":6675,"length":6},"newText":""},{"span":{"start":6681,"end":6683,"length":2},"newText":""},{"span":{"start":6707,"end":6713,"length":6},"newText":""},{"span":{"start":6716,"end":6718,"length":2},"newText":""},{"span":{"start":6742,"end":6748,"length":6},"newText":""},{"span":{"start":6754,"end":6756,"length":2},"newText":""},{"span":{"start":6780,"end":6786,"length":6},"newText":""},{"span":{"start":6789,"end":6790,"length":1},"newText":""},{"span":{"start":6814,"end":6825,"length":11},"newText":""},{"span":{"start":6867,"end":6872,"length":5},"newText":""},{"span":{"start":6896,"end":6899,"length":3},"newText":""},{"span":{"start":6902,"end":6907,"length":5},"newText":""},{"span":{"start":6927,"end":6930,"length":3},"newText":""},{"span":{"start":6936,"end":6941,"length":5},"newText":""},{"span":{"start":6957,"end":6960,"length":3},"newText":""},{"span":{"start":6970,"end":6975,"length":5},"newText":""},{"span":{"start":6991,"end":6994,"length":3},"newText":""},{"span":{"start":7019,"end":7024,"length":5},"newText":""},{"span":{"start":7044,"end":7047,"length":3},"newText":""},{"span":{"start":7076,"end":7081,"length":5},"newText":""},{"span":{"start":7101,"end":7104,"length":3},"newText":""},{"span":{"start":7174,"end":7178,"length":4},"newText":""},{"span":{"start":7194,"end":7198,"length":4},"newText":""},{"span":{"start":7208,"end":7212,"length":4},"newText":""},{"span":{"start":7228,"end":7232,"length":4},"newText":""},{"span":{"start":7313,"end":7317,"length":4},"newText":""},{"span":{"start":7333,"end":7337,"length":4},"newText":""},{"span":{"start":7340,"end":7351,"length":11},"newText":""},{"span":{"start":7367,"end":7368,"length":1},"newText":""},{"span":{"start":7393,"end":7400,"length":7},"newText":""},{"span":{"start":7420,"end":7425,"length":5},"newText":""},{"span":{"start":7468,"end":7471,"length":3},"newText":""},{"span":{"start":7491,"end":7500,"length":9},"newText":""},{"span":{"start":7505,"end":7511,"length":6},"newText":""},{"span":{"start":7535,"end":7541,"length":6},"newText":""},{"span":{"start":7606,"end":7616,"length":10},"newText":""},{"span":{"start":7644,"end":7646,"length":2},"newText":""},{"span":{"start":7668,"end":7674,"length":6},"newText":""},{"span":{"start":7698,"end":7704,"length":6},"newText":""},{"span":{"start":7710,"end":7712,"length":2},"newText":""},{"span":{"start":7732,"end":7742,"length":10},"newText":""},{"span":{"start":7748,"end":7754,"length":6},"newText":""},{"span":{"start":7770,"end":7776,"length":6},"newText":""},{"span":{"start":7784,"end":7786,"length":2},"newText":""},{"span":{"start":7802,"end":7808,"length":6},"newText":""},{"span":{"start":7813,"end":7814,"length":1},"newText":""},{"span":{"start":7830,"end":7837,"length":7},"newText":""},{"span":{"start":7901,"end":7902,"length":1},"newText":""},{"span":{"start":7918,"end":7925,"length":7},"newText":""},{"span":{"start":7928,"end":7929,"length":1},"newText":""},{"span":{"start":7945,"end":7956,"length":11},"newText":""},{"span":{"start":7981,"end":7985,"length":4},"newText":""},{"span":{"start":8005,"end":8013,"length":8},"newText":""},{"span":{"start":8039,"end":8047,"length":8},"newText":""},{"span":{"start":8067,"end":8071,"length":4},"newText":""},{"span":{"start":8101,"end":8112,"length":11},"newText":""},{"span":{"start":8136,"end":8137,"length":1},"newText":""},{"span":{"start":8232,"end":8244,"length":12},"newText":""},{"span":{"start":8271,"end":8287,"length":16},"newText":""},{"span":{"start":8369,"end":8381,"length":12},"newText":""},{"span":{"start":8404,"end":8416,"length":12},"newText":""},{"span":{"start":8440,"end":8452,"length":12},"newText":""},{"span":{"start":8476,"end":8484,"length":8},"newText":""},{"span":{"start":8505,"end":8513,"length":8},"newText":""},{"span":{"start":8611,"end":8619,"length":8},"newText":""},{"span":{"start":8638,"end":8650,"length":12},"newText":""},{"span":{"start":8695,"end":8707,"length":12},"newText":""},{"span":{"start":8747,"end":8759,"length":12},"newText":""},{"span":{"start":8831,"end":8843,"length":12},"newText":""},{"span":{"start":8867,"end":8867,"length":0},"newText":"~\r\n\r\n"},{"span":{"start":8875,"end":8886,"length":11},"newText":" "},{"span":{"start":8911,"end":8918,"length":7},"newText":" "},{"span":{"start":8950,"end":8957,"length":7},"newText":" "},{"span":{"start":8990,"end":8996,"length":6},"newText":" "},{"span":{"start":9006,"end":9012,"length":6},"newText":" "},{"span":{"start":9076,"end":9082,"length":6},"newText":" "},{"span":{"start":9085,"end":9092,"length":7},"newText":" "},{"span":{"start":9117,"end":9125,"length":8},"newText":" "},{"span":{"start":9169,"end":9177,"length":8},"newText":" "},{"span":{"start":9226,"end":9233,"length":7},"newText":" "},{"span":{"start":9241,"end":9247,"length":6},"newText":" "},{"span":{"start":9252,"end":9258,"length":6},"newText":" "},{"span":{"start":9326,"end":9332,"length":6},"newText":" "},{"span":{"start":9335,"end":9342,"length":7},"newText":" "},{"span":{"start":9367,"end":9375,"length":8},"newText":" "},{"span":{"start":9419,"end":9427,"length":8},"newText":" "},{"span":{"start":9480,"end":9487,"length":7},"newText":" "},{"span":{"start":9495,"end":9501,"length":6},"newText":" "},{"span":{"start":9502,"end":9515,"length":13},"newText":""},{"span":{"start":9517,"end":9517,"length":0},"newText":" "},{"span":{"start":9525,"end":9525,"length":0},"newText":"\u003C/div\u003E\r\n"},{"span":{"start":9547,"end":9551,"length":4},"newText":""},{"span":{"start":9595,"end":9603,"length":8},"newText":""},{"span":{"start":9672,"end":9680,"length":8},"newText":""},{"span":{"start":9730,"end":9738,"length":8},"newText":""},{"span":{"start":9753,"end":9765,"length":12},"newText":""},{"span":{"start":9802,"end":9814,"length":12},"newText":""},{"span":{"start":9829,"end":9845,"length":16},"newText":""},{"span":{"start":9875,"end":9887,"length":12},"newText":""},{"span":{"start":9902,"end":9910,"length":8},"newText":""},{"span":{"start":9925,"end":9933,"length":8},"newText":""},{"span":{"start":9951,"end":9959,"length":8},"newText":""},{"span":{"start":9974,"end":9986,"length":12},"newText":""},{"span":{"start":10055,"end":10063,"length":8},"newText":""},{"span":{"start":10080,"end":10088,"length":8},"newText":""},{"span":{"start":10110,"end":10114,"length":4},"newText":""},{"span":{"start":10157,"end":10165,"length":8},"newText":""},{"span":{"start":10235,"end":10243,"length":8},"newText":""},{"span":{"start":10287,"end":10295,"length":8},"newText":""},{"span":{"start":10365,"end":10365,"length":0},"newText":"\r\n "},{"span":{"start":10425,"end":10433,"length":8},"newText":""},{"span":{"start":10503,"end":10511,"length":8},"newText":""},{"span":{"start":10541,"end":10549,"length":8},"newText":""},{"span":{"start":10576,"end":10584,"length":8},"newText":""},{"span":{"start":10604,"end":10612,"length":8},"newText":""},{"span":{"start":10634,"end":10638,"length":4},"newText":""},{"span":{"start":10679,"end":10687,"length":8},"newText":""},{"span":{"start":10771,"end":10779,"length":8},"newText":""},{"span":{"start":10821,"end":10829,"length":8},"newText":""},{"span":{"start":10844,"end":10856,"length":12},"newText":""},{"span":{"start":10886,"end":10894,"length":8},"newText":""},{"span":{"start":10909,"end":10917,"length":8},"newText":""},{"span":{"start":10935,"end":10943,"length":8},"newText":""},{"span":{"start":10958,"end":10967,"length":9},"newText":""},{"span":{"start":10990,"end":11013,"length":23},"newText":""},{"span":{"start":11032,"end":11055,"length":23},"newText":""},{"span":{"start":11076,"end":11085,"length":9},"newText":" "},{"span":{"start":11117,"end":11124,"length":7},"newText":" "},{"span":{"start":11172,"end":11181,"length":9},"newText":" "},{"span":{"start":11213,"end":11220,"length":7},"newText":" "},{"span":{"start":11262,"end":11262,"length":0},"newText":" "},{"span":{"start":11278,"end":11286,"length":8},"newText":""},{"span":{"start":11303,"end":11311,"length":8},"newText":""},{"span":{"start":11359,"end":11367,"length":8},"newText":""},{"span":{"start":11457,"end":11465,"length":8},"newText":""},{"span":{"start":11536,"end":11544,"length":8},"newText":""},{"span":{"start":11575,"end":11583,"length":8},"newText":""},{"span":{"start":11594,"end":11600,"length":6},"newText":" "},{"span":{"start":11652,"end":11652,"length":0},"newText":"\r\n"},{"span":{"start":11653,"end":11653,"length":0},"newText":" "},{"span":{"start":11707,"end":11714,"length":7},"newText":" "},{"span":{"start":11801,"end":11801,"length":0},"newText":" "},{"span":{"start":11805,"end":11812,"length":7},"newText":" "},{"span":{"start":11909,"end":11909,"length":0},"newText":"\r\n"},{"span":{"start":11910,"end":11910,"length":0},"newText":" "},{"span":{"start":11926,"end":11932,"length":6},"newText":" "},{"span":{"start":11943,"end":11949,"length":6},"newText":" "},{"span":{"start":12003,"end":12009,"length":6},"newText":" "},{"span":{"start":12012,"end":12019,"length":7},"newText":" "},{"span":{"start":12073,"end":12081,"length":8},"newText":" "},{"span":{"start":12127,"end":12135,"length":8},"newText":" "},{"span":{"start":12150,"end":12157,"length":7},"newText":" "},{"span":{"start":12168,"end":12177,"length":9},"newText":""},{"span":{"start":12193,"end":12193,"length":0},"newText":"~\r\n"},{"span":{"start":12197,"end":12197,"length":0},"newText":" "},{"span":{"start":12217,"end":12225,"length":8},"newText":""},{"span":{"start":12247,"end":12251,"length":4},"newText":""},{"span":{"start":12293,"end":12301,"length":8},"newText":""},{"span":{"start":12360,"end":12368,"length":8},"newText":""},{"span":{"start":12383,"end":12395,"length":12},"newText":""},{"span":{"start":12438,"end":12450,"length":12},"newText":""},{"span":{"start":12479,"end":12491,"length":12},"newText":""},{"span":{"start":12521,"end":12533,"length":12},"newText":""},{"span":{"start":12571,"end":12583,"length":12},"newText":""},{"span":{"start":12621,"end":12633,"length":12},"newText":""},{"span":{"start":12656,"end":12668,"length":12},"newText":""},{"span":{"start":12694,"end":12706,"length":12},"newText":""},{"span":{"start":12735,"end":12747,"length":12},"newText":""},{"span":{"start":12809,"end":12821,"length":12},"newText":""},{"span":{"start":12844,"end":12860,"length":16},"newText":""},{"span":{"start":12890,"end":12906,"length":16},"newText":""},{"span":{"start":12983,"end":12999,"length":16},"newText":""},{"span":{"start":13033,"end":13049,"length":16},"newText":""},{"span":{"start":13101,"end":13117,"length":16},"newText":""},{"span":{"start":13148,"end":13168,"length":20},"newText":""},{"span":{"start":13251,"end":13275,"length":24},"newText":""},{"span":{"start":13333,"end":13357,"length":24},"newText":""},{"span":{"start":13394,"end":13418,"length":24},"newText":""},{"span":{"start":13456,"end":13476,"length":20},"newText":""},{"span":{"start":13584,"end":13608,"length":24},"newText":""},{"span":{"start":13662,"end":13686,"length":24},"newText":""},{"span":{"start":13716,"end":13729,"length":13},"newText":" "},{"span":{"start":13739,"end":13751,"length":12},"newText":" "},{"span":{"start":13816,"end":13829,"length":13},"newText":" "},{"span":{"start":13867,"end":13880,"length":13},"newText":" "},{"span":{"start":13904,"end":13917,"length":13},"newText":" "},{"span":{"start":13927,"end":13939,"length":12},"newText":" "},{"span":{"start":13992,"end":14005,"length":13},"newText":" "},{"span":{"start":14050,"end":14063,"length":13},"newText":" "},{"span":{"start":14087,"end":14100,"length":13},"newText":" "},{"span":{"start":14110,"end":14122,"length":12},"newText":" "},{"span":{"start":14172,"end":14185,"length":13},"newText":" "},{"span":{"start":14219,"end":14232,"length":13},"newText":" "},{"span":{"start":14286,"end":14299,"length":13},"newText":" "},{"span":{"start":14309,"end":14321,"length":12},"newText":" "},{"span":{"start":14365,"end":14378,"length":13},"newText":" "},{"span":{"start":14416,"end":14429,"length":13},"newText":" "},{"span":{"start":14483,"end":14496,"length":13},"newText":" "},{"span":{"start":14506,"end":14518,"length":12},"newText":" "},{"span":{"start":14559,"end":14572,"length":13},"newText":" "},{"span":{"start":14597,"end":14610,"length":13},"newText":" "},{"span":{"start":14652,"end":14654,"length":2},"newText":""},{"span":{"start":14675,"end":14694,"length":19},"newText":""},{"span":{"start":14698,"end":14700,"length":2},"newText":""},{"span":{"start":14702,"end":14722,"length":20},"newText":""},{"span":{"start":14752,"end":14754,"length":2},"newText":""},{"span":{"start":14758,"end":14758,"length":0},"newText":"\r\n"},{"span":{"start":14760,"end":14760,"length":0},"newText":" "},{"span":{"start":14784,"end":14784,"length":0},"newText":"/*~~~~*/\r\n"},{"span":{"start":14845,"end":14869,"length":24},"newText":""},{"span":{"start":14905,"end":14921,"length":16},"newText":""},{"span":{"start":14952,"end":14968,"length":16},"newText":""},{"span":{"start":14999,"end":15015,"length":16},"newText":""},{"span":{"start":15042,"end":15054,"length":12},"newText":""},{"span":{"start":15077,"end":15089,"length":12},"newText":""},{"span":{"start":15115,"end":15127,"length":12},"newText":""},{"span":{"start":15149,"end":15157,"length":8},"newText":""},{"span":{"start":15172,"end":15180,"length":8},"newText":""},{"span":{"start":15198,"end":15206,"length":8},"newText":""},{"span":{"start":15221,"end":15233,"length":12},"newText":""},{"span":{"start":15263,"end":15271,"length":8},"newText":""},{"span":{"start":15288,"end":15296,"length":8},"newText":""},{"span":{"start":15388,"end":15392,"length":4},"newText":""},{"span":{"start":15763,"end":15767,"length":4},"newText":""},{"span":{"start":15792,"end":15810,"length":18},"newText":""},{"span":{"start":15836,"end":15837,"length":1},"newText":""},{"span":{"start":15839,"end":15843,"length":4},"newText":""},{"span":{"start":15944,"end":15948,"length":4},"newText":""},{"span":{"start":15993,"end":16010,"length":17},"newText":""},{"span":{"start":16103,"end":16104,"length":1},"newText":" "},{"span":{"start":16122,"end":16124,"length":2},"newText":" "},{"span":{"start":16197,"end":16199,"length":2},"newText":" "},{"span":{"start":16202,"end":16205,"length":3},"newText":" "},{"span":{"start":16314,"end":16318,"length":4},"newText":" "},{"span":{"start":16366,"end":16369,"length":3},"newText":" "},{"span":{"start":16380,"end":16382,"length":2},"newText":" "},{"span":{"start":16385,"end":16387,"length":2},"newText":" "},{"span":{"start":16452,"end":16454,"length":2},"newText":" "},{"span":{"start":16457,"end":16460,"length":3},"newText":" "},{"span":{"start":16569,"end":16573,"length":4},"newText":" "},{"span":{"start":16622,"end":16625,"length":3},"newText":" "},{"span":{"start":16636,"end":16638,"length":2},"newText":" "},{"span":{"start":16641,"end":16643,"length":2},"newText":" "},{"span":{"start":16759,"end":16763,"length":4},"newText":""},{"span":{"start":16854,"end":16858,"length":4},"newText":""},{"span":{"start":16923,"end":16927,"length":4},"newText":""}] \ No newline at end of file diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/RealWorldMixedIndentation/InitialDocument.txt b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/RealWorldMixedIndentation/InitialDocument.txt new file mode 100644 index 00000000000..21445a571e4 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/FormattingLog/RealWorldMixedIndentation/InitialDocument.txt @@ -0,0 +1,354 @@ +@using App.Models.Feedback +@using App.Models.Surveys +@using App.Client.Web.Pages.Surveys.Forms.Components.FormViewer + +@inject IJSRuntime JS +@inject HttpClient Http +@inject ILogger Logger + + + +
+

@AttemptModel.Attempt.Recipient.FullName

+

@(AttemptModel.Attempt.RecipientAddress is Attempt.PhoneNumber phoneNumber ? phoneNumber.Number : String.Empty)

+

@(AttemptModel.Attempt.StartedAt?.LocalDateTime.ToString("F"))

+ @if (AttemptModel.Attempt.IsRecordingAvailable) + { + + } +
+
+ + + + + +
+
+
+
+ @switch (_selectedAttemptDetail) + { + case AttemptDetail.Details: +
+ @if (AttemptModel.Attempt.StartedAt.HasValue) + { +
+ +

@AttemptModel.Attempt.StartedAt.Value.LocalDateTime.ToString("F")

+
+ } + + @if (AttemptModel.Attempt is CallAttempt callAttempt && callAttempt.EndedAt.HasValue) + { +
+ +

@callAttempt.EndedAt.Value.LocalDateTime.ToString("F")

+
+ + @if (AttemptModel.Attempt.StartedAt.HasValue) + { +
+ +

@((callAttempt.EndedAt.Value - AttemptModel.Attempt.StartedAt.Value).ToString(@"hh\:mm\:ss"))

+
+ } + } + + @if (AttemptModel.Attempt is CallAttempt { OutboundPhoneNumber: not null } callAttemptWithNumber) + { + + } + +
+ +

+ @if (AttemptModel.Execution is PhoneCallCampaignExecution phoneExecution) + { + + @phoneExecution.RecipientListHeader.DisplayName + + } + else if (AttemptModel.Execution is ManualCampaignExecution manualExecution) + { + + @manualExecution.RecipientListHeader.DisplayName + + } + else if (AttemptModel.Execution is StubCampaignExecution stubExecution) + { + + @stubExecution.RecipientListHeader.DisplayName + + } + else + { + Címzett lista nem érhető el + } +

+
+ +
+ +

@(AttemptModel.Attempt.Recipient.FullName ?? "Nincs megadva")

+
+ + @if (AttemptModel.Attempt.RecipientAddress is Attempt.PhoneNumber phoneAddress) + { +
+ +

+ + @phoneAddress.Number + +

+
+ } + + @if (AttemptModel.Attempt.Recipient.Dimensions?.Any() == true) + { +
+ +
+ @foreach (var dimension in AttemptModel.Attempt.Recipient.Dimensions) + { +

@dimension.Key: @dimension.Value

+ } +
+
+ } + + @if (!string.IsNullOrWhiteSpace(AttemptModel.Attempt.Recipient.FreeFormAddress)) + { +
+ +

@AttemptModel.Attempt.Recipient.FreeFormAddress

+
+ } + +
+ +

@AttemptModel.Attempt.Id

+
+ + @if (!AttemptModel.Attempt.CallProviderCallId.IsNullOrEmpty()) + { +
+ +

@AttemptModel.Attempt.CallProviderCallId

+
+ } + + @if (!AttemptModel.Attempt.ExternalProviderCallId.IsNullOrEmpty()) + { +
+ +

@AttemptModel.Attempt.ExternalProviderCallId

+
+ } +
+ break; + + case AttemptDetail.Transcript: + if (!AttemptModel.Attempt.IsTranscriptAvailable) break; + if (AttemptModel.Transcript is null) + { + if (_transcriptLoading) + { + + } + } + else + { + + } + + break; + + case AttemptDetail.Recording: + if (!AttemptModel.Attempt.IsRecordingAvailable) break; + +
+ +
+ break; + + case AttemptDetail.Answers: + if (AttemptModel.Attempt.Status is not AnsweredAttemptStatus) break; + + if (_surveySnapshot is null) + { + + } + else + { + + } + +
+ + + @if (_isEditingAnswers && _formViewer.HasAnyChanges) + { + + } +
+ break; + + case AttemptDetail.Statuses: + @if (_attemptStatuses is { } attemptStatuses) + { + + + + + + + + + @foreach (var status in attemptStatuses) + { + + + + + } + +
IdőÁllapot
@status.Timestamp.LocalDateTime.ToString() + @switch (status.Value) + { + case DeclinedAttemptStatus { Reason: string reason }: + Visszautasítva: + @reason + break; + + case RescheduledAttemptStatus { RescheduledTo: DateTimeOffset rescheduledTo }: + Átütemezve: + @rescheduledTo.LocalDateTime + break; + + case OutboundAddressConcurrencyLimitReachedAttemptStatus mceas: + Egyidejű korlát elérve: + @mceas.OutboundAddress + break; + + case OutboundAddressUnavailableAttemptStatus mceas: + Kimenő kapcsolat nem elérhető: + @mceas.OutboundAddress + break; + + case ModelConfigurationErrorAttemptStatus mceas: + Konfigurációs hiba: + @mceas.ProviderErrorCode @mceas.ProviderErrorMessage + break; + + case CallProviderErrorAttemptStatus mceas: + Hívás szolgáltató hiba: + @mceas.ProviderErrorCode @mceas.ProviderErrorMessage + break; + + case InvalidAddressAttemptStatus mceas: + Hibás cím: + @mceas.Address @mceas.ProviderErrorCode @mceas.ProviderErrorMessage + break; + + default: + @status.Value.ToDisplayString() + break; + } +
+ } + else + { + + } + + break; + } +
+ @if (_showFeedback) + { + + } + + @if (_showReevaluateDialog) + { + + } +
+ + @if (AttemptModel.Attempt is { PreviousAttemptLink: { } previousLink }) + { + + } + @if (AttemptModel.Attempt is { NextAttemptLink: { } nextLink }) + { + + } + @if (AttemptModel is { Attempt.IsTranscriptAvailable: true } or { Attempt.IsRecordingAvailable: true }) + { + + } + +
\ No newline at end of file