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
47module Main where
58
69import 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
714import Data.Default
815import Data.Foldable
916import 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
1022import Data.Void
1123import qualified Development.IDE.Session as Session
1224import qualified HIE.Bios.Environment as HieBios
1325import HIE.Bios.Types
1426import Ide.Arguments
1527import Ide.Version
28+ import qualified Language.LSP.Types as J
1629import System.Directory
1730import System.Environment
1831import System.Exit
1932import System.FilePath
2033import System.IO
2134import System.Info
2235import 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 () )
5274launchHaskellLanguageServer 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
107135getRuntimeGhcVersion' 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
132159findProjectCradle :: IO (Cradle Void )
133160findProjectCradle = 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+ " \n Executable 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 "
0 commit comments