diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index 3f38abe391..d3fa69ad37 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -1370,35 +1370,45 @@ flag fourmolu default: True manual: True -common fourmolu - if flag(fourmolu) - build-depends: haskell-language-server:hls-fourmolu-plugin - cpp-options: -Dhls_fourmolu - library hls-fourmolu-plugin import: defaults, pedantic, warnings - exposed-modules: Ide.Plugin.Fourmolu + hs-source-dirs: plugins/hls-fourmolu-plugin/src + exposed-modules: Ide.Plugin.Fourmolu + other-modules: Ide.Plugin.Fourmolu.Common + + if flag(fourmolu) + build-depends: + fourmolu ^>= 0.14 || ^>= 0.15 + , filepath + , ghc-boot-th + , lens + , lsp + , mtl + , process-extras >= 0.7.1 + , transformers + hs-source-dirs: plugins/hls-fourmolu-plugin/real + other-modules: Ide.Plugin.Fourmolu.Impl + else + hs-source-dirs: plugins/hls-fourmolu-plugin/disabled + other-modules: Ide.Plugin.Fourmolu.Impl + build-depends: , base >=4.12 && <5 - , filepath - , fourmolu ^>= 0.14 || ^>= 0.15 - , ghc-boot-th , ghcide == 2.7.0.0 , hls-plugin-api == 2.7.0.0 - , lens - , lsp - , mtl - , process-extras >= 0.7.1 , text - , transformers - test-suite hls-fourmolu-plugin-tests import: defaults, pedantic, test-defaults, warnings type: exitcode-stdio-1.0 + + if !flag(fourmolu) + buildable: False + hs-source-dirs: plugins/hls-fourmolu-plugin/test main-is: Main.hs + build-tool-depends: fourmolu:fourmolu build-depends: @@ -1770,7 +1780,6 @@ library , explicitFixity , explicitFields , floskell - , fourmolu , ormolu , stylishHaskell , refactor @@ -1797,6 +1806,7 @@ library , ghc , ghcide == 2.7.0.0 , githash >=0.1.6.1 + , haskell-language-server:{hls-fourmolu-plugin} , hie-bios , hls-plugin-api == 2.7.0.0 , optparse-applicative diff --git a/plugins/hls-fourmolu-plugin/disabled/Ide/Plugin/Fourmolu/Impl.hs b/plugins/hls-fourmolu-plugin/disabled/Ide/Plugin/Fourmolu/Impl.hs new file mode 100644 index 0000000000..3b11f2693d --- /dev/null +++ b/plugins/hls-fourmolu-plugin/disabled/Ide/Plugin/Fourmolu/Impl.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE OverloadedStrings #-} +module Ide.Plugin.Fourmolu.Impl (pluginDesc, handlers) where + +import Data.Text (Text) +import Development.IDE hiding (pluginHandlers) +import Ide.Plugin.Fourmolu.Common +import Ide.Types + +pluginDesc :: Text +pluginDesc = "DISABLED: " <> pluginDescMain + +handlers :: Recorder (WithPriority LogEvent) -> PluginId -> PluginHandlers IdeState +handlers = mempty diff --git a/plugins/hls-fourmolu-plugin/real/Ide/Plugin/Fourmolu/Impl.hs b/plugins/hls-fourmolu-plugin/real/Ide/Plugin/Fourmolu/Impl.hs new file mode 100644 index 0000000000..be5bfbe2b0 --- /dev/null +++ b/plugins/hls-fourmolu-plugin/real/Ide/Plugin/Fourmolu/Impl.hs @@ -0,0 +1,162 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Ide.Plugin.Fourmolu.Impl (pluginDesc, handlers) where + +import Control.Exception +import Control.Lens ((^.)) +import Control.Monad (guard) +import Control.Monad.Error.Class (MonadError (throwError)) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Control.Monad.Trans.Class (MonadTrans (lift)) +import Control.Monad.Trans.Except (ExceptT (..), runExceptT) +import Data.Bifunctor (bimap) +import Data.Maybe (catMaybes) +import Data.Text (Text) +import qualified Data.Text as T +import Development.IDE hiding (pluginHandlers) +import Development.IDE.GHC.Compat as Compat hiding (Cpp, Warning, + hang, vcat) +import qualified Development.IDE.GHC.Compat.Util as S +import GHC.LanguageExtensions.Type (Extension (Cpp)) +import Ide.Plugin.Error +import Ide.Plugin.Fourmolu.Common +import Ide.PluginUtils (makeDiffTextEdit) +import Ide.Types +import Language.LSP.Protocol.Lens (HasTabSize (tabSize)) +import Language.LSP.Protocol.Message +import Language.LSP.Protocol.Types +import Language.LSP.Server hiding (defaultConfig) +import Ormolu +import Ormolu.Config +import System.Exit +import System.FilePath +import System.Process.Run (cwd, proc) +import System.Process.Text (readCreateProcessWithExitCode) +import Text.Read (readMaybe) + +pluginDesc :: Text +pluginDesc = pluginDescMain <> " Built with fourmolu-" <> VERSION_fourmolu + +handlers :: Recorder (WithPriority LogEvent) -> PluginId -> PluginHandlers IdeState +handlers recorder plId = mkFormattingHandlers $ provider recorder plId + +provider :: Recorder (WithPriority LogEvent) -> PluginId -> FormattingHandler IdeState +provider recorder plId ideState token typ contents fp fo = ExceptT $ withIndefiniteProgress title token Cancellable $ \_updater -> runExceptT $ do + fileOpts <- + maybe [] (convertDynFlags . hsc_dflags . hscEnv) + <$> liftIO (runAction "Fourmolu" ideState $ use GhcSession fp) + useCLI <- liftIO $ runAction "Fourmolu" ideState $ usePropertyAction #external plId properties + fourmoluExePath <- fmap T.unpack $ liftIO $ runAction "Fourmolu" ideState $ usePropertyAction #path plId properties + if useCLI + then ExceptT . liftIO $ + handle @IOException (pure . Left . PluginInternalError . T.pack . show) $ + runExceptT (cliHandler fourmoluExePath fileOpts) + else do + logWith recorder Debug $ LogCompiledInVersion VERSION_fourmolu + FourmoluConfig{..} <- + liftIO (loadConfigFile fp') >>= \case + ConfigLoaded file opts -> do + logWith recorder Info $ ConfigPath file + pure opts + ConfigNotFound searchDirs -> do + logWith recorder Info $ NoConfigPath configFileName searchDirs + pure emptyConfig + ConfigParseError f err -> do + lift $ sendNotification SMethod_WindowShowMessage $ + ShowMessageParams + { _type_ = MessageType_Error + , _message = errorMessage + } + throwError $ PluginInternalError errorMessage + where + errorMessage = "Failed to load " <> T.pack f <> ": " <> T.pack (show err) + + let config = +#if MIN_VERSION_fourmolu(0,13,0) + refineConfig ModuleSource Nothing Nothing Nothing +#endif + defaultConfig + { cfgDynOptions = map DynOption fileOpts + , cfgFixityOverrides = cfgFileFixities + , cfgRegion = region + , cfgDebug = False + , cfgPrinterOpts = resolvePrinterOpts [lspPrinterOpts, cfgFilePrinterOpts] + } + ExceptT . liftIO $ + bimap (PluginInternalError . T.pack . show) (InL . makeDiffTextEdit contents) + <$> try @OrmoluException (ormolu config fp' contents) + where + fp' = fromNormalizedFilePath fp + title = "Formatting " <> T.pack (takeFileName fp') + lspPrinterOpts = mempty{poIndentation = Just $ fromIntegral $ fo ^. tabSize} + region = case typ of + FormatText -> + RegionIndices Nothing Nothing + FormatRange (Range (Position sl _) (Position el _)) -> + RegionIndices (Just $ fromIntegral $ sl + 1) (Just $ fromIntegral $ el + 1) + cliHandler :: FilePath -> [String] -> ExceptT PluginError IO ([TextEdit] |? Null) + cliHandler path fileOpts = do + CLIVersionInfo{noCabal} <- do -- check Fourmolu version so that we know which flags to use + (exitCode, out, _err) <- liftIO $ readCreateProcessWithExitCode ( proc path ["-v"] ) "" + let version = do + guard $ exitCode == ExitSuccess + "fourmolu" : v : _ <- pure $ T.words out + traverse (readMaybe @Int . T.unpack) $ T.splitOn "." v + case version of + Just v -> do + logWith recorder Debug $ LogExternalVersion v + pure CLIVersionInfo + { noCabal = v >= [0, 7] + } + Nothing -> do + logWith recorder Debug $ LogExternalVersion [] + logWith recorder Warning $ NoVersion out + pure CLIVersionInfo + { noCabal = True + } + (exitCode, out, err) <- -- run Fourmolu + liftIO $ readCreateProcessWithExitCode + ( proc path $ + map ("-o" <>) fileOpts + <> mwhen noCabal ["--no-cabal"] + <> catMaybes + [ ("--start-line=" <>) . show <$> regionStartLine region + , ("--end-line=" <>) . show <$> regionEndLine region + ] + ){cwd = Just $ takeDirectory fp'} + contents + case exitCode of + ExitSuccess -> do + logWith recorder Debug $ StdErr err + pure $ InL $ makeDiffTextEdit contents out + ExitFailure n -> do + logWith recorder Info $ StdErr err + throwError $ PluginInternalError $ "Fourmolu failed with exit code " <> T.pack (show n) + +convertDynFlags :: DynFlags -> [String] +convertDynFlags df = + let pp = ["-pgmF=" <> p | not (null p)] + p = sPgm_F $ Compat.settings df + pm = map (("-fplugin=" <>) . moduleNameString) $ pluginModNames df + ex = map showExtension $ S.toList $ extensionFlags df + showExtension = \case + Cpp -> "-XCPP" + x -> "-X" ++ show x + in pp <> pm <> ex + +newtype CLIVersionInfo = CLIVersionInfo + { noCabal :: Bool + } + +mwhen :: Monoid a => Bool -> a -> a +mwhen b x = if b then x else mempty + +#if !MIN_VERSION_fourmolu(0,14,0) +resolvePrinterOpts :: [PrinterOptsPartial] -> PrinterOptsTotal +resolvePrinterOpts = foldr fillMissingPrinterOpts defaultPrinterOpts +#endif diff --git a/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs b/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs index f8ed5871e9..ce0801825a 100644 --- a/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs +++ b/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs @@ -1,204 +1,17 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} - module Ide.Plugin.Fourmolu ( descriptor, - provider, LogEvent, ) where -import Control.Exception -import Control.Lens ((^.)) -import Control.Monad (guard) -import Control.Monad.Error.Class (MonadError (throwError)) -import Control.Monad.IO.Class (MonadIO (liftIO)) -import Control.Monad.Trans.Class (MonadTrans (lift)) -import Control.Monad.Trans.Except (ExceptT (..), runExceptT) -import Data.Bifunctor (bimap) -import Data.List (intercalate) -import Data.Maybe (catMaybes) -import Data.Text (Text) -import qualified Data.Text as T -import Development.IDE hiding (pluginHandlers) -import Development.IDE.GHC.Compat as Compat hiding (Cpp, Warning, - hang, vcat) -import qualified Development.IDE.GHC.Compat.Util as S -import GHC.LanguageExtensions.Type (Extension (Cpp)) -import Ide.Plugin.Error -import Ide.Plugin.Properties -import Ide.PluginUtils (makeDiffTextEdit) +import Development.IDE hiding (pluginHandlers) +import Ide.Plugin.Fourmolu.Common +import Ide.Plugin.Fourmolu.Impl import Ide.Types -import Language.LSP.Protocol.Lens (HasTabSize (tabSize)) -import Language.LSP.Protocol.Message -import Language.LSP.Protocol.Types -import Language.LSP.Server hiding (defaultConfig) -import Ormolu -import Ormolu.Config -import System.Exit -import System.FilePath -import System.Process.Run (cwd, proc) -import System.Process.Text (readCreateProcessWithExitCode) -import Text.Read (readMaybe) descriptor :: Recorder (WithPriority LogEvent) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultPluginDescriptor plId desc) - { pluginHandlers = mkFormattingHandlers $ provider recorder plId + (defaultPluginDescriptor plId pluginDesc) + { pluginHandlers = handlers recorder plId , pluginConfigDescriptor = defaultConfigDescriptor{configCustomConfig = mkCustomConfig properties} } - where - desc = "Provides formatting of Haskell files via fourmolu. Built with fourmolu-" <> VERSION_fourmolu - -properties :: Properties '[ 'PropertyKey "external" 'TBoolean, 'PropertyKey "path" 'TString] -properties = - emptyProperties - & defineStringProperty - #path - "Set path to executable (for \"external\" mode)." - "fourmolu" - & defineBooleanProperty - #external - "Call out to an external \"fourmolu\" executable, rather than using the bundled library." - False - -provider :: Recorder (WithPriority LogEvent) -> PluginId -> FormattingHandler IdeState -provider recorder plId ideState token typ contents fp fo = ExceptT $ withIndefiniteProgress title token Cancellable $ \_updater -> runExceptT $ do - fileOpts <- - maybe [] (convertDynFlags . hsc_dflags . hscEnv) - <$> liftIO (runAction "Fourmolu" ideState $ use GhcSession fp) - useCLI <- liftIO $ runAction "Fourmolu" ideState $ usePropertyAction #external plId properties - fourmoluExePath <- fmap T.unpack $ liftIO $ runAction "Fourmolu" ideState $ usePropertyAction #path plId properties - if useCLI - then ExceptT . liftIO $ - handle @IOException (pure . Left . PluginInternalError . T.pack . show) $ - runExceptT (cliHandler fourmoluExePath fileOpts) - else do - logWith recorder Debug $ LogCompiledInVersion VERSION_fourmolu - FourmoluConfig{..} <- - liftIO (loadConfigFile fp') >>= \case - ConfigLoaded file opts -> do - logWith recorder Info $ ConfigPath file - pure opts - ConfigNotFound searchDirs -> do - logWith recorder Info $ NoConfigPath searchDirs - pure emptyConfig - ConfigParseError f err -> do - lift $ sendNotification SMethod_WindowShowMessage $ - ShowMessageParams - { _type_ = MessageType_Error - , _message = errorMessage - } - throwError $ PluginInternalError errorMessage - where - errorMessage = "Failed to load " <> T.pack f <> ": " <> T.pack (show err) - - let config = -#if MIN_VERSION_fourmolu(0,13,0) - refineConfig ModuleSource Nothing Nothing Nothing -#endif - defaultConfig - { cfgDynOptions = map DynOption fileOpts - , cfgFixityOverrides = cfgFileFixities - , cfgRegion = region - , cfgDebug = False - , cfgPrinterOpts = resolvePrinterOpts [lspPrinterOpts, cfgFilePrinterOpts] - } - ExceptT . liftIO $ - bimap (PluginInternalError . T.pack . show) (InL . makeDiffTextEdit contents) - <$> try @OrmoluException (ormolu config fp' contents) - where - fp' = fromNormalizedFilePath fp - title = "Formatting " <> T.pack (takeFileName fp') - lspPrinterOpts = mempty{poIndentation = Just $ fromIntegral $ fo ^. tabSize} - region = case typ of - FormatText -> - RegionIndices Nothing Nothing - FormatRange (Range (Position sl _) (Position el _)) -> - RegionIndices (Just $ fromIntegral $ sl + 1) (Just $ fromIntegral $ el + 1) - cliHandler :: FilePath -> [String] -> ExceptT PluginError IO ([TextEdit] |? Null) - cliHandler path fileOpts = do - CLIVersionInfo{noCabal} <- do -- check Fourmolu version so that we know which flags to use - (exitCode, out, _err) <- liftIO $ readCreateProcessWithExitCode ( proc path ["-v"] ) "" - let version = do - guard $ exitCode == ExitSuccess - "fourmolu" : v : _ <- pure $ T.words out - traverse (readMaybe @Int . T.unpack) $ T.splitOn "." v - case version of - Just v -> do - logWith recorder Debug $ LogExternalVersion v - pure CLIVersionInfo - { noCabal = v >= [0, 7] - } - Nothing -> do - logWith recorder Debug $ LogExternalVersion [] - logWith recorder Warning $ NoVersion out - pure CLIVersionInfo - { noCabal = True - } - (exitCode, out, err) <- -- run Fourmolu - liftIO $ readCreateProcessWithExitCode - ( proc path $ - map ("-o" <>) fileOpts - <> mwhen noCabal ["--no-cabal"] - <> catMaybes - [ ("--start-line=" <>) . show <$> regionStartLine region - , ("--end-line=" <>) . show <$> regionEndLine region - ] - ){cwd = Just $ takeDirectory fp'} - contents - case exitCode of - ExitSuccess -> do - logWith recorder Debug $ StdErr err - pure $ InL $ makeDiffTextEdit contents out - ExitFailure n -> do - logWith recorder Info $ StdErr err - throwError $ PluginInternalError $ "Fourmolu failed with exit code " <> T.pack (show n) - -data LogEvent - = NoVersion Text - | ConfigPath FilePath - | NoConfigPath [FilePath] - | StdErr Text - | LogCompiledInVersion String - | LogExternalVersion [Int] - deriving (Show) - -instance Pretty LogEvent where - pretty = \case - NoVersion t -> "Couldn't get Fourmolu version:" <> line <> indent 2 (pretty t) - ConfigPath p -> "Loaded Fourmolu config from: " <> pretty (show p) - NoConfigPath ps -> "No " <> pretty configFileName <> " found in any of:" - <> line <> indent 2 (vsep (map (pretty . show) ps)) - StdErr t -> "Fourmolu stderr:" <> line <> indent 2 (pretty t) - LogCompiledInVersion v -> "Using compiled in fourmolu-" <> pretty v - LogExternalVersion v -> - "Using external fourmolu" - <> if null v then "" else "-" - <> pretty (intercalate "." $ map show v) - -convertDynFlags :: DynFlags -> [String] -convertDynFlags df = - let pp = ["-pgmF=" <> p | not (null p)] - p = sPgm_F $ Compat.settings df - pm = map (("-fplugin=" <>) . moduleNameString) $ pluginModNames df - ex = map showExtension $ S.toList $ extensionFlags df - showExtension = \case - Cpp -> "-XCPP" - x -> "-X" ++ show x - in pp <> pm <> ex - -newtype CLIVersionInfo = CLIVersionInfo - { noCabal :: Bool - } - -mwhen :: Monoid a => Bool -> a -> a -mwhen b x = if b then x else mempty -#if !MIN_VERSION_fourmolu(0,14,0) -resolvePrinterOpts :: [PrinterOptsPartial] -> PrinterOptsTotal -resolvePrinterOpts = foldr fillMissingPrinterOpts defaultPrinterOpts -#endif diff --git a/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu/Common.hs b/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu/Common.hs new file mode 100644 index 0000000000..2b5741b326 --- /dev/null +++ b/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu/Common.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedStrings #-} + +module Ide.Plugin.Fourmolu.Common where + +import Data.List (intercalate) +import Data.Text (Text) +import Development.IDE hiding (pluginHandlers) +import Ide.Plugin.Properties + +data LogEvent + = NoVersion Text + | ConfigPath FilePath + | NoConfigPath FilePath [FilePath] + | StdErr Text + | LogCompiledInVersion String + | LogExternalVersion [Int] + deriving (Show) + +instance Pretty LogEvent where + pretty = \case + NoVersion t -> "Couldn't get Fourmolu version:" <> line <> indent 2 (pretty t) + ConfigPath p -> "Loaded Fourmolu config from: " <> pretty (show p) + NoConfigPath expected ps -> "No " <> pretty expected <> " found in any of:" + <> line <> indent 2 (vsep (map (pretty . show) ps)) + StdErr t -> "Fourmolu stderr:" <> line <> indent 2 (pretty t) + LogCompiledInVersion v -> "Using compiled in fourmolu-" <> pretty v + LogExternalVersion v -> + "Using external fourmolu" + <> if null v then "" else "-" + <> pretty (intercalate "." $ map show v) + +properties :: Properties '[ 'PropertyKey "external" 'TBoolean, 'PropertyKey "path" 'TString] +properties = + emptyProperties + & defineStringProperty + #path + "Set path to executable (for \"external\" mode)." + "fourmolu" + & defineBooleanProperty + #external + "Call out to an external \"fourmolu\" executable, rather than using the bundled library." + False + +pluginDescMain :: Text +pluginDescMain = "Provides formatting of Haskell files via fourmolu." diff --git a/src/HlsPlugins.hs b/src/HlsPlugins.hs index 1f5d091dc5..ddf2b62990 100644 --- a/src/HlsPlugins.hs +++ b/src/HlsPlugins.hs @@ -103,9 +103,7 @@ import qualified Ide.Plugin.Notes as Notes import qualified Ide.Plugin.Floskell as Floskell #endif -#if hls_fourmolu import qualified Ide.Plugin.Fourmolu as Fourmolu -#endif #if hls_cabalfmt import qualified Ide.Plugin.CabalFmt as CabalFmt @@ -161,9 +159,7 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins #if hls_floskell Floskell.descriptor "floskell" : #endif -#if hls_fourmolu let pId = "fourmolu" in Fourmolu.descriptor (pluginRecorder pId) pId: -#endif #if hls_cabalfmt let pId = "cabal-fmt" in CabalFmt.descriptor (pluginRecorder pId) pId: #endif diff --git a/test/utils/Test/Hls/Flags.hs b/test/utils/Test/Hls/Flags.hs index 7ff17af076..8e60ebb93e 100644 --- a/test/utils/Test/Hls/Flags.hs +++ b/test/utils/Test/Hls/Flags.hs @@ -10,7 +10,7 @@ import Test.Hls (TestTree, ignoreTestBecause) -- | Disable test unless the eval flag is set requiresEvalPlugin :: TestTree -> TestTree -#if eval +#if hls_eval requiresEvalPlugin = id #else requiresEvalPlugin = ignoreTestBecause "Eval plugin disabled" @@ -19,7 +19,7 @@ requiresEvalPlugin = ignoreTestBecause "Eval plugin disabled" -- * Formatters -- | Disable test unless the floskell flag is set requiresFloskellPlugin :: TestTree -> TestTree -#if floskell +#if hls_floskell requiresFloskellPlugin = id #else requiresFloskellPlugin = ignoreTestBecause "Floskell plugin disabled" @@ -27,7 +27,7 @@ requiresFloskellPlugin = ignoreTestBecause "Floskell plugin disabled" -- | Disable test unless the fourmolu flag is set requiresFourmoluPlugin :: TestTree -> TestTree -#if fourmolu +#if hls_fourmolu requiresFourmoluPlugin = id #else requiresFourmoluPlugin = ignoreTestBecause "Fourmolu plugin disabled" @@ -35,7 +35,7 @@ requiresFourmoluPlugin = ignoreTestBecause "Fourmolu plugin disabled" -- | Disable test unless the ormolu flag is set requiresOrmoluPlugin :: TestTree -> TestTree -#if ormolu +#if hls_ormolu requiresOrmoluPlugin = id #else requiresOrmoluPlugin = ignoreTestBecause "Ormolu plugin disabled"