From 17671b6cde40bd8fea56b9d4f1ecd1231f3f513b Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 6 Aug 2024 10:48:00 +1000 Subject: [PATCH 01/12] Extract rename functionality to a separate service --- .../RazorLanguageServer.cs | 2 + .../Refactoring/RenameEndpoint.cs | 333 +---------------- .../Rename/IRenameService.cs | 15 + .../Rename/RenameService.cs | 352 ++++++++++++++++++ .../RenameEndpointDelegationTest.cs | 6 +- .../Refactoring/RenameEndpointTest.cs | 5 +- 6 files changed, 382 insertions(+), 331 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 3f8f6483676..8f7e21257d9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -26,6 +26,7 @@ using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor.FoldingRanges; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Rename; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.DependencyInjection; @@ -177,6 +178,7 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); + services.AddSingleton(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs index 6960617a980..ab1efd66408 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs @@ -20,6 +20,7 @@ using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Rename; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -27,8 +28,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring; [RazorLanguageServerEndpoint(Methods.TextDocumentRenameName)] internal sealed class RenameEndpoint( - IRazorComponentSearchEngine componentSearchEngine, - IProjectCollectionResolver projectResolver, + IRenameService renameService, LanguageServerFeatureOptions languageServerFeatureOptions, IDocumentMappingService documentMappingService, IEditMappingService editMappingService, @@ -40,8 +40,7 @@ internal sealed class RenameEndpoint( clientConnection, loggerFactory.GetOrCreateLogger()), ICapabilitiesProvider { - private readonly IProjectCollectionResolver _projectResolver = projectResolver; - private readonly IRazorComponentSearchEngine _componentSearchEngine = componentSearchEngine; + private readonly IRenameService _renameService = renameService; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly IEditMappingService _editMappingService = editMappingService; @@ -57,27 +56,15 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V protected override string CustomMessageTarget => CustomMessageNames.RazorRenameEndpointName; - protected override async Task TryHandleAsync(RenameParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) + protected override Task TryHandleAsync(RenameParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) { var documentContext = requestContext.DocumentContext; if (documentContext is null) { - return null; - } - - // We only support renaming of .razor components, not .cshtml tag helpers - if (!FileKinds.IsComponent(documentContext.FileKind)) - { - return null; - } - - // If we're in C# then there is no point checking for a component tag, because there won't be one - if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) - { - return null; + return SpecializedTasks.Null(); } - return await TryGetRazorComponentRenameEditsAsync(request, positionInfo.HostDocumentIndex, documentContext, cancellationToken).ConfigureAwait(false); + return _renameService.TryGetRazorRenameEditsAsync(documentContext, positionInfo, request.NewName, cancellationToken); } protected override bool IsSupported() @@ -107,312 +94,4 @@ protected override bool IsSupported() return await _editMappingService.RemapWorkspaceEditAsync(response, cancellationToken).ConfigureAwait(false); } - - private async Task TryGetRazorComponentRenameEditsAsync(RenameParams request, int absoluteIndex, DocumentContext documentContext, CancellationToken cancellationToken) - { - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - - var originTagHelpers = await GetOriginTagHelpersAsync(documentContext, absoluteIndex, cancellationToken).ConfigureAwait(false); - if (originTagHelpers.IsDefaultOrEmpty) - { - return null; - } - - var originComponentDocumentSnapshot = await _componentSearchEngine.TryLocateComponentAsync(documentContext.Snapshot, originTagHelpers.First()).ConfigureAwait(false); - if (originComponentDocumentSnapshot is null) - { - return null; - } - - var originComponentDocumentFilePath = originComponentDocumentSnapshot.FilePath.AssumeNotNull(); - var newPath = MakeNewPath(originComponentDocumentFilePath, request.NewName); - if (File.Exists(newPath)) - { - return null; - } - - using var _ = ListPool>.GetPooledObject(out var documentChanges); - var fileRename = GetFileRenameForComponent(originComponentDocumentSnapshot, newPath); - documentChanges.Add(fileRename); - AddEditsForCodeDocument(documentChanges, originTagHelpers, request.NewName, request.TextDocument.Uri, codeDocument); - - var documentSnapshots = GetAllDocumentSnapshots(documentContext); - - foreach (var documentSnapshot in documentSnapshots) - { - await AddEditsForCodeDocumentAsync(documentChanges, originTagHelpers, request.NewName, documentSnapshot).ConfigureAwait(false); - } - - foreach (var documentChange in documentChanges) - { - if (documentChange.TryGetFirst(out var textDocumentEdit) && - textDocumentEdit.TextDocument.Uri == fileRename.OldUri) - { - textDocumentEdit.TextDocument.Uri = fileRename.NewUri; - } - } - - return new WorkspaceEdit - { - DocumentChanges = documentChanges.ToArray(), - }; - } - - private ImmutableArray GetAllDocumentSnapshots(DocumentContext skipDocumentContext) - { - using var documentSnapshots = new PooledArrayBuilder(); - using var _ = StringHashSetPool.GetPooledObject(out var documentPaths); - - var projects = _projectResolver.EnumerateProjects(skipDocumentContext.Snapshot); - - foreach (var project in projects) - { - foreach (var documentPath in project.DocumentFilePaths) - { - // We've already added refactoring edits for our document snapshot - if (string.Equals(documentPath, skipDocumentContext.FilePath, FilePathComparison.Instance)) - { - continue; - } - - // Don't add duplicates between projects - if (!documentPaths.Add(documentPath)) - { - continue; - } - - // Add to the list and add the path to the set - if (project.GetDocument(documentPath) is not { } snapshot) - { - throw new InvalidOperationException($"{documentPath} in project {project.FilePath} but not retrievable"); - } - - documentSnapshots.Add(snapshot); - } - } - - return documentSnapshots.DrainToImmutable(); - } - - private RenameFile GetFileRenameForComponent(IDocumentSnapshot documentSnapshot, string newPath) - { - // VS Code in Windows expects path to start with '/' - var filePath = documentSnapshot.FilePath.AssumeNotNull(); - var updatedOldPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !filePath.StartsWith("/") - ? '/' + filePath - : filePath; - var oldUri = new UriBuilder - { - Path = updatedOldPath, - Host = string.Empty, - Scheme = Uri.UriSchemeFile, - }.Uri; - - // VS Code in Windows expects path to start with '/' - var updatedNewPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !newPath.StartsWith("/") - ? '/' + newPath - : newPath; - var newUri = new UriBuilder - { - - Path = updatedNewPath, - Host = string.Empty, - Scheme = Uri.UriSchemeFile, - }.Uri; - - return new RenameFile - { - OldUri = oldUri, - NewUri = newUri, - }; - } - - private static string MakeNewPath(string originalPath, string newName) - { - var newFileName = $"{newName}{Path.GetExtension(originalPath)}"; - var directoryName = Path.GetDirectoryName(originalPath); - Assumes.NotNull(directoryName); - var newPath = Path.Combine(directoryName, newFileName); - return newPath; - } - - private async Task AddEditsForCodeDocumentAsync( - List> documentChanges, - ImmutableArray originTagHelpers, - string newName, - IDocumentSnapshot? documentSnapshot) - { - if (documentSnapshot is null) - { - return; - } - - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); - if (codeDocument.IsUnsupported()) - { - return; - } - - if (!FileKinds.IsComponent(codeDocument.GetFileKind())) - { - return; - } - - // VS Code in Windows expects path to start with '/' - var filePath = documentSnapshot.FilePath.AssumeNotNull(); - var updatedPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !filePath.StartsWith("/") - ? "/" + filePath - : filePath; - var uri = new UriBuilder - { - Path = updatedPath, - Host = string.Empty, - Scheme = Uri.UriSchemeFile, - }.Uri; - - AddEditsForCodeDocument(documentChanges, originTagHelpers, newName, uri, codeDocument); - } - - private static void AddEditsForCodeDocument( - List> documentChanges, - ImmutableArray originTagHelpers, - string newName, - Uri uri, - RazorCodeDocument codeDocument) - { - var documentIdentifier = new OptionalVersionedTextDocumentIdentifier { Uri = uri }; - var tagHelperElements = codeDocument.GetSyntaxTree().Root - .DescendantNodes() - .Where(n => n.Kind == SyntaxKind.MarkupTagHelperElement) - .OfType(); - - foreach (var originTagHelper in originTagHelpers) - { - var editedName = newName; - if (originTagHelper.IsComponentFullyQualifiedNameMatch) - { - // Fully qualified binding, our "new name" needs to be fully qualified. - var @namespace = originTagHelper.GetTypeNamespace(); - if (@namespace == null) - { - return; - } - - // The origin TagHelper was fully qualified so any fully qualified rename locations we find will need a fully qualified renamed edit. - editedName = @namespace + "." + newName; - } - - foreach (var node in tagHelperElements) - { - if (node is MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding } tagHelperElement && - BindingContainsTagHelper(originTagHelper, binding)) - { - documentChanges.Add(new TextDocumentEdit - { - TextDocument = documentIdentifier, - Edits = CreateEditsForMarkupTagHelperElement(tagHelperElement, codeDocument, editedName), - }); - } - } - } - } - - private static TextEdit[] CreateEditsForMarkupTagHelperElement(MarkupTagHelperElementSyntax element, RazorCodeDocument codeDocument, string newName) - { - using var _ = ListPool.GetPooledObject(out var edits); - - edits.Add(VsLspFactory.CreateTextEdit(element.StartTag.Name.GetRange(codeDocument.Source), newName)); - - if (element.EndTag is MarkupTagHelperEndTagSyntax endTag) - { - edits.Add(VsLspFactory.CreateTextEdit(endTag.Name.GetRange(codeDocument.Source), newName)); - } - - return [.. edits]; - } - - private static bool BindingContainsTagHelper(TagHelperDescriptor tagHelper, TagHelperBinding potentialBinding) => - potentialBinding.Descriptors.Any(descriptor => descriptor.Equals(tagHelper)); - - private static async Task> GetOriginTagHelpersAsync(DocumentContext documentContext, int absoluteIndex, CancellationToken cancellationToken) - { - var owner = await documentContext.GetSyntaxNodeAsync(absoluteIndex, cancellationToken).ConfigureAwait(false); - if (owner is null) - { - Debug.Fail("Owner should never be null."); - return default; - } - - var node = owner.FirstAncestorOrSelf(n => n.Kind == SyntaxKind.MarkupTagHelperStartTag); - if (node is not MarkupTagHelperStartTagSyntax tagHelperStartTag) - { - return default; - } - - // Ensure the rename action was invoked on the component name - // instead of a component parameter. This serves as an issue - // mitigation till `textDocument/prepareRename` is supported - // and we can ensure renames aren't triggered in unsupported - // contexts. (https://github.com/dotnet/aspnetcore/issues/26407) - if (!tagHelperStartTag.Name.FullSpan.IntersectsWith(absoluteIndex)) - { - return default; - } - - if (tagHelperStartTag?.Parent is not MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding }) - { - return default; - } - - // Can only have 1 component TagHelper belonging to an element at a time - var primaryTagHelper = binding.Descriptors.FirstOrDefault(static d => d.IsComponentTagHelper); - if (primaryTagHelper is null) - { - return default; - } - - using var originTagHelpers = new PooledArrayBuilder(); - originTagHelpers.Add(primaryTagHelper); - - var tagHelpers = await documentContext.Snapshot.Project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); - var associatedTagHelper = FindAssociatedTagHelper(primaryTagHelper, tagHelpers); - if (associatedTagHelper is null) - { - Debug.Fail("Components should always have an associated TagHelper."); - return default; - } - - originTagHelpers.Add(associatedTagHelper); - - return originTagHelpers.DrainToImmutable(); - } - - private static TagHelperDescriptor? FindAssociatedTagHelper(TagHelperDescriptor tagHelper, ImmutableArray tagHelpers) - { - var typeName = tagHelper.GetTypeName(); - var assemblyName = tagHelper.AssemblyName; - foreach (var currentTagHelper in tagHelpers) - { - if (tagHelper == currentTagHelper) - { - // Same as the primary, we're looking for our other pair. - continue; - } - - if (typeName != currentTagHelper.GetTypeName()) - { - continue; - } - - if (assemblyName != currentTagHelper.AssemblyName) - { - continue; - } - - // Found our associated TagHelper, there should only ever be 1 other associated TagHelper (fully qualified and non-fully qualified). - return currentTagHelper; - } - - return null; - } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs new file mode 100644 index 00000000000..36c271f15d2 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Rename; + +internal interface IRenameService +{ + Task TryGetRazorRenameEditsAsync(VersionedDocumentContext documentContext, DocumentPositionInfo positionInfo, string newName, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs new file mode 100644 index 00000000000..27b66d622c6 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs @@ -0,0 +1,352 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using RazorSyntaxKind = Microsoft.AspNetCore.Razor.Language.SyntaxKind; +using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; + +namespace Microsoft.CodeAnalysis.Razor.Rename; + +internal class RenameService( + IRazorComponentSearchEngine componentSearchEngine, + IProjectCollectionResolver projectCollectionResolver, + LanguageServerFeatureOptions languageServerFeatureOptions) : IRenameService +{ + private readonly IRazorComponentSearchEngine _componentSearchEngine = componentSearchEngine; + private readonly IProjectCollectionResolver _projectCollectionResolver = projectCollectionResolver; + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; + + public async Task TryGetRazorRenameEditsAsync(VersionedDocumentContext documentContext, DocumentPositionInfo positionInfo, string newName, CancellationToken cancellationToken) + { + // We only support renaming of .razor components, not .cshtml tag helpers + if (!FileKinds.IsComponent(documentContext.FileKind)) + { + return null; + } + + // If we're in C# then there is no point checking for a component tag, because there won't be one + if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) + { + return null; + } + + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + var originTagHelpers = await GetOriginTagHelpersAsync(documentContext, positionInfo.HostDocumentIndex, cancellationToken).ConfigureAwait(false); + if (originTagHelpers.IsDefaultOrEmpty) + { + return null; + } + + var originComponentDocumentSnapshot = await _componentSearchEngine.TryLocateComponentAsync(documentContext.Snapshot, originTagHelpers.First()).ConfigureAwait(false); + if (originComponentDocumentSnapshot is null) + { + return null; + } + + var originComponentDocumentFilePath = originComponentDocumentSnapshot.FilePath.AssumeNotNull(); + var newPath = MakeNewPath(originComponentDocumentFilePath, newName); + if (File.Exists(newPath)) + { + return null; + } + + using var _ = ListPool>.GetPooledObject(out var documentChanges); + var fileRename = GetFileRenameForComponent(originComponentDocumentSnapshot, newPath); + documentChanges.Add(fileRename); + AddEditsForCodeDocument(documentChanges, originTagHelpers, newName, documentContext.Uri, codeDocument); + + var documentSnapshots = GetAllDocumentSnapshots(documentContext); + + foreach (var documentSnapshot in documentSnapshots) + { + await AddEditsForCodeDocumentAsync(documentChanges, originTagHelpers, newName, documentSnapshot).ConfigureAwait(false); + } + + foreach (var documentChange in documentChanges) + { + if (documentChange.TryGetFirst(out var textDocumentEdit) && + textDocumentEdit.TextDocument.Uri == fileRename.OldUri) + { + textDocumentEdit.TextDocument.Uri = fileRename.NewUri; + } + } + + return new WorkspaceEdit + { + DocumentChanges = documentChanges.ToArray(), + }; + } + + private ImmutableArray GetAllDocumentSnapshots(DocumentContext skipDocumentContext) + { + using var documentSnapshots = new PooledArrayBuilder(); + using var _ = StringHashSetPool.GetPooledObject(out var documentPaths); + + foreach (var project in _projectCollectionResolver.EnumerateProjects(skipDocumentContext.Snapshot)) + { + foreach (var documentPath in project.DocumentFilePaths) + { + // We've already added refactoring edits for our document snapshot + if (string.Equals(documentPath, skipDocumentContext.FilePath, FilePathComparison.Instance)) + { + continue; + } + + // Don't add duplicates between projects + if (!documentPaths.Add(documentPath)) + { + continue; + } + + // Add to the list and add the path to the set + if (project.GetDocument(documentPath) is not { } snapshot) + { + throw new InvalidOperationException($"{documentPath} in project {project.FilePath} but not retrievable"); + } + + documentSnapshots.Add(snapshot); + } + } + + return documentSnapshots.DrainToImmutable(); + } + + private RenameFile GetFileRenameForComponent(IDocumentSnapshot documentSnapshot, string newPath) + { + // VS Code in Windows expects path to start with '/' + var filePath = documentSnapshot.FilePath.AssumeNotNull(); + var updatedOldPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !filePath.StartsWith("/") + ? '/' + filePath + : filePath; + var oldUri = new UriBuilder + { + Path = updatedOldPath, + Host = string.Empty, + Scheme = Uri.UriSchemeFile, + }.Uri; + + // VS Code in Windows expects path to start with '/' + var updatedNewPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !newPath.StartsWith("/") + ? '/' + newPath + : newPath; + var newUri = new UriBuilder + { + + Path = updatedNewPath, + Host = string.Empty, + Scheme = Uri.UriSchemeFile, + }.Uri; + + return new RenameFile + { + OldUri = oldUri, + NewUri = newUri, + }; + } + + private static string MakeNewPath(string originalPath, string newName) + { + var newFileName = $"{newName}{Path.GetExtension(originalPath)}"; + var directoryName = Path.GetDirectoryName(originalPath); + Assumes.NotNull(directoryName); + var newPath = Path.Combine(directoryName, newFileName); + return newPath; + } + + private async Task AddEditsForCodeDocumentAsync( + List> documentChanges, + ImmutableArray originTagHelpers, + string newName, + IDocumentSnapshot? documentSnapshot) + { + if (documentSnapshot is null) + { + return; + } + + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + if (codeDocument.IsUnsupported()) + { + return; + } + + if (!FileKinds.IsComponent(codeDocument.GetFileKind())) + { + return; + } + + // VS Code in Windows expects path to start with '/' + var filePath = documentSnapshot.FilePath.AssumeNotNull(); + var updatedPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !filePath.StartsWith("/") + ? "/" + filePath + : filePath; + var uri = new UriBuilder + { + Path = updatedPath, + Host = string.Empty, + Scheme = Uri.UriSchemeFile, + }.Uri; + + AddEditsForCodeDocument(documentChanges, originTagHelpers, newName, uri, codeDocument); + } + + private static void AddEditsForCodeDocument( + List> documentChanges, + ImmutableArray originTagHelpers, + string newName, + Uri uri, + RazorCodeDocument codeDocument) + { + var documentIdentifier = new OptionalVersionedTextDocumentIdentifier { Uri = uri }; + var tagHelperElements = codeDocument.GetSyntaxTree().Root + .DescendantNodes() + .Where(n => n.Kind == RazorSyntaxKind.MarkupTagHelperElement) + .OfType(); + + foreach (var originTagHelper in originTagHelpers) + { + var editedName = newName; + if (originTagHelper.IsComponentFullyQualifiedNameMatch) + { + // Fully qualified binding, our "new name" needs to be fully qualified. + var @namespace = originTagHelper.GetTypeNamespace(); + if (@namespace == null) + { + return; + } + + // The origin TagHelper was fully qualified so any fully qualified rename locations we find will need a fully qualified renamed edit. + editedName = @namespace + "." + newName; + } + + foreach (var node in tagHelperElements) + { + if (node is MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding } tagHelperElement && + BindingContainsTagHelper(originTagHelper, binding)) + { + documentChanges.Add(new TextDocumentEdit + { + TextDocument = documentIdentifier, + Edits = CreateEditsForMarkupTagHelperElement(tagHelperElement, codeDocument, editedName), + }); + } + } + } + } + + private static TextEdit[] CreateEditsForMarkupTagHelperElement(MarkupTagHelperElementSyntax element, RazorCodeDocument codeDocument, string newName) + { + using var _ = ListPool.GetPooledObject(out var edits); + + edits.Add(VsLspFactory.CreateTextEdit(element.StartTag.Name.GetRange(codeDocument.Source), newName)); + + if (element.EndTag is MarkupTagHelperEndTagSyntax endTag) + { + edits.Add(VsLspFactory.CreateTextEdit(endTag.Name.GetRange(codeDocument.Source), newName)); + } + + return [.. edits]; + } + + private static bool BindingContainsTagHelper(TagHelperDescriptor tagHelper, TagHelperBinding potentialBinding) => + potentialBinding.Descriptors.Any(descriptor => descriptor.Equals(tagHelper)); + + private static async Task> GetOriginTagHelpersAsync(DocumentContext documentContext, int absoluteIndex, CancellationToken cancellationToken) + { + var owner = await documentContext.GetSyntaxNodeAsync(absoluteIndex, cancellationToken).ConfigureAwait(false); + if (owner is null) + { + Debug.Fail("Owner should never be null."); + return default; + } + + var node = owner.FirstAncestorOrSelf(n => n.Kind == RazorSyntaxKind.MarkupTagHelperStartTag); + if (node is not MarkupTagHelperStartTagSyntax tagHelperStartTag) + { + return default; + } + + // Ensure the rename action was invoked on the component name + // instead of a component parameter. This serves as an issue + // mitigation till `textDocument/prepareRename` is supported + // and we can ensure renames aren't triggered in unsupported + // contexts. (https://github.com/dotnet/aspnetcore/issues/26407) + if (!tagHelperStartTag.Name.FullSpan.IntersectsWith(absoluteIndex)) + { + return default; + } + + if (tagHelperStartTag?.Parent is not MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding }) + { + return default; + } + + // Can only have 1 component TagHelper belonging to an element at a time + var primaryTagHelper = binding.Descriptors.FirstOrDefault(static d => d.IsComponentTagHelper); + if (primaryTagHelper is null) + { + return default; + } + + using var originTagHelpers = new PooledArrayBuilder(); + originTagHelpers.Add(primaryTagHelper); + + var tagHelpers = await documentContext.Snapshot.Project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); + var associatedTagHelper = FindAssociatedTagHelper(primaryTagHelper, tagHelpers); + if (associatedTagHelper is null) + { + Debug.Fail("Components should always have an associated TagHelper."); + return default; + } + + originTagHelpers.Add(associatedTagHelper); + + return originTagHelpers.DrainToImmutable(); + } + + private static TagHelperDescriptor? FindAssociatedTagHelper(TagHelperDescriptor tagHelper, ImmutableArray tagHelpers) + { + var typeName = tagHelper.GetTypeName(); + var assemblyName = tagHelper.AssemblyName; + foreach (var currentTagHelper in tagHelpers) + { + if (tagHelper == currentTagHelper) + { + // Same as the primary, we're looking for our other pair. + continue; + } + + if (typeName != currentTagHelper.GetTypeName()) + { + continue; + } + + if (assemblyName != currentTagHelper.AssemblyName) + { + continue; + } + + // Found our associated TagHelper, there should only ever be 1 other associated TagHelper (fully qualified and non-fully qualified). + return currentTagHelper; + } + + return null; + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs index 4188b6bc72b..620a6bba2fe 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Rename; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; @@ -66,9 +67,10 @@ await projectManager.UpdateAsync(updater => var searchEngine = new RazorComponentSearchEngine(projectManager, LoggerFactory); + var renameService = new RenameService(searchEngine, projectManager, LanguageServerFeatureOptions); + var endpoint = new RenameEndpoint( - searchEngine, - projectManager, + renameService, LanguageServerFeatureOptions, DocumentMappingService, EditMappingService, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs index c812fcdbaae..d16c6cd13f2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs @@ -22,6 +22,7 @@ using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Rename; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -708,9 +709,9 @@ await projectManager.UpdateAsync(updater => clientConnection ??= StrictMock.Of(); + var renameService = new RenameService(searchEngine, projectManager, options); var endpoint = new RenameEndpoint( - searchEngine, - projectManager, + renameService, options, documentMappingService, editMappingService, From f3881562f76e051721191d6c86cd1704c753d7f4 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 6 Aug 2024 11:40:02 +1000 Subject: [PATCH 02/12] Cleanups and tweaks --- .../Rename/RenameService.cs | 92 ++++++------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs index 27b66d622c6..6563e5bb059 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs @@ -94,9 +94,9 @@ internal class RenameService( }; } - private ImmutableArray GetAllDocumentSnapshots(DocumentContext skipDocumentContext) + private ImmutableArray GetAllDocumentSnapshots(DocumentContext skipDocumentContext) { - using var documentSnapshots = new PooledArrayBuilder(); + using var documentSnapshots = new PooledArrayBuilder(); using var _ = StringHashSetPool.GetPooledObject(out var documentPaths); foreach (var project in _projectCollectionResolver.EnumerateProjects(skipDocumentContext.Snapshot)) @@ -129,54 +129,41 @@ internal class RenameService( } private RenameFile GetFileRenameForComponent(IDocumentSnapshot documentSnapshot, string newPath) - { - // VS Code in Windows expects path to start with '/' - var filePath = documentSnapshot.FilePath.AssumeNotNull(); - var updatedOldPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !filePath.StartsWith("/") - ? '/' + filePath - : filePath; - var oldUri = new UriBuilder + => new RenameFile { - Path = updatedOldPath, - Host = string.Empty, - Scheme = Uri.UriSchemeFile, - }.Uri; + OldUri = BuildUri(documentSnapshot.FilePath.AssumeNotNull()), + NewUri = BuildUri(newPath), + }; + private Uri BuildUri(string filePath) + { // VS Code in Windows expects path to start with '/' - var updatedNewPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !newPath.StartsWith("/") - ? '/' + newPath - : newPath; - var newUri = new UriBuilder + var updatedPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !filePath.StartsWith("/") + ? '/' + filePath + : filePath; + var oldUri = new UriBuilder { - - Path = updatedNewPath, + Path = updatedPath, Host = string.Empty, Scheme = Uri.UriSchemeFile, }.Uri; - - return new RenameFile - { - OldUri = oldUri, - NewUri = newUri, - }; + return oldUri; } private static string MakeNewPath(string originalPath, string newName) { var newFileName = $"{newName}{Path.GetExtension(originalPath)}"; - var directoryName = Path.GetDirectoryName(originalPath); - Assumes.NotNull(directoryName); - var newPath = Path.Combine(directoryName, newFileName); - return newPath; + var directoryName = Path.GetDirectoryName(originalPath).AssumeNotNull(); + return Path.Combine(directoryName, newFileName); } private async Task AddEditsForCodeDocumentAsync( List> documentChanges, ImmutableArray originTagHelpers, string newName, - IDocumentSnapshot? documentSnapshot) + IDocumentSnapshot documentSnapshot) { - if (documentSnapshot is null) + if (!FileKinds.IsComponent(documentSnapshot.FileKind)) { return; } @@ -187,22 +174,8 @@ private async Task AddEditsForCodeDocumentAsync( return; } - if (!FileKinds.IsComponent(codeDocument.GetFileKind())) - { - return; - } - // VS Code in Windows expects path to start with '/' - var filePath = documentSnapshot.FilePath.AssumeNotNull(); - var updatedPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !filePath.StartsWith("/") - ? "/" + filePath - : filePath; - var uri = new UriBuilder - { - Path = updatedPath, - Host = string.Empty, - Scheme = Uri.UriSchemeFile, - }.Uri; + var uri = BuildUri(documentSnapshot.FilePath.AssumeNotNull()); AddEditsForCodeDocument(documentChanges, originTagHelpers, newName, uri, codeDocument); } @@ -233,7 +206,7 @@ private static void AddEditsForCodeDocument( } // The origin TagHelper was fully qualified so any fully qualified rename locations we find will need a fully qualified renamed edit. - editedName = @namespace + "." + newName; + editedName = $"{@namespace}.{newName}"; } foreach (var node in tagHelperElements) @@ -253,20 +226,20 @@ private static void AddEditsForCodeDocument( private static TextEdit[] CreateEditsForMarkupTagHelperElement(MarkupTagHelperElementSyntax element, RazorCodeDocument codeDocument, string newName) { - using var _ = ListPool.GetPooledObject(out var edits); - - edits.Add(VsLspFactory.CreateTextEdit(element.StartTag.Name.GetRange(codeDocument.Source), newName)); + var startTagEdit = VsLspFactory.CreateTextEdit(element.StartTag.Name.GetRange(codeDocument.Source), newName); if (element.EndTag is MarkupTagHelperEndTagSyntax endTag) { - edits.Add(VsLspFactory.CreateTextEdit(endTag.Name.GetRange(codeDocument.Source), newName)); + var endTagEdit = VsLspFactory.CreateTextEdit(endTag.Name.GetRange(codeDocument.Source), newName); + + return [startTagEdit, endTagEdit]; } - return [.. edits]; + return [startTagEdit]; } - private static bool BindingContainsTagHelper(TagHelperDescriptor tagHelper, TagHelperBinding potentialBinding) => - potentialBinding.Descriptors.Any(descriptor => descriptor.Equals(tagHelper)); + private static bool BindingContainsTagHelper(TagHelperDescriptor tagHelper, TagHelperBinding potentialBinding) + => potentialBinding.Descriptors.Any(descriptor => descriptor.Equals(tagHelper)); private static async Task> GetOriginTagHelpersAsync(DocumentContext documentContext, int absoluteIndex, CancellationToken cancellationToken) { @@ -293,7 +266,7 @@ private static async Task> GetOriginTagHelpe return default; } - if (tagHelperStartTag?.Parent is not MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding }) + if (tagHelperStartTag.Parent is not MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding }) { return default; } @@ -305,20 +278,14 @@ private static async Task> GetOriginTagHelpe return default; } - using var originTagHelpers = new PooledArrayBuilder(); - originTagHelpers.Add(primaryTagHelper); - var tagHelpers = await documentContext.Snapshot.Project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); var associatedTagHelper = FindAssociatedTagHelper(primaryTagHelper, tagHelpers); if (associatedTagHelper is null) { - Debug.Fail("Components should always have an associated TagHelper."); return default; } - originTagHelpers.Add(associatedTagHelper); - - return originTagHelpers.DrainToImmutable(); + return [primaryTagHelper, associatedTagHelper]; } private static TagHelperDescriptor? FindAssociatedTagHelper(TagHelperDescriptor tagHelper, ImmutableArray tagHelpers) @@ -347,6 +314,7 @@ private static async Task> GetOriginTagHelpe return currentTagHelper; } + Debug.Fail("Components should always have an associated TagHelper."); return null; } } From 47665e0d2695a6d44e2033480c20fa41761ddc7a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 7 Aug 2024 14:40:36 +1000 Subject: [PATCH 03/12] Create a remote rename service, and dependencies, to perform the rename --- eng/targets/Services.props | 1 + .../Remote/IRemoteRenameService.cs | 14 ++++ .../Remote/RazorServices.cs | 1 + .../RemoteRazorComponentSearchEngine.cs | 16 ++++ .../Rename/OOPRenameService.cs | 17 ++++ .../Rename/RemoteRenameService.cs | 82 +++++++++++++++++++ 6 files changed, 131 insertions(+) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteRenameService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteRazorComponentSearchEngine.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/OOPRenameService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 747b8e2a392..e4ce786f254 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -25,5 +25,6 @@ + diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteRenameService.cs new file mode 100644 index 00000000000..0ccaf327c37 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteRenameService.cs @@ -0,0 +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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteRenameService : IRemoteJsonService +{ + ValueTask> GetRenameEditAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId documentId, Position position, string newName, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 47d908d75ed..06bf0950825 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -28,6 +28,7 @@ internal static class RazorServices [ (typeof(IRemoteSignatureHelpService), null), (typeof(IRemoteInlayHintService), null), + (typeof(IRemoteRenameService), null), ]; private const string ComponentName = "Razor"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteRazorComponentSearchEngine.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteRazorComponentSearchEngine.cs new file mode 100644 index 00000000000..f8b01245c98 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteRazorComponentSearchEngine.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +[Export(typeof(IRazorComponentSearchEngine)), Shared] +[method: ImportingConstructor] +internal sealed class RemoteRazorComponentSearchEngine( + IProjectCollectionResolver projectCollectionResolver, + ILoggerFactory loggerFactory) : RazorComponentSearchEngine(projectCollectionResolver, loggerFactory) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/OOPRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/OOPRenameService.cs new file mode 100644 index 00000000000..0b2cbdc4272 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/OOPRenameService.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Razor.Rename; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Rename; + +[Export(typeof(IRenameService)), Shared] +[method: ImportingConstructor] +internal sealed class OOPRenameService( + IRazorComponentSearchEngine componentSearchEngine, + IProjectCollectionResolver projectCollectionResolver, + LanguageServerFeatureOptions languageServerFeatureOptions) : RenameService(componentSearchEngine, projectCollectionResolver, languageServerFeatureOptions) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs new file mode 100644 index 00000000000..049413a20f5 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.Rename; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed class RemoteRenameService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteRenameService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteRenameService CreateService(in ServiceArgs args) + => new RemoteRenameService(in args); + } + + private readonly IRenameService _renameService = args.ExportProvider.GetExportedValue(); + private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue(); + private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); + + public ValueTask> GetRenameEditAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + Position position, + string newName, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetRenameEditAsync(context, position, newName, cancellationToken), + cancellationToken); + + private async ValueTask> GetRenameEditAsync( + RemoteDocumentContext context, + Position position, + string newName, + CancellationToken cancellationToken) + { + var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false); + + var hostDocumentIndex = codeDocument.Source.Text.GetRequiredAbsoluteIndex(position); + var positionInfo = _documentMappingService.GetPositionInfo(codeDocument, codeDocument.Source.Text, hostDocumentIndex); + + var razorEdit = await _renameService.TryGetRazorRenameEditsAsync(context, positionInfo, newName, cancellationToken).ConfigureAwait(false); + if (razorEdit is not null) + { + return Results(razorEdit); + } + + if (positionInfo.LanguageKind != CodeAnalysis.Razor.Protocol.RazorLanguageKind.CSharp) + { + return CallHtml; + } + + var csharpEdit = await ExternalHandlers.Rename.GetRenameEditAsync(generatedDocument, positionInfo.Position.ToLinePosition(), newName, cancellationToken).ConfigureAwait(false); + if (csharpEdit is null) + { + return NoFurtherHandling; + } + + // This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go. + var vsEdit = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(csharpEdit)); + if (vsEdit is null) + { + return NoFurtherHandling; + } + + var mappedEdit = await _documentMappingService.RemapWorkspaceEditAsync(vsEdit, cancellationToken).ConfigureAwait(false); + return Results(mappedEdit); + } +} From c1e718153ef1bedda5bc03a835785686a17ae1b6 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 7 Aug 2024 15:02:03 +1000 Subject: [PATCH 04/12] Sync the ReturnCodeActionAndRenamePathsWithPrefixedSlash flag to OOP --- .../Remote/RemoteClientInitializationOptions.cs | 3 +++ .../Initialization/RemoteLanguageServerFeatureOptions.cs | 2 +- .../Remote/RemoteServiceInvoker.cs | 1 + .../Cohost/CohostEndpointTestBase.cs | 3 ++- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs index 88319ee955b..58e0cb07b55 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs @@ -22,4 +22,7 @@ internal struct RemoteClientInitializationOptions [DataMember(Order = 4)] internal required bool IncludeProjectKeyInGeneratedFilePath; + + [DataMember(Order = 5)] + internal required bool ReturnCodeActionAndRenamePathsWithPrefixedSlash; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs index 45cdae212de..8d95d417774 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs @@ -33,7 +33,7 @@ internal class RemoteLanguageServerFeatureOptions : LanguageServerFeatureOptions public override bool UpdateBuffersForClosedDocuments => throw new InvalidOperationException("This option has not been synced to OOP."); - public override bool ReturnCodeActionAndRenamePathsWithPrefixedSlash => throw new InvalidOperationException("This option has not been synced to OOP."); + public override bool ReturnCodeActionAndRenamePathsWithPrefixedSlash => _options.ReturnCodeActionAndRenamePathsWithPrefixedSlash; public override bool IncludeProjectKeyInGeneratedFilePath => _options.IncludeProjectKeyInGeneratedFilePath; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs index ac71f62e1f3..a085fcc21a0 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs @@ -135,6 +135,7 @@ private async Task InitializeRemoteClientAsync(RazorRemoteHostClient remoteClien CSharpVirtualDocumentSuffix = _languageServerFeatureOptions.CSharpVirtualDocumentSuffix, HtmlVirtualDocumentSuffix = _languageServerFeatureOptions.HtmlVirtualDocumentSuffix, IncludeProjectKeyInGeneratedFilePath = _languageServerFeatureOptions.IncludeProjectKeyInGeneratedFilePath, + ReturnCodeActionAndRenamePathsWithPrefixedSlash = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash, }; _logger.LogDebug($"First OOP call, so initializing OOP service."); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 54d9602b0b3..3573e1badf6 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -54,7 +54,8 @@ protected override async Task InitializeAsync() HtmlVirtualDocumentSuffix = ".g.html", IncludeProjectKeyInGeneratedFilePath = false, UsePreciseSemanticTokenRanges = false, - UseRazorCohostServer = true + UseRazorCohostServer = true, + ReturnCodeActionAndRenamePathsWithPrefixedSlash = false, }; UpdateClientInitializationOptions(c => c); From 7d514b3faf6940642a990b621190199a4f88a28a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 7 Aug 2024 16:07:30 +1000 Subject: [PATCH 05/12] Add rename endpoint --- .../RazorLanguageServer.cs | 5 +- .../Cohost/CohostRenameEndpoint.cs | 108 ++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 8f7e21257d9..601df17f06c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -178,12 +178,13 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); - services.AddSingleton(); - services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); if (!featureOptions.UseRazorCohostServer) { + services.AddSingleton(); + services.AddHandlerWithCapabilities(); + services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs new file mode 100644 index 00000000000..c45311582f8 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentRenameName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostRenameEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostRenameEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + LSPRequestInvoker requestInvoker) + : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.TextDocument?.Rename?.DynamicRegistration == true) + { + return new Registration + { + Method = Methods.TextDocumentRenameName, + RegisterOptions = new RenameRegistrationOptions() + { + DocumentSelector = filter, + PrepareProvider = false + } + }; + } + + return null; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(RenameParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + protected override Task HandleRequestAsync(RenameParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken); + + private async Task HandleRequestAsync(RenameParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { + var result = await _remoteServiceInvoker.TryInvokeAsync>( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetRenameEditAsync(solutionInfo, razorDocument.Id, request.Position, request.NewName, cancellationToken), + cancellationToken).ConfigureAwait(false); + + if (result.Result is { } edit) + { + return edit; + } + + if (result.StopHandling) + { + return null; + } + + return await GetHtmlRenameEditAsync(request, razorDocument, cancellationToken).ConfigureAwait(false); + } + + private async Task GetHtmlRenameEditAsync(RenameParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return null; + } + + request.TextDocument.Uri = htmlDocument.Uri; + + var result = await _requestInvoker.ReinvokeRequestOnServerAsync( + htmlDocument.Buffer, + Methods.TextDocumentRenameName, + RazorLSPConstants.HtmlLanguageServerName, + request, + cancellationToken).ConfigureAwait(false); + + return result?.Response; + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostRenameEndpoint instance) + { + public Task HandleRequestAsync(RenameParams request, TextDocument razorDocument, CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + } +} From fb8702671fada9f2cb3dbf3baa114973fd4e90c4 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 8 Aug 2024 12:17:54 +1000 Subject: [PATCH 06/12] Add component rename test to validate Razor behaviour --- .../Cohost/CohostRenameEndpointTest.cs | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs new file mode 100644 index 00000000000..650eb7f44fd --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs @@ -0,0 +1,229 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostRenameEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Theory] + [InlineData("$$Component")] + [InlineData("Com$$ponent")] + [InlineData("Component$$")] + public Task Component_StartTag(string startTag) + => VerifyRenamesAsync( + input: $""" + This is a Razor document. + + + +
+ <{startTag} /> + + +
+ + + +
+
+ + The end. + """, + additionalFiles: [ + // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file + (File("Component.cs"), """ + namespace SomeProject; + + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + // The above will make the component exist, but the .razor file needs to exist too for Uri presentation + (File("Component.razor"), "") + ], + newName: "DifferentName", + expected: """ + This is a Razor document. + + + +
+ + + +
+ + + +
+
+ + The end. + """, + renames: [("Component.razor", "DifferentName.razor")]); + + [Theory(Skip = "https://github.com/dotnet/razor/issues/10717")] + [InlineData("$$Component")] + [InlineData("Com$$ponent")] + [InlineData("Component$$")] + public Task Component_EndTag(string endTag) + => VerifyRenamesAsync( + input: $""" + This is a Razor document. + + + +
+ + + +
+ + + +
+
+ + The end. + """, + additionalFiles: [ + // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file + (File("Component.cs"), """ + namespace SomeProject; + + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + // The above will make the component exist, but the .razor file needs to exist too for Uri presentation + (File("Component.razor"), "") + ], + newName: "DifferentName", + expected: """ + This is a Razor document. + + + +
+ + + +
+ + + +
+
+ + The end. + """, + renames: [("Component.razor", "DifferentName.razor")]); + + [Fact] + public Task Mvc() + => VerifyRenamesAsync( + input: """ + This is a Razor document. + + + + The end. + """, + additionalFiles: [ + // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file + (File("Component.cs"), """ + namespace SomeProject; + + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + // The above will make the component exist, but the .razor file needs to exist too for Uri presentation + (File("Component.razor"), "") + ], + newName: "DifferentName", + expected: "", + fileKind: FileKinds.Legacy); + + private async Task VerifyRenamesAsync(string input, string newName, string expected, string? fileKind = null, (string fileName, string contents)[]? additionalFiles = null, (string oldName, string newName)[]? renames = null) + { + TestFileMarkupParser.GetPosition(input, out var source, out var cursorPosition); + var document = CreateProjectAndRazorDocument(source, fileKind, additionalFiles); + var inputText = await document.GetTextAsync(DisposalToken); + var position = inputText.GetPosition(cursorPosition); + + var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentRenameName, null)]); + + var endpoint = new CohostRenameEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker); + + var renameParams = new RenameParams + { + Position = position, + TextDocument = new TextDocumentIdentifier { Uri = document.CreateUri() }, + NewName = newName, + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(renameParams, document, DisposalToken); + + if (expected.Length == 0) + { + Assert.True(renames is null or []); + Assert.Null(result); + return; + } + + Assumes.NotNull(result); + + foreach (var change in result.DocumentChanges.AssumeNotNull().Second) + { + if (change.TryGetThird(out var renameEdit)) + { + Assert.NotNull(renames); + Assert.Contains(renames, + r => renameEdit.OldUri.GetDocumentFilePath().EndsWith(r.oldName) && + renameEdit.NewUri.GetDocumentFilePath().EndsWith(r.newName)); + } + } + + var actual = ProcessRazorDocumentEdits(inputText, document.CreateUri(), result); + + AssertEx.EqualOrDiff(expected, actual); + } + + private static string ProcessRazorDocumentEdits(SourceText inputText, Uri razorDocumentUri, WorkspaceEdit result) + { + foreach (var change in result.DocumentChanges.AssumeNotNull().Second) + { + if (change.TryGetFirst(out var textDocumentEdit)) + { + if (textDocumentEdit.TextDocument.Uri == razorDocumentUri) + { + foreach (var edit in textDocumentEdit.Edits) + { + inputText = inputText.WithChanges(inputText.GetTextChange(edit)); + } + } + } + } + + return inputText.ToString(); + } + + private static string File(string projectRelativeFileName) + => Path.Combine(TestProjectData.SomeProjectPath, projectRelativeFileName); +} From 6e0bb7004f88bb7bcef1b2035480bce3b1e25314 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 9 Aug 2024 10:57:47 +1000 Subject: [PATCH 07/12] Move EditMappingService, and create Remote and Lsp versions that do document lookups --- .../Html/DefaultHtmlCodeActionProvider.cs | 8 +- .../Html/DefaultHtmlCodeActionResolver.cs | 3 +- .../IServiceCollectionExtensions.cs | 2 +- .../LspEditMappingService.cs | 44 +++++++ .../Refactoring/RenameEndpoint.cs | 3 +- .../AbstractEditMappingService.cs} | 116 +++++++++--------- .../DocumentMapping/IEditMappingService.cs | 3 +- .../RemoteEditMappingService.cs | 56 +++++++++ .../Rename/RemoteRenameService.cs | 5 +- .../SingleServerDelegatingEndpointTestBase.cs | 2 +- 10 files changed, 174 insertions(+), 68 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/LspEditMappingService.cs rename src/Razor/src/{Microsoft.AspNetCore.Razor.LanguageServer/EditMappingService.cs => Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs} (76%) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs index 87ecec1b541..35914ec5e3e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; @@ -27,7 +28,7 @@ public async Task> ProvideAsync( { if (codeAction.Edit is not null) { - await RemapAndFixHtmlCodeActionEditAsync(_editMappingService, context.CodeDocument, codeAction, cancellationToken).ConfigureAwait(false); + await RemapAndFixHtmlCodeActionEditAsync(_editMappingService, context.DocumentSnapshot, codeAction, cancellationToken).ConfigureAwait(false); results.Add(codeAction); } @@ -40,14 +41,15 @@ public async Task> ProvideAsync( return results.ToImmutable(); } - public static async Task RemapAndFixHtmlCodeActionEditAsync(IEditMappingService editMappingService, RazorCodeDocument codeDocument, CodeAction codeAction, CancellationToken cancellationToken) + public static async Task RemapAndFixHtmlCodeActionEditAsync(IEditMappingService editMappingService, IDocumentSnapshot documentSnapshot, CodeAction codeAction, CancellationToken cancellationToken) { Assumes.NotNull(codeAction.Edit); - codeAction.Edit = await editMappingService.RemapWorkspaceEditAsync(codeAction.Edit, cancellationToken).ConfigureAwait(false); + codeAction.Edit = await editMappingService.RemapWorkspaceEditAsync(documentSnapshot, codeAction.Edit, cancellationToken).ConfigureAwait(false); if (codeAction.Edit.TryGetDocumentChanges(out var documentEdits) == true) { + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); var htmlSourceText = codeDocument.GetHtmlSourceText(); foreach (var edit in documentEdits) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionResolver.cs index 0fce29fdc6f..e95ca9d060c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionResolver.cs @@ -40,8 +40,7 @@ public async override Task ResolveAsync( return codeAction; } - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - await DefaultHtmlCodeActionProvider.RemapAndFixHtmlCodeActionEditAsync(_editMappingService, codeDocument, resolvedCodeAction, cancellationToken).ConfigureAwait(false); + await DefaultHtmlCodeActionProvider.RemapAndFixHtmlCodeActionEditAsync(_editMappingService, documentContext.Snapshot, resolvedCodeAction, cancellationToken).ConfigureAwait(false); return resolvedCodeAction; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index be6e74d6c5d..0444b9aa7e7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -203,7 +203,7 @@ public static void AddDocumentManagementServices(this IServiceCollection service services.AddSingleton((services) => (RazorProjectService)services.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/LspEditMappingService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/LspEditMappingService.cs new file mode 100644 index 00000000000..e8d9f6c580e --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/LspEditMappingService.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal class LspEditMappingService( + IDocumentMappingService documentMappingService, + IFilePathService filePathService, + IDocumentContextFactory documentContextFactory) : AbstractEditMappingService(documentMappingService, filePathService) +{ + private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; + + protected override bool TryGetVersionedDocumentContext(IDocumentSnapshot contextDocumentSnapshot, Uri razorDocumentUri, VSProjectContext? projectContext, [NotNullWhen(true)] out VersionedDocumentContext? documentContext) + { + if (!_documentContextFactory.TryCreateForOpenDocument(razorDocumentUri, projectContext, out documentContext)) + { + return false; + } + + return true; + } + + protected override bool TryGetDocumentContext(IDocumentSnapshot contextDocumentSnapshot, Uri razorDocumentUri, [NotNullWhen(true)] out DocumentContext? documentContext) + { + if (!_documentContextFactory.TryCreate(razorDocumentUri, out documentContext)) + { + return false; + } + + return true; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs index ab1efd66408..e7396106fce 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs @@ -92,6 +92,7 @@ protected override bool IsSupported() return null; } - return await _editMappingService.RemapWorkspaceEditAsync(response, cancellationToken).ConfigureAwait(false); + var documentContext = requestContext.DocumentContext.AssumeNotNull(); + return await _editMappingService.RemapWorkspaceEditAsync(documentContext.Snapshot, response, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/EditMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs similarity index 76% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/EditMappingService.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs index da57af39f48..ebd7dd41af3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/EditMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs @@ -3,32 +3,30 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer; +namespace Microsoft.CodeAnalysis.Razor.DocumentMapping; -internal sealed class EditMappingService( +internal abstract class AbstractEditMappingService( IDocumentMappingService documentMappingService, - IFilePathService filePathService, - IDocumentContextFactory documentContextFactory) : IEditMappingService + IFilePathService filePathService) : IEditMappingService { private readonly IDocumentMappingService _documentMappingService = documentMappingService; private readonly IFilePathService _filePathService = filePathService; - private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; - public async Task RemapWorkspaceEditAsync(WorkspaceEdit workspaceEdit, CancellationToken cancellationToken) + public async Task RemapWorkspaceEditAsync(IDocumentSnapshot contextDocumentSnapshot, WorkspaceEdit workspaceEdit, CancellationToken cancellationToken) { if (workspaceEdit.TryGetDocumentChanges(out var documentChanges)) { // The LSP spec says, we should prefer `DocumentChanges` property over `Changes` if available. - var remappedEdits = await RemapVersionedDocumentEditsAsync(documentChanges, cancellationToken).ConfigureAwait(false); + var remappedEdits = await RemapVersionedDocumentEditsAsync(contextDocumentSnapshot, documentChanges, cancellationToken).ConfigureAwait(false); return new WorkspaceEdit() { @@ -38,7 +36,7 @@ public async Task RemapWorkspaceEditAsync(WorkspaceEdit workspace if (workspaceEdit.Changes is { } changeMap) { - var remappedEdits = await RemapDocumentEditsAsync(changeMap, cancellationToken).ConfigureAwait(false); + var remappedEdits = await RemapDocumentEditsAsync(contextDocumentSnapshot, changeMap, cancellationToken).ConfigureAwait(false); return new WorkspaceEdit() { @@ -49,53 +47,7 @@ public async Task RemapWorkspaceEditAsync(WorkspaceEdit workspace return workspaceEdit; } - private async Task RemapVersionedDocumentEditsAsync(TextDocumentEdit[] documentEdits, CancellationToken cancellationToken) - { - using var remappedDocumentEdits = new PooledArrayBuilder(documentEdits.Length); - - foreach (var entry in documentEdits) - { - var generatedDocumentUri = entry.TextDocument.Uri; - - // Check if the edit is actually for a generated document, because if not we don't need to do anything - if (!_filePathService.IsVirtualDocumentUri(generatedDocumentUri)) - { - // This location doesn't point to a background razor file. No need to remap. - remappedDocumentEdits.Add(entry); - continue; - } - - var razorDocumentUri = _filePathService.GetRazorDocumentUri(generatedDocumentUri); - - if (!_documentContextFactory.TryCreateForOpenDocument(razorDocumentUri, entry.TextDocument.GetProjectContext(), out var documentContext)) - { - continue; - } - - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - - var remappedEdits = RemapTextEditsCore(generatedDocumentUri, codeDocument, entry.Edits); - if (remappedEdits.Length == 0) - { - // Nothing to do. - continue; - } - - remappedDocumentEdits.Add(new() - { - TextDocument = new OptionalVersionedTextDocumentIdentifier() - { - Uri = razorDocumentUri, - Version = documentContext.Version - }, - Edits = remappedEdits - }); - } - - return remappedDocumentEdits.ToArray(); - } - - private async Task> RemapDocumentEditsAsync(Dictionary changes, CancellationToken cancellationToken) + private async Task> RemapDocumentEditsAsync(IDocumentSnapshot contextDocumentSnapshot, Dictionary changes, CancellationToken cancellationToken) { var remappedChanges = new Dictionary(capacity: changes.Count); @@ -110,7 +62,7 @@ private async Task> RemapDocumentEditsAsync(Dicti continue; } - if (!_documentContextFactory.TryCreate(uri, out var documentContext)) + if (!TryGetDocumentContext(contextDocumentSnapshot, uri, out var documentContext)) { continue; } @@ -154,4 +106,54 @@ private TextEdit[] RemapTextEditsCore(Uri generatedDocumentUri, RazorCodeDocumen return remappedEdits.ToArray(); } + + private async Task RemapVersionedDocumentEditsAsync(IDocumentSnapshot contextDocumentSnapshot, TextDocumentEdit[] documentEdits, CancellationToken cancellationToken) + { + using var remappedDocumentEdits = new PooledArrayBuilder(documentEdits.Length); + + foreach (var entry in documentEdits) + { + var generatedDocumentUri = entry.TextDocument.Uri; + + // Check if the edit is actually for a generated document, because if not we don't need to do anything + if (!_filePathService.IsVirtualDocumentUri(generatedDocumentUri)) + { + // This location doesn't point to a background razor file. No need to remap. + remappedDocumentEdits.Add(entry); + continue; + } + + var razorDocumentUri = _filePathService.GetRazorDocumentUri(generatedDocumentUri); + + if (!TryGetVersionedDocumentContext(contextDocumentSnapshot, razorDocumentUri, entry.TextDocument.GetProjectContext(), out var documentContext)) + { + continue; + } + + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + var remappedEdits = RemapTextEditsCore(generatedDocumentUri, codeDocument, entry.Edits); + if (remappedEdits.Length == 0) + { + // Nothing to do. + continue; + } + + remappedDocumentEdits.Add(new() + { + TextDocument = new OptionalVersionedTextDocumentIdentifier() + { + Uri = razorDocumentUri, + Version = documentContext.Version + }, + Edits = remappedEdits + }); + } + + return remappedDocumentEdits.ToArray(); + } + + protected abstract bool TryGetVersionedDocumentContext(IDocumentSnapshot contextDocumentSnapshot, Uri razorDocumentUri, VSProjectContext? projectContext, [NotNullWhen(true)] out VersionedDocumentContext? documentContext); + + protected abstract bool TryGetDocumentContext(IDocumentSnapshot contextDocumentSnapshot, Uri razorDocumentUri, [NotNullWhen(true)] out DocumentContext? documentContext); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IEditMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IEditMappingService.cs index 1cf7c098da4..a4085302cbb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IEditMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IEditMappingService.cs @@ -3,11 +3,12 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.DocumentMapping; internal interface IEditMappingService { - Task RemapWorkspaceEditAsync(WorkspaceEdit workspaceEdit, CancellationToken cancellationToken); + Task RemapWorkspaceEditAsync(IDocumentSnapshot contextDocumentSnapshot, WorkspaceEdit workspaceEdit, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs new file mode 100644 index 00000000000..b3f81058f74 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Remote.Razor.DocumentMapping; + +[Export(typeof(IEditMappingService)), Shared] +[method: ImportingConstructor] +internal sealed class RemoteEditMappingService( + IDocumentMappingService documentMappingService, + IFilePathService filePathService, + DocumentSnapshotFactory documentSnapshotFactory) : AbstractEditMappingService(documentMappingService, filePathService) +{ + private readonly DocumentSnapshotFactory _documentSnapshotFactory = documentSnapshotFactory; + + protected override bool TryGetVersionedDocumentContext(IDocumentSnapshot contextDocumentSnapshot, Uri razorDocumentUri, VSProjectContext? projectContext, [NotNullWhen(true)] out VersionedDocumentContext? documentContext) + { + if (contextDocumentSnapshot is not RemoteDocumentSnapshot originSnapshot) + { + throw new InvalidOperationException("RemoteEditMappingService can only be used with RemoteDocumentSnapshot instances."); + } + + var solution = originSnapshot.TextDocument.Project.Solution; + var razorDocumentId = solution.GetDocumentIdsWithUri(razorDocumentUri).FirstOrDefault(); + + // If we couldn't locate the .razor file, just return the generated file. + if (razorDocumentId is null || + solution.GetAdditionalDocument(razorDocumentId) is not TextDocument razorDocument) + { + documentContext = null; + return false; + } + + var razorDocumentSnapshot = _documentSnapshotFactory.GetOrCreate(razorDocument); + + documentContext = new RemoteDocumentContext(razorDocumentUri, razorDocumentSnapshot); + return true; + } + + protected override bool TryGetDocumentContext(IDocumentSnapshot contextDocumentSnapshot, Uri razorDocumentUri, [NotNullWhen(true)] out DocumentContext? documentContext) + { + // In OOP there is no difference between versioned and unversioned document contexts. + var result = TryGetVersionedDocumentContext(contextDocumentSnapshot, razorDocumentUri, projectContext: null, out var versionedDocumentContext); + documentContext = versionedDocumentContext; + return result; + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs index 049413a20f5..3ce1678867b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs @@ -26,7 +26,8 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args) private readonly IRenameService _renameService = args.ExportProvider.GetExportedValue(); private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue(); - private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); + private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); + private readonly IEditMappingService _editMappingService = args.ExportProvider.GetExportedValue(); public ValueTask> GetRenameEditAsync( JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, @@ -76,7 +77,7 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args) return NoFurtherHandling; } - var mappedEdit = await _documentMappingService.RemapWorkspaceEditAsync(vsEdit, cancellationToken).ConfigureAwait(false); + var mappedEdit = await _editMappingService.RemapWorkspaceEditAsync(context.Snapshot, vsEdit, cancellationToken).ConfigureAwait(false); return Results(mappedEdit); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs index 853c036a485..4b2e2f3e871 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs @@ -66,7 +66,7 @@ private protected async Task CreateLanguageServerAsync( MockBehavior.Strict); DocumentMappingService = new LspDocumentMappingService(FilePathService, DocumentContextFactory, LoggerFactory); - EditMappingService = new EditMappingService(DocumentMappingService, FilePathService, DocumentContextFactory); + EditMappingService = new LspEditMappingService(DocumentMappingService, FilePathService, DocumentContextFactory); var csharpServer = await CSharpTestLspServerHelpers.CreateCSharpLspServerAsync( csharpFiles, From 3308f4f4fcd059ad14a91f301c0d7a14c098d559 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 9 Aug 2024 10:57:53 +1000 Subject: [PATCH 08/12] Add C# rename test --- .../Cohost/CohostRenameEndpointTest.cs | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs index 650eb7f44fd..f42d14c30b4 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs @@ -20,6 +20,41 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; public class CohostRenameEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) { + [Fact] + public Task CSharp_Method() + => VerifyRenamesAsync( + input: """ + This is a Razor document. + +

@MyMethod()

+ + @code + { + public string MyMe$$thod() + { + return $"Hi from {nameof(MyMethod)}"; + } + } + + The end. + """, + newName: "CallThisFunction", + expected: """ + This is a Razor document. + +

@CallThisFunction()

+ + @code + { + public string CallThisFunction() + { + return $"Hi from {nameof(CallThisFunction)}"; + } + } + + The end. + """); + [Theory] [InlineData("$$Component")] [InlineData("Com$$ponent")] @@ -189,14 +224,18 @@ private async Task VerifyRenamesAsync(string input, string newName, string expec Assumes.NotNull(result); - foreach (var change in result.DocumentChanges.AssumeNotNull().Second) + if (result.DocumentChanges.AssumeNotNull().TryGetSecond(out var changes)) { - if (change.TryGetThird(out var renameEdit)) + Assert.NotNull(renames); + + foreach (var change in changes) { - Assert.NotNull(renames); - Assert.Contains(renames, - r => renameEdit.OldUri.GetDocumentFilePath().EndsWith(r.oldName) && - renameEdit.NewUri.GetDocumentFilePath().EndsWith(r.newName)); + if (change.TryGetThird(out var renameEdit)) + { + Assert.Contains(renames, + r => renameEdit.OldUri.GetDocumentFilePath().EndsWith(r.oldName) && + renameEdit.NewUri.GetDocumentFilePath().EndsWith(r.newName)); + } } } @@ -207,16 +246,14 @@ private async Task VerifyRenamesAsync(string input, string newName, string expec private static string ProcessRazorDocumentEdits(SourceText inputText, Uri razorDocumentUri, WorkspaceEdit result) { - foreach (var change in result.DocumentChanges.AssumeNotNull().Second) + Assert.True(result.TryGetDocumentChanges(out var textDocumentEdits)); + foreach (var textDocumentEdit in textDocumentEdits) { - if (change.TryGetFirst(out var textDocumentEdit)) + if (textDocumentEdit.TextDocument.Uri == razorDocumentUri) { - if (textDocumentEdit.TextDocument.Uri == razorDocumentUri) + foreach (var edit in textDocumentEdit.Edits) { - foreach (var edit in textDocumentEdit.Edits) - { - inputText = inputText.WithChanges(inputText.GetTextChange(edit)); - } + inputText = inputText.WithChanges(inputText.GetTextChange(edit)); } } } From ee44c75030b09d4cf184951b29fd10556915d163 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 9 Aug 2024 11:03:33 +1000 Subject: [PATCH 09/12] Rename TryGetTextDocumentChanges to something better, and cleanup, and remove duplicate --- .../Html/DefaultHtmlCodeActionProvider.cs | 2 +- ...actTextDocumentPresentationEndpointBase.cs | 64 ++++--------------- .../AbstractEditMappingService.cs | 4 +- .../VsLspExtensions_WorkspaceEdit.cs | 16 ++--- .../Cohost/CohostUriPresentationEndpoint.cs | 2 +- .../CodeActionEndToEndTest.NetFx.cs | 4 +- .../Html/DefaultHtmlCodeActionProviderTest.cs | 6 +- .../Html/DefaultHtmlCodeActionResolverTest.cs | 6 +- .../Cohost/CohostRenameEndpointTest.cs | 2 +- 9 files changed, 34 insertions(+), 72 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs index 35914ec5e3e..1ce1cba0a8e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs @@ -47,7 +47,7 @@ public static async Task RemapAndFixHtmlCodeActionEditAsync(IEditMappingService codeAction.Edit = await editMappingService.RemapWorkspaceEditAsync(documentSnapshot, codeAction.Edit, cancellationToken).ConfigureAwait(false); - if (codeAction.Edit.TryGetDocumentChanges(out var documentEdits) == true) + if (codeAction.Edit.TryGetTextDocumentEdits(out var documentEdits)) { var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); var htmlSourceText = codeDocument.GetHtmlSourceText(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentPresentation/AbstractTextDocumentPresentationEndpointBase.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentPresentation/AbstractTextDocumentPresentationEndpointBase.cs index b61b08aa239..f0059d00a19 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentPresentation/AbstractTextDocumentPresentationEndpointBase.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentPresentation/AbstractTextDocumentPresentationEndpointBase.cs @@ -22,25 +22,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.DocumentPresentation; -internal abstract class AbstractTextDocumentPresentationEndpointBase : IRazorRequestHandler, ICapabilitiesProvider - where TParams : IPresentationParams +internal abstract class AbstractTextDocumentPresentationEndpointBase( + IDocumentMappingService documentMappingService, + IClientConnection clientConnection, + IFilePathService filePathService, + ILogger logger) : IRazorRequestHandler, ICapabilitiesProvider + where TParams : IPresentationParams { - private readonly IDocumentMappingService _documentMappingService; - private readonly IClientConnection _clientConnection; - private readonly IFilePathService _filePathService; - private readonly ILogger _logger; - - protected AbstractTextDocumentPresentationEndpointBase( - IDocumentMappingService documentMappingService, - IClientConnection clientConnection, - IFilePathService filePathService, - ILogger logger) - { - _documentMappingService = documentMappingService; - _clientConnection = clientConnection; - _filePathService = filePathService; - _logger = logger; - } + private readonly IDocumentMappingService _documentMappingService = documentMappingService; + private readonly IClientConnection _clientConnection = clientConnection; + private readonly IFilePathService _filePathService = filePathService; + private readonly ILogger _logger = logger; public abstract string EndpointName { get; } @@ -128,36 +120,6 @@ protected AbstractTextDocumentPresentationEndpointBase( return edit; } - private static bool TryGetDocumentChanges(WorkspaceEdit workspaceEdit, [NotNullWhen(true)] out TextDocumentEdit[]? documentChanges) - { - if (workspaceEdit.DocumentChanges?.Value is TextDocumentEdit[] documentEditArray) - { - documentChanges = documentEditArray; - return true; - } - - if (workspaceEdit.DocumentChanges?.Value is SumType[] sumTypeArray) - { - using var documentEdits = new PooledArrayBuilder(); - foreach (var sumType in sumTypeArray) - { - if (sumType.Value is TextDocumentEdit textDocumentEdit) - { - documentEdits.Add(textDocumentEdit); - } - } - - if (documentEdits.Count > 0) - { - documentChanges = documentEdits.ToArray(); - return true; - } - } - - documentChanges = null; - return false; - } - private Dictionary MapChanges(Dictionary changes, bool mapRanges, RazorCodeDocument codeDocument) { var remappedChanges = new Dictionary(); @@ -216,7 +178,7 @@ private TextDocumentEdit[] MapDocumentChanges(TextDocumentEdit[] documentEdits, Uri = razorDocumentUri, Version = hostDocumentVersion }, - Edits = remappedEdits.ToArray() + Edits = [.. remappedEdits] }); } @@ -247,10 +209,10 @@ private TextEdit[] MapTextEdits(bool mapRanges, RazorCodeDocument codeDocument, private WorkspaceEdit? MapWorkspaceEdit(WorkspaceEdit workspaceEdit, bool mapRanges, RazorCodeDocument codeDocument, int hostDocumentVersion) { - if (TryGetDocumentChanges(workspaceEdit, out var documentChanges)) + if (workspaceEdit.TryGetTextDocumentEdits(out var documentEdits)) { // The LSP spec says, we should prefer `DocumentChanges` property over `Changes` if available. - var remappedEdits = MapDocumentChanges(documentChanges, mapRanges, codeDocument, hostDocumentVersion); + var remappedEdits = MapDocumentChanges(documentEdits, mapRanges, codeDocument, hostDocumentVersion); return new WorkspaceEdit() { DocumentChanges = remappedEdits diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs index ebd7dd41af3..f19d04c3500 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractEditMappingService.cs @@ -23,10 +23,10 @@ internal abstract class AbstractEditMappingService( public async Task RemapWorkspaceEditAsync(IDocumentSnapshot contextDocumentSnapshot, WorkspaceEdit workspaceEdit, CancellationToken cancellationToken) { - if (workspaceEdit.TryGetDocumentChanges(out var documentChanges)) + if (workspaceEdit.TryGetTextDocumentEdits(out var documentEdits)) { // The LSP spec says, we should prefer `DocumentChanges` property over `Changes` if available. - var remappedEdits = await RemapVersionedDocumentEditsAsync(contextDocumentSnapshot, documentChanges, cancellationToken).ConfigureAwait(false); + var remappedEdits = await RemapVersionedDocumentEditsAsync(contextDocumentSnapshot, documentEdits, cancellationToken).ConfigureAwait(false); return new WorkspaceEdit() { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_WorkspaceEdit.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_WorkspaceEdit.cs index 7c732521908..961a810f3de 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_WorkspaceEdit.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_WorkspaceEdit.cs @@ -1,40 +1,40 @@ // 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 Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.VisualStudio.LanguageServer.Protocol; internal static partial class VsLspExtensions { - public static bool TryGetDocumentChanges(this WorkspaceEdit workspaceEdit, [NotNullWhen(true)] out TextDocumentEdit[]? documentChanges) + public static bool TryGetTextDocumentEdits(this WorkspaceEdit workspaceEdit, [NotNullWhen(true)] out TextDocumentEdit[]? textDocumentEdits) { if (workspaceEdit.DocumentChanges?.Value is TextDocumentEdit[] documentEdits) { - documentChanges = documentEdits; + textDocumentEdits = documentEdits; return true; } if (workspaceEdit.DocumentChanges?.Value is SumType[] sumTypeArray) { - var documentEditList = new List(); + using var builder = new PooledArrayBuilder(); foreach (var sumType in sumTypeArray) { if (sumType.Value is TextDocumentEdit textDocumentEdit) { - documentEditList.Add(textDocumentEdit); + builder.Add(textDocumentEdit); } } - if (documentEditList.Count > 0) + if (builder.Count > 0) { - documentChanges = documentEditList.ToArray(); + textDocumentEdits = builder.ToArray(); return true; } } - documentChanges = null; + textDocumentEdits = null; return false; } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs index 4a4c494d00a..7d9d55e302c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs @@ -123,7 +123,7 @@ internal class CohostUriPresentationEndpoint( return null; } - if (!workspaceEdit.TryGetDocumentChanges(out var edits)) + if (!workspaceEdit.TryGetTextDocumentEdits(out var edits)) { return null; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs index e9992610258..33b35e3df98 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs @@ -1228,9 +1228,9 @@ private async Task GetEditsAsync( Assert.NotNull(resolveResult.Edit); var workspaceEdit = resolveResult.Edit; - Assert.True(workspaceEdit.TryGetDocumentChanges(out var changes)); + Assert.True(workspaceEdit.TryGetTextDocumentEdits(out var documentEdits)); - return changes; + return documentEdits; } private class GenerateMethodResolverDocumentContextFactory : TestDocumentContextFactory diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs index a02a209f947..e891b87e6be 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs @@ -126,10 +126,10 @@ public async Task ProvideAsync_RemapsAndFixesEdits() // Assert var action = Assert.Single(providedCodeActions); Assert.NotNull(action.Edit); - Assert.True(action.Edit.TryGetDocumentChanges(out var changes)); - Assert.Equal(documentPath, changes[0].TextDocument.Uri.AbsolutePath); + Assert.True(action.Edit.TryGetTextDocumentEdits(out var documentEdits)); + Assert.Equal(documentPath, documentEdits[0].TextDocument.Uri.AbsolutePath); // Edit should be converted to 2 edits, to remove the tags - Assert.Collection(changes[0].Edits, + Assert.Collection(documentEdits[0].Edits, e => { Assert.Equal("", e.NewText); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs index 9593a139a17..ba32941f73a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs @@ -96,10 +96,10 @@ public async Task ResolveAsync_RemapsAndFixesEdits() // Assert Assert.NotNull(action.Edit); - Assert.True(action.Edit.TryGetDocumentChanges(out var changes)); - Assert.Equal(documentPath, changes[0].TextDocument.Uri.AbsolutePath); + Assert.True(action.Edit.TryGetTextDocumentEdits(out var documentEdits)); + Assert.Equal(documentPath, documentEdits[0].TextDocument.Uri.AbsolutePath); // Edit should be converted to 2 edits, to remove the tags - Assert.Collection(changes[0].Edits, + Assert.Collection(documentEdits[0].Edits, e => { Assert.Equal("", e.NewText); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs index f42d14c30b4..c273c9d63a7 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs @@ -246,7 +246,7 @@ private async Task VerifyRenamesAsync(string input, string newName, string expec private static string ProcessRazorDocumentEdits(SourceText inputText, Uri razorDocumentUri, WorkspaceEdit result) { - Assert.True(result.TryGetDocumentChanges(out var textDocumentEdits)); + Assert.True(result.TryGetTextDocumentEdits(out var textDocumentEdits)); foreach (var textDocumentEdit in textDocumentEdits) { if (textDocumentEdit.TextDocument.Uri == razorDocumentUri) From ef49d716d3db69f8b2c9b1d8bf494eebc8a7846c Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 9 Aug 2024 11:14:37 +1000 Subject: [PATCH 10/12] Fix up tests because sometimes you ask Visual Studio to compile and it decides to skip a few things --- .../CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs | 2 +- .../CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs | 2 +- .../Refactoring/RenameEndpointTest.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs index e891b87e6be..bddb160495e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs @@ -93,7 +93,7 @@ public async Task ProvideAsync_RemapsAndFixesEdits() var editMappingServiceMock = new StrictMock(); editMappingServiceMock - .Setup(x => x.RemapWorkspaceEditAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.RemapWorkspaceEditAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(remappedEdit); var provider = new DefaultHtmlCodeActionProvider(editMappingServiceMock.Object); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs index ba32941f73a..480c4c9c633 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionResolverTest.cs @@ -57,7 +57,7 @@ public async Task ResolveAsync_RemapsAndFixesEdits() var editMappingServiceMock = new StrictMock(); editMappingServiceMock - .Setup(x => x.RemapWorkspaceEditAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.RemapWorkspaceEditAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(remappedEdit); var resolver = new DefaultHtmlCodeActionResolver(documentContextFactory, CreateLanguageServer(resolvedCodeAction), editMappingServiceMock.Object); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs index d16c6cd13f2..3aa916de91a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs @@ -540,7 +540,7 @@ public async Task Handle_Rename_SingleServer_CallsDelegatedLanguageServer() var editMappingServiceMock = new StrictMock(); editMappingServiceMock - .Setup(c => c.RemapWorkspaceEditAsync(It.IsAny(), It.IsAny())) + .Setup(c => c.RemapWorkspaceEditAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(delegatedEdit); var (endpoint, documentContextFactory) = await CreateEndpointAndDocumentContextFactoryAsync( From 1297fea58ada9082fd417a19adb1f2ce8468754c Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 9 Aug 2024 11:30:59 +1000 Subject: [PATCH 11/12] Extract some common code --- .../Extensions/SolutionExtensions.cs | 18 ++++++++++++++++++ .../RemoteDocumentMappingService.cs | 8 +------- .../RemoteEditMappingService.cs | 7 +------ .../RemoteUriPresentationService.cs | 12 +----------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SolutionExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SolutionExtensions.cs index 9aaf5b9a5c9..8dd52c0ced7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SolutionExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SolutionExtensions.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis.Razor; @@ -13,6 +15,22 @@ internal static class SolutionExtensions public static ImmutableArray GetDocumentIdsWithUri(this Solution solution, Uri uri) => solution.GetDocumentIdsWithFilePath(uri.GetDocumentFilePath()); + public static bool TryGetRazorDocument(this Solution solution, Uri razorDocumentUri, [NotNullWhen(true)] out TextDocument? razorDocument) + { + var razorDocumentId = solution.GetDocumentIdsWithUri(razorDocumentUri).FirstOrDefault(); + + // If we couldn't locate the .razor file, just return the generated file. + if (razorDocumentId is null || + solution.GetAdditionalDocument(razorDocumentId) is not TextDocument document) + { + razorDocument = null; + return false; + } + + razorDocument = document; + return true; + } + public static Project GetRequiredProject(this Solution solution, ProjectId projectId) { return solution.GetProject(projectId) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs index a4b946fc645..50fbc2fec25 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs @@ -3,7 +3,6 @@ using System; using System.Composition; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; @@ -47,12 +46,7 @@ internal sealed class RemoteDocumentMappingService( } var solution = originSnapshot.TextDocument.Project.Solution; - - var razorDocumentId = solution.GetDocumentIdsWithUri(razorDocumentUri).FirstOrDefault(); - - // If we couldn't locate the .razor file, just return the generated file. - if (razorDocumentId is null || - solution.GetAdditionalDocument(razorDocumentId) is not TextDocument razorDocument) + if (!solution.TryGetRazorDocument(razorDocumentUri, out var razorDocument)) { return (generatedDocumentUri, generatedDocumentRange); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs index b3f81058f74..359c4e90729 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs @@ -4,7 +4,6 @@ using System; using System.Composition; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -30,11 +29,7 @@ protected override bool TryGetVersionedDocumentContext(IDocumentSnapshot context } var solution = originSnapshot.TextDocument.Project.Solution; - var razorDocumentId = solution.GetDocumentIdsWithUri(razorDocumentUri).FirstOrDefault(); - - // If we couldn't locate the .razor file, just return the generated file. - if (razorDocumentId is null || - solution.GetAdditionalDocument(razorDocumentId) is not TextDocument razorDocument) + if (!solution.TryGetRazorDocument(razorDocumentUri, out var razorDocument)) { documentContext = null; return false; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs index 23336fb656d..393765a7a0a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs @@ -71,17 +71,7 @@ private async ValueTask GetPresentationAsync( } var solution = context.TextDocument.Project.Solution; - - // Make sure we go through Roslyn to go from the Uri the client sent us, to one that it has a chance of finding in the solution - var ids = solution.GetDocumentIdsWithUri(razorFileUri); - if (ids.Length == 0) - { - return Response.CallHtml; - } - - // We assume linked documents would produce the same component tag so just take the first - var otherDocument = solution.GetAdditionalDocument(ids[0]); - if (otherDocument is null) + if (!solution.TryGetRazorDocument(razorFileUri, out var otherDocument)) { return Response.CallHtml; } From 4bbedc61a7cd4860289d5acf51f9c22e2a87ee33 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 9 Aug 2024 12:30:20 +1000 Subject: [PATCH 12/12] Fix after rebase --- .../Rename/RemoteRenameService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs index 3ce1678867b..7f095247258 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs @@ -51,7 +51,7 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args) var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false); var hostDocumentIndex = codeDocument.Source.Text.GetRequiredAbsoluteIndex(position); - var positionInfo = _documentMappingService.GetPositionInfo(codeDocument, codeDocument.Source.Text, hostDocumentIndex); + var positionInfo = _documentMappingService.GetPositionInfo(codeDocument, hostDocumentIndex); var razorEdit = await _renameService.TryGetRazorRenameEditsAsync(context, positionInfo, newName, cancellationToken).ConfigureAwait(false); if (razorEdit is not null)