diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a02d2763d..dd8f575818 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -208,3 +208,7 @@ jobs: - if: ${{ needs.pre_job.outputs.should_skip != 'true' && matrix.test}} name: Test hls-call-hierarchy-plugin test suite run: cabal test hls-call-hierarchy-plugin --test-options="-j1 --rerun-update" || cabal test hls-call-hierarchy-plugin --test-options="-j1 --rerun" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-call-hierarchy-plugin --test-options="-j1 --rerun" + + - if: ${{ needs.pre_job.outputs.should_skip != 'true' && matrix.test && matrix.ghc >= '8.8.0'}} + name: Test hls-rename-plugin test suite + run: cabal test hls-rename-plugin --test-options="-j1 --rerun-update" || cabal test hls-rename-plugin --test-options="-j1 --rerun" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-call-hierarchy-plugin --rename-options="-j1 --rerun" diff --git a/cabal-ghc901.project b/cabal-ghc901.project index 5cd90914b3..aa84008ecb 100644 --- a/cabal-ghc901.project +++ b/cabal-ghc901.project @@ -15,6 +15,7 @@ packages: ./plugins/hls-explicit-imports-plugin ./plugins/hls-refine-imports-plugin ./plugins/hls-hlint-plugin + ./plugins/hls-rename-plugin ./plugins/hls-retrie-plugin ./plugins/hls-haddock-comments-plugin -- ./plugins/hls-splice-plugin @@ -79,7 +80,7 @@ index-state: 2021-08-08T02:21:16Z constraints: -- These plugins doesn't work on GHC9 yet - haskell-language-server -brittany -class -fourmolu -splice -stylishhaskell -tactic -refineImports + haskell-language-server -brittany -class -fourmolu -splice -stylishhaskell -tactic -refineImports -rename allow-newer: diff --git a/cabal.project b/cabal.project index b0fbffe75d..e375335537 100644 --- a/cabal.project +++ b/cabal.project @@ -15,6 +15,7 @@ packages: ./plugins/hls-explicit-imports-plugin ./plugins/hls-refine-imports-plugin ./plugins/hls-hlint-plugin + ./plugins/hls-rename-plugin ./plugins/hls-retrie-plugin ./plugins/hls-haddock-comments-plugin ./plugins/hls-splice-plugin diff --git a/configuration-ghc-901.nix b/configuration-ghc-901.nix index 7f3d38554c..aca5b1a5b1 100644 --- a/configuration-ghc-901.nix +++ b/configuration-ghc-901.nix @@ -7,6 +7,7 @@ let "hls-brittany-plugin" "hls-stylish-haskell-plugin" "hls-fourmolu-plugin" + "hls-rename-plugin" "hls-splice-plugin" "hls-class-plugin" "hls-refine-imports-plugin" @@ -91,6 +92,7 @@ let "-f-brittany" "-f-class" "-f-fourmolu" + "-f-rename" "-f-splice" "-f-stylishhaskell" "-f-tactic" diff --git a/exe/Plugins.hs b/exe/Plugins.hs index 8b2c3178d6..5e4a6165d9 100644 --- a/exe/Plugins.hs +++ b/exe/Plugins.hs @@ -37,6 +37,10 @@ import Ide.Plugin.ExplicitImports as ExplicitImports import Ide.Plugin.RefineImports as RefineImports #endif +#if rename +import Ide.Plugin.Rename as Rename +#endif + #if retrie import Ide.Plugin.Retrie as Retrie #endif @@ -115,6 +119,9 @@ idePlugins includeExamples = pluginDescToIdePlugins allPlugins #if stylishHaskell StylishHaskell.descriptor "stylish-haskell" : #endif +#if rename + Rename.descriptor "rename" : +#endif #if retrie Retrie.descriptor "retrie" : #endif diff --git a/ghcide/session-loader/Development/IDE/Session.hs b/ghcide/session-loader/Development/IDE/Session.hs index b5a7ba893c..b769ed916a 100644 --- a/ghcide/session-loader/Development/IDE/Session.hs +++ b/ghcide/session-loader/Development/IDE/Session.hs @@ -501,9 +501,8 @@ cradleToOptsAndLibDir :: Show a => Cradle a -> FilePath -> IO (Either [CradleError] (ComponentOptions, FilePath)) cradleToOptsAndLibDir cradle file = do -- Start off by getting the session options - let showLine s = hPutStrLn stderr ("> " ++ s) hPutStrLn stderr $ "Output from setting up the cradle " <> show cradle - cradleRes <- runCradle (cradleOptsProg cradle) showLine file + cradleRes <- HieBios.getCompilerOptions file cradle case cradleRes of CradleSuccess r -> do -- Now get the GHC lib dir diff --git a/ghcide/src/Development/IDE/Spans/AtPoint.hs b/ghcide/src/Development/IDE/Spans/AtPoint.hs index da49cc1a54..2c878ebe1b 100644 --- a/ghcide/src/Development/IDE/Spans/AtPoint.hs +++ b/ghcide/src/Development/IDE/Spans/AtPoint.hs @@ -16,6 +16,9 @@ module Development.IDE.Spans.AtPoint ( , computeTypeReferences , FOIReferences(..) , defRowToSymbolInfo + , getAstNamesAtPoint + , toCurrentLocation + , rowToLoc ) where import Development.IDE.GHC.Error @@ -90,8 +93,7 @@ foiReferencesAtPoint file pos (FOIReferences asts) = case HM.lookup file asts of Nothing -> ([],[],[]) Just (HAR _ hf _ _ _,mapping) -> - let posFile = fromMaybe pos $ fromCurrentPosition mapping pos - names = concat $ pointCommand hf posFile (rights . M.keys . getNodeIds) + let names = getAstNamesAtPoint hf pos mapping adjustedLocs = HM.foldr go [] asts go (HAR _ _ rf tr _, mapping) xs = refs ++ typerefs ++ xs where @@ -99,9 +101,18 @@ foiReferencesAtPoint file pos (FOIReferences asts) = $ concat $ mapMaybe (\n -> M.lookup (Right n) rf) names typerefs = mapMaybe (toCurrentLocation mapping . realSrcSpanToLocation) $ concat $ mapMaybe (`M.lookup` tr) names - toCurrentLocation mapping (Location uri range) = Location uri <$> toCurrentRange mapping range in (names, adjustedLocs,map fromNormalizedFilePath $ HM.keys asts) +getAstNamesAtPoint :: HieASTs a -> Position -> PositionMapping -> [Name] +getAstNamesAtPoint hf pos mapping = + concat $ pointCommand hf posFile (rights . M.keys . getNodeIds) + where + posFile = fromMaybe pos $ fromCurrentPosition mapping pos + +toCurrentLocation :: PositionMapping -> Location -> Maybe Location +toCurrentLocation mapping (Location uri range) = + Location uri <$> toCurrentRange mapping range + referencesAtPoint :: MonadIO m => HieDb diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index fd8c5473c2..eb3811ede0 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -131,6 +131,11 @@ flag refineImports default: True manual: True +flag rename + description: Enable rename plugin + default: True + manual: True + flag retrie description: Enable retrie plugin default: True @@ -223,6 +228,11 @@ common refineImports build-depends: hls-refine-imports-plugin ^>=1.0.0.0 cpp-options: -DrefineImports +common rename + if impl(ghc >= 8.8) && (flag(rename) || flag(all-plugins)) + build-depends: hls-rename-plugin ^>= 1.0.0.0 + cpp-options: -Drename + common retrie if flag(retrie) || flag(all-plugins) build-depends: hls-retrie-plugin ^>=1.0.0.1 @@ -290,6 +300,7 @@ executable haskell-language-server , eval , importLens , refineImports + , rename , retrie , tactic , hlint @@ -424,7 +435,6 @@ test-suite func-test Highlight Progress Reference - Rename Symbol TypeDefinition Test.Hls.Command @@ -447,6 +457,8 @@ test-suite func-test cpp-options: -Deval if flag(importLens) || flag(all-plugins) cpp-options: -DimportLens + if impl(ghc >= 8.8) && (flag(rename) || flag(all-plugins)) + cpp-options: -Drename if flag(retrie) || flag(all-plugins) cpp-options: -Dretrie if flag(tactic) || flag(all-plugins) diff --git a/plugins/hls-rename-plugin/LICENSE b/plugins/hls-rename-plugin/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/hls-rename-plugin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/hls-rename-plugin/hls-rename-plugin.cabal b/plugins/hls-rename-plugin/hls-rename-plugin.cabal new file mode 100644 index 0000000000..857ea140e3 --- /dev/null +++ b/plugins/hls-rename-plugin/hls-rename-plugin.cabal @@ -0,0 +1,51 @@ +cabal-version: 2.4 +name: hls-rename-plugin +version: 1.0.0.0 +synopsis: Rename plugin for Haskell Language Server +description: + Please see the README on GitHub at + +license: Apache-2.0 +license-file: LICENSE +author: Oliver Madine +maintainer: madine.oliver@gmail.com +category: Development +build-type: Simple +extra-source-files: + LICENSE + test/testdata/*.hs + test/testdata/*.yaml + +library + exposed-modules: Ide.Plugin.Rename + hs-source-dirs: src + build-depends: + , aeson + , base >=4.12 && <5 + , containers + , ghc + , ghc-exactprint + , ghcide >=1.4 && <1.5 + , hiedb + , hls-plugin-api ^>=1.2 + , hls-retrie-plugin >= 1.0.1.1 + , lsp + , lsp-types + , retrie >=1.0.0.0 + , text + , transformers + , unordered-containers + + default-language: Haskell2010 + +test-suite tests + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: test + main-is: Main.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + , base + , filepath + , hls-rename-plugin + , hls-test-utils >=1.0 && <1.2 diff --git a/plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs b/plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs new file mode 100644 index 0000000000..9726357f57 --- /dev/null +++ b/plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs @@ -0,0 +1,349 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Ide.Plugin.Rename (descriptor) where + +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.Trans.Class +import Control.Monad.Trans.Except +import qualified Data.Bifunctor +import Data.Char +import Data.Containers.ListUtils +import qualified Data.HashMap.Strict as HM +import Data.List +import qualified Data.Map as M +import Data.Maybe +import qualified Data.Text as T +import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.PositionMapping +import Development.IDE.Core.Shake +import Development.IDE.GHC.Compat +import Development.IDE.GHC.ExactPrint +import Development.IDE.Spans.AtPoint +import HieDb.Query +import Ide.Plugin.Config +import Ide.Plugin.Retrie hiding (descriptor) +import Ide.PluginUtils +import Ide.Types +import Language.LSP.Server +import Language.LSP.Types hiding (_changes, _range) +import Retrie hiding (HasSrcSpan, + HsModule, getLoc) +import Retrie.SYB + +descriptor :: PluginId -> PluginDescriptor IdeState +descriptor pluginId = (defaultPluginDescriptor pluginId) { + pluginHandlers = mkPluginHandler STextDocumentRename renameProvider +} + +renameProvider :: PluginMethodHandler IdeState TextDocumentRename +renameProvider state pluginId (RenameParams (TextDocumentIdentifier uri) pos _prog newNameText) = response $ do + nfp <- safeGetNfp uri + oldName <- (handleMaybe "error: could not find name at pos" . listToMaybe) =<< + getNamesAtPos state pos nfp + refs <- refsAtName state nfp oldName + refFiles <- mapM safeGetNfp (nub [uri | Location uri _ <- refs]) + let newNameStr = T.unpack newNameText + newRdrName = mkRdrUnqual $ mkTcOcc newNameStr + + -- Rename Imports / Export + let updateIe = updateExports refs newRdrName . updateImports refs newRdrName + ieFileEdits <- mapMToSnd (getSrcEdits state updateIe) refFiles + + -- Rename left-hand sides (declarations) + filesDeclEdits <- mapMToSnd (getSrcEdits state (updateLhsDecls refs newRdrName)) refFiles + declEdits@(originNfp, _) <- handleMaybe "error: could not rename declaration" $ + find (\(_, List xs) -> not $ null xs) filesDeclEdits + + -- Rename right-hand sides (using retrie) + rhsEditMap <- foldl1 (HM.unionWith (<>)) <$> + mapM (getRhsEdits state refs oldName newNameStr originNfp) refFiles + + -- combine edits + let insertEdits (nfp, edits) = HM.insertWith (<>) (nfpToUri nfp) edits + edits = foldr insertEdits rhsEditMap (declEdits : ieFileEdits) + + pure $ WorkspaceEdit (Just edits) Nothing Nothing + +------------------------------------------------------------------------------- +-- Source renaming + +getSrcEdits :: + IdeState +#if MIN_VERSION_ghc(9,0,1) + -> (HsModule -> HsModule) +#else + -> (HsModule GhcPs -> HsModule GhcPs) +#endif + -> NormalizedFilePath + -> ExceptT String (LspT Config IO) (List TextEdit) +getSrcEdits state updateMod nfp = do + annPs <- handleMaybeM "error: parsed source" $ + liftIO $ runAction + "Rename.GetAnnotatedParsedModule" + state + (use GetAnnotatedParsedSource nfp) + let src = T.pack $ printA annPs + res = T.pack $ printA $ (fmap . fmap) updateMod annPs + pure $ makeDiffTextEdit src res + +updateExports :: + [Location] + -> RdrName +#if MIN_VERSION_ghc(9,0,1) + -> HsModule + -> HsModule +#else + -> HsModule GhcPs + -> HsModule GhcPs +#endif +updateExports refs newRdrName ps@HsModule{hsmodExports} = + ps {hsmodExports = (fmap . fmap) (map (fmap $ renameIE refs newRdrName)) hsmodExports} + +updateImports :: + [Location] + -> RdrName +#if MIN_VERSION_ghc(9,0,1) + -> HsModule + -> HsModule +#else + -> HsModule GhcPs + -> HsModule GhcPs +#endif +updateImports refs newRdrName ps@HsModule{hsmodImports} = + ps {hsmodImports = map (fmap renameImport) hsmodImports} + where + renameImport :: ImportDecl GhcPs -> ImportDecl GhcPs + renameImport importDecl@ImportDecl{ideclHiding = Just (isHiding, names)} = + importDecl { + ideclHiding = + Just (isHiding, fmap (map (fmap $ renameIE refs newRdrName)) names) + } + renameImport importDecl = importDecl + +-- TODO: implement explicit type import/export +renameIE :: [Location] -> RdrName -> IE GhcPs -> IE GhcPs +renameIE refs newRdrName (IEVar xVar ieName) + | isRef refs ieName = + IEVar xVar (replaceLWrappedName ieName newRdrName) +renameIE refs newRdrName (IEThingAbs xThing ieName) + | isRef refs ieName = + IEThingAbs xThing (replaceLWrappedName ieName newRdrName) +renameIE refs newRdrName (IEThingAll xThingAll ieName) + | isRef refs ieName = + IEThingAll xThingAll (replaceLWrappedName ieName newRdrName) +renameIE refs newRdrName IEThingWith{} + = error "not implemented explicit type import/export renames yet" +renameIE _ _ export = export + +updateLhsDecls :: + [Location] + -> RdrName +#if MIN_VERSION_ghc(9,0,1) + -> HsModule + -> HsModule +#else + -> HsModule GhcPs + -> HsModule GhcPs +#endif +updateLhsDecls refs newRdrName ps@HsModule{hsmodDecls} = + ps {hsmodDecls = map (fmap renameLhsDecl) hsmodDecls} + where + renameLhsDecl :: HsDecl GhcPs -> HsDecl GhcPs + renameLhsDecl (SigD xSig (TypeSig xTySig sigNames wc)) = + SigD xSig $ TypeSig xTySig (map renameRdrName' sigNames) wc + renameLhsDecl (ValD xVal funBind@FunBind{fun_id, fun_matches = fun_matches@MG{mg_alts}}) + = ValD xVal $ funBind { + fun_id = renameRdrName' fun_id, + fun_matches = fun_matches {mg_alts = fmap (map (fmap $ renameLhsMatch newRdrName)) mg_alts} + } + renameLhsDecl (TyClD xTy dataDecl@DataDecl{tcdLName, tcdDataDefn = hsDataDefn@HsDataDefn{dd_cons}}) + = TyClD xTy $ dataDecl { + tcdLName = renameRdrName' tcdLName + } + renameLhsDecl (TyClD xTy synDecl@SynDecl{tcdLName}) + = TyClD xTy $ synDecl { + tcdLName = renameRdrName' tcdLName + } + renameLhsDecl decl = decl + + renameRdrName' :: Located RdrName -> Located RdrName + renameRdrName' = renameRdrName refs newRdrName + + renameLhsMatch :: RdrName -> Match GhcPs (LHsExpr GhcPs) -> Match GhcPs (LHsExpr GhcPs) + renameLhsMatch newRdrName match@Match{m_ctxt = funRhs@FunRhs{mc_fun}} = + match{m_ctxt = funRhs{mc_fun = renameRdrName refs newRdrName mc_fun}} + renameLhsMatch _ _ = error "Expected function match" + + +renameRdrName :: [Location] -> RdrName -> Located RdrName -> Located RdrName +renameRdrName refs newRdrName oldRdrName + | isRef refs oldRdrName = fmap (const newRdrName) oldRdrName + | otherwise = oldRdrName + +------------------------------------------------------------------------------- +-- retrie + +getRhsEdits :: + IdeState + -> [Location] + -> Name + -> String + -> NormalizedFilePath + -> NormalizedFilePath + -> ExceptT String (LspT Config IO) WorkspaceEditMap +getRhsEdits state refs oldName newNameStr originNfp nfp = do + rewriteSpecs <- getRewriteSpecs state (getOccString oldName) newNameStr originNfp nfp + (session, _) <- handleMaybeM "error: session deps" $ + liftIO $ runAction "Rename.GhcSessionDeps" state (useWithStale GhcSessionDeps nfp) + (errors, WorkspaceEdit{_changes = edits}) <- + liftIO $ callRetrieWithTransformerAndUpdates + (referenceTransformer refs) + contextUpdater + state + (hscEnv session) + (map Right rewriteSpecs) + nfp + True + lift $ sendRetrieErrors errors + handleMaybe "error: retrie" edits + +getRewriteSpecs :: + IdeState + -> String + -> String + -> NormalizedFilePath + -> NormalizedFilePath + -> ExceptT String (LspT Config IO) [RewriteSpec] +getRewriteSpecs state oldNameStr newNameStr originNfp nfp = do + ParsedModule{pm_parsed_source = L _ HsModule{hsmodName = mbOriginModule}} <- + handleMaybeM "error: parsed source" $ + liftIO $ runAction + "Rename.GetAnnotatedParsedModule" + state + (use GetParsedModule originNfp) + ParsedModule{pm_parsed_source = L _ HsModule{hsmodImports}} <- + handleMaybeM "error: parsed source" $ + liftIO $ runAction + "Rename.GetAnnotatedParsedModule" + state + (use GetParsedModule nfp) + let getNameImport (L _ originModule) = find ((==originModule) . unLoc . ideclName) (map unLoc hsmodImports) + mkRewriteSpec qualStr = (if isUpper $ head oldNameStr then AdhocType else Adhoc) $ + qualStr ++ oldNameStr ++ " = " ++ qualStr ++ newNameStr + mkQualRewrite = mkRewriteSpec . getQualifierStr + unQualRewrite = mkRewriteSpec "" + pure $ case getNameImport =<< mbOriginModule of + Just nameImport -> + if isQualifiedImport nameImport + then [mkQualRewrite nameImport] + else [unQualRewrite, mkQualRewrite nameImport] + Nothing -> [unQualRewrite] + +getQualifierStr :: ImportDecl pass -> String +getQualifierStr ImportDecl{ideclAs, ideclName} = + moduleNameString (unLoc (fromMaybe ideclName ideclAs)) ++ "." +getQualifierStr _ = "" + +-- limits matches to reference locations +referenceTransformer :: [Location] -> MatchResultTransformer +referenceTransformer refs Context{ctxtBinders} match + | MatchResult _sub template <- match + , any (containsRef . getRdrLoc) ctxtBinders = pure match + | otherwise = pure NoMatch + where + containsRef srcSpan = any (flip isSubspanOf srcSpan . locToSpan) refs + getRdrLoc (Exact name) = nameSrcSpan name + getRdrLoc _ = error "Expected exact name" + +-- Hacky use of ctxtBinders to track match spans +contextUpdater :: (Typeable b, Monad f) => Context -> Int -> b -> f Context +contextUpdater c@Context{ctxtBinders} i = const (pure c) + `extQ` (return . updType) + `extQ` (return . updExpr) + `extQ` (return . updTyDecl) + `extQ` (return . updMatch) + where + -- Todo: add statement matches + updType :: LHsType GhcPs -> Context + updType (L _ (HsAppTy _ (L matchSpan _) _)) = + c {ctxtBinders = makeName matchSpan : ctxtBinders} + updType (L matchSpan ty) = + c {ctxtBinders = makeName matchSpan : ctxtBinders} + + updExpr :: LHsExpr GhcPs -> Context + updExpr (L _ (HsApp _ (L matchSpan a) _)) = + c {ctxtBinders = makeName matchSpan : ctxtBinders} + updExpr (L matchSpan _) = + c {ctxtBinders = makeName matchSpan : ctxtBinders} + + updTyDecl :: TyClDecl GhcPs -> Context + updTyDecl SynDecl{tcdRhs} = + c {ctxtBinders = makeName (getLoc tcdRhs) : ctxtBinders} + updTyDecl _ = c + + updMatch :: LMatch GhcPs (LHsExpr GhcPs) -> Context + updMatch (L matchSpan Match{m_ctxt = FunRhs{mc_fun = L funNameSpan _}}) = + c {ctxtBinders = makeName (matchSpan `subtractSrcSpans` funNameSpan) : ctxtBinders} + updMatch (L matchSpan _) = c {ctxtBinders = makeName matchSpan : ctxtBinders} + + makeName = Exact . mkInternalName initTyVarUnique (mkVarOcc "") + +------------------------------------------------------------------------------- +-- reference finding + +refsAtName :: IdeState -> NormalizedFilePath -> Name -> ExceptT [Char] (LspT Config IO) [Location] +refsAtName state nfp name = do + ShakeExtras{hiedb} <- liftIO $ runAction "Rename.HieDb" state getShakeExtras + ast <- handleMaybeM "error: ast" $ liftIO $ runAction "" state $ useWithStale GetHieAst nfp + fileRefs <- handleMaybe "error: name references" $ getNameAstLocations name ast + let mod = nameModule_maybe name + dbRefs <- liftIO $ mapMaybe rowToLoc <$> findReferences + hiedb + True + (nameOccName name) + (moduleName <$> mod) + (moduleUnitId <$> mod) + [fromNormalizedFilePath nfp] + pure $ nubOrd $ fileRefs ++ dbRefs + +getNameAstLocations :: Name -> (HieAstResult, PositionMapping) -> Maybe [Location] +getNameAstLocations name (HAR _ _ rm _ _, mapping) = + mapMaybe (toCurrentLocation mapping . realSrcSpanToLocation . fst) <$> M.lookup (Right name) rm + +------------------------------------------------------------------------------- +-- util + +nfpToUri :: NormalizedFilePath -> Uri +nfpToUri = filePathToUri . fromNormalizedFilePath + +safeGetNfp :: (Monad m) => Uri -> ExceptT String m NormalizedFilePath +safeGetNfp uri = handleMaybe "error: uri" $ toNormalizedFilePath <$> uriToFilePath uri + +isRef :: HasSrcSpan a => [Location] -> a -> Bool +isRef refs = (`elem` refs) . fromJust . srcSpanToLocation . getLoc + +locToSpan :: Location -> SrcSpan +locToSpan (Location uri (Range (Position l c) (Position l' c'))) = + mkSrcSpan (mkSrcLoc' uri (succ l) (succ c)) (mkSrcLoc' uri (succ l') (succ c')) + where + mkSrcLoc' = mkSrcLoc . mkFastString . fromJust . uriToFilePath + +getNamesAtPos :: IdeState -> Position -> NormalizedFilePath -> ExceptT String (LspT Config IO) [Name] +getNamesAtPos state pos nfp = do + (HAR{hieAst}, mapping) <- handleMaybeM "error: ast" $ liftIO $ + runAction "Rename.GetHieAst" state $ useWithStale GetHieAst nfp + pure $ getAstNamesAtPoint hieAst pos mapping + +subtractSrcSpans :: SrcSpan -> SrcSpan -> SrcSpan +subtractSrcSpans minuend (OldRealSrcSpan subtrahend) + = mkSrcSpan startLoc endLoc + where + startLoc = mkSrcLoc (srcSpanFile subtrahend) (srcSpanStartLine subtrahend) (srcSpanEndCol subtrahend) + endLoc = srcSpanEnd minuend +subtractSrcSpans _ _ = error "Expected real SrcSpan" + +mapMToSnd :: Monad f => (a -> f b) -> [a] -> f [(a, b)] +mapMToSnd = liftM2 (<$>) zip . mapM diff --git a/plugins/hls-rename-plugin/test/Main.hs b/plugins/hls-rename-plugin/test/Main.hs new file mode 100644 index 0000000000..1e3b3a3fc1 --- /dev/null +++ b/plugins/hls-rename-plugin/test/Main.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Main (main) where + +import qualified Ide.Plugin.Rename as Rename +import System.FilePath +import Test.Hls + +main :: IO () +main = defaultTestRunner tests + +renamePlugin :: PluginDescriptor IdeState +renamePlugin = Rename.descriptor "rename" + +tests :: TestTree +tests = testGroup "rename" + [ testGroup "Top-level renames" + [ goldenWithRename "exported function" "ExportedFunction" $ \doc -> do + rename doc (Position 2 1) "quux" + , goldenWithRename "function name" "FunctionName" $ \doc -> do + rename doc (Position 3 1) "baz" + , goldenWithRename "GADT" "Gadt" $ \doc -> do + rename doc (Position 6 37) "Expr" + , goldenWithRename "hidden function" "HiddenFunction" $ \doc -> do + rename doc (Position 0 32) "quux" + , goldenWithRename "imported function" "ImportedFunction" $ \doc -> do + rename doc (Position 3 8) "baz" + , goldenWithRename "import hiding" "ImportHiding" $ \doc -> do + rename doc (Position 0 22) "hiddenFoo" + , goldenWithRename "allign indentation" "Indentation" $ \doc -> do + rename doc (Position 0 2) "fooBarQuux" + , goldenWithRename "qualified as" "QualifiedAs" $ \doc -> do + rename doc (Position 3 10) "baz" + , goldenWithRename "qualified shadowing" "QualifiedShadowing" $ \doc -> do + rename doc (Position 3 12) "foobar" + , goldenWithRename "qualified function" "QualifiedFunction" $ \doc -> do + rename doc (Position 3 12) "baz" + , goldenWithRename "shadowed name" "ShadowedName" $ \doc -> do + rename doc (Position 1 1) "baz" + , expectFailBecause "Bug: Test case giving different result to editor" $ + goldenWithRename "type constructor" "TypeConstructor" $ \doc -> do + rename doc (Position 2 17) "BinaryTree" + ] + , expectFailBecause "Only top-level renames are implemented" $ + testGroup "non Top-level renames" + [ goldenWithRename "data constructor" "DataConstructor" $ \doc -> do + rename doc (Position 0 15) "Op" + , goldenWithRename "function argument" "FunctionArgument" $ \doc -> do + rename doc (Position 3 4) "y" + , goldenWithRename "record field" "RecordField" $ \doc -> do + rename doc (Position 6 9) "number" + , goldenWithRename "type variable" "TypeVariable" $ \doc -> do + rename doc (Position 0 13) "b" + ] + ] + +goldenWithRename :: TestName -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree +goldenWithRename title path act = + goldenWithHaskellDoc renamePlugin title testDataDir path "expected" "hs" $ \doc -> do + waitForProgressDone + act doc + +testDataDir :: FilePath +testDataDir = "test" "testdata" diff --git a/plugins/hls-rename-plugin/test/testdata/DataConstructor.expected.hs b/plugins/hls-rename-plugin/test/testdata/DataConstructor.expected.hs new file mode 100644 index 0000000000..d1ee10a6d1 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/DataConstructor.expected.hs @@ -0,0 +1,4 @@ +data Expr = Op Int Int + +plus :: Expr -> Expr +plus (Op n m) = Op (n + m) 0 diff --git a/plugins/hls-rename-plugin/test/testdata/DataConstructor.hs b/plugins/hls-rename-plugin/test/testdata/DataConstructor.hs new file mode 100644 index 0000000000..b614d72291 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/DataConstructor.hs @@ -0,0 +1,4 @@ +data Expr = Apply Int Int + +plus :: Expr -> Expr +plus (Apply n m) = Apply (n + m) 0 diff --git a/plugins/hls-rename-plugin/test/testdata/ExportedFunction.expected.hs b/plugins/hls-rename-plugin/test/testdata/ExportedFunction.expected.hs new file mode 100644 index 0000000000..568edb36db --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ExportedFunction.expected.hs @@ -0,0 +1,5 @@ +module ExportedFunction (quux) where + +quux :: Num p => [a] -> p +quux [] = 0 +quux xs = 1 diff --git a/plugins/hls-rename-plugin/test/testdata/ExportedFunction.hs b/plugins/hls-rename-plugin/test/testdata/ExportedFunction.hs new file mode 100644 index 0000000000..3adb72dc9f --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ExportedFunction.hs @@ -0,0 +1,5 @@ +module ExportedFunction (foo) where + +foo :: Num p => [a] -> p +foo [] = 0 +foo xs = 1 diff --git a/plugins/hls-rename-plugin/test/testdata/Foo.hs b/plugins/hls-rename-plugin/test/testdata/Foo.hs new file mode 100644 index 0000000000..c4850149b4 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/Foo.hs @@ -0,0 +1,4 @@ +module Foo where + +foo :: Int -> Int +foo x = 0 diff --git a/plugins/hls-rename-plugin/test/testdata/FunctionArgument.expected.hs b/plugins/hls-rename-plugin/test/testdata/FunctionArgument.expected.hs new file mode 100644 index 0000000000..bd8d83b334 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/FunctionArgument.expected.hs @@ -0,0 +1,4 @@ +module FunctionArgument where + +foo :: Int -> Int +foo y = y + 1 diff --git a/plugins/hls-rename-plugin/test/testdata/FunctionArgument.hs b/plugins/hls-rename-plugin/test/testdata/FunctionArgument.hs new file mode 100644 index 0000000000..a6006e6fac --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/FunctionArgument.hs @@ -0,0 +1,4 @@ +module FunctionArgument where + +foo :: Int -> Int +foo x = x + 1 diff --git a/plugins/hls-rename-plugin/test/testdata/FunctionName.expected.hs b/plugins/hls-rename-plugin/test/testdata/FunctionName.expected.hs new file mode 100644 index 0000000000..a8cb8fc3bd --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/FunctionName.expected.hs @@ -0,0 +1,6 @@ +main = do + x <- return $ baz 42 + return (baz x) +baz :: Int -> Int +baz x = x + 1 +bar = (+ 1) . baz diff --git a/test/testdata/rename/Rename.hs b/plugins/hls-rename-plugin/test/testdata/FunctionName.hs similarity index 57% rename from test/testdata/rename/Rename.hs rename to plugins/hls-rename-plugin/test/testdata/FunctionName.hs index 19f566795f..d13fa2c286 100644 --- a/test/testdata/rename/Rename.hs +++ b/plugins/hls-rename-plugin/test/testdata/FunctionName.hs @@ -1,6 +1,6 @@ main = do - x <- return $ foo 42 - return (foo x) + x <- return $ foo 42 + return (foo x) foo :: Int -> Int foo x = x + 1 bar = (+ 1) . foo diff --git a/plugins/hls-rename-plugin/test/testdata/Gadt.expected.hs b/plugins/hls-rename-plugin/test/testdata/Gadt.expected.hs new file mode 100644 index 0000000000..65115d42d7 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/Gadt.expected.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE GADTs #-} + +data Expr a where + Number :: Int -> Expr Int + Boolean :: Bool -> Expr Bool + Not :: Expr Bool -> Expr Bool + Even :: Expr Int -> Expr Bool + Add :: Enum a => Expr a -> Expr a -> Expr Int + Max :: Ord a => Expr a -> Expr a -> Expr a + +evaluate :: Expr a -> a +evaluate (Number n) = n +evaluate (Boolean p) = p +evaluate (Add n m) = fromEnum (evaluate n) + fromEnum (evaluate m) +evaluate (Even n) = even $ evaluate n +evaluate (Not p) = not $ evaluate p +evaluate (Max x y) = max (evaluate x) (evaluate y) diff --git a/plugins/hls-rename-plugin/test/testdata/Gadt.hs b/plugins/hls-rename-plugin/test/testdata/Gadt.hs new file mode 100644 index 0000000000..408f516900 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/Gadt.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE GADTs #-} + +data Expression a where + Number :: Int -> Expression Int + Boolean :: Bool -> Expression Bool + Not :: Expression Bool -> Expression Bool + Even :: Expression Int -> Expression Bool + Add :: Enum a => Expression a -> Expression a -> Expression Int + Max :: Ord a => Expression a -> Expression a -> Expression a + +evaluate :: Expression a -> a +evaluate (Number n) = n +evaluate (Boolean p) = p +evaluate (Add n m) = fromEnum (evaluate n) + fromEnum (evaluate m) +evaluate (Even n) = even $ evaluate n +evaluate (Not p) = not $ evaluate p +evaluate (Max x y) = max (evaluate x) (evaluate y) diff --git a/plugins/hls-rename-plugin/test/testdata/HiddenFunction.expected.hs b/plugins/hls-rename-plugin/test/testdata/HiddenFunction.expected.hs new file mode 100644 index 0000000000..3195291c66 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/HiddenFunction.expected.hs @@ -0,0 +1,4 @@ +import Foo hiding (quux) + +foo :: Int -> Int +foo x = 0 diff --git a/plugins/hls-rename-plugin/test/testdata/HiddenFunction.hs b/plugins/hls-rename-plugin/test/testdata/HiddenFunction.hs new file mode 100644 index 0000000000..eacb9d1a4c --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/HiddenFunction.hs @@ -0,0 +1,4 @@ +import Foo hiding (foo) + +foo :: Int -> Int +foo x = 0 diff --git a/plugins/hls-rename-plugin/test/testdata/ImportHiding.expected.hs b/plugins/hls-rename-plugin/test/testdata/ImportHiding.expected.hs new file mode 100644 index 0000000000..e1b600aa1c --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ImportHiding.expected.hs @@ -0,0 +1,4 @@ +import Foo hiding (hiddenFoo) + +foo :: Int -> Int +foo _ = 5 diff --git a/plugins/hls-rename-plugin/test/testdata/ImportHiding.hs b/plugins/hls-rename-plugin/test/testdata/ImportHiding.hs new file mode 100644 index 0000000000..c14099e68b --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ImportHiding.hs @@ -0,0 +1,4 @@ +import Foo hiding (foo) + +foo :: Int -> Int +foo _ = 5 diff --git a/plugins/hls-rename-plugin/test/testdata/ImportedFunction.expected.hs b/plugins/hls-rename-plugin/test/testdata/ImportedFunction.expected.hs new file mode 100644 index 0000000000..8f0cbcf888 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ImportedFunction.expected.hs @@ -0,0 +1,4 @@ +import Foo (baz) + +bar :: Int -> Int +bar = baz diff --git a/plugins/hls-rename-plugin/test/testdata/ImportedFunction.hs b/plugins/hls-rename-plugin/test/testdata/ImportedFunction.hs new file mode 100644 index 0000000000..56361fc64b --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ImportedFunction.hs @@ -0,0 +1,4 @@ +import Foo (foo) + +bar :: Int -> Int +bar = foo diff --git a/plugins/hls-rename-plugin/test/testdata/Indentation.expected.hs b/plugins/hls-rename-plugin/test/testdata/Indentation.expected.hs new file mode 100644 index 0000000000..9033a89d87 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/Indentation.expected.hs @@ -0,0 +1,4 @@ +fooBarQuux :: Maybe Integer +fooBarQuux = do x <- Just 5 + t <- Just 10 + pure $ x + t diff --git a/plugins/hls-rename-plugin/test/testdata/Indentation.hs b/plugins/hls-rename-plugin/test/testdata/Indentation.hs new file mode 100644 index 0000000000..aa121ac984 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/Indentation.hs @@ -0,0 +1,4 @@ +foo :: Maybe Integer +foo = do x <- Just 5 + t <- Just 10 + pure $ x + t diff --git a/plugins/hls-rename-plugin/test/testdata/QualifiedAs.expected.hs b/plugins/hls-rename-plugin/test/testdata/QualifiedAs.expected.hs new file mode 100644 index 0000000000..a864119ef2 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/QualifiedAs.expected.hs @@ -0,0 +1,4 @@ +import qualified Foo as F + +bar :: Int -> Int +bar = F.baz diff --git a/plugins/hls-rename-plugin/test/testdata/QualifiedAs.hs b/plugins/hls-rename-plugin/test/testdata/QualifiedAs.hs new file mode 100644 index 0000000000..022b2f8e31 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/QualifiedAs.hs @@ -0,0 +1,4 @@ +import qualified Foo as F + +bar :: Int -> Int +bar = F.foo diff --git a/plugins/hls-rename-plugin/test/testdata/QualifiedFunction.expected.hs b/plugins/hls-rename-plugin/test/testdata/QualifiedFunction.expected.hs new file mode 100644 index 0000000000..808c12b066 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/QualifiedFunction.expected.hs @@ -0,0 +1,4 @@ +import qualified Foo + +bar :: Int -> Int +bar = Foo.baz diff --git a/plugins/hls-rename-plugin/test/testdata/QualifiedFunction.hs b/plugins/hls-rename-plugin/test/testdata/QualifiedFunction.hs new file mode 100644 index 0000000000..01581c0c8d --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/QualifiedFunction.hs @@ -0,0 +1,4 @@ +import qualified Foo + +bar :: Int -> Int +bar = Foo.foo diff --git a/plugins/hls-rename-plugin/test/testdata/QualifiedShadowing.expected.hs b/plugins/hls-rename-plugin/test/testdata/QualifiedShadowing.expected.hs new file mode 100644 index 0000000000..52dbcea2b0 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/QualifiedShadowing.expected.hs @@ -0,0 +1,7 @@ +import qualified Foo as F + +bar :: Int -> Int +bar x = F.foobar x + foo x + +foo :: Int -> Int +foo _ = 5 diff --git a/plugins/hls-rename-plugin/test/testdata/QualifiedShadowing.hs b/plugins/hls-rename-plugin/test/testdata/QualifiedShadowing.hs new file mode 100644 index 0000000000..aa5e50caf6 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/QualifiedShadowing.hs @@ -0,0 +1,7 @@ +import qualified Foo as F + +bar :: Int -> Int +bar x = F.foo x + foo x + +foo :: Int -> Int +foo _ = 5 diff --git a/plugins/hls-rename-plugin/test/testdata/RecordField.expected.hs b/plugins/hls-rename-plugin/test/testdata/RecordField.expected.hs new file mode 100644 index 0000000000..6646df611c --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/RecordField.expected.hs @@ -0,0 +1,7 @@ +data Bam = Bam { + number :: Int, + s :: String +} + +foo :: Bam -> Bam +foo Bam {number = y} = Bam {number = y + 5, s = ""} diff --git a/plugins/hls-rename-plugin/test/testdata/RecordField.hs b/plugins/hls-rename-plugin/test/testdata/RecordField.hs new file mode 100644 index 0000000000..873150935d --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/RecordField.hs @@ -0,0 +1,7 @@ +data Bam = Bam { + n :: Int, + s :: String +} + +foo :: Bam -> Bam +foo Bam {n = y} = Bam {n = y + 5, s = ""} diff --git a/plugins/hls-rename-plugin/test/testdata/ShadowedName.expected.hs b/plugins/hls-rename-plugin/test/testdata/ShadowedName.expected.hs new file mode 100644 index 0000000000..7c6391176a --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ShadowedName.expected.hs @@ -0,0 +1,4 @@ +baz :: Int -> Int +baz x = foo + 10 + where + foo = 20 diff --git a/plugins/hls-rename-plugin/test/testdata/ShadowedName.hs b/plugins/hls-rename-plugin/test/testdata/ShadowedName.hs new file mode 100644 index 0000000000..513f8fa89f --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/ShadowedName.hs @@ -0,0 +1,4 @@ +foo :: Int -> Int +foo x = foo + 10 + where + foo = 20 diff --git a/plugins/hls-rename-plugin/test/testdata/TypeConstructor.expected.hs b/plugins/hls-rename-plugin/test/testdata/TypeConstructor.expected.hs new file mode 100644 index 0000000000..0c46ffa077 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/TypeConstructor.expected.hs @@ -0,0 +1,5 @@ +data BinaryTree a = Node a (BinaryTree a) (BinaryTree a) | Leaf a + +rotateRight :: BinaryTree a -> BinaryTree a +rotateRight (Node v (Node v' l' r') r) = Node v' l' (Node v r' r) +rotateRight t = t diff --git a/plugins/hls-rename-plugin/test/testdata/TypeConstructor.hs b/plugins/hls-rename-plugin/test/testdata/TypeConstructor.hs new file mode 100644 index 0000000000..4e728aa1a4 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/TypeConstructor.hs @@ -0,0 +1,5 @@ +data Tree a = Node a (Tree a) (Tree a) | Leaf a + +rotateRight :: Tree a -> Tree a +rotateRight (Node v (Node v' l' r') r) = Node v' l' (Node v r' r) +rotateRight t = t diff --git a/plugins/hls-rename-plugin/test/testdata/TypeVariable.expected.hs b/plugins/hls-rename-plugin/test/testdata/TypeVariable.expected.hs new file mode 100644 index 0000000000..75891f4290 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/TypeVariable.expected.hs @@ -0,0 +1,2 @@ +bar :: Maybe b -> Maybe b +bar a = a diff --git a/plugins/hls-rename-plugin/test/testdata/TypeVariable.hs b/plugins/hls-rename-plugin/test/testdata/TypeVariable.hs new file mode 100644 index 0000000000..782be9a7f3 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/TypeVariable.hs @@ -0,0 +1,2 @@ +bar :: Maybe a -> Maybe a +bar a = a diff --git a/plugins/hls-rename-plugin/test/testdata/hie.yaml b/plugins/hls-rename-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..535eb08fd1 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/hie.yaml @@ -0,0 +1,20 @@ +cradle: + direct: + arguments: + - "DataConstructor" + - "ExportedFunction" + - "Foo" + - "FunctionArgument" + - "FunctionName" + - "Gadt" + - "HiddenFunction" + - "ImportHiding" + - "ImportedFunction" + - "Indentation" + - "QualifiedAs" + - "QualifiedFunction" + - "QualifiedShadowing" + - "RecordField" + - "ShadowedName" + - "TypeConstructor" + - "TypeVariable" diff --git a/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs b/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs index f1c3487138..f67aa9f8a8 100644 --- a/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs +++ b/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs @@ -13,8 +13,9 @@ {-# LANGUAGE TypeFamilies #-} {-# OPTIONS -Wno-orphans #-} +{-# LANGUAGE RankNTypes #-} -module Ide.Plugin.Retrie (descriptor) where +module Ide.Plugin.Retrie (descriptor, callRetrieWithTransformerAndUpdates, response, handleMaybe, handleMaybeM, sendRetrieErrors) where import Control.Concurrent.Extra (readVar) import Control.Exception.Safe (Exception (..), @@ -51,7 +52,6 @@ import Development.IDE.Core.PositionMapping import Development.IDE.Core.Shake (ShakeExtras (knownTargetsVar), toKnownFiles) import Development.IDE.GHC.Compat (GenLocated (L), GhcRn, - HsBindLR (FunBind), HsGroup (..), HsValBindsLR (..), HscEnv, IdP, LRuleDecls, @@ -79,9 +79,10 @@ import GhcPlugins (Outputable, nameRdrName, occNameFS, occNameString, rdrNameOcc, unpackFS) +import HsBinds import Ide.PluginUtils import Ide.Types -import Language.LSP.Server (LspM, +import Language.LSP.Server (LspM, MonadLsp, ProgressCancellable (Cancellable), sendNotification, sendRequest, @@ -91,11 +92,14 @@ import Language.LSP.Types as J hiding SemanticTokenRelative (length), SemanticTokensEdit (_start)) import Retrie.CPP (CPP (NoCPP), parseCPP) +import Retrie.Context (ContextUpdater, + updateContext) import Retrie.ExactPrint (fix, relativiseApiAnns, transformA, unsafeMkA) import Retrie.Fixity (mkFixityEnv) import qualified Retrie.GHC as GHC -import Retrie.Monad (addImports, apply, +import Retrie.Monad (addImports, + applyWithUpdate, getGroundTerms, runRetrie) import Retrie.Options (defaultOptions, @@ -105,6 +109,9 @@ import Retrie.Replace (Change (..), Replacement (..)) import Retrie.Rewrites import Retrie.SYB (listify) +import Retrie.Types (MatchResultTransformer, + defaultTransformer, + setRewriteTransformer) import Retrie.Util (Verbosity (Loud)) import StringBuffer (stringToStringBuffer) import System.Directory (makeAbsolute) @@ -152,16 +159,22 @@ runRetrieCmd state RunRetrieParams{originatingFile = uri, ..} = (map Right rewrites <> map Left importRewrites) nfp restrictToOriginatingFile - unless (null errors) $ - lift $ sendNotification SWindowShowMessage $ - ShowMessageParams MtWarning $ - T.unlines $ - "## Found errors during rewrite:" : - ["-" <> T.pack (show e) | e <- errors] + lift $ sendRetrieErrors errors + lift $ sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edits) (\_ -> pure ()) return () return $ Right Null + +sendRetrieErrors :: (MonadLsp c f) => [CallRetrieError] -> f () +sendRetrieErrors errors = do + unless (null errors) $ + sendNotification SWindowShowMessage $ + ShowMessageParams MtWarning $ + T.unlines $ + "## Found errors during rewrite:" : + ["-" <> T.pack (show e) | e <- errors] + extractImports :: ModSummary -> [HsBindLR GhcRn GhcRn] -> RewriteSpec -> [ImportSpec] extractImports ModSummary{ms_mod} topLevelBinds (Unfold thing) | Just FunBind {fun_matches} @@ -355,7 +368,21 @@ callRetrie :: NormalizedFilePath -> Bool -> IO ([CallRetrieError], WorkspaceEdit) -callRetrie state session rewrites origin restrictToOriginatingFile = do +callRetrie = + callRetrieWithTransformerAndUpdates defaultTransformer updateContext + +-- | allows custom 'ContextUpdater' to be given to 'applyWithUpdates' +-- applies transformations to the spec +callRetrieWithTransformerAndUpdates :: + MatchResultTransformer -> + ContextUpdater -> + IdeState -> + HscEnv -> + [Either ImportSpec RewriteSpec] -> + NormalizedFilePath -> + Bool -> + IO ([CallRetrieError], WorkspaceEdit) +callRetrieWithTransformerAndUpdates transformer contextUpdater state session rewrites origin restrictToOriginatingFile = do knownFiles <- toKnownFiles . unhashed <$> readVar (knownTargetsVar $ shakeExtras state) let reuseParsedModule f = do pm <- @@ -417,8 +444,8 @@ callRetrie state session rewrites origin restrictToOriginatingFile = do unsafeMkA (map (GHC.noLoc . toImportDecl) theImports) mempty 0 (originFixities, originParsedModule) <- reuseParsedModule origin - retrie <- - (\specs -> apply specs >> addImports annotatedImports) + retrie <- (\specs -> applyWithUpdate contextUpdater (map (setRewriteTransformer transformer) specs) + >> addImports annotatedImports) <$> parseRewriteSpecs (\_f -> return $ NoCPP originParsedModule) originFixities diff --git a/stack-8.10.2.yaml b/stack-8.10.2.yaml index 893a340c60..8736053f3c 100644 --- a/stack-8.10.2.yaml +++ b/stack-8.10.2.yaml @@ -15,6 +15,7 @@ packages: - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin @@ -51,7 +52,7 @@ extra-deps: - implicit-hie-0.1.2.6 - monad-dijkstra-0.1.1.2 - refinery-0.4.0.0 - - retrie-0.1.1.1 + - retrie-1.0.0.0 - shake-0.19.4 - stylish-haskell-0.12.2.0 - semigroups-0.18.5 diff --git a/stack-8.10.3.yaml b/stack-8.10.3.yaml index c0d360d3f6..59d9672173 100644 --- a/stack-8.10.3.yaml +++ b/stack-8.10.3.yaml @@ -15,6 +15,7 @@ packages: - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin @@ -51,7 +52,7 @@ extra-deps: - implicit-hie-0.1.2.6 - monad-dijkstra-0.1.1.2 - refinery-0.4.0.0 - - retrie-0.1.1.1 + - retrie-1.0.0.0 - shake-0.19.4 - stylish-haskell-0.12.2.0 - semigroups-0.18.5 diff --git a/stack-8.10.4.yaml b/stack-8.10.4.yaml index b4c06fc578..a88d6bbfdb 100644 --- a/stack-8.10.4.yaml +++ b/stack-8.10.4.yaml @@ -15,6 +15,7 @@ packages: - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin @@ -48,7 +49,7 @@ extra-deps: - implicit-hie-0.1.2.6 - monad-dijkstra-0.1.1.2 - refinery-0.4.0.0 - - retrie-0.1.1.1 + - retrie-1.0.0.0 - stylish-haskell-0.12.2.0 - semigroups-0.18.5 - temporary-1.2.1.1 diff --git a/stack-8.10.5.yaml b/stack-8.10.5.yaml index 1e9b4281a3..168e2dcb7c 100644 --- a/stack-8.10.5.yaml +++ b/stack-8.10.5.yaml @@ -17,6 +17,7 @@ packages: - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin diff --git a/stack-8.8.3.yaml b/stack-8.8.3.yaml index c202d21c82..cb1ad7186a 100644 --- a/stack-8.8.3.yaml +++ b/stack-8.8.3.yaml @@ -15,6 +15,7 @@ packages: - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin @@ -64,7 +65,7 @@ extra-deps: - opentelemetry-extra-0.6.1 - ormolu-0.1.4.1 - refinery-0.4.0.0 - - retrie-0.1.1.1 + - retrie-1.0.0.0 - semigroups-0.18.5 - shake-0.19.4 - stylish-haskell-0.12.2.0 diff --git a/stack-8.8.4.yaml b/stack-8.8.4.yaml index 33c01989a6..a6fd3f147e 100644 --- a/stack-8.8.4.yaml +++ b/stack-8.8.4.yaml @@ -15,6 +15,7 @@ packages: - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin @@ -63,7 +64,7 @@ extra-deps: - opentelemetry-0.6.1 - opentelemetry-extra-0.6.1 - refinery-0.4.0.0 - - retrie-0.1.1.1 + - retrie-1.0.0.0 - semigroups-0.18.5 - shake-0.19.4 - stylish-haskell-0.12.2.0 diff --git a/stack-9.0.1.yaml b/stack-9.0.1.yaml index 112dada930..6a9c143054 100644 --- a/stack-9.0.1.yaml +++ b/stack-9.0.1.yaml @@ -16,6 +16,7 @@ packages: - ./plugins/hls-explicit-imports-plugin # - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin # - ./plugins/hls-splice-plugin # - ./plugins/hls-tactics-plugin @@ -109,6 +110,7 @@ flags: pedantic: true class: false splice: false + rename: false refineImports: false tactic: false # Dependencies fail diff --git a/stack.yaml b/stack.yaml index b9ded8b709..bbc23834c7 100644 --- a/stack.yaml +++ b/stack.yaml @@ -15,6 +15,7 @@ packages: - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin + - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin @@ -45,7 +46,7 @@ extra-deps: - implicit-hie-0.1.2.6 - monad-dijkstra-0.1.1.2 - refinery-0.4.0.0 - - retrie-0.1.1.1 + - retrie-1.0.0.0 - stylish-haskell-0.12.2.0 - semigroups-0.18.5 - temporary-1.2.1.1 diff --git a/test/functional/Main.hs b/test/functional/Main.hs index 8da258b708..ff9473e56c 100644 --- a/test/functional/Main.hs +++ b/test/functional/Main.hs @@ -14,7 +14,6 @@ import HieBios import Highlight import Progress import Reference -import Rename import Symbol import Test.Hls import TypeDefinition @@ -37,7 +36,6 @@ main = defaultTestRunner , Highlight.tests , Progress.tests , Reference.tests - , Rename.tests , Symbol.tests , TypeDefinition.tests ] diff --git a/test/functional/Rename.hs b/test/functional/Rename.hs deleted file mode 100644 index 08de8a63d9..0000000000 --- a/test/functional/Rename.hs +++ /dev/null @@ -1,23 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -module Rename (tests) where - -import Test.Hls -import Test.Hls.Command - -tests :: TestTree -tests = testGroup "rename" [ - ignoreTestBecause "no symbol renaming (yet!)" $ - testCase "works" $ - runSession hlsCommand fullCaps "test/testdata/rename" $ do - doc <- openDoc "Rename.hs" "haskell" - rename doc (Position 3 1) "baz" -- foo :: Int -> Int - contents <- documentContents doc - let expected = - "main = do\n\ - \ x <- return $ baz 42\n\ - \ return (baz x)\n\ - \baz :: Int -> Int\n\ - \baz x = x + 1\n\ - \bar = (+ 1) . baz\n" - liftIO $ contents @?= expected - ] diff --git a/test/utils/Test/Hls/Flags.hs b/test/utils/Test/Hls/Flags.hs index 84ff263f76..ce17c7568e 100644 --- a/test/utils/Test/Hls/Flags.hs +++ b/test/utils/Test/Hls/Flags.hs @@ -39,6 +39,14 @@ requiresImportLensPlugin = id requiresImportLensPlugin = ignoreTestBecause "ImportLens plugin disabled" #endif +-- | Disable test unless the rename flag is set +requiresRenamePlugin :: TestTree -> TestTree +#if rename +requiresRenamePlugin = id +#else +requiresRenamePlugin = ignoreTestBecause "Rename plugin disabled" +#endif + -- | Disable test unless the retrie flag is set requiresRetriePlugin :: TestTree -> TestTree #if retrie