Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Remote.Razor.DocumentMapping;

Expand All @@ -33,6 +34,12 @@ public async Task MapWorkspaceEditAsync(IDocumentSnapshot contextDocumentSnapsho
if (edit.TryGetFirst(out var textDocumentEdit))
{
await MapTextDocumentEditAsync(originSnapshot, textDocumentEdit, cancellationToken).ConfigureAwait(false);

// Roslyn may return edits that already target the real Razor document when it maps
// source-generated documents itself. If both generated C# documents contribute the
// same Razor edit, keep a single edit so applying the workspace edit doesn't fail
// with overlapping changes.
await DeduplicateTextDocumentEditAsync(originSnapshot.TextDocument.Project.Solution, textDocumentEdit, cancellationToken).ConfigureAwait(false);
if (textDocumentEdit.Edits.Length == 0)
{
continue;
Expand Down Expand Up @@ -160,6 +167,71 @@ private async Task<ImmutableArray<TextEdit>> GetMappedTextEditsAsync(RemoteDocum
var textChanges = edits.SelectAsArray(csharpSourceText.GetRazorTextChange);
var mappedEdits = await MapCSharpEditsAsync(textChanges, snapshot, csharpDocument.IsDeclarationDocument, includeCSharpLanguageFeatureEdits: true, directlyMappedEditFilter: null, cancellationToken).ConfigureAwait(false);

return mappedEdits.SelectAsArray(razorSourceText.GetTextEdit);
// Multiple generated C# edits can map to the same Razor span, especially when rename touches
// both implementation and declaration generated documents. SourceText.WithChanges treats exact
// duplicate edits as overlapping, so collapse them after mapping back to Razor coordinates.
return Deduplicate(razorSourceText, mappedEdits.SelectAsArray(razorSourceText.GetTextEdit));
}

private static async Task DeduplicateTextDocumentEditAsync(Solution solution, TextDocumentEdit entry, CancellationToken cancellationToken)
{
if (entry.Edits.Length <= 1)
{
return;
}

var document = await solution.GetTextDocumentAsync(entry.TextDocument, cancellationToken).ConfigureAwait(false);
if (document is null)
{
return;
}

var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
entry.Edits = Deduplicate(sourceText, entry.Edits);
Comment thread
davidwengier marked this conversation as resolved.
Resolved
}

private static ImmutableArray<TextEdit> Deduplicate(SourceText sourceText, ImmutableArray<TextEdit> edits)
{
if (edits.Length <= 1)
{
return edits;
}

using var _ = HashSetPool<(int Start, int Length, string? NewText)>.GetPooledObject(out var seenEdits);
using var builder = new PooledArrayBuilder<TextEdit>(edits.Length);

foreach (var edit in edits)
{
var change = sourceText.GetTextChange(edit);
if (seenEdits.Add((change.Span.Start, change.Span.Length, change.NewText)))
Comment thread
davidwengier marked this conversation as resolved.
Resolved
{
builder.Add(edit);
}
}

return builder.ToImmutableAndClear();
}

private static SumType<TextEdit, AnnotatedTextEdit>[] Deduplicate(SourceText sourceText, SumType<TextEdit, AnnotatedTextEdit>[] edits)
{
if (edits.Length <= 1)
{
return edits;
}

using var _ = HashSetPool<(int Start, int Length, string? NewText)>.GetPooledObject(out var seenEdits);
using var builder = new PooledArrayBuilder<SumType<TextEdit, AnnotatedTextEdit>>(edits.Length);

foreach (var edit in edits)
{
var textEdit = (TextEdit)edit;
var change = sourceText.GetTextChange(textEdit);
if (seenEdits.Add((change.Span.Start, change.Span.Length, change.NewText)))
{
builder.Add(edit);
}
}
Comment thread
davidwengier marked this conversation as resolved.
Resolved

return builder.ToArrayAndClear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);

var generatedDocument = await context.Snapshot
.GetGeneratedDocumentAsync(cancellationToken)
.GetGeneratedDocumentAsync(positionInfo.InDeclDocument, cancellationToken)
.ConfigureAwait(false);

var razorEdit = await _renameService
Expand Down Expand Up @@ -134,7 +134,7 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
return RemoteResponse<LspRange?>.CallHtml;
}

var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false);
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(positionInfo.InDeclDocument, cancellationToken).ConfigureAwait(false);

var csharpRange = await PrepareRenameHandler.GetRenameRangeAsync(generatedDocument, positionInfo.Position.ToLinePosition(), cancellationToken).ConfigureAwait(false);

Expand All @@ -143,7 +143,7 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
return RemoteResponse<LspRange?>.NoFurtherHandling;
}

if (!DocumentMappingService.TryMapToRazorDocumentRange(codeDocument.GetRequiredImplCSharpDocument(), csharpRange, out var mappedRange))
if (!DocumentMappingService.TryMapToRazorDocumentRange(codeDocument.GetRequiredCSharpDocument(positionInfo.InDeclDocument), csharpRange, out var mappedRange))
{
return RemoteResponse<LspRange?>.NoFurtherHandling;
}
Expand Down Expand Up @@ -228,7 +228,11 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
return null;
}

var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false);
// Component file rename updates the generated component class declaration, which lives in the
// declaration document under SONIC. Fall back to implementation for legacy documents that do
// not have a separate declaration document.
var generatedDocument = await context.Snapshot.TryGetGeneratedDocumentAsync(declarationDocument: true, cancellationToken).ConfigureAwait(false)
?? await context.Snapshot.GetGeneratedDocumentAsync(declarationDocument: false, cancellationToken).ConfigureAwait(false);
Comment thread
davidwengier marked this conversation as resolved.
Resolved
var text = await generatedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
var tree = await generatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var declaration = tree.AssumeNotNull().DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
Expand Down