Skip to content

Commit 3d08ccf

Browse files
author
Julien Debon
committed
Add support for external Ormolu
Related to #411
1 parent 861aba7 commit 3d08ccf

File tree

4 files changed

+171
-61
lines changed

4 files changed

+171
-61
lines changed

docs/configuration.md

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ Plugins have a generic config to control their behaviour. The schema of such con
6969
- `haskell.plugin.ghcide-type-lenses.config.mode`, default `always`: Control how type lenses are shown. One of `always`, `exported`, `diagnostics`.
7070
- `hlint`:
7171
- `haskell.plugin.hlint.config.flags`, default empty: List of flags used by hlint.
72+
- `ormolu`:
73+
- `haskell.plugin.ormolu.config.external`, default `false`: Use an external `ormolu` executable rather than the one packaged with HLS.
74+
- `fourmolu`:
75+
- `haskell.plugin.fourmolu.config.external`, default `false`: Use an external `fourmolu` executable rather than the one packaged with HLS.
7276
This reference of configuration can be outdated at any time but we can query the `haskell-server-executable` about what configuration is effectively used:
7377
- `haskell-language-server generate-default-config`: will print the json configuration with all default values. It can be used as template to modify it.
7478
- `haskell-language-server vscode-extension-schema`: will print a json schema used to setup the haskell vscode extension. But it is useful to see what range of values can an option take and a description about it.

plugins/hls-ormolu-plugin/hls-ormolu-plugin.cabal

+7
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ library
3838
, lens
3939
, lsp
4040
, mtl
41+
, process-extras >= 0.7.1
4142
, ormolu ^>=0.1.2 || ^>= 0.2 || ^>= 0.3 || ^>= 0.5 || ^>= 0.6 || ^>= 0.7
4243
, text
44+
, transformers
4345

4446
default-language: Haskell2010
4547

@@ -51,10 +53,15 @@ test-suite tests
5153
hs-source-dirs: test
5254
main-is: Main.hs
5355
ghc-options: -threaded -rtsopts -with-rtsopts=-N
56+
build-tool-depends:
57+
ormolu:ormolu
5458
build-depends:
5559
, base
60+
, aeson
61+
, containers
5662
, filepath
5763
, hls-ormolu-plugin
64+
, hls-plugin-api
5865
, hls-test-utils == 2.2.0.0
5966
, lsp-types
6067
, text

plugins/hls-ormolu-plugin/src/Ide/Plugin/Ormolu.hs

+142-50
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,203 @@
11
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE DataKinds #-}
23
{-# LANGUAGE LambdaCase #-}
4+
{-# LANGUAGE NamedFieldPuns #-}
5+
{-# LANGUAGE OverloadedLabels #-}
36
{-# LANGUAGE OverloadedStrings #-}
47
{-# LANGUAGE TypeApplications #-}
58
{-# LANGUAGE TypeOperators #-}
69
module Ide.Plugin.Ormolu
710
( descriptor
811
, provider
12+
, LogEvent
913
)
1014
where
1115

1216
import Control.Exception (Handler (..), IOException,
13-
SomeException (..), catches)
17+
SomeException (..), catches,
18+
handle)
1419
import Control.Monad.Except (ExceptT (ExceptT), runExceptT,
1520
throwError)
1621
import Control.Monad.Extra
1722
import Control.Monad.IO.Class (liftIO)
1823
import Control.Monad.Trans
24+
import Control.Monad.Trans.Except (ExceptT (..), mapExceptT,
25+
runExceptT)
1926
import Data.Functor ((<&>))
27+
import Data.List (intercalate)
28+
import Data.Maybe (catMaybes)
29+
import Data.Text (Text)
2030
import qualified Data.Text as T
2131
import Development.IDE hiding (pluginHandlers)
2232
import Development.IDE.GHC.Compat (hsc_dflags, moduleNameString)
2333
import qualified Development.IDE.GHC.Compat as D
2434
import qualified Development.IDE.GHC.Compat.Util as S
2535
import GHC.LanguageExtensions.Type
2636
import Ide.Plugin.Error (PluginError (PluginInternalError))
37+
import Ide.Plugin.Properties
2738
import Ide.PluginUtils
2839
import Ide.Types hiding (Config)
2940
import qualified Ide.Types as Types
3041
import Language.LSP.Protocol.Message
3142
import Language.LSP.Protocol.Types
3243
import Language.LSP.Server hiding (defaultConfig)
3344
import Ormolu
34-
import System.FilePath (takeFileName)
45+
import System.Exit
46+
import System.FilePath
47+
import System.Process.Run (cwd, proc)
48+
import System.Process.Text (readCreateProcessWithExitCode)
49+
import Text.Read (readMaybe)
3550

3651
-- ---------------------------------------------------------------------
3752

38-
descriptor :: Recorder (WithPriority T.Text) -> PluginId -> PluginDescriptor IdeState
39-
descriptor recorder plId = (defaultPluginDescriptor plId)
40-
{ pluginHandlers = mkFormattingHandlers $ provider recorder
41-
}
53+
descriptor :: Recorder (WithPriority LogEvent) -> PluginId -> PluginDescriptor IdeState
54+
descriptor recorder plId =
55+
(defaultPluginDescriptor plId)
56+
{ pluginHandlers = mkFormattingHandlers $ provider recorder plId,
57+
pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties}
58+
}
59+
60+
properties :: Properties '[ 'PropertyKey "external" 'TBoolean]
61+
properties =
62+
emptyProperties
63+
& defineBooleanProperty
64+
#external
65+
"Call out to an external \"ormolu\" executable, rather than using the bundled library"
66+
False
4267

4368
-- ---------------------------------------------------------------------
4469

45-
provider :: Recorder (WithPriority T.Text) -> FormattingHandler IdeState
46-
provider recorder ideState typ contents fp _ = ExceptT $ withIndefiniteProgress title Cancellable $ runExceptT $ do
47-
ghc <- liftIO $ runAction "Ormolu" ideState $ use GhcSession fp
48-
let df = hsc_dflags . hscEnv <$> ghc
49-
fileOpts <- case df of
50-
Nothing -> pure []
51-
Just df -> pure $ fromDyn df
52-
53-
logWith recorder Debug $ "Using ormolu-" <> VERSION_ormolu
54-
55-
let
56-
fullRegion = RegionIndices Nothing Nothing
57-
rangeRegion s e = RegionIndices (Just $ s + 1) (Just $ e + 1)
58-
mkConf o region = defaultConfig { cfgDynOptions = o, cfgRegion = region }
59-
fmt :: T.Text -> Config RegionIndices -> IO (Either SomeException T.Text)
60-
fmt cont conf = flip catches handlers $ do
61-
let fp' = fromNormalizedFilePath fp
70+
provider :: Recorder (WithPriority LogEvent) -> PluginId -> FormattingHandler IdeState
71+
provider recorder plId ideState typ contents fp _ = ExceptT $ withIndefiniteProgress title Cancellable $ runExceptT $ do
72+
fileOpts <-
73+
maybe [] (fromDyn . hsc_dflags . hscEnv)
74+
<$> liftIO (runAction "Ormolu" ideState $ use GhcSession fp)
75+
useCLI <- liftIO $ runAction "Ormolu" ideState $ usePropertyAction #external plId properties
76+
77+
if useCLI
78+
then mapExceptT liftIO $ ExceptT
79+
$ handle @IOException
80+
(pure . Left . PluginInternalError . T.pack . show)
81+
$ runExceptT $ cliHandler fileOpts
82+
else do
83+
logWith recorder Debug $ LogCompiledInVersion VERSION_ormolu
84+
85+
let
86+
fmt :: T.Text -> Config RegionIndices -> IO (Either SomeException T.Text)
87+
fmt cont conf = flip catches handlers $ do
6288
#if MIN_VERSION_ormolu(0,5,3)
63-
cabalInfo <- getCabalInfoForSourceFile fp' <&> \case
64-
CabalNotFound -> Nothing
65-
CabalDidNotMention cabalInfo -> Just cabalInfo
66-
CabalFound cabalInfo -> Just cabalInfo
89+
cabalInfo <- getCabalInfoForSourceFile fp' <&> \case
90+
CabalNotFound -> Nothing
91+
CabalDidNotMention cabalInfo -> Just cabalInfo
92+
CabalFound cabalInfo -> Just cabalInfo
6793
#if MIN_VERSION_ormolu(0,7,0)
68-
(fixityOverrides, moduleReexports) <- getDotOrmoluForSourceFile fp'
69-
let conf' = refineConfig ModuleSource cabalInfo (Just fixityOverrides) (Just moduleReexports) conf
94+
(fixityOverrides, moduleReexports) <- getDotOrmoluForSourceFile fp'
95+
let conf' = refineConfig ModuleSource cabalInfo (Just fixityOverrides) (Just moduleReexports) conf
7096
#else
71-
fixityOverrides <- traverse getFixityOverridesForSourceFile cabalInfo
72-
let conf' = refineConfig ModuleSource cabalInfo fixityOverrides conf
97+
fixityOverrides <- traverse getFixityOverridesForSourceFile cabalInfo
98+
let conf' = refineConfig ModuleSource cabalInfo fixityOverrides conf
7399
#endif
74-
let cont' = cont
100+
let cont' = cont
75101
#else
76-
let conf' = conf
77-
cont' = T.unpack cont
102+
let conf' = conf
103+
cont' = T.unpack cont
78104
#endif
79-
Right <$> ormolu conf' fp' cont'
80-
handlers =
81-
[ Handler $ pure . Left . SomeException @OrmoluException
82-
, Handler $ pure . Left . SomeException @IOException
83-
]
84-
85-
case typ of
86-
FormatText -> do
87-
res <- liftIO $ fmt contents (mkConf fileOpts fullRegion)
88-
ret res
89-
FormatRange (Range (Position sl _) (Position el _)) -> do
90-
res <- liftIO $ fmt contents (mkConf fileOpts (rangeRegion (fromIntegral sl) (fromIntegral el)))
91-
ret res
105+
Right <$> ormolu conf' fp' cont'
106+
handlers =
107+
[ Handler $ pure . Left . SomeException @OrmoluException
108+
, Handler $ pure . Left . SomeException @IOException
109+
]
110+
111+
res <- liftIO $ fmt contents defaultConfig { cfgDynOptions = map DynOption fileOpts, cfgRegion = region }
112+
ret res
92113
where
114+
fp' = fromNormalizedFilePath fp
115+
116+
region :: RegionIndices
117+
region = case typ of
118+
FormatText ->
119+
RegionIndices Nothing Nothing
120+
FormatRange (Range (Position sl _) (Position el _)) ->
121+
RegionIndices (Just $ fromIntegral $ sl + 1) (Just $ fromIntegral $ el + 1)
122+
93123
title = T.pack $ "Formatting " <> takeFileName (fromNormalizedFilePath fp)
94124

95125
ret :: Either SomeException T.Text -> ExceptT PluginError (LspM Types.Config) ([TextEdit] |? Null)
96126
ret (Left err) = throwError $ PluginInternalError . T.pack $ "ormoluCmd: " ++ show err
97127
ret (Right new) = pure $ InL $ makeDiffTextEdit contents new
98128

99-
fromDyn :: D.DynFlags -> [DynOption]
129+
fromDyn :: D.DynFlags -> [String]
100130
fromDyn df =
101131
let
102132
pp =
103133
let p = D.sPgm_F $ D.settings df
104134
in ["-pgmF=" <> p | not (null p)]
105135
pm = ("-fplugin=" <>) . moduleNameString <$> D.pluginModNames df
106136
ex = showExtension <$> S.toList (D.extensionFlags df)
107-
in
108-
DynOption <$> pp <> pm <> ex
137+
in pp <> pm <> ex
138+
139+
cliHandler :: [String] -> ExceptT PluginError IO ([TextEdit] |? Null)
140+
cliHandler fileOpts = do
141+
CLIVersionInfo{noCabal} <- do -- check Ormolu version so that we know which flags to use
142+
(exitCode, out, _err) <- liftIO $ readCreateProcessWithExitCode ( proc "ormolu" ["--version"] ) ""
143+
let version = do
144+
guard $ exitCode == ExitSuccess
145+
"ormolu" : v : _ <- pure $ T.words out
146+
traverse (readMaybe @Int . T.unpack) $ T.splitOn "." v
147+
case version of
148+
Just v -> do
149+
logWith recorder Debug $ LogExternalVersion v
150+
pure CLIVersionInfo
151+
{ noCabal = v >= [0, 7]
152+
}
153+
Nothing -> do
154+
logWith recorder Debug $ LogExternalVersion []
155+
logWith recorder Warning $ NoVersion out
156+
pure CLIVersionInfo
157+
{ noCabal = True
158+
}
159+
(exitCode, out, err) <- do -- run Ormolu
160+
let commandArgs = map ("-o" <>) fileOpts
161+
-- "The --stdin-input-file option is necessary when using input from
162+
-- stdin and accounting for .cabal files" as per Ormolu documentation
163+
<> (if noCabal then ["--no-cabal"] else ["--stdin-input-file", fp'])
164+
<> catMaybes
165+
[ ("--start-line=" <>) . show <$> regionStartLine region
166+
, ("--end-line=" <>) . show <$> regionEndLine region
167+
]
168+
cwd = takeDirectory fp'
169+
logWith recorder Debug $ LogOrmoluCommand commandArgs cwd
170+
liftIO $ readCreateProcessWithExitCode (proc "ormolu" commandArgs) {cwd = Just cwd} contents
171+
case exitCode of
172+
ExitSuccess -> do
173+
when (not $ T.null err) $ logWith recorder Debug $ StdErr err
174+
pure $ InL $ makeDiffTextEdit contents out
175+
ExitFailure n -> do
176+
logWith recorder Info $ StdErr err
177+
throwError $ PluginInternalError $ "Ormolu failed with exit code " <> T.pack (show n)
178+
179+
newtype CLIVersionInfo = CLIVersionInfo
180+
{ noCabal :: Bool
181+
}
182+
183+
data LogEvent
184+
= NoVersion Text
185+
| StdErr Text
186+
| LogCompiledInVersion String
187+
| LogExternalVersion [Int]
188+
| LogOrmoluCommand [String] FilePath
189+
deriving (Show)
190+
191+
instance Pretty LogEvent where
192+
pretty = \case
193+
NoVersion t -> "Couldn't get Ormolu version:" <> line <> indent 2 (pretty t)
194+
StdErr t -> "Ormolu stderr:" <> line <> indent 2 (pretty t)
195+
LogCompiledInVersion v -> "Using compiled in ormolu-" <> pretty v
196+
LogExternalVersion v ->
197+
"Using external ormolu"
198+
<> if null v then "" else "-"
199+
<> pretty (intercalate "." $ map show v)
200+
LogOrmoluCommand commandArgs cwd -> "Running: `ormolu " <> pretty (unwords commandArgs) <> "` in directory " <> pretty cwd
109201

110202
showExtension :: Extension -> String
111203
showExtension Cpp = "-XCPP"

plugins/hls-ormolu-plugin/test/Main.hs

+18-11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ module Main
44
( main
55
) where
66

7+
import Data.Aeson
8+
import Data.Functor
79
import qualified Data.Text as T
10+
import Ide.Plugin.Config
811
import qualified Ide.Plugin.Ormolu as Ormolu
912
import Language.LSP.Protocol.Types
1013
import System.FilePath
@@ -13,23 +16,27 @@ import Test.Hls
1316
main :: IO ()
1417
main = defaultTestRunner tests
1518

16-
ormoluPlugin :: PluginTestDescriptor T.Text
19+
ormoluPlugin :: PluginTestDescriptor Ormolu.LogEvent
1720
ormoluPlugin = mkPluginTestDescriptor Ormolu.descriptor "ormolu"
1821

1922
tests :: TestTree
20-
tests = testGroup "ormolu"
21-
[ goldenWithOrmolu "formats correctly" "Ormolu" "formatted" $ \doc -> do
22-
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
23-
, goldenWithOrmolu "formats imports correctly" "Ormolu2" "formatted" $ \doc -> do
24-
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
23+
tests = testGroup "ormolu" $
24+
[False, True] <&> \cli ->
25+
testGroup (if cli then "cli" else "lib")
26+
[ goldenWithOrmolu cli "formats correctly" "Ormolu" "formatted" $ \doc -> do
27+
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
28+
, goldenWithOrmolu cli "formats imports correctly" "Ormolu2" "formatted" $ \doc -> do
29+
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
2530
#if MIN_VERSION_ormolu(0,5,3)
26-
, goldenWithOrmolu "formats operators correctly" "Ormolu3" "formatted" $ \doc -> do
27-
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
31+
, goldenWithOrmolu cli "formats operators correctly" "Ormolu3" "formatted" $ \doc -> do
32+
formatDoc doc (FormattingOptions 4 True Nothing Nothing Nothing)
2833
#endif
29-
]
34+
]
3035

31-
goldenWithOrmolu :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
32-
goldenWithOrmolu title path desc = goldenWithHaskellDocFormatter def ormoluPlugin "ormolu" def title testDataDir path desc "hs"
36+
goldenWithOrmolu :: Bool -> TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
37+
goldenWithOrmolu cli title path desc = goldenWithHaskellDocFormatter def ormoluPlugin "ormolu" def title testDataDir path desc "hs"
38+
where
39+
conf = def{plcConfig = (\(Object obj) -> obj) $ object ["external" .= cli]}
3340

3441
testDataDir :: FilePath
3542
testDataDir = "test" </> "testdata"

0 commit comments

Comments
 (0)