Skip to content

Commit 758dc37

Browse files
committed
Allow HLS-wrapper to send LSP messages
Introduces better error handling to HLS-wrapper to show LSP clients dedicated error messages. This should help users understand why their Language Server isn't starting.
1 parent 9c2bc32 commit 758dc37

File tree

2 files changed

+132
-34
lines changed

2 files changed

+132
-34
lines changed

exe/Wrapper.hs

Lines changed: 126 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
{-# LANGUAGE RecordWildCards #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE LambdaCase #-}
4+
{-# LANGUAGE NumericUnderscores #-}
25
-- | This module is based on the hie-wrapper.sh script in
36
-- https://github.com/alanz/vscode-hie-server
47
module Main where
58

69
import Control.Monad.Extra
10+
import Control.Monad.IO.Class
11+
import Control.Monad.Trans.Except
12+
import qualified Data.Aeson as Aeson
13+
import qualified Data.ByteString.Lazy as BSL
714
import Data.Default
815
import Data.Foldable
916
import Data.List
17+
import Data.Maybe
18+
import qualified Data.Text as T
19+
import qualified Data.Text.IO as T
20+
import qualified Data.Text.Lazy as TL
21+
import qualified Data.Text.Lazy.Encoding as TL
1022
import Data.Void
1123
import qualified Development.IDE.Session as Session
1224
import qualified HIE.Bios.Environment as HieBios
1325
import HIE.Bios.Types
1426
import Ide.Arguments
1527
import Ide.Version
28+
import qualified Language.LSP.Types as J
1629
import System.Directory
1730
import System.Environment
1831
import System.Exit
1932
import System.FilePath
2033
import System.IO
2134
import System.Info
2235
import System.Process
36+
import Control.Concurrent.Strict (threadDelay)
2337

2438
-- ---------------------------------------------------------------------
2539

@@ -46,9 +60,17 @@ main = do
4660
BiosMode PrintCradleType ->
4761
print =<< findProjectCradle
4862

49-
_ -> launchHaskellLanguageServer args
50-
51-
launchHaskellLanguageServer :: Arguments -> IO ()
63+
_ -> launchHaskellLanguageServer args >>= \case
64+
Right () -> pure ()
65+
Left err -> do
66+
T.hPutStrLn stderr "*** Startup ERROR"
67+
T.hPutStrLn stderr (prettyError err NoShorten)
68+
putWindowMessage J.MtError (prettyError err Shorten)
69+
-- Wait for 45 seconds before shutdown, so we don't spam the same message
70+
-- since some LSP clients attempt to re-launch the server.
71+
threadDelay (45 * 1_000_000)
72+
73+
launchHaskellLanguageServer :: Arguments -> IO (Either WrapperSetupError ())
5274
launchHaskellLanguageServer parsedArgs = do
5375
case parsedArgs of
5476
Ghcide GhcideArguments{..} -> whenJust argsCwd setCurrentDirectory
@@ -64,7 +86,10 @@ launchHaskellLanguageServer parsedArgs = do
6486

6587
case parsedArgs of
6688
Ghcide GhcideArguments{..} ->
67-
when argsProjectGhcVersion $ getRuntimeGhcVersion' cradle >>= putStrLn >> exitSuccess
89+
when argsProjectGhcVersion $ do
90+
runExceptT (getRuntimeGhcVersion' cradle) >>= \case
91+
Right ghcVersion -> putStrLn ghcVersion >> exitSuccess
92+
Left err -> T.putStrLn (prettyError err NoShorten) >> exitFailure
6893
_ -> pure ()
6994

7095
progName <- getProgName
@@ -83,51 +108,53 @@ launchHaskellLanguageServer parsedArgs = do
83108
hPutStrLn stderr ""
84109
-- Get the ghc version -- this might fail!
85110
hPutStrLn stderr "Consulting the cradle to get project GHC version..."
86-
ghcVersion <- getRuntimeGhcVersion' cradle
87-
hPutStrLn stderr $ "Project GHC version: " ++ ghcVersion
88-
89-
let
90-
hlsBin = "haskell-language-server-" ++ ghcVersion
91-
candidates' = [hlsBin, "haskell-language-server"]
92-
candidates = map (++ exeExtension) candidates'
93-
94-
hPutStrLn stderr $ "haskell-language-server exe candidates: " ++ show candidates
95-
96-
mexes <- traverse findExecutable candidates
97-
98-
case asum mexes of
99-
Nothing -> hPutStrLn stderr $ "Cannot find any haskell-language-server exe, looked for: " ++ intercalate ", " candidates
100-
Just e -> do
101-
hPutStrLn stderr $ "Launching haskell-language-server exe at:" ++ e
102-
callProcess e args
103-
104-
-- | Version of 'getRuntimeGhcVersion' that dies if we can't get it, and also
105-
-- checks to see if the tool is missing if it is one of
106-
getRuntimeGhcVersion' :: Show a => Cradle a -> IO String
111+
runExceptT (getRuntimeGhcVersion' cradle) >>= \case
112+
Left err -> pure $ Left err
113+
Right ghcVersion -> do
114+
hPutStrLn stderr $ "Project GHC version: " ++ ghcVersion
115+
116+
let
117+
hlsBin = "haskell-language-server-" ++ ghcVersion
118+
candidates' = [hlsBin, "haskell-language-server"]
119+
candidates = map (++ exeExtension) candidates'
120+
121+
hPutStrLn stderr $ "haskell-language-server exe candidates: " ++ show candidates
122+
123+
mexes <- traverse findExecutable candidates
124+
125+
case asum mexes of
126+
Nothing -> pure $ Left $ NoLanguageServer ghcVersion candidates
127+
Just e -> do
128+
hPutStrLn stderr $ "Launching haskell-language-server exe at:" ++ e
129+
callProcess e args
130+
pure $ Right ()
131+
132+
-- | Version of 'getRuntimeGhcVersion' that throws a 'WrapperSetupError' if we
133+
-- can't get it, and also checks if run-time tool dependencies are missing.
134+
getRuntimeGhcVersion' :: Cradle Void -> ExceptT WrapperSetupError IO String
107135
getRuntimeGhcVersion' cradle = do
108136

137+
let cradleName = actionName (cradleOptsProg cradle)
109138
-- See if the tool is installed
110-
case actionName (cradleOptsProg cradle) of
139+
case cradleName of
111140
Stack -> checkToolExists "stack"
112141
Cabal -> checkToolExists "cabal"
113142
Default -> checkToolExists "ghc"
114143
Direct -> checkToolExists "ghc"
115144
_ -> pure ()
116145

117-
ghcVersionRes <- HieBios.getRuntimeGhcVersion cradle
146+
ghcVersionRes <- liftIO $ HieBios.getRuntimeGhcVersion cradle
118147
case ghcVersionRes of
119148
CradleSuccess ver -> do
120149
return ver
121-
CradleFail error -> die $ "Failed to get project GHC version:" ++ show error
122-
CradleNone -> die "Failed get project GHC version, since we have a none cradle"
150+
CradleFail error -> throwE $ FailedToObtainGhcVersion cradleName error
151+
CradleNone -> throwE $ NoneCradleGhcVersion cradleName
123152
where
124153
checkToolExists exe = do
125-
exists <- findExecutable exe
154+
exists <- liftIO $ findExecutable exe
126155
case exists of
127156
Just _ -> pure ()
128-
Nothing ->
129-
die $ "Cradle requires " ++ exe ++ " but couldn't find it" ++ "\n"
130-
++ show cradle
157+
Nothing -> throwE $ ToolRequirementMissing exe (actionName (cradleOptsProg cradle))
131158

132159
findProjectCradle :: IO (Cradle Void)
133160
findProjectCradle = do
@@ -142,3 +169,69 @@ findProjectCradle = do
142169
Nothing -> hPutStrLn stderr "No 'hie.yaml' found. Try to discover the project type!"
143170

144171
Session.loadCradle def hieYaml d
172+
173+
data WrapperSetupError
174+
= FailedToObtainGhcVersion (ActionName Void) CradleError
175+
| NoneCradleGhcVersion (ActionName Void)
176+
| NoLanguageServer String [FilePath]
177+
| ToolRequirementMissing String (ActionName Void)
178+
deriving (Show)
179+
180+
data Shorten = Shorten | NoShorten
181+
182+
-- | Pretty error message displayable to the future.
183+
-- Extra argument 'Shorten' can be used to shorten error message.
184+
-- Reduces usefulness, but allows us to show the error message via LSP
185+
-- as LSP doesn't allow any newlines and makes it really hard to read
186+
-- the message otherwise.
187+
prettyError :: WrapperSetupError -> Shorten -> T.Text
188+
prettyError (FailedToObtainGhcVersion name crdlError) shorten =
189+
"Failed to find the GHC version of this " <> T.pack (show name) <> " project." <>
190+
case shorten of
191+
Shorten ->
192+
"\n" <> T.pack (fromMaybe "" . listToMaybe $ cradleErrorStderr crdlError)
193+
NoShorten ->
194+
"\n" <> T.pack (intercalate "\r\n" (cradleErrorStderr crdlError))
195+
prettyError (NoneCradleGhcVersion name) _ =
196+
"Failed to get the GHC version of the " <> T.pack (show name) <>
197+
" project, since we have a none cradle"
198+
prettyError (NoLanguageServer ghcVersion candidates) _ =
199+
"Failed to find a HLS version for GHC " <> T.pack ghcVersion <>
200+
"\nExecutable names we failed to find: " <> T.pack (intercalate "," candidates)
201+
prettyError (ToolRequirementMissing toolExe name) _ =
202+
"This is a " <> T.pack (show name) <> " Project, but we failed to find \"" <>
203+
T.pack toolExe <> "\" on the $PATH"
204+
205+
-- LSP Helper functions
206+
-- ~~~~~~~~~~~~~~~~~~~~
207+
--
208+
-- To send lsp messages without a full-fledged LSP server.
209+
-- Should be used to indicate errors to the user.
210+
211+
putWindowMessage :: J.MessageType -> T.Text -> IO ()
212+
putWindowMessage mt message = do
213+
let bsMessage = toLspMessage windowMsg
214+
BSL.hPut stdout bsMessage
215+
hFlush stdout
216+
where
217+
windowMsg = windowMessage mt message
218+
219+
windowMessage :: J.MessageType -> T.Text -> J.FromServerMessage
220+
windowMessage mt message = J.FromServerMess J.SWindowShowMessage $
221+
J.NotificationMessage "2.0" J.SWindowShowMessage
222+
(J.ShowMessageParams mt message)
223+
224+
225+
-- | Given a server message, attach the relevant header information.
226+
toLspMessage :: J.FromServerMessage -> BSL.ByteString
227+
toLspMessage msg =
228+
BSL.concat
229+
[ TL.encodeUtf8 $ TL.pack $ "Content-Length: " ++ show (BSL.length str)
230+
, _TWO_CRLF
231+
, str ]
232+
where
233+
str = Aeson.encode msg
234+
235+
-- | Constant taken from the 'lsp' package.
236+
_TWO_CRLF :: BSL.ByteString
237+
_TWO_CRLF = "\r\n\r\n"

haskell-language-server.cabal

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cabal-version: 2.4
22
category: Development
33
name: haskell-language-server
4-
version: 1.5.1.0
4+
version: 1.5.2.0
55
synopsis: LSP server for GHC
66
description:
77
Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>
@@ -404,15 +404,20 @@ executable haskell-language-server-wrapper
404404
"-with-rtsopts=-I0 -A128M"
405405

406406
build-depends:
407+
, aeson
408+
, bytestring
407409
, data-default
408410
, ghc
409411
, ghc-paths
410412
, ghcide
411413
, gitrev
412414
, haskell-language-server
413415
, hie-bios
416+
, lsp-types
414417
, optparse-applicative
415418
, optparse-simple
419+
, text
420+
, transformers
416421
, process
417422

418423
default-language: Haskell2010

0 commit comments

Comments
 (0)