Skip to content

Commit fd34887

Browse files
authored
Merge pull request #2763 from haskell/fourmolu-cli
Add an option to run Fourmolu via the CLI interface of a separate binary, rather than the bundled library
2 parents a9ae9d1 + 510e180 commit fd34887

File tree

8 files changed

+123
-67
lines changed

8 files changed

+123
-67
lines changed

Diff for: hls-test-utils/src/Test/Hls.hs

+11-6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import qualified Data.Aeson as A
4848
import Data.ByteString.Lazy (ByteString)
4949
import Data.Default (def)
5050
import Data.Maybe (fromMaybe)
51+
import qualified Data.Map as M
5152
import qualified Data.Text as T
5253
import qualified Data.Text.Lazy as TL
5354
import qualified Data.Text.Lazy.Encoding as TL
@@ -68,7 +69,7 @@ import Development.IDE.Types.Logger (Logger (Logger),
6869
import Development.IDE.Types.Options
6970
import GHC.IO.Handle
7071
import GHC.Stack (emptyCallStack)
71-
import Ide.Plugin.Config (Config, formattingProvider)
72+
import Ide.Plugin.Config (Config, formattingProvider, PluginConfig, plugins)
7273
import Ide.PluginUtils (idePluginsToPluginDesc,
7374
pluginDescToIdePlugins)
7475
import Ide.Types
@@ -131,16 +132,17 @@ goldenWithHaskellDoc plugin title testDataDir path desc ext act =
131132
goldenWithHaskellDocFormatter
132133
:: PluginDescriptor IdeState
133134
-> String
135+
-> PluginConfig
134136
-> TestName
135137
-> FilePath
136138
-> FilePath
137139
-> FilePath
138140
-> FilePath
139141
-> (TextDocumentIdentifier -> Session ())
140142
-> TestTree
141-
goldenWithHaskellDocFormatter plugin formatter title testDataDir path desc ext act =
143+
goldenWithHaskellDocFormatter plugin formatter conf title testDataDir path desc ext act =
142144
goldenGitDiff title (testDataDir </> path <.> desc <.> ext)
143-
$ runSessionWithServerFormatter plugin formatter testDataDir
145+
$ runSessionWithServerFormatter plugin formatter conf testDataDir
144146
$ TL.encodeUtf8 . TL.fromStrict
145147
<$> do
146148
doc <- openDoc (path <.> ext) "haskell"
@@ -151,11 +153,14 @@ goldenWithHaskellDocFormatter plugin formatter title testDataDir path desc ext a
151153
runSessionWithServer :: PluginDescriptor IdeState -> FilePath -> Session a -> IO a
152154
runSessionWithServer plugin = runSessionWithServer' [plugin] def def fullCaps
153155

154-
runSessionWithServerFormatter :: PluginDescriptor IdeState -> String -> FilePath -> Session a -> IO a
155-
runSessionWithServerFormatter plugin formatter =
156+
runSessionWithServerFormatter :: PluginDescriptor IdeState -> String -> PluginConfig -> FilePath -> Session a -> IO a
157+
runSessionWithServerFormatter plugin formatter conf =
156158
runSessionWithServer'
157159
[plugin]
158-
def {formattingProvider = T.pack formatter}
160+
def
161+
{ formattingProvider = T.pack formatter
162+
, plugins = M.singleton (T.pack formatter) conf
163+
}
159164
def
160165
fullCaps
161166

Diff for: plugins/hls-brittany-plugin/test/Main.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ tests = testGroup "brittany"
3131
]
3232

3333
brittanyGolden :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
34-
brittanyGolden title path desc = goldenWithHaskellDocFormatter brittanyPlugin "brittany" title testDataDir path desc "hs"
34+
brittanyGolden title path desc = goldenWithHaskellDocFormatter brittanyPlugin "brittany" def title testDataDir path desc "hs"
3535

3636
testDataDir :: FilePath
3737
testDataDir = "test" </> "testdata"

Diff for: plugins/hls-floskell-plugin/test/Main.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ tests = testGroup "floskell"
2424
]
2525

2626
goldenWithFloskell :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
27-
goldenWithFloskell title path desc = goldenWithHaskellDocFormatter floskellPlugin "floskell" title testDataDir path desc "hs"
27+
goldenWithFloskell title path desc = goldenWithHaskellDocFormatter floskellPlugin "floskell" def title testDataDir path desc "hs"
2828

2929
testDataDir :: FilePath
3030
testDataDir = "test" </> "testdata"

Diff for: plugins/hls-fourmolu-plugin/hls-fourmolu-plugin.cabal

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ library
3030
, hls-plugin-api ^>=1.3
3131
, lens
3232
, lsp
33+
, process-extras
3334
, text
3435

3536
default-language: Haskell2010
@@ -40,9 +41,14 @@ test-suite tests
4041
hs-source-dirs: test
4142
main-is: Main.hs
4243
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
44+
build-tool-depends:
45+
fourmolu:fourmolu
4346
build-depends:
4447
, base
48+
, aeson
49+
, containers
4550
, filepath
4651
, hls-fourmolu-plugin
52+
, hls-plugin-api
4753
, hls-test-utils ^>=1.2
4854
, lsp-test

Diff for: plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs

+85-49
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,127 @@
22
{-# LANGUAGE LambdaCase #-}
33
{-# LANGUAGE OverloadedStrings #-}
44
{-# LANGUAGE TypeApplications #-}
5+
{-# LANGUAGE DataKinds #-}
6+
{-# LANGUAGE OverloadedLabels #-}
57

68
module Ide.Plugin.Fourmolu (
79
descriptor,
810
provider,
911
) where
1012

11-
import Control.Exception (try)
13+
import Control.Exception (IOException, try)
1214
import Control.Lens ((^.))
15+
import Control.Monad
1316
import Control.Monad.IO.Class
1417
import Data.Bifunctor (first)
18+
import Data.Maybe
1519
import qualified Data.Text as T
20+
import qualified Data.Text.IO as T
1621
import Development.IDE hiding (pluginHandlers)
1722
import Development.IDE.GHC.Compat as Compat hiding (Cpp)
1823
import qualified Development.IDE.GHC.Compat.Util as S
1924
import GHC.LanguageExtensions.Type (Extension (Cpp))
20-
import Ide.PluginUtils (makeDiffTextEdit)
25+
import Ide.Plugin.Properties
26+
import Ide.PluginUtils (makeDiffTextEdit, usePropertyLsp)
2127
import Ide.Types
2228
import Language.LSP.Server hiding (defaultConfig)
2329
import Language.LSP.Types
2430
import Language.LSP.Types.Lens (HasTabSize (tabSize))
2531
import Ormolu
32+
import System.Exit
2633
import System.FilePath
27-
28-
-- ---------------------------------------------------------------------
34+
import System.IO (stderr)
35+
import System.Process.Run (proc, cwd)
36+
import System.Process.Text (readCreateProcessWithExitCode)
2937

3038
descriptor :: PluginId -> PluginDescriptor IdeState
3139
descriptor plId =
3240
(defaultPluginDescriptor plId)
33-
{ pluginHandlers = mkFormattingHandlers provider
41+
{ pluginHandlers = mkFormattingHandlers $ provider plId
3442
}
3543

36-
-- ---------------------------------------------------------------------
37-
38-
provider :: FormattingHandler IdeState
39-
provider ideState typ contents fp fo = withIndefiniteProgress title Cancellable $ do
40-
ghc <- liftIO $ runAction "Fourmolu" ideState $ use GhcSession fp
41-
fileOpts <- case hsc_dflags . hscEnv <$> ghc of
42-
Nothing -> return []
43-
Just df -> liftIO $ convertDynFlags df
44-
45-
let format printerOpts =
46-
first (responseError . ("Fourmolu: " <>) . T.pack . show)
47-
<$> try @OrmoluException (makeDiffTextEdit contents <$> ormolu config fp' (T.unpack contents))
48-
where
49-
config =
50-
defaultConfig
51-
{ cfgDynOptions = fileOpts
52-
, cfgRegion = region
53-
, cfgDebug = True
54-
, cfgPrinterOpts =
55-
fillMissingPrinterOpts
56-
(printerOpts <> lspPrinterOpts)
57-
defaultPrinterOpts
58-
}
44+
properties :: Properties '[ 'PropertyKey "external" 'TBoolean]
45+
properties =
46+
emptyProperties
47+
& defineBooleanProperty
48+
#external
49+
"Call out to an external \"fourmolu\" executable, rather than using the bundled library"
50+
False
5951

60-
liftIO (loadConfigFile fp') >>= \case
61-
ConfigLoaded file opts -> liftIO $ do
62-
putStrLn $ "Loaded Fourmolu config from: " <> file
63-
format opts
64-
ConfigNotFound searchDirs -> liftIO $ do
65-
putStrLn
66-
. unlines
67-
$ ("No " ++ show configFileName ++ " found in any of:") :
68-
map (" " ++) searchDirs
69-
format mempty
70-
ConfigParseError f (_, err) -> do
71-
sendNotification SWindowShowMessage $
72-
ShowMessageParams
73-
{ _xtype = MtError
74-
, _message = errorMessage
75-
}
76-
return . Left $ responseError errorMessage
77-
where
78-
errorMessage = "Failed to load " <> T.pack f <> ": " <> T.pack err
52+
provider :: PluginId -> FormattingHandler IdeState
53+
provider plId ideState typ contents fp fo = withIndefiniteProgress title Cancellable $ do
54+
fileOpts <-
55+
maybe [] (convertDynFlags . hsc_dflags . hscEnv)
56+
<$> liftIO (runAction "Fourmolu" ideState $ use GhcSession fp)
57+
useCLI <- usePropertyLsp #external plId properties
58+
if useCLI
59+
then liftIO
60+
. fmap (join . first (mkError . show))
61+
. try @IOException
62+
$ do
63+
(exitCode, out, err) <-
64+
readCreateProcessWithExitCode
65+
( proc "fourmolu" $
66+
["-d"]
67+
<> catMaybes
68+
[ ("--start-line=" <>) . show <$> regionStartLine region
69+
, ("--end-line=" <>) . show <$> regionEndLine region
70+
]
71+
<> map ("-o" <>) fileOpts
72+
){cwd = Just $ takeDirectory fp'}
73+
contents
74+
T.hPutStrLn stderr err
75+
case exitCode of
76+
ExitSuccess ->
77+
pure . Right $ makeDiffTextEdit contents out
78+
ExitFailure n ->
79+
pure . Left . responseError $ "Fourmolu failed with exit code " <> T.pack (show n)
80+
else do
81+
let format printerOpts =
82+
first (mkError . show)
83+
<$> try @OrmoluException (makeDiffTextEdit contents <$> ormolu config fp' (T.unpack contents))
84+
where
85+
config =
86+
defaultConfig
87+
{ cfgDynOptions = map DynOption fileOpts
88+
, cfgRegion = region
89+
, cfgDebug = True
90+
, cfgPrinterOpts =
91+
fillMissingPrinterOpts
92+
(printerOpts <> lspPrinterOpts)
93+
defaultPrinterOpts
94+
}
95+
in liftIO (loadConfigFile fp') >>= \case
96+
ConfigLoaded file opts -> liftIO $ do
97+
putStrLn $ "Loaded Fourmolu config from: " <> file
98+
format opts
99+
ConfigNotFound searchDirs -> liftIO $ do
100+
putStrLn
101+
. unlines
102+
$ ("No " ++ show configFileName ++ " found in any of:") :
103+
map (" " ++) searchDirs
104+
format mempty
105+
ConfigParseError f (_, err) -> do
106+
sendNotification SWindowShowMessage $
107+
ShowMessageParams
108+
{ _xtype = MtError
109+
, _message = errorMessage
110+
}
111+
return . Left $ responseError errorMessage
112+
where
113+
errorMessage = "Failed to load " <> T.pack f <> ": " <> T.pack err
79114
where
80115
fp' = fromNormalizedFilePath fp
81116
title = "Formatting " <> T.pack (takeFileName fp')
117+
mkError = responseError . ("Fourmolu: " <>) . T.pack
82118
lspPrinterOpts = mempty{poIndentation = Just $ fromIntegral $ fo ^. tabSize}
83119
region = case typ of
84120
FormatText ->
85121
RegionIndices Nothing Nothing
86122
FormatRange (Range (Position sl _) (Position el _)) ->
87123
RegionIndices (Just $ fromIntegral $ sl + 1) (Just $ fromIntegral $ el + 1)
88124

89-
convertDynFlags :: DynFlags -> IO [DynOption]
125+
convertDynFlags :: DynFlags -> [String]
90126
convertDynFlags df =
91127
let pp = ["-pgmF=" <> p | not (null p)]
92128
p = sPgm_F $ Compat.settings df
@@ -95,4 +131,4 @@ convertDynFlags df =
95131
showExtension = \case
96132
Cpp -> "-XCPP"
97133
x -> "-X" ++ show x
98-
in return $ map DynOption $ pp <> pm <> ex
134+
in pp <> pm <> ex

Diff for: plugins/hls-fourmolu-plugin/test/Main.hs

+17-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ module Main
33
( main
44
) where
55

6+
import Data.Aeson
7+
import Data.Functor
8+
import Ide.Plugin.Config
69
import qualified Ide.Plugin.Fourmolu as Fourmolu
710
import Language.LSP.Test
811
import Language.LSP.Types
@@ -16,15 +19,21 @@ fourmoluPlugin :: PluginDescriptor IdeState
1619
fourmoluPlugin = Fourmolu.descriptor "fourmolu"
1720

1821
tests :: TestTree
19-
tests = testGroup "fourmolu"
20-
[ goldenWithFourmolu "formats correctly" "Fourmolu" "formatted" $ \doc -> do
21-
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
22-
, goldenWithFourmolu "formats imports correctly" "Fourmolu" "formatted" $ \doc -> do
23-
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
24-
]
22+
tests =
23+
testGroup "fourmolu" $
24+
[False, True] <&> \cli ->
25+
testGroup
26+
(if cli then "cli" else "lib")
27+
[ goldenWithFourmolu cli "formats correctly" "Fourmolu" "formatted" $ \doc -> do
28+
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
29+
, goldenWithFourmolu cli "formats imports correctly" "Fourmolu" "formatted" $ \doc -> do
30+
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
31+
]
2532

26-
goldenWithFourmolu :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
27-
goldenWithFourmolu title path desc = goldenWithHaskellDocFormatter fourmoluPlugin "fourmolu" title testDataDir path desc "hs"
33+
goldenWithFourmolu :: Bool -> TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
34+
goldenWithFourmolu cli title path desc = goldenWithHaskellDocFormatter fourmoluPlugin "fourmolu" conf title testDataDir path desc "hs"
35+
where
36+
conf = def{plcConfig = (\(Object obj) -> obj) $ object ["external" .= cli]}
2837

2938
testDataDir :: FilePath
3039
testDataDir = "test" </> "testdata"

Diff for: plugins/hls-ormolu-plugin/test/Main.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ tests = testGroup "ormolu"
2323
]
2424

2525
goldenWithOrmolu :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
26-
goldenWithOrmolu title path desc = goldenWithHaskellDocFormatter ormoluPlugin "ormolu" title testDataDir path desc "hs"
26+
goldenWithOrmolu title path desc = goldenWithHaskellDocFormatter ormoluPlugin "ormolu" def title testDataDir path desc "hs"
2727

2828
testDataDir :: FilePath
2929
testDataDir = "test" </> "testdata"

Diff for: plugins/hls-stylish-haskell-plugin/test/Main.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ tests = testGroup "stylish-haskell"
2222
]
2323

2424
goldenWithStylishHaskell :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
25-
goldenWithStylishHaskell title fp desc = goldenWithHaskellDocFormatter stylishHaskellPlugin "stylishHaskell" title testDataDir fp desc "hs"
25+
goldenWithStylishHaskell title fp desc = goldenWithHaskellDocFormatter stylishHaskellPlugin "stylishHaskell" def title testDataDir fp desc "hs"
2626

2727
testDataDir :: FilePath
2828
testDataDir = "test" </> "testdata"

0 commit comments

Comments
 (0)