diff --git a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs index a1425a219936c..5ccde24051114 100644 --- a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs +++ b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs @@ -54,7 +54,10 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) capabilities.DefinitionProvider = true; capabilities.DocumentHighlightProvider = true; - capabilities.RenameProvider = true; + capabilities.RenameProvider = new RenameOptions + { + PrepareProvider = true, + }; capabilities.ImplementationProvider = true; capabilities.CodeActionProvider = new CodeActionOptions { CodeActionKinds = [CodeActionKind.QuickFix, CodeActionKind.Refactor], ResolveProvider = true }; capabilities.CompletionProvider = new VisualStudio.LanguageServer.Protocol.CompletionOptions diff --git a/src/Features/LanguageServer/Protocol/Handler/Rename/PrepareRenameHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Rename/PrepareRenameHandler.cs new file mode 100644 index 0000000000000..fb5719219b924 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Rename/PrepareRenameHandler.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Rename; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler; + +[ExportCSharpVisualBasicStatelessLspService(typeof(PrepareRenameHandler)), Shared] +[Method(Methods.TextDocumentPrepareRenameName)] +internal class PrepareRenameHandler : ILspServiceDocumentRequestHandler +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public PrepareRenameHandler() + { + } + + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(PrepareRenameParams request) => request.TextDocument; + + public async Task HandleRequestAsync(PrepareRenameParams request, RequestContext context, CancellationToken cancellationToken) + { + var document = context.Document; + Contract.ThrowIfNull(document); + + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + + var symbolicRenameInfo = await SymbolicRenameInfo.GetRenameInfoAsync( + document, position, cancellationToken).ConfigureAwait(false); + if (symbolicRenameInfo.IsError) + return null; + + return new DefaultBehaviorPrepareRename + { + DefaultBehavior = true, + }; + } +} diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Rename/PrepareRenameTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Rename/PrepareRenameTests.cs new file mode 100644 index 0000000000000..ba1809483c3e0 --- /dev/null +++ b/src/Features/LanguageServer/ProtocolUnitTests/Rename/PrepareRenameTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Rename +{ + public class PrepareRenameTests : AbstractLanguageServerProtocolTests + { + public PrepareRenameTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + [Theory, CombinatorialData] + public async Task TestPrepareRenameValidLocationAsync(bool mutatingLspWorkspace) + { + var markup = +@"class A +{ + void {|caret:|}M() + { + } +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + var renameLocation = testLspServer.GetLocations("caret").First(); + + var results = await RunPrepareRenameAsync(testLspServer, CreatePrepareRenameParams(renameLocation)); + Assert.True(results?.DefaultBehavior); + } + + [Theory, CombinatorialData] + public async Task TestPrepareRenameInvalidLocationAsync(bool mutatingLspWorkspace) + { + var markup = +@"class A +{ + // Cannot rename {|caret:|}inside a comment. + void M() + { + } +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + var renameLocation = testLspServer.GetLocations("caret").First(); + + var results = await RunPrepareRenameAsync(testLspServer, CreatePrepareRenameParams(renameLocation)); + Assert.Null(results); + } + + private static LSP.PrepareRenameParams CreatePrepareRenameParams(LSP.Location location) + => new LSP.PrepareRenameParams() + { + Position = location.Range.Start, + TextDocument = CreateTextDocumentIdentifier(location.Uri) + }; + + private static async Task RunPrepareRenameAsync(TestLspServer testLspServer, LSP.PrepareRenameParams prepareRenameParams) + { + return await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentPrepareRenameName, prepareRenameParams, CancellationToken.None); + } + } +}