Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -48,15 +48,16 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V

protected override string CustomMessageTarget => CustomMessageNames.RazorRenameEndpointName;

protected override Task<WorkspaceEdit?> TryHandleAsync(RenameParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
protected override async Task<WorkspaceEdit?> TryHandleAsync(RenameParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
{
var documentContext = requestContext.DocumentContext;
if (documentContext is null)
{
return SpecializedTasks.Null<WorkspaceEdit>();
return null;
}

return _renameService.TryGetRazorRenameEditsAsync(documentContext, positionInfo, request.NewName, _projectManager.GetQueryOperations(), cancellationToken);
var result = await _renameService.TryGetRazorRenameEditsAsync(documentContext, positionInfo, request.NewName, _projectManager.GetQueryOperations(), cancellationToken).ConfigureAwait(false);
return result.Edit;
}

protected override bool IsSupported()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ namespace Microsoft.CodeAnalysis.Razor.Rename;

internal interface IRenameService
{
Task<WorkspaceEdit?> TryGetRazorRenameEditsAsync(
Task<RenameResult> TryGetRazorRenameEditsAsync(
DocumentContext documentContext,
DocumentPositionInfo positionInfo,
string newName,
ISolutionQueryOperations solutionQueryOperations,
CancellationToken cancellationToken);
}

internal readonly record struct RenameResult(WorkspaceEdit? Edit, bool FallbackToCSharp = true);
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ namespace Microsoft.CodeAnalysis.Razor.Rename;

internal class RenameService(
IRazorComponentSearchEngine componentSearchEngine,
IFileSystem fileSystem,
LanguageServerFeatureOptions languageServerFeatureOptions) : IRenameService
{
private readonly IRazorComponentSearchEngine _componentSearchEngine = componentSearchEngine;
private readonly IFileSystem _fileSystem = fileSystem;
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;

public async Task<WorkspaceEdit?> TryGetRazorRenameEditsAsync(
public async Task<RenameResult> TryGetRazorRenameEditsAsync(
DocumentContext documentContext,
DocumentPositionInfo positionInfo,
string newName,
Expand All @@ -39,36 +41,39 @@ internal class RenameService(
// We only support renaming of .razor components, not .cshtml tag helpers
if (!documentContext.FileKind.IsComponent())
{
return null;
return new(Edit: null);
}

var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);

var originTagHelpers = await GetOriginTagHelpersAsync(documentContext, positionInfo.HostDocumentIndex, cancellationToken).ConfigureAwait(false);
if (originTagHelpers.IsDefaultOrEmpty)
{
return null;
return new(Edit: null);
}

var originComponentDocumentSnapshot = await _componentSearchEngine
.TryLocateComponentAsync(originTagHelpers.First(), solutionQueryOperations, cancellationToken)
.ConfigureAwait(false);
if (originComponentDocumentSnapshot is null)
{
return null;
return new(Edit: null);
}

var originComponentDocumentFilePath = originComponentDocumentSnapshot.FilePath;
var newPath = MakeNewPath(originComponentDocumentFilePath, newName);
if (File.Exists(newPath))
if (_fileSystem.FileExists(newPath))
{
return null;
// We found a tag, but the new name would cause a conflict, so we can't proceed with the rename,
// even if C# might have worked.
return new(Edit: null, FallbackToCSharp: false);
}

using var _ = ListPool<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>.GetPooledObject(out var documentChanges);
var fileRename = GetFileRenameForComponent(originComponentDocumentSnapshot, newPath);
var fileRename = GetRenameFileEdit(originComponentDocumentFilePath, newPath);
documentChanges.Add(fileRename);
AddEditsForCodeDocument(documentChanges, originTagHelpers, newName, new(documentContext.Uri), codeDocument);
AddAdditionalFileRenames(documentChanges, originComponentDocumentFilePath, newPath);

var documentSnapshots = GetAllDocumentSnapshots(documentContext.FilePath, solutionQueryOperations);

Expand All @@ -86,10 +91,10 @@ internal class RenameService(
}
}

return new WorkspaceEdit
return new(new WorkspaceEdit
{
DocumentChanges = documentChanges.ToArray(),
};
});
}

private static ImmutableArray<IDocumentSnapshot> GetAllDocumentSnapshots(string filePath, ISolutionQueryOperations solutionQueryOperations)
Expand Down Expand Up @@ -126,11 +131,26 @@ private static ImmutableArray<IDocumentSnapshot> GetAllDocumentSnapshots(string
return documentSnapshots.ToImmutableAndClear();
}

private RenameFile GetFileRenameForComponent(IDocumentSnapshot documentSnapshot, string newPath)
private void AddAdditionalFileRenames(List<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>> documentChanges, string oldFilePath, string newFilePath)
{
TryAdd(".cs");
TryAdd(".css");
Comment on lines +136 to +137
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like oldFilePath would be something like C:\Dir\MyFile.razor. So, if they'd exist, we'd be looking for C:\Dir\MyFile.razor.cs and C:\Dir\MyFile.razor.css. Is that right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct


void TryAdd(string extension)
{
var changedPath = oldFilePath + extension;
if (_fileSystem.FileExists(changedPath))
{
documentChanges.Add(GetRenameFileEdit(changedPath, newFilePath + extension));
}
}
}

private RenameFile GetRenameFileEdit(string oldFilePath, string newFilePath)
=> new RenameFile
{
OldDocumentUri = new(LspFactory.CreateFilePathUri(documentSnapshot.FilePath, _languageServerFeatureOptions)),
NewDocumentUri = new(LspFactory.CreateFilePathUri(newPath, _languageServerFeatureOptions)),
OldDocumentUri = new(LspFactory.CreateFilePathUri(oldFilePath, _languageServerFeatureOptions)),
NewDocumentUri = new(LspFactory.CreateFilePathUri(newFilePath, _languageServerFeatureOptions)),
};

private static string MakeNewPath(string originalPath, string newName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.Rename;
[method: ImportingConstructor]
internal sealed class OOPRenameService(
IRazorComponentSearchEngine componentSearchEngine,
IFileSystem fileSystem,
LanguageServerFeatureOptions languageServerFeatureOptions)
: RenameService(componentSearchEngine, languageServerFeatureOptions)
: RenameService(componentSearchEngine, fileSystem, languageServerFeatureOptions)
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,21 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
.TryGetRazorRenameEditsAsync(context, positionInfo, newName, context.GetSolutionQueryOperations(), cancellationToken)
.ConfigureAwait(false);

if (razorEdit is not null)
if (razorEdit.Edit is { } edit)
{
return Results(razorEdit);
return Results(edit);
}

if (positionInfo.LanguageKind != CodeAnalysis.Razor.Protocol.RazorLanguageKind.CSharp)
{
return CallHtml;
}

if (!razorEdit.FallbackToCSharp)
{
return NoFurtherHandling;
}

var csharpEdit = await ExternalHandlers.Rename
.GetRenameEditAsync(generatedDocument, positionInfo.Position.ToLinePosition(), newName, cancellationToken)
.ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ await projectManager.UpdateAsync(updater =>

var searchEngine = new RazorComponentSearchEngine(LoggerFactory);

var renameService = new RenameService(searchEngine, LanguageServerFeatureOptions);
var renameService = new RenameService(searchEngine, new FileSystem(), LanguageServerFeatureOptions);

var endpoint = new RenameEndpoint(
renameService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ await projectManager.UpdateAsync(updater =>

clientConnection ??= StrictMock.Of<IClientConnection>();

var renameService = new RenameService(searchEngine, options);
var renameService = new RenameService(searchEngine, new FileSystem(), options);
var endpoint = new RenameEndpoint(
renameService,
options,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Workspaces;

namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;

internal sealed class TestFileSystem((string filePath, string contents)[]? files) : IFileSystem
{
public bool FileExists(string filePath)
=> files?.Any(f => FilePathNormalizingComparer.Instance.Equals(f.filePath, filePath)) ?? false;

public string ReadFile(string filePath)
=> files.AssumeNotNull().Single(f => FilePathNormalizingComparer.Instance.Equals(f.filePath, filePath)).contents;

public Stream OpenReadStream(string filePath)
=> new MemoryStream(Encoding.UTF8.GetBytes(ReadFile(filePath)));

public IEnumerable<string> GetDirectories(string workspaceDirectory)
=> throw new NotImplementedException();

public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
=> throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor;
using Roslyn.Test.Utilities;
using Roslyn.Text.Adornments;
using Xunit;

Expand All @@ -18,4 +28,83 @@ internal static void AssertExpectedClassification(
Assert.Equal(expectedClassificationType, run.ClassificationTypeName);
Assert.Equal(expectedClassificationStyle, run.Style);
}

public static async Task AssertWorkspaceEditAsync(this WorkspaceEdit workspaceEdit, Solution solution, IEnumerable<(Uri fileUri, string contents)> expectedChanges, CancellationToken cancellationToken)
{
var changes = Assert.NotNull(workspaceEdit.DocumentChanges);

foreach (var change in Flatten(changes))
{
if (change.TryGetFirst(out var textDocumentEdit))
{
var uri = textDocumentEdit.TextDocument.DocumentUri.GetRequiredParsedUri();
var documentId = solution.GetDocumentIdsWithFilePath(RazorUri.GetDocumentFilePathFromUri(uri)).Single();
var document = solution.GetDocument(documentId) ?? solution.GetAdditionalDocument(documentId);
Assert.NotNull(document);
var text = await document.GetTextAsync(cancellationToken);

text = text.WithChanges(textDocumentEdit.Edits.Select(e => text.GetTextChange((TextEdit)e)));

solution = document is Document
? solution.WithDocumentText(document.Id, text)
: solution.WithAdditionalDocumentText(document.Id, text);
}
else if (change.TryGetSecond(out var createFile))
{
var uri = createFile.DocumentUri.GetRequiredParsedUri();
var documentId = DocumentId.CreateNewId(solution.ProjectIds.Single());
var filePath = createFile.DocumentUri.GetRequiredParsedUri().GetDocumentFilePath();
var documentInfo = DocumentInfo.Create(documentId, Path.GetFileName(filePath), filePath: filePath);
solution = solution.AddDocument(documentInfo);
}
else if (change.TryGetThird(out var renameFile))
{
var (oldUri, newUri) = (renameFile.OldDocumentUri.GetRequiredParsedUri(), renameFile.NewDocumentUri.GetRequiredParsedUri());
var documentId = solution.GetDocumentIdsWithFilePath(RazorUri.GetDocumentFilePathFromUri(oldUri)).Single();
var document = solution.GetDocument(documentId) ?? solution.GetAdditionalDocument(documentId);
Assert.NotNull(document);
if (document is Document)
{
solution = solution.WithDocumentFilePath(document.Id, newUri.GetDocumentFilePath());
}
else
{
var filePath = newUri.GetDocumentFilePath();
var text = await document.GetTextAsync(cancellationToken);
solution = document.Project
.RemoveAdditionalDocument(document.Id)
.AddAdditionalDocument(Path.GetFileName(filePath), text, filePath: filePath).Project.Solution;
}
}
else
{
Assert.Fail($"Don't know how to process a {change.Value?.GetType().Name}.");
}
}

foreach (var (uri, contents) in expectedChanges)
{
var document = solution.GetTextDocuments(uri).First();
var text = await document.GetTextAsync(cancellationToken);
AssertEx.EqualOrDiff(contents, text.ToString());
}

static IEnumerable<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>> Flatten(SumType<TextDocumentEdit[], SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[]> documentChanges)
{
if (documentChanges.TryGetFirst(out var textDocumentEdits))
{
foreach (var edit in textDocumentEdits)
{
yield return edit;
}
}
else if (documentChanges.TryGetSecond(out var changes))
{
foreach (var change in changes)
{
yield return change;
}
}
}
}
}
Loading