Switch formatting engine over to using `TextChange` instead of `TextEdit` (#10855)

Fixes https://github.com/dotnet/razor/issues/10842

The formatting self-nerd-sniping continues.

The formatting engine was written to use the LSP `TextEdit` class, which
makes some sense, but also uses Roslyn APIs like `SourceText` a lot,
which uses the `TextChange` struct instead. This meant lots of code to
convert to and from the two types. Changing the whole formatting engine
over to `TextChange`, and using more `TextSpan`, `LinePositionSpan` etc.
removes a lot of this code. It also makes a lot more sense in cohosting,
to boot.

I wouldn't claim that I've gone through and improved the perf of the
formatting engine, but rather I've use the changes to lead me to things
that need fixing. ie, I started out moving from `TextEdit[]` to
`ImmutableArray<TextChange>`, and this let me to places where pooled
array builders could be used, and places where `Range` and `Position`
were used which didn't make much sense, and then the constructor for
`LinePosition` threw at one point because it turns out we were only
using the `Line` property from the `Position` that used to be used, and
so never validated the characters, so that API moved to `int`, etc.

TL;DR the commits tell the story, and there could well be something I
missed, if it never came across my plate for another reason.
This commit is contained in:
David Wengier 2024-09-10 15:08:15 +10:00 коммит произвёл GitHub
Родитель f9e09f5bbb 05e317fc98
Коммит 2511efed42
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
46 изменённых файлов: 607 добавлений и 653 удалений

Просмотреть файл

@ -5,14 +5,12 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer;
@ -112,11 +110,11 @@ public class RazorCSharpFormattingBenchmark : RazorLanguageServerBenchmarkBase
{
var documentContext = new DocumentContext(DocumentUri, DocumentSnapshot, projectContext: null);
var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits: [], range: null, new RazorFormattingOptions(), CancellationToken.None);
var changes = await RazorFormattingService.GetDocumentFormattingChangesAsync(documentContext, htmlEdits: [], span: null, new RazorFormattingOptions(), CancellationToken.None);
#if DEBUG
// For debugging purposes only.
var changedText = DocumentText.WithChanges(edits.Select(DocumentText.GetTextChange));
var changedText = DocumentText.WithChanges(changes);
_ = changedText.ToString();
#endif
}

Просмотреть файл

@ -187,14 +187,20 @@ internal class OnAutoInsertEndpoint(
var options = RazorFormattingOptions.From(originalRequest.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
var mappedEdit = delegatedResponse.TextEditFormat == InsertTextFormat.Snippet
? await _razorFormattingService.GetCSharpSnippetFormattingEditAsync(documentContext, [delegatedResponse.TextEdit], options, cancellationToken).ConfigureAwait(false)
: await _razorFormattingService.GetSingleCSharpEditAsync(documentContext, delegatedResponse.TextEdit, options, cancellationToken).ConfigureAwait(false);
if (mappedEdit is null)
var csharpSourceText = await documentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false);
var textChange = csharpSourceText.GetTextChange(delegatedResponse.TextEdit);
var mappedChange = delegatedResponse.TextEditFormat == InsertTextFormat.Snippet
? await _razorFormattingService.TryGetCSharpSnippetFormattingEditAsync(documentContext, [textChange], options, cancellationToken).ConfigureAwait(false)
: await _razorFormattingService.TryGetSingleCSharpEditAsync(documentContext, textChange, options, cancellationToken).ConfigureAwait(false);
if (mappedChange is not { } change)
{
return null;
}
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var mappedEdit = sourceText.GetTextEdit(change);
return new VSInternalDocumentOnAutoInsertResponseItem()
{
TextEdit = mappedEdit,

Просмотреть файл

@ -61,18 +61,20 @@ internal sealed class DefaultCSharpCodeActionResolver(
cancellationToken.ThrowIfCancellationRequested();
var csharpTextEdits = textDocumentEdit.Edits;
var csharpSourceText = await documentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false);
var csharpTextChanges = textDocumentEdit.Edits.SelectAsArray(csharpSourceText.GetTextChange);
// Remaps the text edits from the generated C# to the razor file,
// as well as applying appropriate formatting.
var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
var formattedChange = await _razorFormattingService.TryGetCSharpCodeActionEditAsync(
documentContext,
csharpTextEdits,
csharpTextChanges,
new RazorFormattingOptions(),
cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var codeDocumentIdentifier = new OptionalVersionedTextDocumentIdentifier()
{
Uri = csharpParams.RazorFileIdentifier.Uri
@ -83,7 +85,7 @@ internal sealed class DefaultCSharpCodeActionResolver(
new TextDocumentEdit()
{
TextDocument = codeDocumentIdentifier,
Edits = formattedEdit is null ? [] : [formattedEdit],
Edits = formattedChange is { } change ? [sourceText.GetTextEdit(change)] : [],
}
}
};

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@ -21,6 +22,7 @@ using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using CSharpSyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
@ -211,13 +213,13 @@ internal sealed class GenerateMethodCodeActionResolver(
CodeBlockBraceOnNextLine = _razorLSPOptionsMonitor.CurrentValue.CodeBlockBraceOnNextLine
};
var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
var formattedChange = await _razorFormattingService.TryGetCSharpCodeActionEditAsync(
documentContext,
result,
result.SelectAsArray(code.Source.Text.GetTextChange),
formattingOptions,
cancellationToken).ConfigureAwait(false);
edits = formattedEdit is null ? [] : [formattedEdit];
edits = formattedChange is { } change ? [code.Source.Text.GetTextEdit(change)] : [];
}
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
@ -114,17 +115,24 @@ internal class DelegatedCompletionItemResolver(
var options = RazorFormattingOptions.From(formattingOptions, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var csharpSourceText = await documentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false);
if (resolvedCompletionItem.TextEdit is not null)
{
if (resolvedCompletionItem.TextEdit.Value.TryGetFirst(out var textEdit))
{
var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync(
var textChange = csharpSourceText.GetTextChange(textEdit);
var formattedTextChange = await _formattingService.TryGetCSharpSnippetFormattingEditAsync(
documentContext,
[textEdit],
[textChange],
options,
cancellationToken).ConfigureAwait(false);
resolvedCompletionItem.TextEdit = formattedTextEdit;
if (formattedTextChange is { } change)
{
resolvedCompletionItem.TextEdit = sourceText.GetTextEdit(change);
}
}
else
{
@ -136,13 +144,14 @@ internal class DelegatedCompletionItemResolver(
if (resolvedCompletionItem.AdditionalTextEdits is not null)
{
var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync(
var additionalChanges = resolvedCompletionItem.AdditionalTextEdits.SelectAsArray(csharpSourceText.GetTextChange);
var formattedTextChange = await _formattingService.TryGetCSharpSnippetFormattingEditAsync(
documentContext,
resolvedCompletionItem.AdditionalTextEdits,
additionalChanges,
options,
cancellationToken).ConfigureAwait(false);
resolvedCompletionItem.AdditionalTextEdits = formattedTextEdit is null ? null : [formattedTextEdit];
resolvedCompletionItem.AdditionalTextEdits = formattedTextChange is { } change ? [sourceText.GetTextEdit(change)] : null;
}
return resolvedCompletionItem;

Просмотреть файл

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
@ -54,8 +56,9 @@ internal class DocumentFormattingEndpoint(
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
var htmlEdits = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false);
var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range: null, options, cancellationToken).ConfigureAwait(false);
return edits;
var htmlChanges = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false);
var changes = await _razorFormattingService.GetDocumentFormattingChangesAsync(documentContext, htmlChanges, span: null, options, cancellationToken).ConfigureAwait(false);
return [.. changes.Select(codeDocument.Source.Text.GetTextEdit)];
}
}

Просмотреть файл

@ -5,6 +5,7 @@ using System;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
@ -97,15 +98,15 @@ internal class DocumentOnTypeFormattingEndpoint(
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
TextEdit[] formattedEdits;
ImmutableArray<TextChange> formattedChanges;
if (triggerCharacterKind == RazorLanguageKind.CSharp)
{
formattedEdits = await _razorFormattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false);
formattedChanges = await _razorFormattingService.GetCSharpOnTypeFormattingChangesAsync(documentContext, options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false);
}
else if (triggerCharacterKind == RazorLanguageKind.Html)
{
var htmlEdits = await _htmlFormatter.GetOnTypeFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Position, request.Character, request.Options, cancellationToken).ConfigureAwait(false);
formattedEdits = await _razorFormattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false);
var htmlChanges = await _htmlFormatter.GetOnTypeFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Position, request.Character, request.Options, cancellationToken).ConfigureAwait(false);
formattedChanges = await _razorFormattingService.GetHtmlOnTypeFormattingChangesAsync(documentContext, htmlChanges, options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false);
}
else
{
@ -113,13 +114,13 @@ internal class DocumentOnTypeFormattingEndpoint(
return null;
}
if (formattedEdits.Length == 0)
if (formattedChanges.Length == 0)
{
_logger.LogInformation($"No formatting changes were necessary");
return null;
}
_logger.LogInformation($"Returning {formattedEdits.Length} final formatted results.");
return formattedEdits;
_logger.LogInformation($"Returning {formattedChanges.Length} final formatted results.");
return [.. formattedChanges.Select(sourceText.GetTextEdit)];
}
}

Просмотреть файл

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
@ -54,9 +56,9 @@ internal class DocumentRangeFormattingEndpoint(
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
var htmlEdits = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false);
var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, request.Range, options, cancellationToken).ConfigureAwait(false);
var htmlChanges = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false);
var changes = await _razorFormattingService.GetDocumentFormattingChangesAsync(documentContext, htmlChanges, request.Range.ToLinePositionSpan(), options, cancellationToken).ConfigureAwait(false);
return edits;
return [.. changes.Select(codeDocument.Source.Text.GetTextEdit)];
}
}

Просмотреть файл

@ -2,6 +2,8 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -19,7 +21,7 @@ internal sealed class HtmlFormatter(
{
private readonly IClientConnection _clientConnection = clientConnection;
public async Task<TextEdit[]> GetDocumentFormattingEditsAsync(
public async Task<ImmutableArray<TextChange>> GetDocumentFormattingEditsAsync(
IDocumentSnapshot documentSnapshot,
Uri uri,
FormattingOptions options,
@ -40,10 +42,16 @@ internal sealed class HtmlFormatter(
@params,
cancellationToken).ConfigureAwait(false);
return result?.Edits ?? [];
if (result?.Edits is null)
{
return [];
}
var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false);
return result.Edits.SelectAsArray(sourceText.GetTextChange);
}
public async Task<TextEdit[]> GetOnTypeFormattingEditsAsync(
public async Task<ImmutableArray<TextChange>> GetOnTypeFormattingEditsAsync(
IDocumentSnapshot documentSnapshot,
Uri uri,
Position position,
@ -65,7 +73,13 @@ internal sealed class HtmlFormatter(
@params,
cancellationToken).ConfigureAwait(false);
return result?.Edits ?? [];
if (result?.Edits is null)
{
return [];
}
var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false);
return result.Edits.SelectAsArray(sourceText.GetTextChange);
}
/// <summary>
@ -80,6 +94,9 @@ internal sealed class HtmlFormatter(
if (!edits.Any(static e => e.NewText.Contains("~")))
return edits;
return htmlSourceText.MinimizeTextEdits(edits);
var changes = edits.SelectAsArray(htmlSourceText.GetTextChange);
var fixedChanges = htmlSourceText.MinimizeTextChanges(changes);
return [.. fixedChanges.Select(htmlSourceText.GetTextEdit)];
}
}

Просмотреть файл

@ -2,15 +2,17 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
internal interface IHtmlFormatter
{
Task<TextEdit[]> GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken);
Task<TextEdit[]> GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken);
Task<ImmutableArray<TextChange>> GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken);
Task<ImmutableArray<TextChange>> GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken);
}

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
@ -62,7 +63,7 @@ internal sealed class GeneratedDocumentPublisher : IGeneratedDocumentPublisher,
: new DocumentKey(ProjectKey.Unknown, filePath);
PublishData? previouslyPublishedData;
IReadOnlyList<TextChange> textChanges;
ImmutableArray<TextChange> textChanges;
lock (_publishedCSharpData)
{
@ -80,7 +81,7 @@ internal sealed class GeneratedDocumentPublisher : IGeneratedDocumentPublisher,
}
textChanges = SourceTextDiffer.GetMinimalTextChanges(previouslyPublishedData.SourceText, sourceText);
if (textChanges.Count == 0 && hostDocumentVersion == previouslyPublishedData.HostDocumentVersion)
if (textChanges.Length == 0 && hostDocumentVersion == previouslyPublishedData.HostDocumentVersion)
{
// Source texts match along with host document versions. We've already published something that looks like this. No-op.
return;
@ -89,7 +90,7 @@ internal sealed class GeneratedDocumentPublisher : IGeneratedDocumentPublisher,
_logger.LogDebug(
$"Updating C# buffer of {filePath} for project {documentKey.ProjectKey} to correspond with host document " +
$"version {hostDocumentVersion}. {previouslyPublishedData.SourceText.Length} -> {sourceText.Length} = Change delta of " +
$"{sourceText.Length - previouslyPublishedData.SourceText.Length} via {textChanges.Count} text changes.");
$"{sourceText.Length - previouslyPublishedData.SourceText.Length} via {textChanges.Length} text changes.");
_publishedCSharpData[documentKey] = new PublishData(sourceText, hostDocumentVersion);
}
@ -109,7 +110,7 @@ internal sealed class GeneratedDocumentPublisher : IGeneratedDocumentPublisher,
public void PublishHtml(ProjectKey projectKey, string filePath, SourceText sourceText, int hostDocumentVersion)
{
PublishData? previouslyPublishedData;
IReadOnlyList<TextChange> textChanges;
ImmutableArray<TextChange> textChanges;
lock (_publishedHtmlData)
{
@ -126,14 +127,14 @@ internal sealed class GeneratedDocumentPublisher : IGeneratedDocumentPublisher,
}
textChanges = SourceTextDiffer.GetMinimalTextChanges(previouslyPublishedData.SourceText, sourceText);
if (textChanges.Count == 0 && hostDocumentVersion == previouslyPublishedData.HostDocumentVersion)
if (textChanges.Length == 0 && hostDocumentVersion == previouslyPublishedData.HostDocumentVersion)
{
// Source texts match along with host document versions. We've already published something that looks like this. No-op.
return;
}
_logger.LogDebug(
$"Updating HTML buffer of {filePath} to correspond with host document version {hostDocumentVersion}. {previouslyPublishedData.SourceText.Length} -> {sourceText.Length} = Change delta of {sourceText.Length - previouslyPublishedData.SourceText.Length} via {textChanges.Count} text changes.");
$"Updating HTML buffer of {filePath} to correspond with host document version {hostDocumentVersion}. {previouslyPublishedData.SourceText.Length} -> {sourceText.Length} = Change delta of {sourceText.Length - previouslyPublishedData.SourceText.Length} via {textChanges.Length} text changes.");
_publishedHtmlData[filePath] = new PublishData(sourceText, hostDocumentVersion);
}

Просмотреть файл

@ -22,7 +22,7 @@ internal abstract class AbstractDocumentMappingService(IFilePathService filePath
protected readonly IFilePathService FilePathService = filePathService;
protected readonly ILogger Logger = logger;
public IEnumerable<TextChange> GetHostDocumentEdits(IRazorGeneratedDocument generatedDocument, IEnumerable<TextChange> generatedDocumentChanges)
public IEnumerable<TextChange> GetHostDocumentEdits(IRazorGeneratedDocument generatedDocument, ImmutableArray<TextChange> generatedDocumentChanges)
{
var generatedDocumentSourceText = generatedDocument.GetGeneratedSourceText();
var lastNewLineAddedToLine = 0;

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
@ -9,7 +10,7 @@ namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
internal interface IDocumentMappingService
{
IEnumerable<TextChange> GetHostDocumentEdits(IRazorGeneratedDocument generatedDocument, IEnumerable<TextChange> generatedDocumentEdits);
IEnumerable<TextChange> GetHostDocumentEdits(IRazorGeneratedDocument generatedDocument, ImmutableArray<TextChange> generatedDocumentEdits);
bool TryMapToHostDocumentRange(IRazorGeneratedDocument generatedDocument, LinePositionSpan generatedDocumentRange, MappingBehavior mappingBehavior, out LinePositionSpan hostDocumentRange);

Просмотреть файл

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
@ -22,7 +23,7 @@ internal static class IDocumentMappingServiceExtensions
var generatedDocumentSourceText = generatedDocument.GetGeneratedSourceText();
var documentText = generatedDocument.CodeDocument.AssumeNotNull().Source.Text;
var changes = generatedDocumentEdits.Select(generatedDocumentSourceText.GetTextChange);
var changes = generatedDocumentEdits.SelectAsArray(generatedDocumentSourceText.GetTextChange);
var mappedChanges = service.GetHostDocumentEdits(generatedDocument, changes);
return mappedChanges.Select(documentText.GetTextEdit).ToArray();
}

Просмотреть файл

@ -28,4 +28,22 @@ internal static class LinePositionSpanExtensions
// Empty ranges do not overlap with any range.
return overlapStart.CompareTo(overlapEnd) < 0;
}
public static bool LineOverlapsWith(this LinePositionSpan span, LinePositionSpan other)
{
var overlapStart = span.Start.Line < other.Start.Line
? other.Start.Line
: span.Start.Line;
var overlapEnd = span.End.Line > other.End.Line
? other.End.Line
: span.End.Line;
return overlapStart <= overlapEnd;
}
public static bool Contains(this LinePositionSpan span, LinePositionSpan other)
{
return span.Start <= other.Start && span.End >= other.End;
}
}

Просмотреть файл

@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.Language.Syntax;
@ -311,7 +312,7 @@ internal static class RazorSyntaxNodeExtensions
public static bool IsAnyAttributeSyntax(this SyntaxNode node)
{
return node is
return node is
MarkupAttributeBlockSyntax or
MarkupMinimizedAttributeBlockSyntax or
MarkupTagHelperAttributeSyntax or
@ -320,4 +321,63 @@ internal static class RazorSyntaxNodeExtensions
MarkupMinimizedTagHelperDirectiveAttributeSyntax or
MarkupMiscAttributeContentSyntax;
}
public static bool TryGetLinePositionSpanWithoutWhitespace(this SyntaxNode node, RazorSourceDocument source, out LinePositionSpan linePositionSpan)
{
var tokens = node.GetTokens();
SyntaxToken? firstToken = null;
foreach (var token in tokens)
{
if (!token.IsWhitespace())
{
firstToken = token;
break;
}
}
SyntaxToken? lastToken = null;
for (var i = tokens.Count - 1; i >= 0; i--)
{
var token = tokens[i];
if (!token.IsWhitespace())
{
lastToken = token;
break;
}
}
// These two are either both null or neither null, but the || means the compiler doesn't give us nullability warnings
if (firstToken is null || lastToken is null)
{
linePositionSpan = default;
return false;
}
var startPositionSpan = GetLinePositionSpan(firstToken, source, node.SpanStart);
var endPositionSpan = GetLinePositionSpan(lastToken, source, node.SpanStart);
linePositionSpan = new LinePositionSpan(startPositionSpan.Start, endPositionSpan.End);
return true;
// This is needed because SyntaxToken positions taken from GetTokens
// are relative to their parent node and not to the document.
static LinePositionSpan GetLinePositionSpan(SyntaxNode node, RazorSourceDocument source, int parentStart)
{
var sourceText = source.Text;
var start = node.Position + parentStart;
var end = node.EndPosition + parentStart;
if (start == sourceText.Length && node.FullWidth == 0)
{
// Marker symbol at the end of the document.
var location = node.GetSourceLocation(source);
var position = location.ToLinePosition();
return new LinePositionSpan(position, position);
}
return sourceText.GetLinePositionSpan(start, end);
}
}
}

Просмотреть файл

@ -3,12 +3,12 @@
using System;
using System.Buffers;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.TextDifferencing;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.Text;
@ -276,15 +276,14 @@ internal static class SourceTextExtensions
/// <summary>
/// Applies the set of edits specified, and returns the minimal set needed to make the same changes
/// </summary>
public static TextEdit[] MinimizeTextEdits(this SourceText text, TextEdit[] edits)
=> MinimizeTextEdits(text, edits, out _);
public static ImmutableArray<TextChange> MinimizeTextChanges(this SourceText text, ImmutableArray<TextChange> changes)
=> MinimizeTextChanges(text, changes, out _);
/// <summary>
/// Applies the set of edits specified, and returns the minimal set needed to make the same changes
/// </summary>
public static TextEdit[] MinimizeTextEdits(this SourceText text, TextEdit[] edits, out SourceText originalTextWithChanges)
public static ImmutableArray<TextChange> MinimizeTextChanges(this SourceText text, ImmutableArray<TextChange> changes, out SourceText originalTextWithChanges)
{
var changes = edits.Select(text.GetTextChange);
originalTextWithChanges = text.WithChanges(changes);
if (text.ContentEquals(originalTextWithChanges))
@ -292,9 +291,7 @@ internal static class SourceTextExtensions
return [];
}
var cleanChanges = SourceTextDiffer.GetMinimalTextChanges(text, originalTextWithChanges, DiffKind.Char);
var cleanEdits = cleanChanges.Select(text.GetTextEdit).ToArray();
return cleanEdits;
return SourceTextDiffer.GetMinimalTextChanges(text, originalTextWithChanges, DiffKind.Char);
}
/// <summary>
@ -325,4 +322,17 @@ internal static class SourceTextExtensions
return lfCount > crlfCount;
}
public static ImmutableArray<TextChange> GetTextChangesArray(this SourceText newText, SourceText oldText)
{
var list = newText.GetTextChanges(oldText);
// Fast path for the common case. The base SourceText.GetTextChanges method returns an ImmutableArray
if (list is ImmutableArray<TextChange> array)
{
return array;
}
return list.ToImmutableArray();
}
}

Просмотреть файл

@ -61,7 +61,6 @@ internal static partial class VsLspExtensions
public static bool Contains(this Range range, Range other)
{
return range.Start.CompareTo(other.Start) <= 0 && range.End.CompareTo(other.End) >= 0;
}

Просмотреть файл

@ -1,10 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.VisualStudio.LanguageServer.Protocol;
@ -17,64 +15,4 @@ internal static partial class VsLspExtensions
return VsLspFactory.CreateRange(linePositionSpan);
}
public static Range? GetRangeWithoutWhitespace(this SyntaxNode node, RazorSourceDocument source)
{
var tokens = node.GetTokens();
SyntaxToken? firstToken = null;
for (var i = 0; i < tokens.Count; i++)
{
var token = tokens[i];
if (!token.IsWhitespace())
{
firstToken = token;
break;
}
}
SyntaxToken? lastToken = null;
for (var i = tokens.Count - 1; i >= 0; i--)
{
var token = tokens[i];
if (!token.IsWhitespace())
{
lastToken = token;
break;
}
}
if (firstToken is null && lastToken is null)
{
return null;
}
var startPositionSpan = GetLinePositionSpan(firstToken, source, node.SpanStart);
var endPositionSpan = GetLinePositionSpan(lastToken, source, node.SpanStart);
return VsLspFactory.CreateRange(startPositionSpan.Start, endPositionSpan.End);
// This is needed because SyntaxToken positions taken from GetTokens
// are relative to their parent node and not to the document.
static LinePositionSpan GetLinePositionSpan(SyntaxNode? node, RazorSourceDocument source, int parentStart)
{
ArgHelper.ThrowIfNull(node);
ArgHelper.ThrowIfNull(source);
var sourceText = source.Text;
var start = node.Position + parentStart;
var end = node.EndPosition + parentStart;
if (start == sourceText.Length && node.FullWidth == 0)
{
// Marker symbol at the end of the document.
var location = node.GetSourceLocation(source);
var position = location.ToLinePosition();
return new LinePositionSpan(position, position);
}
return sourceText.GetLinePositionSpan(start, end);
}
}
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -14,7 +15,6 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -24,55 +24,53 @@ internal sealed class CSharpFormatter(IDocumentMappingService documentMappingSer
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
public async Task<TextEdit[]> FormatAsync(FormattingContext context, Range rangeToFormat, CancellationToken cancellationToken)
public async Task<ImmutableArray<TextChange>> FormatAsync(FormattingContext context, LinePositionSpan spanToFormat, CancellationToken cancellationToken)
{
if (!_documentMappingService.TryMapToGeneratedDocumentRange(context.CodeDocument.GetCSharpDocument(), rangeToFormat, out var projectedRange))
if (!_documentMappingService.TryMapToGeneratedDocumentRange(context.CodeDocument.GetCSharpDocument(), spanToFormat, out var projectedSpan))
{
return [];
}
var edits = await GetFormattingEditsAsync(context, projectedRange, cancellationToken).ConfigureAwait(false);
var edits = await GetFormattingEditsAsync(context, projectedSpan, cancellationToken).ConfigureAwait(false);
var mappedEdits = MapEditsToHostDocument(context.CodeDocument, edits);
return mappedEdits;
}
public static async Task<IReadOnlyDictionary<int, int>> GetCSharpIndentationAsync(
FormattingContext context,
IReadOnlyCollection<int> projectedDocumentLocations,
HashSet<int> projectedDocumentLocations,
CancellationToken cancellationToken)
{
// Sorting ensures we count the marker offsets correctly.
// We also want to ensure there are no duplicates to avoid duplicate markers.
var filteredLocations = projectedDocumentLocations.Distinct().OrderBy(l => l).ToList();
var filteredLocations = projectedDocumentLocations.OrderAsArray();
var indentations = await GetCSharpIndentationCoreAsync(context, filteredLocations, cancellationToken).ConfigureAwait(false);
return indentations;
}
private TextEdit[] MapEditsToHostDocument(RazorCodeDocument codeDocument, TextEdit[] csharpEdits)
private ImmutableArray<TextChange> MapEditsToHostDocument(RazorCodeDocument codeDocument, ImmutableArray<TextChange> csharpEdits)
{
var actualEdits = _documentMappingService.GetHostDocumentEdits(codeDocument.GetCSharpDocument(), csharpEdits);
return actualEdits;
return actualEdits.ToImmutableArray();
}
private static async Task<TextEdit[]> GetFormattingEditsAsync(FormattingContext context, Range projectedRange, CancellationToken cancellationToken)
private static async Task<ImmutableArray<TextChange>> GetFormattingEditsAsync(FormattingContext context, LinePositionSpan projectedSpan, CancellationToken cancellationToken)
{
var csharpSourceText = context.CodeDocument.GetCSharpSourceText();
var spanToFormat = csharpSourceText.GetTextSpan(projectedRange);
var spanToFormat = csharpSourceText.GetTextSpan(projectedSpan);
var root = await context.CSharpWorkspaceDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
Assumes.NotNull(root);
var changes = RazorCSharpFormattingInteractionService.GetFormattedTextChanges(context.CSharpWorkspace.Services, root, spanToFormat, context.Options.ToIndentationOptions(), cancellationToken);
var edits = changes.Select(csharpSourceText.GetTextEdit).ToArray();
return edits;
return changes.ToImmutableArray();
}
private static async Task<Dictionary<int, int>> GetCSharpIndentationCoreAsync(FormattingContext context, List<int> projectedDocumentLocations, CancellationToken cancellationToken)
private static async Task<Dictionary<int, int>> GetCSharpIndentationCoreAsync(FormattingContext context, ImmutableArray<int> projectedDocumentLocations, CancellationToken cancellationToken)
{
// No point calling the C# formatting if we won't be interested in any of its work anyway
if (projectedDocumentLocations.Count == 0)
if (projectedDocumentLocations.Length == 0)
{
return [];
}

Просмотреть файл

@ -1,13 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
internal interface IFormattingPass
{
Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken);
Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken);
}

Просмотреть файл

@ -1,54 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
internal interface IRazorFormattingService
{
Task<TextEdit[]> GetDocumentFormattingEditsAsync(
Task<ImmutableArray<TextChange>> GetDocumentFormattingChangesAsync(
DocumentContext documentContext,
TextEdit[] htmlEdits,
Range? range,
ImmutableArray<TextChange> htmlEdits,
LinePositionSpan? span,
RazorFormattingOptions options,
CancellationToken cancellationToken);
Task<TextEdit[]> GetHtmlOnTypeFormattingEditsAsync(
Task<ImmutableArray<TextChange>> GetHtmlOnTypeFormattingChangesAsync(
DocumentContext documentContext,
TextEdit[] htmlEdits,
ImmutableArray<TextChange> htmlEdits,
RazorFormattingOptions options,
int hostDocumentIndex,
char triggerCharacter,
CancellationToken cancellationToken);
Task<TextEdit[]> GetCSharpOnTypeFormattingEditsAsync(
Task<ImmutableArray<TextChange>> GetCSharpOnTypeFormattingChangesAsync(
DocumentContext documentContext,
RazorFormattingOptions options,
int hostDocumentIndex,
char triggerCharacter,
CancellationToken cancellationToken);
Task<TextEdit?> GetSingleCSharpEditAsync(
Task<TextChange?> TryGetSingleCSharpEditAsync(
DocumentContext documentContext,
TextEdit csharpEdit,
TextChange csharpEdit,
RazorFormattingOptions options,
CancellationToken cancellationToken);
Task<TextEdit?> GetCSharpCodeActionEditAsync(
Task<TextChange?> TryGetCSharpCodeActionEditAsync(
DocumentContext documentContext,
TextEdit[] csharpEdits,
ImmutableArray<TextChange> csharpEdits,
RazorFormattingOptions options,
CancellationToken cancellationToken);
Task<TextEdit?> GetCSharpSnippetFormattingEditAsync(
Task<TextChange?> TryGetCSharpSnippetFormattingEditAsync(
DocumentContext documentContext,
TextEdit[] csharpEdits,
ImmutableArray<TextChange> csharpEdits,
RazorFormattingOptions options,
CancellationToken cancellationToken);

Просмотреть файл

@ -2,7 +2,6 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
@ -12,7 +11,6 @@ using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -27,15 +25,14 @@ internal sealed class CSharpFormattingPass(
private readonly CSharpFormatter _csharpFormatter = new CSharpFormatter(documentMappingService);
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CSharpFormattingPass>();
public async override Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
public async override Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
{
// Apply previous edits if any.
var originalText = context.SourceText;
var changedText = originalText;
var changedContext = context;
if (edits.Length > 0)
if (changes.Length > 0)
{
var changes = edits.Select(originalText.GetTextChange);
changedText = changedText.WithChanges(changes);
changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false);
}
@ -43,10 +40,9 @@ internal sealed class CSharpFormattingPass(
cancellationToken.ThrowIfCancellationRequested();
// Apply original C# edits
var csharpEdits = await FormatCSharpAsync(changedContext, cancellationToken).ConfigureAwait(false);
if (csharpEdits.Length > 0)
var csharpChanges = await FormatCSharpAsync(changedContext, cancellationToken).ConfigureAwait(false);
if (csharpChanges.Length > 0)
{
var csharpChanges = csharpEdits.Select(changedText.GetTextChange);
changedText = changedText.WithChanges(csharpChanges);
changedContext = await changedContext.WithTextAsync(changedText).ConfigureAwait(false);
@ -55,8 +51,8 @@ internal sealed class CSharpFormattingPass(
cancellationToken.ThrowIfCancellationRequested();
var indentationChanges = await AdjustIndentationAsync(changedContext, cancellationToken).ConfigureAwait(false);
if (indentationChanges.Count > 0)
var indentationChanges = await AdjustIndentationAsync(changedContext, startLine: 0, endLineInclusive: changedText.Lines.Count - 1, cancellationToken).ConfigureAwait(false);
if (indentationChanges.Length > 0)
{
// Apply the edits that modify indentation.
changedText = changedText.WithChanges(indentationChanges);
@ -66,17 +62,14 @@ internal sealed class CSharpFormattingPass(
_logger.LogTestOnly($"Generated C#:\r\n{context.CSharpSourceText}");
var finalChanges = changedText.GetTextChanges(originalText);
var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray();
return finalEdits;
return changedText.GetTextChangesArray(originalText);
}
private async Task<ImmutableArray<TextEdit>> FormatCSharpAsync(FormattingContext context, CancellationToken cancellationToken)
private async Task<ImmutableArray<TextChange>> FormatCSharpAsync(FormattingContext context, CancellationToken cancellationToken)
{
var sourceText = context.SourceText;
using var csharpEdits = new PooledArrayBuilder<TextEdit>();
using var csharpChanges = new PooledArrayBuilder<TextChange>();
foreach (var mapping in context.CodeDocument.GetCSharpDocument().SourceMappings)
{
var span = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length);
@ -87,11 +80,11 @@ internal sealed class CSharpFormattingPass(
}
// These should already be remapped.
var range = sourceText.GetRange(span);
var edits = await _csharpFormatter.FormatAsync(context, range, cancellationToken).ConfigureAwait(false);
csharpEdits.AddRange(edits.Where(e => range.Contains(e.Range)));
var spanToFormat = sourceText.GetLinePositionSpan(span);
var changes = await _csharpFormatter.FormatAsync(context, spanToFormat, cancellationToken).ConfigureAwait(false);
csharpChanges.AddRange(changes.Where(e => spanToFormat.Contains(sourceText.GetLinePositionSpan(e.Span))));
}
return csharpEdits.ToImmutable();
return csharpChanges.ToImmutable();
}
}

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
@ -11,10 +12,10 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -25,9 +26,9 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService;
public abstract Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken);
public abstract Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken);
protected async Task<List<TextChange>> AdjustIndentationAsync(FormattingContext context, CancellationToken cancellationToken, Range? range = null)
protected async Task<ImmutableArray<TextChange>> AdjustIndentationAsync(FormattingContext context, int startLine, int endLineInclusive, CancellationToken cancellationToken)
{
// In this method, the goal is to make final adjustments to the indentation of each line.
// We will take into account the following,
@ -35,7 +36,6 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
// 2. The indentation due to Razor and HTML constructs
var text = context.SourceText;
range ??= text.GetRange(TextSpan.FromBounds(0, text.Length));
// To help with figuring out the correct indentation, first we will need the indentation
// that the C# formatter wants to apply in the following locations,
@ -45,7 +45,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
// Due to perf concerns, we only want to invoke the real C# formatter once.
// So, let's collect all the significant locations that we want to obtain the CSharpDesiredIndentations for.
var significantLocations = new HashSet<int>();
using var _1 = HashSetPool<int>.GetPooledObject(out var significantLocations);
// First, collect all the locations at the beginning and end of each source mapping.
var sourceMappingMap = new Dictionary<int, int>();
@ -53,7 +53,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
{
var mappingSpan = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length);
#if DEBUG
var spanText = context.SourceText.GetSubText(mappingSpan).ToString();
var spanText = context.SourceText.GetSubTextString(mappingSpan);
#endif
var options = new ShouldFormatOptions(
@ -87,7 +87,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
// Next, collect all the line starts that start in C# context
var indentations = context.GetIndentations();
var lineStartMap = new Dictionary<int, int>();
for (var i = range.Start.Line; i <= range.End.Line; i++)
for (var i = startLine; i <= endLineInclusive; i++)
{
if (indentations[i].EmptyOrWhitespaceLine)
{
@ -172,7 +172,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
// Now, let's combine the C# desired indentation with the Razor and HTML indentation for each line.
var newIndentations = new Dictionary<int, int>();
for (var i = range.Start.Line; i <= range.End.Line; i++)
for (var i = startLine; i <= endLineInclusive; i++)
{
if (indentations[i].EmptyOrWhitespaceLine)
{
@ -275,7 +275,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
}
// Now that we have collected all the indentations for each line, let's convert them to text edits.
var changes = new List<TextChange>();
using var changes = new PooledArrayBuilder<TextChange>(capacity: newIndentations.Count);
foreach (var item in newIndentations)
{
var line = item.Key;
@ -288,7 +288,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen
changes.Add(new TextChange(spanToReplace, effectiveDesiredIndentation));
}
return changes;
return changes.DrainToImmutable();
}
protected static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements)

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
@ -18,7 +19,6 @@ using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -32,18 +32,18 @@ internal sealed class CSharpOnTypeFormattingPass(
{
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CSharpOnTypeFormattingPass>();
public async override Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
public async override Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
{
// Normalize and re-map the C# edits.
var codeDocument = context.CodeDocument;
var csharpText = codeDocument.GetCSharpSourceText();
if (edits.Length == 0)
if (changes.Length == 0)
{
if (!DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), context.HostDocumentIndex, out _, out var projectedIndex))
{
_logger.LogWarning($"Failed to map to projected position for document {context.Uri}.");
return edits;
return changes;
}
// Ask C# for formatting changes.
@ -62,60 +62,58 @@ internal sealed class CSharpOnTypeFormattingPass(
if (formattingChanges.IsEmpty)
{
_logger.LogInformation($"Received no results.");
return edits;
return changes;
}
edits = formattingChanges.Select(csharpText.GetTextEdit).ToArray();
_logger.LogInformation($"Received {edits.Length} results from C#.");
changes = formattingChanges;
_logger.LogInformation($"Received {changes.Length} results from C#.");
}
// Sometimes the C# document is out of sync with our document, so Roslyn can return edits to us that will throw when we try
// to normalize them. Instead of having this flow up and log a NFW, we just capture it here. Since this only happens when typing
// very quickly, it is a safe assumption that we'll get another chance to do on type formatting, since we know the user is typing.
// The proper fix for this is https://github.com/dotnet/razor-tooling/issues/6650 at which point this can be removed
foreach (var edit in edits)
foreach (var edit in changes)
{
var startLine = edit.Range.Start.Line;
var endLine = edit.Range.End.Line;
var count = csharpText.Lines.Count;
if (startLine >= count || endLine >= count)
var startPos = edit.Span.Start;
var endPos = edit.Span.End;
var count = csharpText.Length;
if (startPos >= count || endPos >= count)
{
_logger.LogWarning($"Got a bad edit that couldn't be applied. Edit is {startLine}-{endLine} but there are only {count} lines in C#.");
return edits;
_logger.LogWarning($"Got a bad edit that couldn't be applied. Edit is {startPos}-{endPos} but there are only {count} characters in C#.");
return changes;
}
}
var normalizedEdits = csharpText.MinimizeTextEdits(edits, out var originalTextWithChanges);
var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits);
var filteredEdits = FilterCSharpTextEdits(context, mappedEdits);
if (filteredEdits.Length == 0)
var normalizedChanges = csharpText.MinimizeTextChanges(changes, out var originalTextWithChanges);
var mappedChanges = RemapTextChanges(codeDocument, normalizedChanges);
var filteredChanges = FilterCSharpTextChanges(context, mappedChanges);
if (filteredChanges.Length == 0)
{
// There are no C# edits for us to apply that could be mapped, but we might still need to check for using statements
// because they are non mappable, but might be the only thing changed (eg from the Add Using code action)
//
// If there aren't any edits that are likely to contain using statement changes, this call will no-op.
filteredEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, edits, originalTextWithChanges, filteredEdits, cancellationToken).ConfigureAwait(false);
filteredChanges = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, changes, originalTextWithChanges, filteredChanges, cancellationToken).ConfigureAwait(false);
return filteredEdits;
return filteredChanges;
}
// Find the lines that were affected by these edits.
var originalText = codeDocument.Source.Text;
_logger.LogTestOnly($"Original text:\r\n{originalText}");
var changes = filteredEdits.Select(originalText.GetTextChange);
// Apply the format on type edits sent over by the client.
var formattedText = ApplyChangesAndTrackChange(originalText, changes, out _, out var spanAfterFormatting);
var formattedText = ApplyChangesAndTrackChange(originalText, filteredChanges, out _, out var spanAfterFormatting);
_logger.LogTestOnly($"After C# changes:\r\n{formattedText}");
var changedContext = await context.WithTextAsync(formattedText).ConfigureAwait(false);
var rangeAfterFormatting = formattedText.GetRange(spanAfterFormatting);
var linePositionSpanAfterFormatting = formattedText.GetLinePositionSpan(spanAfterFormatting);
cancellationToken.ThrowIfCancellationRequested();
// We make an optimistic attempt at fixing corner cases.
var cleanupChanges = CleanupDocument(changedContext, rangeAfterFormatting);
var cleanupChanges = CleanupDocument(changedContext, linePositionSpanAfterFormatting);
var cleanedText = formattedText.WithChanges(cleanupChanges);
_logger.LogTestOnly($"After CleanupDocument:\r\n{cleanedText}");
@ -128,7 +126,7 @@ internal sealed class CSharpOnTypeFormattingPass(
// We only want to adjust the range that was affected.
// We need to take into account the lines affected by formatting as well as cleanup.
var lineDelta = LineDelta(formattedText, cleanupChanges, out var firstPosition, out var lastPosition);
var lineDelta = LineDelta(formattedText, cleanupChanges, out var firstLine, out var lastLine);
// Okay hear me out, I know this looks lazy, but it totally makes sense.
// This method is called with edits that the C# formatter wants to make, and from those edits we work out which
@ -146,7 +144,7 @@ internal sealed class CSharpOnTypeFormattingPass(
//
// We'll happy format lines 1 and 2, and ignore the closing brace altogether. So, by looking one line further
// we won't have that problem.
if (rangeAfterFormatting.End.Line + lineDelta < cleanedText.Lines.Count - 1)
if (linePositionSpanAfterFormatting.End.Line + lineDelta < cleanedText.Lines.Count - 1)
{
lineDelta++;
}
@ -164,25 +162,13 @@ internal sealed class CSharpOnTypeFormattingPass(
// we'd format line 6 and call it a day, even though the formatter made an edit on line 3. To fix this we use the
// first and last position of edits made above, and make sure our range encompasses them as well. For convenience
// we calculate these positions in the LineDelta method called above.
// This is essentially: rangeToAdjust = new Range(Math.Min(firstFormattingEdit, userEdit), Math.Max(lastFormattingEdit, userEdit))
var start = rangeAfterFormatting.Start;
if (firstPosition is not null && firstPosition.CompareTo(start) < 0)
{
start = firstPosition;
}
var startLine = Math.Min(firstLine, linePositionSpanAfterFormatting.Start.Line);
var endLineInclusive = Math.Max(lastLine, linePositionSpanAfterFormatting.End.Line + lineDelta);
var end = VsLspFactory.CreatePosition(rangeAfterFormatting.End.Line + lineDelta, 0);
if (lastPosition is not null && lastPosition.CompareTo(start) < 0)
{
end = lastPosition;
}
Debug.Assert(cleanedText.Lines.Count > endLineInclusive, "Invalid range. This is unexpected.");
var rangeToAdjust = VsLspFactory.CreateRange(start, end);
Debug.Assert(cleanedText.IsValidPosition(rangeToAdjust.End), "Invalid range. This is unexpected.");
var indentationChanges = await AdjustIndentationAsync(changedContext, cancellationToken, rangeToAdjust).ConfigureAwait(false);
if (indentationChanges.Count > 0)
var indentationChanges = await AdjustIndentationAsync(changedContext, startLine, endLineInclusive, cancellationToken).ConfigureAwait(false);
if (indentationChanges.Length > 0)
{
// Apply the edits that modify indentation.
cleanedText = cleanedText.WithChanges(indentationChanges);
@ -191,39 +177,39 @@ internal sealed class CSharpOnTypeFormattingPass(
}
// Now that we have made all the necessary changes to the document. Let's diff the original vs final version and return the diff.
var finalChanges = cleanedText.GetTextChanges(originalText);
var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray();
var finalChanges = cleanedText.GetTextChangesArray(originalText);
finalEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, edits, originalTextWithChanges, finalEdits, cancellationToken).ConfigureAwait(false);
finalChanges = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, changes, originalTextWithChanges, finalChanges, cancellationToken).ConfigureAwait(false);
return finalEdits;
return finalChanges;
}
private TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits)
private ImmutableArray<TextChange> RemapTextChanges(RazorCodeDocument codeDocument, ImmutableArray<TextChange> projectedTextChanges)
{
if (codeDocument.IsUnsupported())
{
return [];
}
var edits = DocumentMappingService.GetHostDocumentEdits(codeDocument.GetCSharpDocument(), projectedTextEdits);
var changes = DocumentMappingService.GetHostDocumentEdits(codeDocument.GetCSharpDocument(), projectedTextChanges);
return edits;
return changes.ToImmutableArray();
}
private static async Task<TextEdit[]> AddUsingStatementEditsIfNecessaryAsync(FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken)
private static async Task<ImmutableArray<TextChange>> AddUsingStatementEditsIfNecessaryAsync(FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, ImmutableArray<TextChange> changes, SourceText originalTextWithChanges, ImmutableArray<TextChange> finalChanges, CancellationToken cancellationToken)
{
if (context.AutomaticallyAddUsings)
{
// Because we need to parse the C# code twice for this operation, lets do a quick check to see if its even necessary
if (textEdits.Any(static e => e.NewText.IndexOf("using") != -1))
if (changes.Any(static e => e.NewText is not null && e.NewText.IndexOf("using") != -1))
{
var usingStatementEdits = await AddUsingsHelper.GetUsingStatementEditsAsync(codeDocument, csharpText, originalTextWithChanges, cancellationToken).ConfigureAwait(false);
finalEdits = [.. usingStatementEdits, .. finalEdits];
var usingStatementChanges = usingStatementEdits.Select(codeDocument.Source.Text.GetTextChange);
finalChanges = [.. usingStatementChanges, .. finalChanges];
}
}
return finalEdits;
return finalChanges;
}
// Returns the minimal TextSpan that encompasses all the differences between the old and the new text.
@ -238,21 +224,15 @@ internal sealed class CSharpOnTypeFormattingPass(
return newText;
}
private static TextEdit[] FilterCSharpTextEdits(FormattingContext context, TextEdit[] edits)
private static ImmutableArray<TextChange> FilterCSharpTextChanges(FormattingContext context, ImmutableArray<TextChange> changes)
{
var filteredEdits = edits.Where(e =>
{
var span = context.SourceText.GetTextSpan(e.Range);
return ShouldFormat(context, span, allowImplicitStatements: false);
}).ToArray();
return filteredEdits;
return changes.WhereAsArray(e => ShouldFormat(context, e.Span, allowImplicitStatements: false));
}
private static int LineDelta(SourceText text, IEnumerable<TextChange> changes, out Position? firstPosition, out Position? lastPosition)
private static int LineDelta(SourceText text, IEnumerable<TextChange> changes, out int firstLine, out int lastLine)
{
firstPosition = null;
lastPosition = null;
firstLine = 0;
lastLine = 0;
// Let's compute the number of newlines added/removed by the incoming changes.
var delta = 0;
@ -261,20 +241,11 @@ internal sealed class CSharpOnTypeFormattingPass(
{
var newLineCount = change.NewText is null ? 0 : change.NewText.Split('\n').Length - 1;
var range = text.GetRange(change.Span);
Debug.Assert(range.Start.Line <= range.End.Line, "Invalid range.");
// For convenience, since we're already iterating through things, we also find the extremes
// of the range of edits that were made.
if (firstPosition is null || firstPosition.CompareTo(range.Start) > 0)
{
firstPosition = range.Start;
}
if (lastPosition is null || lastPosition.CompareTo(range.End) < 0)
{
lastPosition = range.End;
}
var range = text.GetLinePositionSpan(change.Span);
firstLine = Math.Min(firstLine, range.Start.Line);
lastLine = Math.Max(lastLine, range.End.Line);
// The number of lines added/removed will be,
// the number of lines added by the change - the number of lines the change span represents
@ -284,32 +255,31 @@ internal sealed class CSharpOnTypeFormattingPass(
return delta;
}
private static List<TextChange> CleanupDocument(FormattingContext context, Range? range = null)
private static ImmutableArray<TextChange> CleanupDocument(FormattingContext context, LinePositionSpan spanAfterFormatting)
{
var text = context.SourceText;
range ??= text.GetRange(TextSpan.FromBounds(0, text.Length));
var csharpDocument = context.CodeDocument.GetCSharpDocument();
var changes = new List<TextChange>();
using var changes = new PooledArrayBuilder<TextChange>();
foreach (var mapping in csharpDocument.SourceMappings)
{
var mappingSpan = new TextSpan(mapping.OriginalSpan.AbsoluteIndex, mapping.OriginalSpan.Length);
var mappingRange = text.GetRange(mappingSpan);
if (!range.LineOverlapsWith(mappingRange))
var mappingLinePositionSpan = text.GetLinePositionSpan(mappingSpan);
if (!spanAfterFormatting.LineOverlapsWith(mappingLinePositionSpan))
{
// We don't care about this range. It didn't change.
continue;
}
CleanupSourceMappingStart(context, mappingRange, changes, out var newLineAdded);
CleanupSourceMappingStart(context, mappingLinePositionSpan, ref changes.AsRef(), out var newLineAdded);
CleanupSourceMappingEnd(context, mappingRange, changes, newLineAdded);
CleanupSourceMappingEnd(context, mappingLinePositionSpan, ref changes.AsRef(), newLineAdded);
}
return changes;
return changes.ToImmutable();
}
private static void CleanupSourceMappingStart(FormattingContext context, Range sourceMappingRange, List<TextChange> changes, out bool newLineAdded)
private static void CleanupSourceMappingStart(FormattingContext context, LinePositionSpan sourceMappingRange, ref PooledArrayBuilder<TextChange> changes, out bool newLineAdded)
{
newLineAdded = false;
@ -432,7 +402,7 @@ internal sealed class CSharpOnTypeFormattingPass(
return builder.ToString();
}
private static void CleanupSourceMappingEnd(FormattingContext context, Range sourceMappingRange, List<TextChange> changes, bool newLineWasAddedAtStart)
private static void CleanupSourceMappingEnd(FormattingContext context, LinePositionSpan sourceMappingRange, ref PooledArrayBuilder<TextChange> changes, bool newLineWasAddedAtStart)
{
//
// We look through every source mapping that intersects with the affected range and

Просмотреть файл

@ -1,13 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -18,10 +18,9 @@ internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFacto
// Internal for testing.
internal bool DebugAssertsEnabled { get; set; } = true;
public Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
public Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
{
var text = context.SourceText;
var changes = edits.Select(text.GetTextChange);
var changedText = text.WithChanges(changes);
if (!text.NonWhitespaceContentEquals(changedText))
@ -31,15 +30,15 @@ internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFacto
_logger.LogWarning($"{SR.Format_operation_changed_nonwhitespace}");
foreach (var edit in edits)
foreach (var change in changes)
{
if (edit.NewText.Any(c => !char.IsWhiteSpace(c)))
if (change.NewText?.Any(c => !char.IsWhiteSpace(c)) ?? false)
{
_logger.LogWarning($"{SR.FormatEdit_at_adds(edit.Range.ToDisplayString(), edit.NewText)}");
_logger.LogWarning($"{SR.FormatEdit_at_adds(text.GetLinePositionSpan(change.Span), change.NewText)}");
}
else if (text.TryGetFirstNonWhitespaceOffset(text.GetTextSpan(edit.Range), out _))
else if (text.TryGetFirstNonWhitespaceOffset(change.Span, out _))
{
_logger.LogWarning($"{SR.FormatEdit_at_deletes(edit.Range.ToDisplayString(), text.ToString(text.GetTextSpan(edit.Range)))}");
_logger.LogWarning($"{SR.FormatEdit_at_deletes(text.GetLinePositionSpan(change.Span), text.ToString(change.Span))}");
}
}
@ -48,9 +47,9 @@ internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFacto
Debug.Fail("A formatting result was rejected because it was going to change non-whitespace content in the document.");
}
return Task.FromResult<TextEdit[]>([]);
return Task.FromResult<ImmutableArray<TextChange>>([]);
}
return Task.FromResult(edits);
return Task.FromResult(changes);
}
}

Просмотреть файл

@ -3,13 +3,14 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -20,12 +21,11 @@ internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFa
// Internal for testing.
internal bool DebugAssertsEnabled { get; set; } = true;
public async Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
public async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
{
var originalDiagnostics = context.CodeDocument.GetSyntaxTree().Diagnostics;
var text = context.SourceText;
var changes = edits.Select(text.GetTextChange);
var changedText = text.WithChanges(changes);
var changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false);
var changedDiagnostics = changedContext.CodeDocument.GetSyntaxTree().Diagnostics;
@ -58,7 +58,7 @@ internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFa
return [];
}
return edits;
return changes;
}
private class LocationIgnoringDiagnosticComparer : IEqualityComparer<RazorDiagnostic>

Просмотреть файл

@ -2,11 +2,13 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
@ -17,7 +19,7 @@ internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass
{
private readonly ILogger _logger = logger;
public virtual async Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
public virtual async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
{
var originalText = context.SourceText;
@ -26,9 +28,8 @@ internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass
_logger.LogTestOnly($"Before HTML formatter:\r\n{changedText}");
if (edits.Length > 0)
if (changes.Length > 0)
{
var changes = edits.Select(originalText.GetTextChange);
changedText = originalText.WithChanges(changes);
// Create a new formatting context for the changed razor document.
changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false);
@ -37,28 +38,25 @@ internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass
}
var indentationChanges = AdjustRazorIndentation(changedContext);
if (indentationChanges.Count > 0)
if (indentationChanges.Length > 0)
{
// Apply the edits that adjust indentation.
changedText = changedText.WithChanges(indentationChanges);
_logger.LogTestOnly($"After AdjustRazorIndentation:\r\n{changedText}");
}
var finalChanges = changedText.GetTextChanges(originalText);
var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray();
return finalEdits;
return changedText.GetTextChangesArray(originalText);
}
private static List<TextChange> AdjustRazorIndentation(FormattingContext context)
private static ImmutableArray<TextChange> AdjustRazorIndentation(FormattingContext context)
{
// Assume HTML formatter has already run at this point and HTML is relatively indented correctly.
// But HTML doesn't know about Razor blocks.
// Our goal here is to indent each line according to the surrounding Razor blocks.
var sourceText = context.SourceText;
var editsToApply = new List<TextChange>();
var indentations = context.GetIndentations();
using var editsToApply = new PooledArrayBuilder<TextChange>(capacity: sourceText.Lines.Count);
for (var i = 0; i < sourceText.Lines.Count; i++)
{
var line = sourceText.Lines[i];
@ -164,7 +162,7 @@ internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass
}
}
return editsToApply;
return editsToApply.DrainToImmutable();
}
private static bool IsPartOfHtmlTag(FormattingContext context, int position)

Просмотреть файл

@ -1,10 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -13,14 +15,14 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting;
/// </summary>
internal sealed class HtmlOnTypeFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger<HtmlOnTypeFormattingPass>())
{
public override Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
public override Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
{
if (edits.Length == 0)
if (changes.Length == 0)
{
// There are no HTML edits for us to apply. No op.
return Task.FromResult<TextEdit[]>([]);
return SpecializedTasks.EmptyImmutableArray<TextChange>();
}
return base.ExecuteAsync(context, edits, cancellationToken);
return base.ExecuteAsync(context, changes, cancellationToken);
}
}

Просмотреть файл

@ -1,10 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
@ -23,15 +21,14 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting;
internal sealed class RazorFormattingPass : IFormattingPass
{
public async Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
public async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
{
// Apply previous edits if any.
var originalText = context.SourceText;
var changedText = originalText;
var changedContext = context;
if (edits.Length > 0)
if (changes.Length > 0)
{
var changes = edits.Select(originalText.GetTextChange);
changedText = changedText.WithChanges(changes);
changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false);
@ -40,46 +37,42 @@ internal sealed class RazorFormattingPass : IFormattingPass
// Format the razor bits of the file
var syntaxTree = changedContext.CodeDocument.GetSyntaxTree();
var razorEdits = FormatRazor(changedContext, syntaxTree);
var razorChanges = FormatRazor(changedContext, syntaxTree);
// Compute the final combined set of edits
var formattingChanges = razorEdits.Select(changedText.GetTextChange);
changedText = changedText.WithChanges(formattingChanges);
changedText = changedText.WithChanges(razorChanges);
var finalChanges = changedText.GetTextChanges(originalText);
var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray();
return finalEdits;
return changedText.GetTextChangesArray(originalText);
}
private static ImmutableArray<TextEdit> FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree)
private static ImmutableArray<TextChange> FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree)
{
using var edits = new PooledArrayBuilder<TextEdit>();
using var changes = new PooledArrayBuilder<TextChange>();
var source = syntaxTree.Source;
foreach (var node in syntaxTree.Root.DescendantNodes())
{
// Disclaimer: CSharpCodeBlockSyntax is used a _lot_ in razor so these methods are probably
// being overly careful to only try to format syntax forms they care about.
TryFormatCSharpBlockStructure(context, ref edits.AsRef(), source, node);
TryFormatSingleLineDirective(ref edits.AsRef(), source, node);
TryFormatBlocks(context, ref edits.AsRef(), source, node);
TryFormatCSharpBlockStructure(context, ref changes.AsRef(), source, node);
TryFormatSingleLineDirective(ref changes.AsRef(), node);
TryFormatBlocks(context, ref changes.AsRef(), source, node);
}
return edits.ToImmutable();
return changes.ToImmutable();
}
private static void TryFormatBlocks(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static void TryFormatBlocks(FormattingContext context, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, RazorSyntaxNode node)
{
// We only want to run one of these
_ = TryFormatFunctionsBlock(context, ref edits, source, node) ||
TryFormatCSharpExplicitTransition(context, ref edits, source, node) ||
TryFormatHtmlInCSharp(context, ref edits, source, node) ||
TryFormatComplexCSharpBlock(context, ref edits, source, node) ||
TryFormatSectionBlock(context, ref edits, source, node);
_ = TryFormatFunctionsBlock(context, ref changes, source, node) ||
TryFormatCSharpExplicitTransition(context, ref changes, source, node) ||
TryFormatHtmlInCSharp(context, ref changes, source, node) ||
TryFormatComplexCSharpBlock(context, ref changes, source, node) ||
TryFormatSectionBlock(context, ref changes, source, node);
}
private static bool TryFormatSectionBlock(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static bool TryFormatSectionBlock(FormattingContext context, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, RazorSyntaxNode node)
{
// @section Goo {
// }
@ -98,16 +91,15 @@ internal sealed class RazorFormattingPass : IFormattingPass
if (TryGetWhitespace(children, out var whitespaceBeforeSectionName, out var whitespaceAfterSectionName))
{
// For whitespace we normalize it differently depending on if its multi-line or not
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceBeforeSectionName, directive, ref edits, source, context, forceNewLine: false);
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceAfterSectionName, directive, ref edits, source, context, forceNewLine: false);
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceBeforeSectionName, directive, ref changes, source, context, forceNewLine: false);
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceAfterSectionName, directive, ref changes, source, context, forceNewLine: false);
return true;
}
else if (children.TryGetOpenBraceToken(out var brace))
{
// If there is no whitespace at all we normalize to a single space
var edit = VsLspFactory.CreateTextEdit(brace.GetRange(source).Start, " ");
edits.Add(edit);
changes.Add(new TextChange(new TextSpan(brace.SpanStart, 0), " "));
return true;
}
@ -136,7 +128,7 @@ internal sealed class RazorFormattingPass : IFormattingPass
}
}
private static bool TryFormatFunctionsBlock(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static bool TryFormatFunctionsBlock(FormattingContext context, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, RazorSyntaxNode node)
{
// @functions
// {
@ -168,13 +160,13 @@ internal sealed class RazorFormattingPass : IFormattingPass
var codeNode = code.AssumeNotNull();
var closeBraceNode = closeBrace;
return FormatBlock(context, source, directive, openBraceNode, codeNode, closeBraceNode, ref edits);
return FormatBlock(context, source, directive, openBraceNode, codeNode, closeBraceNode, ref changes);
}
return false;
}
private static bool TryFormatCSharpExplicitTransition(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static bool TryFormatCSharpExplicitTransition(FormattingContext context, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, RazorSyntaxNode node)
{
// We're looking for a code block like this:
//
@ -189,13 +181,13 @@ internal sealed class RazorFormattingPass : IFormattingPass
var codeNode = csharpStatementBody.CSharpCode;
var closeBraceNode = csharpStatementBody.CloseBrace;
return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, ref edits);
return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, ref changes);
}
return false;
}
private static bool TryFormatComplexCSharpBlock(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static bool TryFormatComplexCSharpBlock(FormattingContext context, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, RazorSyntaxNode node)
{
// complex situations like
// @{
@ -211,13 +203,13 @@ internal sealed class RazorFormattingPass : IFormattingPass
var openBraceNode = outerCodeBlock.Children.PreviousSiblingOrSelf(innerCodeBlock);
var closeBraceNode = outerCodeBlock.Children.NextSiblingOrSelf(innerCodeBlock);
return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, ref edits);
return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, ref changes);
}
return false;
}
private static bool TryFormatHtmlInCSharp(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static bool TryFormatHtmlInCSharp(FormattingContext context, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, RazorSyntaxNode node)
{
// void Method()
// {
@ -229,13 +221,13 @@ internal sealed class RazorFormattingPass : IFormattingPass
var openBraceNode = cSharpCodeBlock.Children.PreviousSiblingOrSelf(markupBlockNode);
var closeBraceNode = cSharpCodeBlock.Children.NextSiblingOrSelf(markupBlockNode);
return FormatBlock(context, source, directiveNode: null, openBraceNode, markupBlockNode, closeBraceNode, ref edits);
return FormatBlock(context, source, directiveNode: null, openBraceNode, markupBlockNode, closeBraceNode, ref changes);
}
return false;
}
private static void TryFormatCSharpBlockStructure(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static void TryFormatCSharpBlockStructure(FormattingContext context, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, RazorSyntaxNode node)
{
// We're looking for a code block like this:
//
@ -263,19 +255,17 @@ internal sealed class RazorFormattingPass : IFormattingPass
if (TryGetLeadingWhitespace(children, out var whitespace))
{
// For whitespace we normalize it differently depending on if its multi-line or not
FormatWhitespaceBetweenDirectiveAndBrace(whitespace, directive, ref edits, source, context, forceNewLine);
FormatWhitespaceBetweenDirectiveAndBrace(whitespace, directive, ref changes, source, context, forceNewLine);
}
else if (children.TryGetOpenBraceToken(out var brace))
{
// If there is no whitespace at all we normalize to a single space
var edit = VsLspFactory.CreateTextEdit(
brace.GetRange(source).Start,
forceNewLine
? context.NewLineString + FormattingUtilities.GetIndentationString(
directive.GetLinePositionSpan(source).Start.Character, context.Options.InsertSpaces, context.Options.TabSize)
: " ");
var newText = forceNewLine
? context.NewLineString + FormattingUtilities.GetIndentationString(
directive.GetLinePositionSpan(source).Start.Character, context.Options.InsertSpaces, context.Options.TabSize)
: " ";
edits.Add(edit);
changes.Add(new TextChange(new TextSpan(brace.SpanStart, 0), newText));
}
}
@ -295,7 +285,7 @@ internal sealed class RazorFormattingPass : IFormattingPass
}
}
private static void TryFormatSingleLineDirective(ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
private static void TryFormatSingleLineDirective(ref PooledArrayBuilder<TextChange> changes, RazorSyntaxNode node)
{
// Looking for single line directives like
//
@ -311,7 +301,7 @@ internal sealed class RazorFormattingPass : IFormattingPass
{
if (child.ContainsOnlyWhitespace(includingNewLines: false))
{
ShrinkToSingleSpace(child, ref edits, source);
ShrinkToSingleSpace(child, ref changes);
}
}
}
@ -331,45 +321,43 @@ internal sealed class RazorFormattingPass : IFormattingPass
}
}
private static void FormatWhitespaceBetweenDirectiveAndBrace(RazorSyntaxNode node, RazorDirectiveSyntax directive, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine)
private static void FormatWhitespaceBetweenDirectiveAndBrace(RazorSyntaxNode node, RazorDirectiveSyntax directive, ref PooledArrayBuilder<TextChange> changes, RazorSourceDocument source, FormattingContext context, bool forceNewLine)
{
if (node.ContainsOnlyWhitespace(includingNewLines: false) && !forceNewLine)
{
ShrinkToSingleSpace(node, ref edits, source);
ShrinkToSingleSpace(node, ref changes);
}
else
{
// If there is a newline then we want to have just one newline after the directive
// and indent the { to match the @
var edit = VsLspFactory.CreateTextEdit(
node.GetRange(source),
context.NewLineString + FormattingUtilities.GetIndentationString(
directive.GetLinePositionSpan(source).Start.Character, context.Options.InsertSpaces, context.Options.TabSize));
var newText = context.NewLineString + FormattingUtilities.GetIndentationString(
directive.GetLinePositionSpan(source).Start.Character, context.Options.InsertSpaces, context.Options.TabSize);
edits.Add(edit);
changes.Add(new TextChange(node.Span, newText));
}
}
private static void ShrinkToSingleSpace(RazorSyntaxNode node, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source)
private static void ShrinkToSingleSpace(RazorSyntaxNode node, ref PooledArrayBuilder<TextChange> changes)
{
// If there is anything other than one single space then we replace with one space between directive and brace.
//
// ie, "@code {" will become "@code {"
var edit = VsLspFactory.CreateTextEdit(node.GetRange(source), " ");
edits.Add(edit);
changes.Add(new TextChange(node.Span, " "));
}
private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, RazorSyntaxNode? directiveNode, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode, RazorSyntaxNode closeBraceNode, ref PooledArrayBuilder<TextEdit> edits)
private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, RazorSyntaxNode? directiveNode, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode, RazorSyntaxNode closeBraceNode, ref PooledArrayBuilder<TextChange> changes)
{
var didFormat = false;
var openBraceRange = openBraceNode.GetRangeWithoutWhitespace(source);
var codeRange = codeNode.GetRangeWithoutWhitespace(source);
if (!codeNode.TryGetLinePositionSpanWithoutWhitespace(source, out var codeRange))
{
return didFormat;
}
if (openBraceRange is not null &&
codeRange is not null &&
if (openBraceNode.TryGetLinePositionSpanWithoutWhitespace(source, out var openBraceRange) &&
openBraceRange.End.Line == codeRange.Start.Line &&
!RangeHasBeenModified(ref edits, codeRange))
!RangeHasBeenModified(ref changes, source.Text, codeRange))
{
var additionalIndentationLevel = GetAdditionalIndentationLevel(context, openBraceRange, openBraceNode, codeNode);
var newText = context.NewLineString;
@ -378,49 +366,46 @@ internal sealed class RazorFormattingPass : IFormattingPass
newText += FormattingUtilities.GetIndentationString(additionalIndentationLevel, context.Options.InsertSpaces, context.Options.TabSize);
}
var edit = VsLspFactory.CreateTextEdit(openBraceRange.End, newText);
edits.Add(edit);
changes.Add(new TextChange(source.Text.GetTextSpan(openBraceRange.End, openBraceRange.End), newText));
didFormat = true;
}
var closeBraceRange = closeBraceNode.GetRangeWithoutWhitespace(source);
if (codeRange is not null &&
closeBraceRange is not null &&
!RangeHasBeenModified(ref edits, codeRange))
if (closeBraceNode.TryGetLinePositionSpanWithoutWhitespace(source, out var closeBraceRange) &&
!RangeHasBeenModified(ref changes, source.Text, codeRange))
{
if (directiveNode is not null &&
directiveNode.GetRange(source).Start.Character < closeBraceRange.Start.Character)
{
// If we have a directive, then we line the close brace up with it, and ensure
// there is a close brace
var edit = VsLspFactory.CreateTextEdit(start: codeRange.End, end: closeBraceRange.Start,
context.NewLineString + FormattingUtilities.GetIndentationString(
directiveNode.GetRange(source).Start.Character, context.Options.InsertSpaces, context.Options.TabSize));
var span = new LinePositionSpan(codeRange.End, closeBraceRange.Start);
var newText = context.NewLineString + FormattingUtilities.GetIndentationString(
directiveNode.GetRange(source).Start.Character, context.Options.InsertSpaces, context.Options.TabSize);
edits.Add(edit);
changes.Add(new TextChange(source.Text.GetTextSpan(span), newText));
didFormat = true;
}
else if (codeRange.End.Line == closeBraceRange.Start.Line)
{
// Add a Newline between the content and the "}" if one doesn't already exist.
var edit = VsLspFactory.CreateTextEdit(codeRange.End, context.NewLineString);
edits.Add(edit);
changes.Add(new TextChange(source.Text.GetTextSpan(codeRange.End, codeRange.End), context.NewLineString));
didFormat = true;
}
}
return didFormat;
static bool RangeHasBeenModified(ref readonly PooledArrayBuilder<TextEdit> edits, Range range)
static bool RangeHasBeenModified(ref readonly PooledArrayBuilder<TextChange> changes, SourceText sourceText, LinePositionSpan span)
{
// Because we don't always know what kind of Razor object we're operating on we have to do this to avoid duplicate edits.
// The other way to accomplish this would be to apply the edits after every node and function, but that's not in scope for my current work.
var hasBeenModified = edits.Any(e => e.Range.End == range.End);
var endIndex = sourceText.GetRequiredAbsoluteIndex(span.End);
var hasBeenModified = changes.Any(e => e.Span.End == endIndex);
return hasBeenModified;
}
static int GetAdditionalIndentationLevel(FormattingContext context, Range range, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode)
static int GetAdditionalIndentationLevel(FormattingContext context, LinePositionSpan range, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode)
{
if (!context.TryGetIndentationLevel(codeNode.Position, out var desiredIndentationLevel))
{

Просмотреть файл

@ -9,6 +9,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
@ -16,8 +17,6 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -63,10 +62,10 @@ internal class RazorFormattingService : IRazorFormattingService
];
}
public async Task<TextEdit[]> GetDocumentFormattingEditsAsync(
public async Task<ImmutableArray<TextChange>> GetDocumentFormattingChangesAsync(
DocumentContext documentContext,
TextEdit[] htmlEdits,
Range? range,
ImmutableArray<TextChange> htmlChanges,
LinePositionSpan? range,
RazorFormattingOptions options,
CancellationToken cancellationToken)
{
@ -86,10 +85,10 @@ internal class RazorFormattingService : IRazorFormattingService
// Despite what it looks like, codeDocument.GetCSharpDocument().Diagnostics is actually the
// Razor diagnostics, not the C# diagnostics 🤦‍
if (range is not null)
var sourceText = codeDocument.Source.Text;
if (range is { } span)
{
var sourceText = codeDocument.Source.Text;
if (codeDocument.GetCSharpDocument().Diagnostics.Any(d => d.Span != SourceSpan.Undefined && range.OverlapsWith(sourceText.GetRange(d.Span))))
if (codeDocument.GetCSharpDocument().Diagnostics.Any(d => d.Span != SourceSpan.Undefined && span.OverlapsWith(sourceText.GetLinePositionSpan(d.Span))))
{
return [];
}
@ -107,93 +106,93 @@ internal class RazorFormattingService : IRazorFormattingService
_workspaceFactory);
var originalText = context.SourceText;
var result = htmlEdits;
var result = htmlChanges;
foreach (var pass in _documentFormattingPasses)
{
cancellationToken.ThrowIfCancellationRequested();
result = await pass.ExecuteAsync(context, result, cancellationToken).ConfigureAwait(false);
}
var filteredEdits = range is null
var filteredChanges = range is not { } linePositionSpan
? result
: result.Where(e => range.LineOverlapsWith(e.Range)).ToArray();
: result.Where(e => linePositionSpan.LineOverlapsWith(sourceText.GetLinePositionSpan(e.Span))).ToImmutableArray();
var normalizedEdits = NormalizeLineEndings(originalText, filteredEdits);
return originalText.MinimizeTextEdits(normalizedEdits);
var normalizedChanges = NormalizeLineEndings(originalText, filteredChanges);
return originalText.MinimizeTextChanges(normalizedChanges);
}
public Task<TextEdit[]> GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
=> ApplyFormattedEditsAsync(
public Task<ImmutableArray<TextChange>> GetCSharpOnTypeFormattingChangesAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
=> ApplyFormattedChangesAsync(
documentContext,
generatedDocumentEdits: [],
generatedDocumentChanges: [],
options,
hostDocumentIndex,
triggerCharacter,
[_csharpOnTypeFormattingPass, .. _validationPasses],
collapseEdits: false,
collapseChanges: false,
automaticallyAddUsings: false,
cancellationToken: cancellationToken);
public Task<TextEdit[]> GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
=> ApplyFormattedEditsAsync(
public Task<ImmutableArray<TextChange>> GetHtmlOnTypeFormattingChangesAsync(DocumentContext documentContext, ImmutableArray<TextChange> htmlChanges, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
=> ApplyFormattedChangesAsync(
documentContext,
htmlEdits,
htmlChanges,
options,
hostDocumentIndex,
triggerCharacter,
[_htmlOnTypeFormattingPass, .. _validationPasses],
collapseEdits: false,
collapseChanges: false,
automaticallyAddUsings: false,
cancellationToken: cancellationToken);
public async Task<TextEdit?> GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit csharpEdit, RazorFormattingOptions options, CancellationToken cancellationToken)
public async Task<TextChange?> TryGetSingleCSharpEditAsync(DocumentContext documentContext, TextChange csharpEdit, RazorFormattingOptions options, CancellationToken cancellationToken)
{
var razorEdits = await ApplyFormattedEditsAsync(
var razorChanges = await ApplyFormattedChangesAsync(
documentContext,
[csharpEdit],
options,
hostDocumentIndex: 0,
triggerCharacter: '\0',
[_csharpOnTypeFormattingPass, .. _validationPasses],
collapseEdits: false,
collapseChanges: false,
automaticallyAddUsings: false,
cancellationToken: cancellationToken).ConfigureAwait(false);
return razorEdits.SingleOrDefault();
return razorChanges.SingleOrDefault();
}
public async Task<TextEdit?> GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken)
public async Task<TextChange?> TryGetCSharpCodeActionEditAsync(DocumentContext documentContext, ImmutableArray<TextChange> csharpChanges, RazorFormattingOptions options, CancellationToken cancellationToken)
{
var razorEdits = await ApplyFormattedEditsAsync(
var razorChanges = await ApplyFormattedChangesAsync(
documentContext,
csharpEdits,
csharpChanges,
options,
hostDocumentIndex: 0,
triggerCharacter: '\0',
[_csharpOnTypeFormattingPass],
collapseEdits: true,
collapseChanges: true,
automaticallyAddUsings: true,
cancellationToken: cancellationToken).ConfigureAwait(false);
return razorEdits.SingleOrDefault();
return razorChanges.SingleOrDefault();
}
public async Task<TextEdit?> GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken)
public async Task<TextChange?> TryGetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, ImmutableArray<TextChange> csharpChanges, RazorFormattingOptions options, CancellationToken cancellationToken)
{
WrapCSharpSnippets(csharpEdits);
csharpChanges = WrapCSharpSnippets(csharpChanges);
var razorEdits = await ApplyFormattedEditsAsync(
var razorChanges = await ApplyFormattedChangesAsync(
documentContext,
csharpEdits,
csharpChanges,
options,
hostDocumentIndex: 0,
triggerCharacter: '\0',
[_csharpOnTypeFormattingPass],
collapseEdits: true,
collapseChanges: true,
automaticallyAddUsings: false,
cancellationToken: cancellationToken).ConfigureAwait(false);
UnwrapCSharpSnippets(razorEdits);
razorChanges = UnwrapCSharpSnippets(razorChanges);
return razorEdits.SingleOrDefault();
return razorChanges.SingleOrDefault();
}
public bool TryGetOnTypeFormattingTriggerKind(RazorCodeDocument codeDocument, int hostDocumentIndex, string triggerCharacter, out RazorLanguageKind triggerCharacterKind)
@ -208,20 +207,20 @@ internal class RazorFormattingService : IRazorFormattingService
};
}
private async Task<TextEdit[]> ApplyFormattedEditsAsync(
private async Task<ImmutableArray<TextChange>> ApplyFormattedChangesAsync(
DocumentContext documentContext,
TextEdit[] generatedDocumentEdits,
ImmutableArray<TextChange> generatedDocumentChanges,
RazorFormattingOptions options,
int hostDocumentIndex,
char triggerCharacter,
ImmutableArray<IFormattingPass> formattingPasses,
bool collapseEdits,
bool collapseChanges,
bool automaticallyAddUsings,
CancellationToken cancellationToken)
{
// If we only received a single edit, let's always return a single edit back.
// Otherwise, merge only if explicitly asked.
collapseEdits |= generatedDocumentEdits.Length == 1;
collapseChanges |= generatedDocumentChanges.Length == 1;
var documentSnapshot = documentContext.Snapshot;
var uri = documentContext.Uri;
@ -236,7 +235,7 @@ internal class RazorFormattingService : IRazorFormattingService
automaticallyAddUsings: automaticallyAddUsings,
hostDocumentIndex,
triggerCharacter);
var result = generatedDocumentEdits;
var result = generatedDocumentChanges;
foreach (var pass in formattingPasses)
{
@ -245,13 +244,13 @@ internal class RazorFormattingService : IRazorFormattingService
}
var originalText = context.SourceText;
var razorEdits = originalText.MinimizeTextEdits(result);
var razorChanges = originalText.MinimizeTextChanges(result);
if (collapseEdits)
if (collapseChanges)
{
var collapsedEdit = MergeEdits(razorEdits, originalText);
if (collapsedEdit.NewText.Length == 0 &&
collapsedEdit.Range.IsZeroWidth())
var collapsedEdit = MergeChanges(razorChanges, originalText);
if (collapsedEdit.NewText is null or { Length: 0 } &&
collapsedEdit.Span.IsEmpty)
{
return [];
}
@ -259,66 +258,71 @@ internal class RazorFormattingService : IRazorFormattingService
return [collapsedEdit];
}
return razorEdits;
return razorChanges;
}
// Internal for testing
internal static TextEdit MergeEdits(TextEdit[] edits, SourceText sourceText)
internal static TextChange MergeChanges(ImmutableArray<TextChange> changes, SourceText sourceText)
{
if (edits.Length == 1)
if (changes.Length == 1)
{
return edits[0];
return changes[0];
}
var changedText = sourceText.WithChanges(edits.Select(sourceText.GetTextChange));
var changedText = sourceText.WithChanges(changes);
var affectedRange = changedText.GetEncompassingTextChangeRange(sourceText);
var spanBeforeChange = affectedRange.Span;
var spanAfterChange = new TextSpan(spanBeforeChange.Start, affectedRange.NewLength);
var newText = changedText.GetSubTextString(spanAfterChange);
var encompassingChange = new TextChange(spanBeforeChange, newText);
return sourceText.GetTextEdit(encompassingChange);
return new TextChange(spanBeforeChange, newText);
}
private static void WrapCSharpSnippets(TextEdit[] csharpEdits)
private static ImmutableArray<TextChange> WrapCSharpSnippets(ImmutableArray<TextChange> csharpChanges)
{
// Currently this method only supports wrapping `$0`, any additional markers aren't formatted properly.
foreach (var edit in csharpEdits)
{
// Formatting doesn't work with syntax errors caused by the cursor marker ($0).
// So, let's avoid the error by wrapping the cursor marker in a comment.
edit.NewText = edit.NewText.Replace("$0", "/*$0*/");
}
return ReplaceInChanges(csharpChanges, "$0", "/*$0*/");
}
private static void UnwrapCSharpSnippets(TextEdit[] razorEdits)
private static ImmutableArray<TextChange> UnwrapCSharpSnippets(ImmutableArray<TextChange> razorChanges)
{
foreach (var edit in razorEdits)
{
// Formatting doesn't work with syntax errors caused by the cursor marker ($0).
// So, let's avoid the error by wrapping the cursor marker in a comment.
edit.NewText = edit.NewText.Replace("/*$0*/", "$0");
}
return ReplaceInChanges(razorChanges, "/*$0*/", "$0");
}
/// <summary>
/// This method counts the occurrences of CRLF and LF line endings in the original text.
/// If LF line endings are more prevalent, it removes any CR characters from the text edits
/// If LF line endings are more prevalent, it removes any CR characters from the text changes
/// to ensure consistency with the LF style.
/// </summary>
private static TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edits)
private static ImmutableArray<TextChange> NormalizeLineEndings(SourceText originalText, ImmutableArray<TextChange> changes)
{
if (originalText.HasLFLineEndings())
{
foreach (var edit in edits)
{
edit.NewText = edit.NewText.Replace("\r", "");
}
return ReplaceInChanges(changes, "\r", "");
}
return edits;
return changes;
}
private static ImmutableArray<TextChange> ReplaceInChanges(ImmutableArray<TextChange> csharpChanges, string toFind, string replacement)
{
using var changes = new PooledArrayBuilder<TextChange>(csharpChanges.Length);
foreach (var change in csharpChanges)
{
if (change.NewText is not { } newText ||
newText.IndexOf(toFind) == -1)
{
changes.Add(change);
continue;
}
// Formatting doesn't work with syntax errors caused by the cursor marker ($0).
// So, let's avoid the error by wrapping the cursor marker in a comment.
changes.Add(new(change.Span, newText.Replace(toFind, replacement)));
}
return changes.DrainToImmutable();
}
internal static class TestAccessor

Просмотреть файл

@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
@ -63,12 +64,12 @@ internal abstract partial class SourceTextDiffer : TextDiffer, IDisposable
}
}
private IReadOnlyList<TextChange> ConsolidateEdits(List<DiffEdit> edits)
private ImmutableArray<TextChange> ConsolidateEdits(List<DiffEdit> edits)
{
// Scan through the list of edits and collapse them into a minimal set of TextChanges.
// This method assumes that there are no overlapping changes and the changes are sorted.
var minimalChanges = new List<TextChange>();
using var minimalChanges = new PooledArrayBuilder<TextChange>(capacity: edits.Count);
var start = 0;
var end = 0;
@ -99,28 +100,18 @@ internal abstract partial class SourceTextDiffer : TextDiffer, IDisposable
minimalChanges.Add(new TextChange(TextSpan.FromBounds(start, end), builder.ToString()));
}
return minimalChanges;
return minimalChanges.DrainToImmutable();
}
public static IReadOnlyList<TextChange> GetMinimalTextChanges(SourceText oldText, SourceText newText, DiffKind kind = DiffKind.Line)
public static ImmutableArray<TextChange> GetMinimalTextChanges(SourceText oldText, SourceText newText, DiffKind kind = DiffKind.Line)
{
if (oldText is null)
{
throw new ArgumentNullException(nameof(oldText));
}
if (newText is null)
{
throw new ArgumentNullException(nameof(newText));
}
if (oldText.ContentEquals(newText))
{
return Array.Empty<TextChange>();
return [];
}
else if (oldText.Length == 0 || newText.Length == 0)
{
return newText.GetTextChanges(oldText);
return newText.GetTextChangesArray(oldText);
}
using SourceTextDiffer differ = kind == DiffKind.Line

Просмотреть файл

@ -154,32 +154,32 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
var razorFormattingOptions = options.FormattingOptions;
var vsLspTextEdit = VsLspFactory.CreateTextEdit(
autoInsertResponseItem.TextEdit.Range.ToLinePositionSpan(),
autoInsertResponseItem.TextEdit.NewText);
var mappedEdit = autoInsertResponseItem.TextEditFormat == RoslynInsertTextFormat.Snippet
? await _razorFormattingService.GetCSharpSnippetFormattingEditAsync(
var csharpSourceText = await remoteDocumentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false);
var csharpTextChange = new TextChange(csharpSourceText.GetTextSpan(autoInsertResponseItem.TextEdit.Range), autoInsertResponseItem.TextEdit.NewText);
var mappedChange = autoInsertResponseItem.TextEditFormat == RoslynInsertTextFormat.Snippet
? await _razorFormattingService.TryGetCSharpSnippetFormattingEditAsync(
remoteDocumentContext,
[vsLspTextEdit],
[csharpTextChange],
razorFormattingOptions,
cancellationToken)
.ConfigureAwait(false)
: await _razorFormattingService.GetSingleCSharpEditAsync(
: await _razorFormattingService.TryGetSingleCSharpEditAsync(
remoteDocumentContext,
vsLspTextEdit,
csharpTextChange,
razorFormattingOptions,
cancellationToken)
.ConfigureAwait(false);
if (mappedEdit is null)
if (mappedChange is not { NewText: not null } change)
{
return Response.NoFurtherHandling;
}
var sourceText = await remoteDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
return Response.Results(
new RemoteAutoInsertTextEdit(
mappedEdit.Range.ToLinePositionSpan(),
mappedEdit.NewText,
sourceText.GetLinePositionSpan(change.Span),
change.NewText,
autoInsertResponseItem.TextEditFormat));
}
}

Просмотреть файл

@ -1,13 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Protocol;
@ -38,60 +35,21 @@ internal sealed class RemoteFormattingService(in ServiceArgs args) : RazorDocume
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetDocumentFormattingEditsAsync(context, htmlChanges, options, cancellationToken),
context => new ValueTask<ImmutableArray<TextChange>>(_formattingService.GetDocumentFormattingChangesAsync(context, htmlChanges, span: null, options, cancellationToken)),
cancellationToken);
private async ValueTask<ImmutableArray<TextChange>> GetDocumentFormattingEditsAsync(
RemoteDocumentContext context,
ImmutableArray<TextChange> htmlChanges,
RazorFormattingOptions options,
CancellationToken cancellationToken)
{
var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var htmlEdits = htmlChanges.Select(sourceText.GetTextEdit).ToArray();
var edits = await _formattingService.GetDocumentFormattingEditsAsync(context, htmlEdits, range: null, options, cancellationToken).ConfigureAwait(false);
if (edits is null)
{
return [];
}
return edits.SelectAsArray(sourceText.GetTextChange);
}
public ValueTask<ImmutableArray<TextChange>> GetRangeFormattingEditsAsync(
RazorPinnedSolutionInfoWrapper solutionInfo,
DocumentId documentId,
ImmutableArray<TextChange> htmlChanges,
LinePositionSpan linePositionSpan,
RazorFormattingOptions options,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetRangeFormattingEditsAsync(context, htmlChanges, linePositionSpan, options, cancellationToken),
cancellationToken);
private async ValueTask<ImmutableArray<TextChange>> GetRangeFormattingEditsAsync(
RemoteDocumentContext context,
RazorPinnedSolutionInfoWrapper solutionInfo,
DocumentId documentId,
ImmutableArray<TextChange> htmlChanges,
LinePositionSpan linePositionSpan,
RazorFormattingOptions options,
CancellationToken cancellationToken)
{
var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var htmlEdits = htmlChanges.Select(sourceText.GetTextEdit).ToArray();
var edits = await _formattingService.GetDocumentFormattingEditsAsync(context, htmlEdits, range: linePositionSpan.ToRange(), options, cancellationToken).ConfigureAwait(false);
if (edits is null)
{
return [];
}
return edits.SelectAsArray(sourceText.GetTextChange);
}
=> RunServiceAsync(
solutionInfo,
documentId,
context => new ValueTask<ImmutableArray<TextChange>>(_formattingService.GetDocumentFormattingChangesAsync(context, htmlChanges, linePositionSpan, options, cancellationToken)),
cancellationToken);
public ValueTask<ImmutableArray<TextChange>> GetOnTypeFormattingEditsAsync(
RazorPinnedSolutionInfoWrapper solutionInfo,
@ -121,22 +79,13 @@ internal sealed class RemoteFormattingService(in ServiceArgs args) : RazorDocume
return [];
}
TextEdit[] result;
if (triggerCharacterKind is RazorLanguageKind.Html)
{
var htmlEdits = htmlChanges.Select(sourceText.GetTextEdit).ToArray();
result = await _formattingService.GetHtmlOnTypeFormattingEditsAsync(context, htmlEdits, options, hostDocumentIndex, triggerCharacter[0], cancellationToken).ConfigureAwait(false);
}
else if (triggerCharacterKind is RazorLanguageKind.CSharp)
{
result = await _formattingService.GetCSharpOnTypeFormattingEditsAsync(context, options, hostDocumentIndex, triggerCharacter[0], cancellationToken).ConfigureAwait(false);
}
else
{
return Assumed.Unreachable<ImmutableArray<TextChange>>();
return await _formattingService.GetHtmlOnTypeFormattingChangesAsync(context, htmlChanges, options, hostDocumentIndex, triggerCharacter[0], cancellationToken).ConfigureAwait(false);
}
return result.SelectAsArray(sourceText.GetTextChange);
Debug.Assert(triggerCharacterKind is RazorLanguageKind.CSharp);
return await _formattingService.GetCSharpOnTypeFormattingChangesAsync(context, options, hostDocumentIndex, triggerCharacter[0], cancellationToken).ConfigureAwait(false);
}
public ValueTask<Response> GetOnTypeFormattingTriggerKindAsync(

Просмотреть файл

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -80,7 +81,7 @@ internal sealed partial class RemoteInlayHintService(in ServiceArgs args) : Razo
if (hint.TextEdits is not null)
{
var changes = hint.TextEdits.Select(csharpSourceText.GetTextChange);
var changes = hint.TextEdits.SelectAsArray(csharpSourceText.GetTextChange);
var mappedChanges = DocumentMappingService.GetHostDocumentEdits(csharpDocument, changes);
hint.TextEdits = mappedChanges.Select(razorSourceText.GetTextEdit).ToArray();
}

Просмотреть файл

@ -106,7 +106,7 @@ internal sealed class CohostOnTypeFormattingEndpoint(
if (triggerKind == IRemoteFormattingService.TriggerKind.ValidHtml)
{
_logger.LogDebug($"Getting Html formatting changes for {razorDocument.FilePath}");
var htmlResult = await GetHtmlFormattingEditsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
var htmlResult = await TryGetHtmlFormattingEditsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlResult is not { } htmlEdits)
{
@ -136,7 +136,7 @@ internal sealed class CohostOnTypeFormattingEndpoint(
return null;
}
private async Task<TextEdit[]?> GetHtmlFormattingEditsAsync(DocumentOnTypeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
private async Task<TextEdit[]?> TryGetHtmlFormattingEditsAsync(DocumentOnTypeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
{
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlDocument is null)

Просмотреть файл

@ -71,7 +71,7 @@ internal sealed class CohostRangeFormattingEndpoint(
private async Task<TextEdit[]?> HandleRequestAsync(DocumentRangeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
{
_logger.LogDebug($"Getting Html formatting changes for {razorDocument.FilePath}");
var htmlResult = await GetHtmlFormattingEditsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
var htmlResult = await TryGetHtmlFormattingEditsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlResult is not { } htmlEdits)
{
@ -101,7 +101,7 @@ internal sealed class CohostRangeFormattingEndpoint(
return null;
}
private async Task<TextEdit[]?> GetHtmlFormattingEditsAsync(DocumentRangeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
private async Task<TextEdit[]?> TryGetHtmlFormattingEditsAsync(DocumentRangeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
{
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlDocument is null)

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using System.Threading;
@ -15,6 +16,7 @@ using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Moq;
using Xunit;
@ -40,6 +42,7 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) :
};
private static readonly TextEdit s_defaultFormattedEdit = VsLspFactory.CreateTextEdit(position: (0, 0), "Remapped & Formatted Edit");
private static readonly TextChange s_defaultFormattedChange = new TextChange(new TextSpan(0, 0), s_defaultFormattedEdit.NewText);
private static readonly CodeAction s_defaultUnresolvedCodeAction = new CodeAction()
{
@ -63,7 +66,8 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) :
var returnedEdits = returnedCodeAction.Edit.DocumentChanges.Value;
Assert.True(returnedEdits.TryGetFirst(out var textDocumentEdits));
var returnedTextDocumentEdit = Assert.Single(textDocumentEdits[0].Edits);
Assert.Equal(s_defaultFormattedEdit, returnedTextDocumentEdit);
Assert.Equal(s_defaultFormattedEdit.NewText, returnedTextDocumentEdit.NewText);
Assert.Equal(s_defaultFormattedEdit.Range, returnedTextDocumentEdit.Range);
}
[Fact]
@ -188,11 +192,11 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) :
private static IRazorFormattingService CreateRazorFormattingService(Uri documentUri)
{
var razorFormattingService = Mock.Of<IRazorFormattingService>(
rfs => rfs.GetCSharpCodeActionEditAsync(
rfs => rfs.TryGetCSharpCodeActionEditAsync(
It.Is<DocumentContext>(c => c.Uri == documentUri),
It.IsAny<TextEdit[]>(),
It.IsAny<ImmutableArray<TextChange>>(),
It.IsAny<RazorFormattingOptions>(),
It.IsAny<CancellationToken>()) == Task.FromResult(s_defaultFormattedEdit), MockBehavior.Strict);
It.IsAny<CancellationToken>()) == Task.FromResult<TextChange?>(s_defaultFormattedChange), MockBehavior.Strict);
return razorFormattingService;
}

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
@ -18,7 +19,7 @@ internal class TestDocumentMappingService : IDocumentMappingService
public LinePosition? GeneratedPosition { get; set; }
public int GeneratedIndex { get; set; }
public IEnumerable<TextChange> GetHostDocumentEdits(IRazorGeneratedDocument generatedDocument, IEnumerable<TextChange> generatedDocumentEdits)
public IEnumerable<TextChange> GetHostDocumentEdits(IRazorGeneratedDocument generatedDocument, ImmutableArray<TextChange> generatedDocumentEdits)
=> [];
public RazorLanguageKind GetLanguageKind(RazorCodeDocument codeDocument, int hostDocumentIndex, bool rightAssociative)

Просмотреть файл

@ -29,7 +29,8 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) :
var @params = new DocumentRangeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, },
Options = new FormattingOptions()
Options = new FormattingOptions(),
Range = VsLspFactory.DefaultRange
};
var requestContext = CreateRazorRequestContext(documentContext);

Просмотреть файл

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -24,16 +25,13 @@ public class FormattingContentValidationPassTest(ITestOutputHelper testOutput) :
public async Task Execute_NonDestructiveEdit_Allowed()
{
// Arrange
var source = SourceText.From(@"
@code {
public class Foo { }
}
");
TestCode source = """
@code {
[||]public class Foo { }
}
""";
using var context = CreateFormattingContext(source);
var edits = new[]
{
VsLspFactory.CreateTextEdit(2, 0, " ")
};
var edits = ImmutableArray.Create(new TextChange(source.Span, " "));
var input = edits;
var pass = GetPass();
@ -41,23 +39,20 @@ public class Foo { }
var result = await pass.ExecuteAsync(context, edits, DisposalToken);
// Assert
Assert.Same(input, result);
Assert.Equal(input, result);
}
[Fact]
public async Task Execute_DestructiveEdit_Rejected()
{
// Arrange
var source = SourceText.From(@"
@code {
public class Foo { }
}
");
TestCode source = """
@code {
[|public class Foo { }
|]}
""";
using var context = CreateFormattingContext(source);
var edits = new[]
{
VsLspFactory.CreateTextEdit(2, 0, 3, 0, " ") // Nukes a line
};
var edits = ImmutableArray.Create(new TextChange(source.Span, " "));
var input = edits;
var pass = GetPass();
@ -78,8 +73,9 @@ public class Foo { }
return pass;
}
private static FormattingContext CreateFormattingContext(SourceText source, int tabSize = 4, bool insertSpaces = true, string? fileKind = null)
private static FormattingContext CreateFormattingContext(TestCode input, int tabSize = 4, bool insertSpaces = true, string? fileKind = null)
{
var source = SourceText.From(input.Text);
var path = "file:///path/to/document.razor";
var uri = new Uri(path);
var (codeDocument, documentSnapshot) = CreateCodeDocumentAndSnapshot(source, uri.AbsolutePath, fileKind: fileKind);

Просмотреть файл

@ -6,11 +6,11 @@ using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
using Xunit.Abstractions;
@ -22,16 +22,13 @@ public class FormattingDiagnosticValidationPassTest(ITestOutputHelper testOutput
public async Task ExecuteAsync_NonDestructiveEdit_Allowed()
{
// Arrange
var source = SourceText.From(@"
@code {
public class Foo { }
}
");
TestCode source = """
@code {
[||]public class Foo { }
}
""";
using var context = CreateFormattingContext(source);
var edits = new[]
{
VsLspFactory.CreateTextEdit(2, 0, " ")
};
var edits = ImmutableArray.Create(new TextChange(source.Span, " "));
var input = edits;
var pass = GetPass();
@ -39,20 +36,21 @@ public class Foo { }
var result = await pass.ExecuteAsync(context, input, DisposalToken);
// Assert
Assert.Same(input, result);
Assert.Equal(input, result);
}
[Fact]
public async Task ExecuteAsync_DestructiveEdit_Rejected()
{
// Arrange
var source = SourceText.From(@"
@code {
public class Foo { }
}
");
// Arrange
TestCode source = """
[||]@code {
public class Foo { }
}
""";
using var context = CreateFormattingContext(source);
var badEdit = VsLspFactory.CreateTextEdit(position: (0, 0), "@ "); // Creates a diagnostic
var badEdit = new TextChange(source.Span, "@ "); // Creates a diagnostic
var pass = GetPass();
// Act
@ -72,8 +70,9 @@ public class Foo { }
return pass;
}
private static FormattingContext CreateFormattingContext(SourceText source, int tabSize = 4, bool insertSpaces = true, string? fileKind = null)
private static FormattingContext CreateFormattingContext(TestCode input, int tabSize = 4, bool insertSpaces = true, string? fileKind = null)
{
var source = SourceText.From(input.Text);
var path = "file:///path/to/document.razor";
var uri = new Uri(path);
var (codeDocument, documentSnapshot) = CreateCodeDocumentAndSnapshot(source, uri.AbsolutePath, fileKind: fileKind);

Просмотреть файл

@ -11,9 +11,8 @@ using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.CodeAnalysis.Text;
using Xunit.Abstractions;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
@ -36,33 +35,33 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut
{
public bool Called { get; private set; }
public Task<TextEdit[]> GetDocumentFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, Range? range, RazorFormattingOptions options, CancellationToken cancellationToken)
public Task<ImmutableArray<TextChange>> GetDocumentFormattingChangesAsync(DocumentContext documentContext, ImmutableArray<TextChange> htmlChanges, LinePositionSpan? span, RazorFormattingOptions options, CancellationToken cancellationToken)
{
Called = true;
return SpecializedTasks.EmptyArray<TextEdit>();
return SpecializedTasks.EmptyImmutableArray<TextChange>();
}
public Task<TextEdit?> GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] formattedEdits, RazorFormattingOptions options, CancellationToken cancellationToken)
public Task<TextChange?> TryGetCSharpCodeActionEditAsync(DocumentContext documentContext, ImmutableArray<TextChange> formattedChanges, RazorFormattingOptions options, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<TextEdit[]> GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
public Task<ImmutableArray<TextChange>> GetCSharpOnTypeFormattingChangesAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<TextEdit?> GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, RazorFormattingOptions options, CancellationToken cancellationToken)
public Task<TextChange?> TryGetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, ImmutableArray<TextChange> edits, RazorFormattingOptions options, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<TextEdit[]> GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
public Task<ImmutableArray<TextChange>> GetHtmlOnTypeFormattingChangesAsync(DocumentContext documentContext, ImmutableArray<TextChange> htmlChanges, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
{
return Task.FromResult(htmlEdits);
return Task.FromResult(htmlChanges);
}
public Task<TextEdit?> GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit initialEdit, RazorFormattingOptions options, CancellationToken cancellationToken)
public Task<TextChange?> TryGetSingleCSharpEditAsync(DocumentContext documentContext, TextChange initialEdit, RazorFormattingOptions options, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

Просмотреть файл

@ -77,9 +77,9 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
TestFileMarkupParser.GetSpans(input, out input, out ImmutableArray<TextSpan> spans);
var source = SourceText.From(input);
var range = spans.IsEmpty
LinePositionSpan? range = spans.IsEmpty
? null
: source.GetRange(spans.Single());
: source.GetLinePositionSpan(spans.Single());
var path = "file:///path/to/Document." + fileKind;
var uri = new Uri(path);
@ -98,20 +98,20 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
client.AddCodeDocument(codeDocument);
var htmlFormatter = new HtmlFormatter(client);
var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken);
var htmlChanges = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken);
// Act
var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range, razorOptions, DisposalToken);
var changes = await formattingService.GetDocumentFormattingChangesAsync(documentContext, htmlChanges, range, razorOptions, DisposalToken);
// Assert
var edited = ApplyEdits(source, edits);
var edited = source.WithChanges(changes);
var actual = edited.ToString();
AssertEx.EqualOrDiff(expected, actual);
if (input.Equals(expected))
{
Assert.Empty(edits);
Assert.Empty(changes);
}
}
@ -152,10 +152,10 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null);
// Act
TextEdit[] edits;
ImmutableArray<TextChange> changes;
if (languageKind == RazorLanguageKind.CSharp)
{
edits = await formattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken);
changes = await formattingService.GetCSharpOnTypeFormattingChangesAsync(documentContext, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken);
}
else
{
@ -163,26 +163,26 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
client.AddCodeDocument(codeDocument);
var htmlFormatter = new HtmlFormatter(client);
var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken);
edits = await formattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken);
var htmlChanges = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken);
changes = await formattingService.GetHtmlOnTypeFormattingChangesAsync(documentContext, htmlChanges, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken);
}
// Assert
var edited = ApplyEdits(razorSourceText, edits);
var edited = razorSourceText.WithChanges( changes);
var actual = edited.ToString();
AssertEx.EqualOrDiff(expected, actual);
if (input.Equals(expected))
{
Assert.Empty(edits);
Assert.Empty(changes);
}
if (expectedChangedLines is not null)
{
var firstLine = edits.Min(e => e.Range.Start.Line);
var lastLine = edits.Max(e => e.Range.End.Line);
var delta = lastLine - firstLine + edits.Count(e => e.NewText.Contains(Environment.NewLine));
var firstLine = changes.Min(e => razorSourceText.GetLinePositionSpan(e.Span).Start.Line);
var lastLine = changes.Max(e => razorSourceText.GetLinePositionSpan(e.Span).End.Line);
var delta = lastLine - firstLine + changes.Count(e => e.NewText.Contains(Environment.NewLine));
Assert.Equal(expectedChangedLines.Value, delta + 1);
}
}
@ -196,11 +196,6 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
string? fileKind = null,
bool inGlobalNamespace = false)
{
if (codeActionEdits is null)
{
throw new NotImplementedException("Code action formatting must provide edits.");
}
// Arrange
fileKind ??= FileKinds.Component;
@ -233,10 +228,12 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null);
// Act
var edit = await formattingService.GetCSharpCodeActionEditAsync(documentContext, codeActionEdits, options, DisposalToken);
var csharpSourceText = codeDocument.GetCSharpSourceText();
var changes = codeActionEdits.SelectAsArray(csharpSourceText.GetTextChange);
var edit = await formattingService.TryGetCSharpCodeActionEditAsync(documentContext, changes, options, DisposalToken);
// Assert
var edited = ApplyEdits(razorSourceText, [edit]);
var edited = razorSourceText.WithChanges(edit.Value);
var actual = edited.ToString();
AssertEx.EqualOrDiff(expected, actual);
@ -245,12 +242,6 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
protected static TextEdit Edit(int startLine, int startChar, int endLine, int endChar, string newText)
=> VsLspFactory.CreateTextEdit(startLine, startChar, endLine, endChar, newText);
private static SourceText ApplyEdits(SourceText source, TextEdit[] edits)
{
var changes = edits.Select(source.GetTextChange);
return source.WithChanges(changes);
}
private static (RazorCodeDocument, IDocumentSnapshot) CreateCodeDocumentAndSnapshot(SourceText text, string path, ImmutableArray<TagHelperDescriptor> tagHelpers = default, string? fileKind = default, bool allowDiagnostics = false, bool inGlobalNamespace = false, bool forceRuntimeCodeGeneration = false)
{
fileKind ??= FileKinds.Component;

Просмотреть файл

@ -3,11 +3,10 @@
#nullable disable
using System.Linq;
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
using Xunit.Abstractions;
@ -16,27 +15,27 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class RazorFormattingServiceTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput)
{
[Fact]
public void MergeEdits_ReturnsSingleEditAsExpected()
public void MergeChanges_ReturnsSingleEditAsExpected()
{
// Arrange
var source = @"
@code {
public class Foo{}
}
";
var sourceText = SourceText.From(source);
var edits = new[]
{
VsLspFactory.CreateTextEdit(VsLspFactory.CreateSingleLineRange(line: 2, character: 13, length: 3), "Bar"),
VsLspFactory.CreateTextEdit(2, 0, " ")
};
TestCode source = """
@code {
[||]public class [|Foo|]{}
}
""";
var sourceText = SourceText.From(source.Text);
var changes = ImmutableArray.CreateRange(
[
new TextChange(source.Spans[0], " "),
new TextChange(source.Spans[1], "Bar")
]);
// Act
var collapsedEdit = RazorFormattingService.MergeEdits(edits, sourceText);
var collapsedEdit = RazorFormattingService.MergeChanges(changes, sourceText);
// Assert
var multiEditChange = sourceText.WithChanges(edits.Select(sourceText.GetTextChange));
var singleEditChange = sourceText.WithChanges(sourceText.GetTextChange(collapsedEdit));
var multiEditChange = sourceText.WithChanges(changes);
var singleEditChange = sourceText.WithChanges(collapsedEdit);
Assert.Equal(multiEditChange.ToString(), singleEditChange.ToString());
}

Просмотреть файл

@ -2,23 +2,25 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
internal class TestHtmlFormatter : IHtmlFormatter
{
public Task<TextEdit[]> GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken)
public Task<ImmutableArray<TextChange>> GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken)
{
return SpecializedTasks.EmptyArray<TextEdit>();
return SpecializedTasks.EmptyImmutableArray<TextChange>();
}
public Task<TextEdit[]> GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken)
public Task<ImmutableArray<TextChange>> GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken)
{
return SpecializedTasks.EmptyArray<TextEdit>();
return SpecializedTasks.EmptyImmutableArray<TextChange>();
}
}