зеркало из https://github.com/dotnet/razor.git
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:
Коммит
2511efed42
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче