Skip to content

Commit 65173d4

Browse files
authored
Improve prepareRename responses (#4867)
* Return range of renameable `Name` at cursor If the cursor is on a `Name` identifier, the plugin considers it valid for a rename operation. This commit modifies `prepareRenameProvider` to return the range of this `Name`. * Amend AI disclosure, approach, links in `NOTES.md` * Remove redundant imports * Call `pointCommand` only once; resolve PR comments --------- Authored-by: izuzu <izuzu@izuzu.local>
1 parent dd3dbd7 commit 65173d4

3 files changed

Lines changed: 93 additions & 12 deletions

File tree

plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,22 @@ descriptor recorder pluginId = mkExactprintPluginDescriptor exactPrintRecorder $
9090
prepareRenameProvider :: PluginMethodHandler IdeState Method_TextDocumentPrepareRename
9191
prepareRenameProvider state _pluginId (PrepareRenameParams (TextDocumentIdentifier uri) pos _progressToken) = do
9292
nfp <- getNormalizedFilePathE uri
93-
namesUnderCursor <- getNamesAtPos state nfp pos
93+
HAR{hieAst} <- handleGetHieAst state nfp
94+
let spansWithNamesUnderCursor =
95+
[ srcSpan
96+
| (names, srcSpan) <- getNamesSpansAtPoint' hieAst pos
97+
, not (null names)]
9498
-- When this handler says that rename is invalid, VSCode shows "The element can't be renamed"
9599
-- and doesn't even allow you to create full rename request.
96100
-- This handler deliberately approximates "things that definitely can't be renamed"
97-
-- to mean "there is no Name at given position".
101+
-- to mean "there is no Name at given position" (in which case
102+
-- `spansWithNamesUnderCursor` would be empty).
98103
--
99104
-- In particular it allows some cases through (e.g. cross-module renames),
100105
-- so that the full rename handler can give more informative error about them.
101-
let renameValid = not $ null namesUnderCursor
102-
pure $ InL $ PrepareRenameResult $ InR $ InR $ PrepareRenameDefaultBehavior renameValid
106+
pure $ case spansWithNamesUnderCursor of
107+
[] -> InR Null
108+
srcSpan : _ -> InL $ PrepareRenameResult $ InL (realSrcSpanToRange srcSpan)
103109

104110
renameProvider :: PluginMethodHandler IdeState Method_TextDocumentRename
105111
renameProvider state pluginId (RenameParams _prog (TextDocumentIdentifier uri) pos newNameText) = do
@@ -287,6 +293,12 @@ getNamesAtPoint' :: HieASTs a -> Position -> [Name]
287293
getNamesAtPoint' hf pos =
288294
concat $ pointCommand hf pos (rights . M.keys . getNodeIds)
289295

296+
-- | A variant of `getNamesAtPoint'` that also returns source spans.
297+
getNamesSpansAtPoint' :: HieASTs a -> Position -> [([Name], RealSrcSpan)]
298+
getNamesSpansAtPoint' hf pos =
299+
pointCommand hf pos $
300+
\astNode -> (rights . M.keys . getNodeIds $ astNode, nodeSpan astNode)
301+
290302
locToUri :: Location -> Uri
291303
locToUri (Location uri _) = uri
292304

plugins/hls-rename-plugin/test/Main.hs

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
module Main (main) where
66

7-
import Control.Lens ((^.))
8-
import Data.Aeson
9-
import Data.Functor (void)
10-
import qualified Data.Map as M
11-
import Data.Text (Text, pack)
7+
import Control.Lens ((^.))
8+
import Data.Aeson (KeyValue ((.=)))
9+
import Data.Functor (void)
10+
import qualified Data.Map as M
11+
import Data.Text (Text, pack)
1212
import Ide.Plugin.Config
13-
import qualified Ide.Plugin.Rename as Rename
14-
import qualified Language.LSP.Protocol.Lens as L
13+
import qualified Ide.Plugin.Rename as Rename
14+
import qualified Language.LSP.Protocol.Lens as L
15+
import Language.LSP.Protocol.Types (Null (Null))
1516
import System.FilePath
1617
import Test.Hls
1718

@@ -23,10 +24,54 @@ renamePlugin = mkPluginTestDescriptor Rename.descriptor "rename"
2324

2425
tests :: TestTree
2526
tests = testGroup "Rename"
26-
[ renameTests
27+
[ prepareRenameTests
28+
, renameTests
2729
, moduleNameTests
2830
]
2931

32+
prepareRenameTests :: TestTree
33+
prepareRenameTests = testGroup "PrepareRename"
34+
[ testCase "Module name (not yet renameable)" $ runRenameSession "" $ do
35+
doc <- openDoc "PrepareRename.hs" "haskell"
36+
void waitForBuildQueue
37+
result <- prepareRename doc (Position 0 9)
38+
liftIO $ result @?= InR Null
39+
40+
, testCase "Function name" $ runRenameSession "" $ do
41+
doc <- openDoc "PrepareRename.hs" "haskell"
42+
void waitForBuildQueue
43+
result <- prepareRename doc (Position 8 1)
44+
liftIO $ result @?=
45+
InL (PrepareRenameResult (InL (Range (Position 8 0) (Position 8 3))))
46+
47+
, testCase "Imported function name" $ runRenameSession "" $ do
48+
doc <- openDoc "PrepareRename.hs" "haskell"
49+
void waitForBuildQueue
50+
result <- prepareRename doc (Position 10 16)
51+
liftIO $ result @?=
52+
InL (PrepareRenameResult (InL (Range (Position 10 14) (Position 10 19))))
53+
54+
, testCase "Non-renameable position" $ runRenameSession "" $ do
55+
doc <- openDoc "PrepareRename.hs" "haskell"
56+
void waitForBuildQueue
57+
result <- prepareRename doc (Position 6 23)
58+
liftIO $ result @?= InR Null
59+
60+
, testCase "Operator" $ runRenameSession "" $ do
61+
doc <- openDoc "PrepareRename.hs" "haskell"
62+
void waitForBuildQueue
63+
result <- prepareRename doc (Position 10 7)
64+
liftIO $ result @?=
65+
InL (PrepareRenameResult (InL (Range (Position 10 6) (Position 10 9))))
66+
67+
, testCase "Built-in operator" $ runRenameSession "" $ do
68+
doc <- openDoc "PrepareRename.hs" "haskell"
69+
void waitForBuildQueue
70+
result <- prepareRename doc (Position 13 7)
71+
liftIO $ result @?=
72+
InL (PrepareRenameResult (InL (Range (Position 13 7) (Position 13 8))))
73+
]
74+
3075
renameTests :: TestTree
3176
renameTests = testGroup "Identifier"
3277
[ goldenWithRename "Data constructor" "DataConstructor" $ \doc ->
@@ -175,6 +220,16 @@ goldenWithRename title path act =
175220
goldenWithHaskellDoc (def { plugins = M.fromList [("rename", def { plcConfig = "crossModule" .= True })] })
176221
renamePlugin title testDataDir path "expected" "hs" act
177222

223+
-- NOTE: This should eventually be moved upstream to lsp-test (see
224+
-- https://github.com/haskell/lsp/issues/636).
225+
prepareRename :: TextDocumentIdentifier -> Position -> Session (PrepareRenameResult |? Null)
226+
prepareRename doc pos = do
227+
let params = PrepareRenameParams doc pos Nothing
228+
rsp <- request SMethod_TextDocumentPrepareRename params
229+
case rsp ^. L.result of
230+
Left rspError -> liftIO $ assertFailure $ "prepareRename failed: " <> show rspError
231+
Right rspResult -> pure rspResult
232+
178233
renameExpectError :: TResponseError Method_TextDocumentRename -> TextDocumentIdentifier -> Position -> Text -> Session ()
179234
renameExpectError expectedError doc pos newName = do
180235
let params = RenameParams Nothing doc pos newName
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module PrepareRename where
2+
3+
import qualified Foo as F
4+
5+
main :: IO Int
6+
main = do
7+
x <- return $ foo 42
8+
return (foo x)
9+
foo, bar :: Int -> Int
10+
foo x = x + 1
11+
bar = (+) 1 . F.foo . foo
12+
13+
baz :: a -> [a]
14+
baz = (: [])

0 commit comments

Comments
 (0)