Skip to content

Commit cdc8f78

Browse files
smattingpepeiborra
andauthored
Turn HLS-wrapper into an LSP Server (#2960)
* Make wrapper a LSP on failure * Fix incorrect imports * revert import block for smaller diff * add missing imports * Fix: callProcess on win32 machines not called * import container only on win32 * add missing liftIO Co-authored-by: Pepe Iborra <[email protected]>
1 parent efcb8e2 commit cdc8f78

File tree

5 files changed

+385
-200
lines changed

5 files changed

+385
-200
lines changed

exe/Wrapper.hs

Lines changed: 176 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
{-# LANGUAGE RecordWildCards #-}
2-
{-# LANGUAGE NamedFieldPuns #-}
3-
{-# LANGUAGE CPP #-}
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE DataKinds #-}
3+
{-# LANGUAGE ExplicitNamespaces #-}
4+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+
{-# LANGUAGE LambdaCase #-}
6+
{-# LANGUAGE NamedFieldPuns #-}
7+
{-# LANGUAGE OverloadedStrings #-}
8+
{-# LANGUAGE RecordWildCards #-}
9+
{-# LANGUAGE ScopedTypeVariables #-}
410
-- | This module is based on the hie-wrapper.sh script in
511
-- https://github.com/alanz/vscode-hie-server
612
module Main where
@@ -28,6 +34,28 @@ import qualified Data.Map.Strict as Map
2834
#else
2935
import System.Process
3036
#endif
37+
import qualified Data.Text.IO as T
38+
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
39+
import qualified Data.Text as T
40+
import Language.LSP.Server (LspM)
41+
import Control.Monad.IO.Class (MonadIO (liftIO))
42+
import Control.Monad.IO.Unlift (MonadUnliftIO)
43+
import qualified Language.LSP.Server as LSP
44+
import qualified Development.IDE.Main as Main
45+
import Ide.Plugin.Config (Config)
46+
import Language.LSP.Types (RequestMessage, ResponseError, MessageActionItem (MessageActionItem), Method(Initialize), MessageType (MtError), SMethod (SWindowShowMessageRequest, SExit), ShowMessageRequestParams (ShowMessageRequestParams))
47+
import Development.IDE.Types.Logger ( makeDefaultStderrRecorder,
48+
cmapWithPrio,
49+
Pretty(pretty),
50+
Logger(Logger),
51+
Priority(Error, Debug, Info, Warning),
52+
Recorder(logger_),
53+
WithPriority(WithPriority) )
54+
import Data.Maybe
55+
import GHC.Stack.Types (emptyCallStack)
56+
import Control.Concurrent (tryPutMVar)
57+
import Development.IDE.LSP.LanguageServer (runLanguageServer)
58+
import HIE.Bios.Internal.Log
3159

3260
-- ---------------------------------------------------------------------
3361

@@ -57,9 +85,15 @@ main = do
5785
cradle <- findProjectCradle' False
5886
(CradleSuccess libdir) <- HieBios.getRuntimeGhcLibDir cradle
5987
putStr libdir
60-
_ -> launchHaskellLanguageServer args
88+
_ -> launchHaskellLanguageServer args >>= \case
89+
Right () -> pure ()
90+
Left err -> do
91+
T.hPutStrLn stderr (prettyError err NoShorten)
92+
case args of
93+
Ghcide _ -> launchErrorLSP (prettyError err Shorten)
94+
_ -> pure ()
6195

62-
launchHaskellLanguageServer :: Arguments -> IO ()
96+
launchHaskellLanguageServer :: Arguments -> IO (Either WrapperSetupError ())
6397
launchHaskellLanguageServer parsedArgs = do
6498
case parsedArgs of
6599
Ghcide GhcideArguments{..} -> whenJust argsCwd setCurrentDirectory
@@ -75,7 +109,10 @@ launchHaskellLanguageServer parsedArgs = do
75109

76110
case parsedArgs of
77111
Ghcide GhcideArguments{..} ->
78-
when argsProjectGhcVersion $ getRuntimeGhcVersion' cradle >>= putStrLn >> exitSuccess
112+
when argsProjectGhcVersion $ do
113+
runExceptT (getRuntimeGhcVersion' cradle) >>= \case
114+
Right ghcVersion -> putStrLn ghcVersion >> exitSuccess
115+
Left err -> T.putStrLn (prettyError err NoShorten) >> exitFailure
79116
_ -> pure ()
80117

81118
progName <- getProgName
@@ -94,64 +131,74 @@ launchHaskellLanguageServer parsedArgs = do
94131
hPutStrLn stderr ""
95132
-- Get the ghc version -- this might fail!
96133
hPutStrLn stderr "Consulting the cradle to get project GHC version..."
97-
ghcVersion <- getRuntimeGhcVersion' cradle
98-
hPutStrLn stderr $ "Project GHC version: " ++ ghcVersion
99134

100-
let
101-
hlsBin = "haskell-language-server-" ++ ghcVersion
102-
candidates' = [hlsBin, "haskell-language-server"]
103-
candidates = map (++ exeExtension) candidates'
135+
runExceptT $ do
136+
ghcVersion <- getRuntimeGhcVersion' cradle
137+
liftIO $ hPutStrLn stderr $ "Project GHC version: " ++ ghcVersion
104138

105-
hPutStrLn stderr $ "haskell-language-server exe candidates: " ++ show candidates
139+
let
140+
hlsBin = "haskell-language-server-" ++ ghcVersion
141+
candidates' = [hlsBin, "haskell-language-server"]
142+
candidates = map (++ exeExtension) candidates'
106143

107-
mexes <- traverse findExecutable candidates
144+
liftIO $ hPutStrLn stderr $ "haskell-language-server exe candidates: " ++ show candidates
145+
146+
mexes <- liftIO $ traverse findExecutable candidates
147+
148+
case asum mexes of
149+
Nothing -> throwE (NoLanguageServer ghcVersion candidates)
150+
Just e -> do
151+
liftIO $ hPutStrLn stderr $ "Launching haskell-language-server exe at:" ++ e
108152

109-
case asum mexes of
110-
Nothing -> die $ "Cannot find any haskell-language-server exe, looked for: " ++ intercalate ", " candidates
111-
Just e -> do
112-
hPutStrLn stderr $ "Launching haskell-language-server exe at:" ++ e
113153
#ifdef mingw32_HOST_OS
114-
callProcess e args
154+
liftIO $ callProcess e args
115155
#else
116-
let Cradle { cradleOptsProg = CradleAction { runGhcCmd } } = cradle
117-
-- we need to be compatible with NoImplicitPrelude
118-
ghcBinary <- (fmap trim <$> runGhcCmd ["-v0", "-package-env=-", "-ignore-dot-ghci", "-e", "Control.Monad.join (Control.Monad.fmap System.IO.putStr System.Environment.getExecutablePath)"])
119-
>>= cradleResult "Failed to get project GHC executable path"
120-
libdir <- HieBios.getRuntimeGhcLibDir cradle
121-
>>= cradleResult "Failed to get project GHC libdir path"
122-
env <- Map.fromList <$> getEnvironment
123-
let newEnv = Map.insert "GHC_BIN" ghcBinary $ Map.insert "GHC_LIBDIR" libdir env
124-
executeFile e True args (Just (Map.toList newEnv))
156+
157+
let Cradle { cradleOptsProg = CradleAction { runGhcCmd } } = cradle
158+
159+
let cradleName = actionName (cradleOptsProg cradle)
160+
-- we need to be compatible with NoImplicitPrelude
161+
ghcBinary <- liftIO (fmap trim <$> runGhcCmd ["-v0", "-package-env=-", "-ignore-dot-ghci", "-e", "Control.Monad.join (Control.Monad.fmap System.IO.putStr System.Environment.getExecutablePath)"])
162+
>>= cradleResult cradleName
163+
164+
libdir <- liftIO (HieBios.getRuntimeGhcLibDir cradle)
165+
>>= cradleResult cradleName
166+
167+
env <- Map.fromList <$> liftIO getEnvironment
168+
let newEnv = Map.insert "GHC_BIN" ghcBinary $ Map.insert "GHC_LIBDIR" libdir env
169+
liftIO $ executeFile e True args (Just (Map.toList newEnv))
125170
#endif
126171

127172

128-
cradleResult :: String -> CradleLoadResult a -> IO a
129-
cradleResult _ (CradleSuccess a) = pure a
130-
cradleResult str (CradleFail e) = die $ str ++ ": " ++ show e
131-
cradleResult str CradleNone = die $ str ++ ": no cradle"
173+
174+
cradleResult :: ActionName Void -> CradleLoadResult a -> ExceptT WrapperSetupError IO a
175+
cradleResult _ (CradleSuccess ver) = pure ver
176+
cradleResult cradleName (CradleFail error) = throwE $ FailedToObtainGhcVersion cradleName error
177+
cradleResult cradleName CradleNone = throwE $ NoneCradleGhcVersion cradleName
132178

133179
-- | Version of 'getRuntimeGhcVersion' that dies if we can't get it, and also
134180
-- checks to see if the tool is missing if it is one of
135-
getRuntimeGhcVersion' :: Show a => Cradle a -> IO String
181+
getRuntimeGhcVersion' :: Cradle Void -> ExceptT WrapperSetupError IO String
136182
getRuntimeGhcVersion' cradle = do
183+
let cradleName = actionName (cradleOptsProg cradle)
137184

138185
-- See if the tool is installed
139-
case actionName (cradleOptsProg cradle) of
186+
case cradleName of
140187
Stack -> checkToolExists "stack"
141188
Cabal -> checkToolExists "cabal"
142189
Default -> checkToolExists "ghc"
143190
Direct -> checkToolExists "ghc"
144191
_ -> pure ()
145192

146-
HieBios.getRuntimeGhcVersion cradle >>= cradleResult "Failed to get project GHC version"
193+
ghcVersionRes <- liftIO $ HieBios.getRuntimeGhcVersion cradle
194+
cradleResult cradleName ghcVersionRes
195+
147196
where
148197
checkToolExists exe = do
149-
exists <- findExecutable exe
198+
exists <- liftIO $ findExecutable exe
150199
case exists of
151200
Just _ -> pure ()
152-
Nothing ->
153-
die $ "Cradle requires " ++ exe ++ " but couldn't find it" ++ "\n"
154-
++ show cradle
201+
Nothing -> throwE $ ToolRequirementMissing exe (actionName (cradleOptsProg cradle))
155202

156203
findProjectCradle :: IO (Cradle Void)
157204
findProjectCradle = findProjectCradle' True
@@ -175,3 +222,93 @@ trim :: String -> String
175222
trim s = case lines s of
176223
[] -> s
177224
ls -> dropWhileEnd isSpace $ last ls
225+
226+
data WrapperSetupError
227+
= FailedToObtainGhcVersion (ActionName Void) CradleError
228+
| NoneCradleGhcVersion (ActionName Void)
229+
| NoLanguageServer String [FilePath]
230+
| ToolRequirementMissing String (ActionName Void)
231+
deriving (Show)
232+
233+
data Shorten = Shorten | NoShorten
234+
235+
-- | Pretty error message displayable to the future.
236+
-- Extra argument 'Shorten' can be used to shorten error message.
237+
-- Reduces usefulness, but allows us to show the error message via LSP
238+
-- as LSP doesn't allow any newlines and makes it really hard to read
239+
-- the message otherwise.
240+
prettyError :: WrapperSetupError -> Shorten -> T.Text
241+
prettyError (FailedToObtainGhcVersion name crdlError) shorten =
242+
"Failed to find the GHC version of this " <> T.pack (show name) <> " project." <>
243+
case shorten of
244+
Shorten ->
245+
"\n" <> T.pack (fromMaybe "" . listToMaybe $ cradleErrorStderr crdlError)
246+
NoShorten ->
247+
"\n" <> T.pack (intercalate "\n" (cradleErrorStderr crdlError))
248+
prettyError (NoneCradleGhcVersion name) _ =
249+
"Failed to get the GHC version of this " <> T.pack (show name) <>
250+
" project because a none cradle is configured"
251+
prettyError (NoLanguageServer ghcVersion candidates) _ =
252+
"Failed to find a HLS version for GHC " <> T.pack ghcVersion <>
253+
"\nExecutable names we failed to find: " <> T.pack (intercalate "," candidates)
254+
prettyError (ToolRequirementMissing toolExe name) _ =
255+
"Failed to find executable \"" <> T.pack toolExe <> "\" in $PATH for this " <> T.pack (show name) <> " project."
256+
257+
newtype ErrorLSPM c a = ErrorLSPM { unErrorLSPM :: (LspM c) a }
258+
deriving (Functor, Applicative, Monad, MonadIO, MonadUnliftIO, LSP.MonadLsp c)
259+
260+
-- | Launches a LSP that displays an error and presents the user with a request
261+
-- to shut down the LSP.
262+
launchErrorLSP :: T.Text -> IO ()
263+
launchErrorLSP errorMsg = do
264+
recorder <- makeDefaultStderrRecorder Nothing Info
265+
266+
let logger = Logger $ \p m -> logger_ recorder (WithPriority p emptyCallStack (pretty m))
267+
268+
let defaultArguments = Main.defaultArguments (cmapWithPrio pretty recorder) logger
269+
270+
inH <- Main.argsHandleIn defaultArguments
271+
272+
outH <- Main.argsHandleOut defaultArguments
273+
274+
let onConfigurationChange cfg _ = Right cfg
275+
276+
let setup clientMsgVar = do
277+
-- Forcefully exit
278+
let exit = void $ tryPutMVar clientMsgVar ()
279+
280+
let doInitialize :: LSP.LanguageContextEnv Config -> RequestMessage Initialize -> IO (Either ResponseError (LSP.LanguageContextEnv Config, ()))
281+
doInitialize env _ = do
282+
283+
let restartTitle = "Try to restart"
284+
void $ LSP.runLspT env $ LSP.sendRequest SWindowShowMessageRequest (ShowMessageRequestParams MtError errorMsg (Just [MessageActionItem restartTitle])) $ \case
285+
Right (Just (MessageActionItem title))
286+
| title == restartTitle -> liftIO exit
287+
_ -> pure ()
288+
289+
pure (Right (env, ()))
290+
291+
let asyncHandlers = mconcat
292+
[ exitHandler exit ]
293+
294+
let interpretHandler (env, _st) = LSP.Iso (LSP.runLspT env . unErrorLSPM) liftIO
295+
pure (doInitialize, asyncHandlers, interpretHandler)
296+
297+
runLanguageServer
298+
(Main.argsLspOptions defaultArguments)
299+
inH
300+
outH
301+
(Main.argsDefaultHlsConfig defaultArguments)
302+
onConfigurationChange
303+
setup
304+
305+
exitHandler :: IO () -> LSP.Handlers (ErrorLSPM c)
306+
exitHandler exit = LSP.notificationHandler SExit $ const $ liftIO exit
307+
308+
hlsWrapperLogger :: Logger
309+
hlsWrapperLogger = Logger $ \pri txt ->
310+
case pri of
311+
Debug -> debugm (T.unpack txt)
312+
Info -> logm (T.unpack txt)
313+
Warning -> warningm (T.unpack txt)
314+
Error -> errorm (T.unpack txt)

0 commit comments

Comments
 (0)