Skip to content

Commit 657f99a

Browse files
author
Julien Debon
committed
Add support for external Ormolu
Related to haskell#411
1 parent 3ffde0d commit 657f99a

File tree

4 files changed

+170
-59
lines changed

4 files changed

+170
-59
lines changed

docs/configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ Plugins have a generic config to control their behaviour. The schema of such con
7575
- `haskell.plugin.ghcide-type-lenses.config.mode`, default `always`: Control how type lenses are shown. One of `always`, `exported`, `diagnostics`.
7676
- `hlint`:
7777
- `haskell.plugin.hlint.config.flags`, default empty: List of flags used by hlint.
78+
- `ormolu`:
79+
- `haskell.plugin.ormolu.config.external`, default `false`: Use an external `ormolu` executable rather than the one packaged with HLS.
80+
- `fourmolu`:
81+
- `haskell.plugin.fourmolu.config.external`, default `false`: Use an external `fourmolu` executable rather than the one packaged with HLS.
7882
This reference of configuration can be outdated at any time but we can query the `haskell-server-executable` about what configuration is effectively used:
7983
- `haskell-language-server generate-default-config`: will print the json configuration with all default values. It can be used as template to modify it.
8084
- `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

Lines changed: 7 additions & 0 deletions
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.1.0.0
5966
, lsp-types
6067
, text
Lines changed: 141 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,205 @@
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
53+
descriptor :: Recorder (WithPriority LogEvent) -> PluginId -> PluginDescriptor IdeState
3954
descriptor recorder plId = (defaultPluginDescriptor plId)
40-
{ pluginHandlers = mkFormattingHandlers $ provider recorder
55+
{ pluginHandlers = mkFormattingHandlers $ provider recorder plId
56+
, pluginConfigDescriptor = defaultConfigDescriptor{configCustomConfig = mkCustomConfig properties}
4157
}
4258

59+
properties :: Properties '[ 'PropertyKey "external" 'TBoolean]
60+
properties =
61+
emptyProperties
62+
& defineBooleanProperty
63+
#external
64+
"Call out to an external \"ormolu\" executable, rather than using the bundled library"
65+
False
66+
4367
-- ---------------------------------------------------------------------
4468

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
69+
provider :: Recorder (WithPriority LogEvent) -> PluginId -> FormattingHandler IdeState
70+
provider recorder plId ideState typ contents fp _ = ExceptT $ withIndefiniteProgress title Cancellable $ runExceptT $ do
71+
fileOpts <-
72+
maybe [] (fromDyn . hsc_dflags . hscEnv)
73+
<$> liftIO (runAction "Ormolu" ideState $ use GhcSession fp)
74+
useCLI <- liftIO $ runAction "Ormolu" ideState $ usePropertyAction #external plId properties
75+
76+
if useCLI
77+
then mapExceptT liftIO $ ExceptT
78+
$ handle @IOException
79+
(pure . Left . PluginInternalError . T.pack . show)
80+
$ runExceptT $ cliHandler fileOpts
81+
else do
82+
logWith recorder Debug $ LogCompiledInVersion VERSION_ormolu
83+
84+
let
85+
fmt :: T.Text -> Config RegionIndices -> IO (Either SomeException T.Text)
86+
fmt cont conf = flip catches handlers $ do
6287
#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
88+
cabalInfo <- getCabalInfoForSourceFile fp' <&> \case
89+
CabalNotFound -> Nothing
90+
CabalDidNotMention cabalInfo -> Just cabalInfo
91+
CabalFound cabalInfo -> Just cabalInfo
6792
#if MIN_VERSION_ormolu(0,7,0)
68-
(fixityOverrides, moduleReexports) <- getDotOrmoluForSourceFile fp'
69-
let conf' = refineConfig ModuleSource cabalInfo (Just fixityOverrides) (Just moduleReexports) conf
93+
(fixityOverrides, moduleReexports) <- getDotOrmoluForSourceFile fp'
94+
let conf' = refineConfig ModuleSource cabalInfo (Just fixityOverrides) (Just moduleReexports) conf
7095
#else
71-
fixityOverrides <- traverse getFixityOverridesForSourceFile cabalInfo
72-
let conf' = refineConfig ModuleSource cabalInfo fixityOverrides conf
96+
fixityOverrides <- traverse getFixityOverridesForSourceFile cabalInfo
97+
let conf' = refineConfig ModuleSource cabalInfo fixityOverrides conf
7398
#endif
74-
let cont' = cont
99+
let cont' = cont
75100
#else
76-
let conf' = conf
77-
cont' = T.unpack cont
101+
let conf' = conf
102+
cont' = T.unpack cont
78103
#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
104+
Right <$> ormolu conf' fp' cont'
105+
handlers =
106+
[ Handler $ pure . Left . SomeException @OrmoluException
107+
, Handler $ pure . Left . SomeException @IOException
108+
]
109+
110+
res <- liftIO $ fmt contents defaultConfig { cfgDynOptions = map DynOption fileOpts, cfgRegion = region }
111+
ret res
92112
where
113+
fp' = fromNormalizedFilePath fp
114+
115+
region :: RegionIndices
116+
region = case typ of
117+
FormatText ->
118+
RegionIndices Nothing Nothing
119+
FormatRange (Range (Position sl _) (Position el _)) ->
120+
RegionIndices (Just $ fromIntegral $ sl + 1) (Just $ fromIntegral $ el + 1)
121+
93122
title = T.pack $ "Formatting " <> takeFileName (fromNormalizedFilePath fp)
94123

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

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

110200
showExtension :: Extension -> String
111201
showExtension Cpp = "-XCPP"
112202
showExtension other = "-X" ++ show other
203+
204+
mwhen :: Monoid a => Bool -> a -> a
205+
mwhen b x = if b then x else mempty

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

Lines changed: 18 additions & 11 deletions
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 ormoluPlugin "ormolu" def title testDataDir path desc "hs"
36+
goldenWithOrmolu :: Bool -> TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
37+
goldenWithOrmolu cli title path desc = goldenWithHaskellDocFormatter 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)