diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs index 83e2b5ea1..5d5ed7894 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs @@ -20,7 +20,7 @@ internal class PrepareRenameHandler RenameService renameService ) : IPrepareRenameHandler { - public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); + public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability?.PrepareSupport == true ? new() { PrepareProvider = true } : new(); public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) => await renameService.PrepareRenameSymbol(request, cancellationToken).ConfigureAwait(false); @@ -34,7 +34,9 @@ RenameService renameService ) : IRenameHandler { // RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request. - public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); + // The framework passes a null capability when the client omits textDocument.rename from its advertised capabilities (e.g. a completion-only client). + // The parameter keeps the interface's non-nullable signature; the null-conditional operator avoids a NullReferenceException during initialize. + public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability?.PrepareSupport == true ? new() { PrepareProvider = true } : new(); public async Task Handle(RenameParams request, CancellationToken cancellationToken) => await renameService.RenameSymbol(request, cancellationToken).ConfigureAwait(false); diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index 3f0be5be4..30cc75144 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Test.Shared; using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; using System.Linq; @@ -24,6 +26,7 @@ public class RenameHandlerTests private readonly WorkspaceService workspace = new(NullLoggerFactory.Instance); private readonly RenameHandler testHandler; + private readonly PrepareRenameHandler testPrepareHandler; public RenameHandlerTests() { workspace.WorkspaceFolders.Add(new WorkspaceFolder @@ -31,18 +34,17 @@ public RenameHandlerTests() Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("Refactoring")) }); - testHandler = new + RenameService renameService = new ( - new RenameService - ( - workspace, - new FakeLspSendMessageRequestFacade("I Accept"), - new EmptyConfiguration() - ) - { - DisclaimerAcceptedForSession = true //Disables UI prompts - } - ); + workspace, + new FakeLspSendMessageRequestFacade("I Accept"), + new EmptyConfiguration() + ) + { + DisclaimerAcceptedForSession = true //Disables UI prompts + }; + testHandler = new(renameService); + testPrepareHandler = new(renameService); } // Decided to keep this DAMP instead of DRY due to memberdata boundaries, duplicates with PrepareRenameHandler @@ -125,4 +127,43 @@ public async Task RenamedVariable(RenameTestTarget s) Assert.Equal(expected, actual); } + + public enum RegistrationHandlerKind + { + Rename, + PrepareRename + } + + // prepareSupport has three distinct inputs: null = client omitted the capability entirely (framework hands us null), + // true = client supports prepareRename, false = client explicitly does not. Only true should enable PrepareProvider. + public static TheoryData RegistrationOptionsTestCases() => new() + { + { RegistrationHandlerKind.Rename, null, false }, + { RegistrationHandlerKind.Rename, false, false }, + { RegistrationHandlerKind.Rename, true, true }, + { RegistrationHandlerKind.PrepareRename, null, false }, + { RegistrationHandlerKind.PrepareRename, false, false }, + { RegistrationHandlerKind.PrepareRename, true, true } + }; + + [Theory] + [MemberData(nameof(RegistrationOptionsTestCases))] + public void GetRegistrationOptionsReflectsPrepareSupport(RegistrationHandlerKind handlerKind, bool? prepareSupport, bool expectedPrepareProvider) + { + RenameCapability capability = prepareSupport is bool ps + ? new RenameCapability { PrepareSupport = ps } + : null; + + Func getRegistrationOptions = handlerKind switch + { + RegistrationHandlerKind.Rename => testHandler.GetRegistrationOptions, + RegistrationHandlerKind.PrepareRename => testPrepareHandler.GetRegistrationOptions, + _ => throw new ArgumentOutOfRangeException(nameof(handlerKind)) + }; + + RenameRegistrationOptions opts = getRegistrationOptions(capability, new ClientCapabilities()); + + Assert.NotNull(opts); + Assert.Equal(expectedPrepareProvider, opts.PrepareProvider); + } }