packages feed

hls-rename-plugin-1.0.0.0: src/Ide/Plugin/Rename.hs

{-# LANGUAGE CPP            #-}
{-# LANGUAGE DataKinds      #-}
{-# LANGUAGE GADTs          #-}
{-# LANGUAGE NamedFieldPuns #-}

module Ide.Plugin.Rename (descriptor) where

import           Control.Monad
import           Control.Monad.IO.Class               (MonadIO (liftIO))
import           Control.Monad.Trans.Class
import           Control.Monad.Trans.Except
import           Data.Containers.ListUtils
import           Data.Generics
import           Data.List.Extra                      hiding (nubOrd)
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.Spans.AtPoint
#if MIN_VERSION_ghc(9,0,1)
import           GHC.Types.Name
#else
import           Name
#endif
import           HieDb.Query
import           Ide.Plugin.Config
import           Ide.Plugin.Retrie                    hiding (descriptor)
import           Ide.PluginUtils
import           Ide.Types
import           Language.Haskell.GHC.ExactPrint
import           Language.LSP.Server
import           Language.LSP.Types

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 <- safeUriToNfp uri
        oldName <- getNameAtPos state nfp pos
        workspaceRefs <- refsAtName state nfp oldName
        let filesRefs = groupOn locToUri workspaceRefs
            getFileEdits = ap (getSrcEdits state . renameModRefs newNameText) (locToUri . head)

        fileEdits <- mapM getFileEdits filesRefs
        pure $ foldl1 (<>) fileEdits

-------------------------------------------------------------------------------
-- Source renaming

-- | Compute a `WorkspaceEdit` by applying a given function to the `ParsedModule` for a given `Uri`.
getSrcEdits ::
    (MonadLsp config m) =>
    IdeState ->
#if MIN_VERSION_ghc(9,0,1)
    (HsModule -> HsModule) ->
#else
    (HsModule GhcPs -> HsModule GhcPs) ->
#endif
    Uri ->
    ExceptT String m WorkspaceEdit
getSrcEdits state updateMod uri = do
    ccs <- lift getClientCapabilities
    nfp <- safeUriToNfp uri
    ParsedModule{pm_parsed_source = ps, pm_annotations = apiAnns} <-
        handleMaybeM "Error: could not get parsed source" $ liftIO $ runAction
            "Rename.GetParsedModuleWithComments"
            state
            (use GetParsedModuleWithComments nfp)

    let anns = relativiseApiAnns ps apiAnns
        src = T.pack $ exactPrint ps anns
        res = T.pack $ exactPrint (updateMod <$> ps) anns

    pure $ diffText ccs (uri, src) res IncludeDeletions

-- | Replace a name at every given `Location` (in a given `HsModule`) with a given new name.
renameModRefs ::
    T.Text ->
    [Location] ->
#if MIN_VERSION_ghc(9,0,1)
    HsModule
    -> HsModule
#else
    HsModule GhcPs
    -> HsModule GhcPs
#endif
renameModRefs newNameText refs = everywhere $ mkT replace
    where
        replace :: Located RdrName -> Located RdrName
        replace (L srcSpan oldRdrName)
            | isRef srcSpan = L srcSpan $ newRdrName oldRdrName
        replace lOldRdrName = lOldRdrName

        newRdrName :: RdrName -> RdrName
        newRdrName oldRdrName = case oldRdrName of
            Qual modName _ -> Qual modName newOccName
            _              -> Unqual newOccName

        newOccName = mkTcOcc $ T.unpack newNameText

        isRef :: SrcSpan -> Bool
        isRef = (`elem` refs) . fromJust . srcSpanToLocation

-------------------------------------------------------------------------------
-- Reference finding

-- | Note: We only find exact name occurences (i.e. type reference "depth" is 0).
refsAtName :: IdeState -> NormalizedFilePath -> Name -> ExceptT [Char] (LspT Config IO) [Location]
refsAtName state nfp name = do
    ShakeExtras{hiedb} <- liftIO $ runAction "Rename.HieDb" state getShakeExtras
    ast <- safeGetHieAst state nfp
    astRefs <- handleMaybe "Error: Could not get name AST references" $ getNameAstLocations name ast
    dbRefs <- case nameModule_maybe name of
        Nothing -> pure []
        Just mod -> liftIO $ mapMaybe rowToLoc <$>
            findReferences
                hiedb
                True
                (nameOccName name)
                (Just $ moduleName mod)
                (Just $ moduleUnitId mod)
                [fromNormalizedFilePath nfp]
    pure $ nubOrd $ astRefs ++ dbRefs

getNameAstLocations :: Name -> (HieAstResult, PositionMapping) -> Maybe [Location]
getNameAstLocations name (HAR _ _ rm _ _, mapping) =
    mapMaybe (toCurrentLocation mapping . realSrcSpanToLocation . fst) <$> M.lookup (Right name) rm

-------------------------------------------------------------------------------
-- Util

getNameAtPos :: IdeState -> NormalizedFilePath -> Position -> ExceptT String (LspT Config IO) Name
getNameAtPos state nfp pos = do
    (HAR{hieAst}, mapping) <- safeGetHieAst state nfp
    handleMaybe "Error: could not find name at position" $ listToMaybe $
        getAstNamesAtPoint hieAst pos mapping

nfpToUri :: NormalizedFilePath -> Uri
nfpToUri = filePathToUri . fromNormalizedFilePath

safeUriToNfp :: (Monad m) => Uri -> ExceptT String m NormalizedFilePath
safeUriToNfp = handleMaybe "Error: Could not get uri" . fmap toNormalizedFilePath . uriToFilePath

safeGetHieAst ::
    MonadIO m =>
    IdeState ->
    NormalizedFilePath ->
    ExceptT String m (HieAstResult, PositionMapping)
safeGetHieAst state = handleMaybeM "Error: Could not get AST" . liftIO .
    runAction "Rename.GetHieAst" state . useWithStale GetHieAst

locToUri :: Location -> Uri
locToUri (Location uri _) = uri