packages feed

haskell-language-server-0.8.0.0: plugins/default/src/Ide/Plugin/ModuleName.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# OPTIONS_GHC -Wall -Wwarn -fno-warn-type-defaults -fno-warn-unused-binds -fno-warn-unused-imports #-}

{- | Keep the module name in sync with its file path.

Provide CodeLenses to:
* Add a module header ("module /moduleName/ where") to empty Haskell files
* Fix the module name if incorrect
-}
module Ide.Plugin.ModuleName (
    descriptor,
) where

import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Aeson (
    ToJSON (toJSON),
    Value (Null),
 )
import Data.Char (isLower)
import qualified Data.HashMap.Strict as Map
import Data.List (find, intercalate, isPrefixOf)
import Data.Maybe (maybeToList)
import Data.String (IsString)
import Data.Text (Text, pack)
import qualified Data.Text as T
-- import Debug.Trace (trace)
import Development.IDE (
    GetParsedModule (
        GetParsedModule
    ),
    GhcSession (GhcSession),
    HscEnvEq,
    IdeState,
    List (..),
    NormalizedFilePath,
    Position (Position),
    Range (Range),
    evalGhcEnv,
    hscEnvWithImportPaths,
    realSrcSpanToRange,
    runAction,
    toNormalizedUri,
    uriToFilePath',
    use,
    use_,
 )
import GHC (
    DynFlags (importPaths),
    GenLocated (L),
    HsModule (hsmodName),
    ParsedModule (pm_parsed_source),
    SrcSpan (RealSrcSpan),
    getSessionDynFlags,
    unLoc,
 )
import Ide.PluginUtils (mkLspCmdId, getProcessID)
import Ide.Types (
    CommandFunction,
    PluginCommand (..),
    PluginDescriptor (..),
    PluginId (..),
    defaultPluginDescriptor,
 )
import Language.Haskell.LSP.Core (
    LspFuncs,
    getVirtualFileFunc,
 )
import Language.Haskell.LSP.Types (
    ApplyWorkspaceEditParams (..),
    CodeLens (CodeLens),
    CodeLensParams (CodeLensParams),
    Command (Command),
    ServerMethod (..),
    TextDocumentIdentifier (
        TextDocumentIdentifier
    ),
    TextEdit (TextEdit),
    Uri,
    WorkspaceEdit (..),
    uriToNormalizedFilePath,
 )
import Language.Haskell.LSP.VFS (virtualFileText)
import System.Directory (canonicalizePath)
import System.FilePath (
    dropExtension,
    splitDirectories,
    takeFileName,
 )

-- |Plugin descriptor
descriptor :: PluginId -> PluginDescriptor IdeState
descriptor plId =
    (defaultPluginDescriptor plId)
        { pluginCodeLensProvider = Just codeLens
        , pluginCommands = [PluginCommand editCommandName editCommandName command]
        }

editCommandName :: IsString p => p
editCommandName = "edit"

asCodeLens :: Text -> Action -> CodeLens
asCodeLens cid Replace{..} =
    CodeLens
        aRange
        (Just $ Command aTitle cid (Just (List [toJSON aUri])))
        Nothing

-- | Generate code lenses
codeLens ::
    LspFuncs c ->
    IdeState ->
    PluginId ->
    CodeLensParams ->
    IO (Either a2 (List CodeLens))
codeLens lsp state pluginId (CodeLensParams (TextDocumentIdentifier uri) _) =
    do
        pid <- pack . show <$> getProcessID
        Right . List . maybeToList . (asCodeLens (mkLspCmdId pid pluginId editCommandName) <$>) <$> action lsp state uri

-- | (Quasi) Idempotent command execution: recalculate action to execute on command request
command :: CommandFunction IdeState Uri
command lsp state uri = do
    actMaybe <- action lsp state uri
    return
        ( Right Null
        , (\act -> (WorkspaceApplyEdit, ApplyWorkspaceEditParams $ asEdit act)) <$> actMaybe
        )

-- | A source code change
data Action = Replace {aUri :: Uri, aRange :: Range, aTitle :: Text, aCode :: Text} deriving (Show)

-- | Convert an Action to the corresponding edit operation
asEdit :: Action -> WorkspaceEdit
asEdit act@Replace{..} =
    WorkspaceEdit (Just $ Map.singleton aUri $ List (asTextEdits act)) Nothing

asTextEdits :: Action -> [TextEdit]
asTextEdits Replace{..} = [TextEdit aRange aCode]

-- | Required action (that can be converted to either CodeLenses or CodeActions)
action :: LspFuncs c -> IdeState -> Uri -> IO (Maybe Action)
action lsp state uri =
    traceAs "action" <$> do
        let Just nfp = uriToNormalizedFilePath $ toNormalizedUri uri
        let Just fp = uriToFilePath' uri

        contents <- liftIO $ getVirtualFileFunc lsp $ toNormalizedUri uri
        let emptyModule = maybe True (T.null . T.strip . virtualFileText) contents

        correctNameMaybe <- traceAs "correctName" <$> pathModuleName state nfp fp
        statedNameMaybe <- traceAs "statedName" <$> codeModuleName state nfp

        let act = Replace uri
        let todo = case (correctNameMaybe, statedNameMaybe) of
                (Just correctName, Just (nameRange, statedName))
                    | correctName /= statedName ->
                        Just $
                            act
                                nameRange
                                ("Set module name to " <> correctName)
                                correctName
                (Just correctName, _)
                    | emptyModule ->
                        let code = T.unwords ["module", correctName, "where\n"]
                         in Just $ act (Range (Position 0 0) (Position 0 0)) code code
                _ -> Nothing
        return todo

-- | The module name, as derived by the position of the module in its source directory
pathModuleName :: IdeState -> NormalizedFilePath -> String -> IO (Maybe Text)
pathModuleName state normFilePath filePath
    | isLower (head $ takeFileName filePath) = return $ Just "Main"
    | otherwise = do
        session :: HscEnvEq <- runAction "ModuleName.ghcSession" state $ use_ GhcSession normFilePath
        srcPaths <- evalGhcEnv (hscEnvWithImportPaths session) $ importPaths <$> getSessionDynFlags
        paths <- mapM canonicalizePath srcPaths
        mdlPath <- canonicalizePath filePath
        let maybePrefix = find (`isPrefixOf` mdlPath) paths

        let maybeMdlName =
                ( \prefix ->
                    intercalate "."
                        . splitDirectories
                        . drop (length prefix + 1)
                        $ dropExtension mdlPath
                )
                    <$> maybePrefix
        return $ T.pack <$> maybeMdlName

-- | The module name, as stated in the module
codeModuleName :: IdeState -> NormalizedFilePath -> IO (Maybe (Range, Text))
codeModuleName state nfp =
    ((\(L (RealSrcSpan l) m) -> (realSrcSpanToRange l, T.pack . show $ m)) <$>)
        . ((hsmodName . unLoc . pm_parsed_source) =<<)
        <$> runAction "ModuleName.GetParsedModule" state (use GetParsedModule nfp)

-- traceAs :: Show a => String -> a -> a
-- traceAs lbl a = trace (lbl ++ " = " ++ show a) a

traceAs :: b -> a -> a
traceAs _ a = a