haskell-language-server-1.0.0.0: plugins/default/src/Ide/Plugin/Brittany.hs
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
module Ide.Plugin.Brittany where
import Control.Exception (bracket_)
import Control.Lens
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Data.Coerce
import Data.Maybe (mapMaybe, maybeToList)
import Data.Semigroup
import Data.Text (Text)
import qualified Data.Text as T
import Development.IDE hiding (pluginHandlers)
import Development.IDE.GHC.Compat (topDir, ModSummary(ms_hspp_opts))
import qualified DynFlags as D
import qualified EnumSet as S
import GHC.LanguageExtensions.Type
import Language.Haskell.Brittany
import Language.LSP.Types as J
import qualified Language.LSP.Types.Lens as J
import Ide.PluginUtils
import Ide.Types
import System.FilePath
import System.Environment (setEnv, unsetEnv)
descriptor :: PluginId -> PluginDescriptor IdeState
descriptor plId = (defaultPluginDescriptor plId)
{ pluginHandlers = mkFormattingHandlers provider
}
-- | Formatter provider of Brittany.
-- Formats the given source in either a given Range or the whole Document.
-- If the provider fails an error is returned that can be displayed to the user.
provider :: FormattingHandler IdeState
provider ide typ contents nfp opts = liftIO $ do
confFile <- getConfFile nfp
let (range, selectedContents) = case typ of
FormatText -> (fullRange contents, contents)
FormatRange r -> (normalize r, extractRange r contents)
(modsum, _) <- runAction "brittany" ide $ use_ GetModSummary nfp
let dflags = ms_hspp_opts modsum
let withRuntimeLibdir = bracket_ (setEnv key $ topDir dflags) (unsetEnv key)
where key = "GHC_EXACTPRINT_GHC_LIBDIR"
res <- withRuntimeLibdir $ formatText dflags confFile opts selectedContents
case res of
Left err -> return $ Left $ responseError (T.pack $ "brittanyCmd: " ++ unlines (map showErr err))
Right newText -> return $ Right $ J.List [TextEdit range newText]
-- | Primitive to format text with the given option.
-- May not throw exceptions but return a Left value.
-- Errors may be presented to the user.
formatText
:: MonadIO m
=> D.DynFlags
-> Maybe FilePath -- ^ Path to configs. If Nothing, default configs will be used.
-> FormattingOptions -- ^ Options for the formatter such as indentation.
-> Text -- ^ Text to format
-> m (Either [BrittanyError] Text) -- ^ Either formatted Text or a error from Brittany.
formatText df confFile opts text =
liftIO $ runBrittany tabSize df confFile text
where tabSize = opts ^. J.tabSize
-- | Recursively search in every directory of the given filepath for brittany.yaml.
-- If no such file has been found, return Nothing.
getConfFile :: NormalizedFilePath -> IO (Maybe FilePath)
getConfFile = findLocalConfigPath . takeDirectory . fromNormalizedFilePath
-- | Run Brittany on the given text with the given tab size and
-- a configuration path. If no configuration path is given, a
-- default configuration is chosen. The configuration may overwrite
-- tab size parameter.
--
-- Returns either a list of Brittany Errors or the reformatted text.
-- May not throw an exception.
runBrittany :: Int -- ^ tab size
-> D.DynFlags
-> Maybe FilePath -- ^ local config file
-> Text -- ^ text to format
-> IO (Either [BrittanyError] Text)
runBrittany tabSize df confPath text = do
let cfg = mempty
{ _conf_layout =
mempty { _lconfig_indentAmount = opt (coerce tabSize)
}
, _conf_forward =
(mempty :: CForwardOptions Option)
{ _options_ghc = opt (getExtensions df)
}
}
config <- fromMaybeT (pure staticDefaultConfig) (readConfigsWithUserConfig cfg (maybeToList confPath))
parsePrintModule config text
fromMaybeT :: Monad m => m a -> MaybeT m a -> m a
fromMaybeT def act = runMaybeT act >>= maybe def return
opt :: a -> Option a
opt = Option . Just
showErr :: BrittanyError -> String
showErr (ErrorInput s) = s
showErr (ErrorMacroConfig err input)
= "Error: parse error in inline configuration: " ++ err ++ " in the string \"" ++ input ++ "\"."
showErr (ErrorUnusedComment s) = s
showErr (LayoutWarning s) = s
showErr (ErrorUnknownNode s _) = s
showErr ErrorOutputCheck = "Brittany error - invalid output"
showExtension :: Extension -> Maybe String
showExtension Cpp = Just "-XCPP"
-- Brittany chokes on parsing extensions that produce warnings
showExtension DatatypeContexts = Nothing
showExtension RecordPuns = Just "-XNamedFieldPuns"
showExtension other = Just $ "-X" ++ show other
getExtensions :: D.DynFlags -> [String]
getExtensions = mapMaybe showExtension . S.toList . D.extensionFlags