on master help sign in
Significantly alter the hie-core LanguageServer (#1862)

* Inline chunks of LSP.Server into LanguageServer * Inline runServer * Start figuring out a better API for gotoDefinition * Remove old JIRA ticket numbers * Add a hover handler in the new form * Change the new handlers slightly * Add a new module to handle notifications updating the virtual file system * Rewrite the language server in hie-core to use the Handler more directly * Add a cancel handler * Ignore a few more handlers * HLint * REname functions that set handlers * Rename a few more set handlers * Delete the unused makeResponse * Move mergeHandlers over to LanguageServer * Rename RunHandler to WithMessage * Switch from STM to IO * Avoid the Protocol module * Rename AddItem to Message * Document why we use clientMsgChan * Add comments around Message

commit7870ee804332adc2d3c791fef663e05e92d1082b authorNeil Mitchell <35463327+neil-da@users.noreply.github.com> dateTue 25 Jun 2019, 16:13 (7 years ago) parent6f79fd1
7 files changed+256−168
hie-core.cabal+1 src/Development/IDE/LSP/Definition.hs+24−21 src/Development/IDE/LSP/Hover.hs+17−10 src/Development/IDE/LSP/LanguageServer.hs+104−136 src/Development/IDE/LSP/Notifications.hs+100 src/Development/IDE/LSP/Server.hs+9 test/Demo.hs+1−1
hie-core.cabal+1
@@ -89,6 +89,7 @@ library
8989 Development.IDE.LSP.Definition
9090 Development.IDE.LSP.Hover
9191 Development.IDE.LSP.LanguageServer
92+ Development.IDE.LSP.Notifications
9293 Development.IDE.LSP.Protocol
9394 Development.IDE.LSP.Server
9495 Development.IDE.Spans.AtPoint
src/Development/IDE/LSP/Definition.hs+24−21
@@ -5,7 +5,8 @@
55
66-- | Go to the definition of a variable.
77module Development.IDE.LSP.Definition
8− ( handle
8+ ( gotoDefinition
9+ , setHandlersDefinition
910 ) where
1011
1112import Language.Haskell.LSP.Types
@@ -13,30 +14,32 @@ import Development.IDE.Types.Location
1314
1415import Development.IDE.Types.Logger
1516import Development.IDE.Core.Rules
17+import Development.IDE.Core.Service
18+import Development.IDE.LSP.Server
19+import qualified Language.Haskell.LSP.Core as LSP
20+import Language.Haskell.LSP.Messages
1621
1722import qualified Data.Text as T
1823
1924-- | Go to the definition of a variable.
20−handle
21− :: Logger
22− -> IdeState
25+gotoDefinition
26+ :: IdeState
2327 -> TextDocumentPositionParams
2428 -> IO LocationResponseParams
25−handle logger compilerH (TextDocumentPositionParams (TextDocumentIdentifier uri) pos) = do
26−
27−
29+gotoDefinition ide (TextDocumentPositionParams (TextDocumentIdentifier uri) pos) = do
2830 mbResult <- case uriToFilePath' uri of
29− Just (toNormalizedFilePath -> filePath) -> do
30− logInfo logger $
31− "Definition request at position " <>
32− T.pack (showPosition pos) <>
33− " in file: " <> T.pack (fromNormalizedFilePath filePath)
34− runAction compilerH (getDefinition filePath pos)
35− Nothing -> pure Nothing
36−
37− case mbResult of
38− Nothing ->
39− pure $ MultiLoc []
40−
41− Just loc ->
42− pure $ SingleLoc loc
31+ Just path -> do
32+ logInfo (ideLogger ide) $
33+ "Definition request at position " <> T.pack (showPosition pos) <>
34+ " in file: " <> T.pack path
35+ runAction ide $ getDefinition (toNormalizedFilePath path) pos
36+ Nothing -> pure Nothing
37+ pure $ case mbResult of
38+ Nothing -> MultiLoc []
39+ Just loc -> SingleLoc loc
40+
41+
42+setHandlersDefinition :: WithMessage -> LSP.Handlers -> IO LSP.Handlers
43+setHandlersDefinition WithMessage{..} x = return x{
44+ LSP.definitionHandler = withResponse RspDefinition gotoDefinition
45+ }
src/Development/IDE/LSP/Hover.hs+17−10
@@ -5,32 +5,34 @@
55
66-- | Display information on hover.
77module Development.IDE.LSP.Hover
8− ( handle
8+ ( onHover
9+ , setHandlersHover
910 ) where
1011
1112import Language.Haskell.LSP.Types
1213import Development.IDE.Types.Location
13−
14+import Development.IDE.Core.Service
15+import Development.IDE.LSP.Server
1416import Development.IDE.Types.Logger
17+import qualified Language.Haskell.LSP.Core as LSP
18+import Language.Haskell.LSP.Messages
1519
1620import qualified Data.Text as T
1721
1822import Development.IDE.Core.Rules
1923
2024-- | Display information on hover.
21−handle
22− :: Logger
23− -> IdeState
25+onHover
26+ :: IdeState
2427 -> TextDocumentPositionParams
2528 -> IO (Maybe Hover)
26−handle loggerH compilerH (TextDocumentPositionParams (TextDocumentIdentifier uri) pos) = do
29+onHover ide (TextDocumentPositionParams (TextDocumentIdentifier uri) pos) = do
2730 mbResult <- case uriToFilePath' uri of
2831 Just (toNormalizedFilePath -> filePath) -> do
29− logInfo loggerH $
30− "Hover request at position " <>
31− T.pack (showPosition pos) <>
32+ logInfo (ideLogger ide) $
33+ "Hover request at position " <> T.pack (showPosition pos) <>
3234 " in file: " <> T.pack (fromNormalizedFilePath filePath)
33− runAction compilerH $ getAtPoint filePath pos
35+ runAction ide $ getAtPoint filePath pos
3436 Nothing -> pure Nothing
3537
3638 case mbResult of
@@ -40,3 +42,8 @@ handle loggerH compilerH (TextDocumentPositionParams (TextDocumentIdentifier uri
4042 mbRange
4143
4244 Nothing -> pure Nothing
45+
46+setHandlersHover :: WithMessage -> LSP.Handlers -> IO LSP.Handlers
47+setHandlersHover WithMessage{..} x = return x{
48+ LSP.hoverHandler = withResponse RspHover onHover
49+ }
src/Development/IDE/LSP/LanguageServer.hs+104−136
@@ -1,9 +1,8 @@
11-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
22-- SPDX-License-Identifier: Apache-2.0
33
4−{-# LANGUAGE DuplicateRecordFields #-}
54{-# LANGUAGE OverloadedStrings #-}
6−{-# LANGUAGE RankNTypes #-}
5+{-# LANGUAGE ExistentialQuantification #-}
76
87-- WARNING: A copy of DA.Service.Daml.LanguageServer, try to keep them in sync
98-- This version removes the daml: handling
@@ -11,145 +10,114 @@ module Development.IDE.LSP.LanguageServer
1110 ( runLanguageServer
1211 ) where
1312
14−import Development.IDE.LSP.Protocol
15−import Development.IDE.LSP.Server
16−
17−import Control.Monad.IO.Class
18−import qualified Development.IDE.LSP.Definition as LS.Definition
19−import qualified Development.IDE.LSP.Hover as LS.Hover
20−import Development.IDE.Types.Logger
13+import Language.Haskell.LSP.Types
14+import Development.IDE.LSP.Server hiding (runServer)
15+import qualified Language.Haskell.LSP.Control as LSP
16+import qualified Language.Haskell.LSP.Core as LSP
17+import Control.Concurrent.Chan
18+import Control.Concurrent.Extra
19+import Control.Concurrent.Async
20+import Data.Default
21+import GHC.IO.Handle (hDuplicate, hDuplicateTo)
22+import System.IO
23+import Control.Monad
24+
25+import Development.IDE.LSP.Definition
26+import Development.IDE.LSP.Hover
27+import Development.IDE.LSP.Notifications
2128import Development.IDE.Core.Service
22−import Development.IDE.Types.Location
23−
24−import qualified Data.Aeson as Aeson
25−import qualified Data.Rope.UTF16 as Rope
26−import qualified Data.Set as S
27−import qualified Data.Text as T
28−
2929import Development.IDE.Core.FileStore
30−import Development.IDE.Core.OfInterest
31−
32−import qualified Network.URI as URI
33−
34−import qualified System.Exit
35−
3630import Language.Haskell.LSP.Core (LspFuncs(..))
3731import Language.Haskell.LSP.Messages
38−import Language.Haskell.LSP.VFS
39−
40−textShow :: Show a => a -> T.Text
41−textShow = T.pack . show
42−
43−------------------------------------------------------------------------
44−-- Request handlers
45−------------------------------------------------------------------------
46−
47−handleRequest
48− :: Logger
49− -> IdeState
50− -> (forall resp. resp -> ResponseMessage resp)
51− -> (ErrorCode -> ResponseMessage ())
52− -> ServerRequest
53− -> IO FromServerMessage
54−handleRequest logger compilerH makeResponse makeErrorResponse = \case
55− Shutdown -> do
56− logInfo logger "Shutdown request received, terminating."
57− System.Exit.exitSuccess
58−
59− KeepAlive -> pure $ RspCustomServer $ makeResponse Aeson.Null
60−
61− Definition params -> RspDefinition . makeResponse <$> LS.Definition.handle logger compilerH params
62− Hover params -> RspHover . makeResponse <$> LS.Hover.handle logger compilerH params
63− CodeLens _params -> pure $ RspCodeLens $ makeResponse mempty
64−
65− req -> do
66− logWarning logger ("Method not found" <> T.pack (show req))
67− pure $ RspError $ makeErrorResponse MethodNotFound
68−
69−
70−handleNotification :: LspFuncs () -> Logger -> IdeState -> ServerNotification -> IO ()
71−handleNotification lspFuncs logger compilerH = \case
72−
73− DidOpenTextDocument (DidOpenTextDocumentParams item) -> do
74− case URI.parseURI $ T.unpack $ getUri $ _uri (item :: TextDocumentItem) of
75− Just uri
76− | URI.uriScheme uri == "file:"
77− -> handleDidOpenFile item
78−
79− | otherwise
80− -> logWarning logger $ "Unknown scheme in URI: "
81− <> textShow uri
82−
83− _ -> logSeriousError logger $ "Invalid URI in DidOpenTextDocument: "
84− <> textShow (_uri (item :: TextDocumentItem))
85−
86− DidChangeTextDocument (DidChangeTextDocumentParams docId _) -> do
87− let uri = _uri (docId :: VersionedTextDocumentIdentifier)
88−
89− case uriToFilePath' uri of
90− Just (toNormalizedFilePath -> filePath) -> do
91− mbVirtual <- getVirtualFileFunc lspFuncs $ toNormalizedUri uri
92− let contents = maybe "" (Rope.toText . (_text :: VirtualFile -> Rope.Rope)) mbVirtual
93− onFileModified compilerH filePath (Just contents)
94− logInfo logger
95− $ "Updated text document: " <> textShow (fromNormalizedFilePath filePath)
96−
97− Nothing ->
98− logSeriousError logger
99− $ "Invalid file path: " <> textShow (_uri (docId :: VersionedTextDocumentIdentifier))
100−
101− DidCloseTextDocument (DidCloseTextDocumentParams (TextDocumentIdentifier uri)) ->
102− case URI.parseURI $ T.unpack $ getUri uri of
103− Just uri'
104− | URI.uriScheme uri' == "file:" -> do
105− Just fp <- pure $ toNormalizedFilePath <$> uriToFilePath' uri
106− handleDidCloseFile fp
107− | otherwise -> logWarning logger $ "Unknown scheme in URI: " <> textShow uri
108−
109− _ -> logSeriousError logger
110− $ "Invalid URI in DidCloseTextDocument: "
111− <> textShow uri
112−
113− DidSaveTextDocument _params ->
114− pure ()
115−
116− UnknownNotification _method _params -> return ()
117− where
118− -- Note that the state changes here are not atomic.
119− -- When we have parallel compilation we could manage the state
120− -- changes in STM so that we can atomically change the state.
121− -- Internally it should be done via the IO oracle. See PROD-2808.
122− handleDidOpenFile (TextDocumentItem uri _ _ contents) = do
123− Just filePath <- pure $ toNormalizedFilePath <$> uriToFilePath' uri
124− onFileModified compilerH filePath (Just contents)
125− modifyFilesOfInterest compilerH (S.insert filePath)
126− logInfo logger $ "Opened text document: " <> textShow filePath
127−
128− handleDidCloseFile filePath = do
129− logInfo logger $ "Closed text document: " <> textShow (fromNormalizedFilePath filePath)
130− onFileModified compilerH filePath Nothing
131− modifyFilesOfInterest compilerH (S.delete filePath)
132−
133−-- | Manages the file store (caching compilation results and unsaved content).
134−onFileModified
135− :: IdeState
136− -> NormalizedFilePath
137− -> Maybe T.Text
138− -> IO ()
139−onFileModified service fp mbContents = do
140− logDebug (ideLogger service) $ "File modified " <> T.pack (show fp)
141− setBufferModified service fp mbContents
14232
143−------------------------------------------------------------------------
144−-- Server execution
145−------------------------------------------------------------------------
14633
14734runLanguageServer
148− :: Logger
149− -> ((FromServerMessage -> IO ()) -> VFSHandle -> IO IdeState)
35+ :: ((FromServerMessage -> IO ()) -> VFSHandle -> IO IdeState)
15036 -> IO ()
151−runLanguageServer loggerH getIdeState = do
152− let getHandlers lspFuncs = do
153− compilerH <- getIdeState (sendFunc lspFuncs) (makeLSPVFSHandle lspFuncs)
154− pure $ Handlers (handleRequest loggerH compilerH) (handleNotification lspFuncs loggerH compilerH)
155− liftIO $ runServer loggerH getHandlers
37+runLanguageServer getIdeState = do
38+ -- Move stdout to another file descriptor and duplicate stderr
39+ -- to stdout. This guards against stray prints from corrupting the JSON-RPC
40+ -- message stream.
41+ newStdout <- hDuplicate stdout
42+ stderr `hDuplicateTo` stdout
43+
44+ -- Print out a single space to assert that the above redirection works.
45+ -- This is interleaved with the logger, hence we just print a space here in
46+ -- order not to mess up the output too much. Verified that this breaks
47+ -- the language server tests without the redirection.
48+ putStr " " >> hFlush stdout
49+
50+ -- Send everything over a channel, since you need to wait until after initialise before
51+ -- LspFuncs is available
52+ clientMsgChan :: Chan Message <- newChan
53+
54+ -- These barriers are signaled when the threads reading from these chans exit.
55+ -- This should not happen but if it does, we will make sure that the whole server
56+ -- dies and can be restarted instead of losing threads silently.
57+ clientMsgBarrier <- newBarrier
58+
59+ let withResponse wrap f = Just $ \r -> writeChan clientMsgChan $ Response r wrap f
60+ let withNotification f = Just $ \r -> writeChan clientMsgChan $ Notification r f
61+ let runHandler = WithMessage{withResponse, withNotification}
62+ handlers <- mergeHandlers [setHandlersDefinition, setHandlersHover, setHandlersNotifications, setHandlersIgnore] runHandler def
63+
64+ void $ waitAnyCancel =<< traverse async
65+ [ void $ LSP.runWithHandles
66+ stdin
67+ newStdout
68+ ( const $ Right ()
69+ , handleInit (signalBarrier clientMsgBarrier ()) clientMsgChan
70+ )
71+ handlers
72+ options
73+ Nothing
74+ , void $ waitBarrier clientMsgBarrier
75+ ]
76+ where
77+ handleInit :: IO () -> Chan Message -> LSP.LspFuncs () -> IO (Maybe err)
78+ handleInit exitClientMsg clientMsgChan lspFuncs@LSP.LspFuncs{..} = do
79+ ide <- getIdeState sendFunc (makeLSPVFSHandle lspFuncs)
80+ _ <- flip forkFinally (const exitClientMsg) $ forever $ do
81+ msg <- readChan clientMsgChan
82+ case msg of
83+ Notification NotificationMessage{_params} act -> act ide _params
84+ Response RequestMessage{_id, _params} wrap act -> do
85+ res <- act ide _params
86+ sendFunc $ wrap $ ResponseMessage "2.0" (responseId _id) (Just res) Nothing
87+ pure Nothing
88+
89+
90+-- | Things that get sent to us, but we don't deal with.
91+-- Set them to avoid a warning in VS Code output.
92+setHandlersIgnore :: WithMessage -> LSP.Handlers -> IO LSP.Handlers
93+setHandlersIgnore _ x = return x
94+ {LSP.cancelNotificationHandler = none
95+ ,LSP.initializedHandler = none
96+ ,LSP.codeLensHandler = none -- FIXME: Stop saying we support it in 'options'
97+ }
98+ where none = Just $ const $ return ()
99+
100+
101+mergeHandlers :: [WithMessage -> LSP.Handlers -> IO LSP.Handlers] -> WithMessage -> LSP.Handlers -> IO LSP.Handlers
102+mergeHandlers = foldl f (\_ a -> return a)
103+ where f x1 x2 r a = x1 r a >>= x2 r
104+
105+
106+-- | A message that we need to deal with - the pieces are split up with existentials to gain additional type safety
107+-- and defer precise processing until later (allows us to keep at a higher level of abstraction slightly longer)
108+data Message
109+ = forall m req resp . Response (RequestMessage m req resp) (ResponseMessage resp -> FromServerMessage) (IdeState -> req -> IO resp)
110+ | forall m req . Notification (NotificationMessage m req) (IdeState -> req -> IO ())
111+
112+
113+options :: LSP.Options
114+options = def
115+ { LSP.textDocumentSync = Just TextDocumentSyncOptions
116+ { _openClose = Just True
117+ , _change = Just TdSyncIncremental
118+ , _willSave = Nothing
119+ , _willSaveWaitUntil = Nothing
120+ , _save = Just $ SaveOptions $ Just False
121+ }
122+ , LSP.codeLensProvider = Just $ CodeLensOptions $ Just False
123+ }
src/Development/IDE/LSP/Notifications.hsnew file+100
@@ -0,0 +1,100 @@
1+-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+-- SPDX-License-Identifier: Apache-2.0
3+
4+{-# LANGUAGE DuplicateRecordFields #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE RankNTypes #-}
7+
8+module Development.IDE.LSP.Notifications
9+ ( setHandlersNotifications
10+ ) where
11+
12+import Development.IDE.LSP.Protocol
13+import Development.IDE.LSP.Server hiding (runServer)
14+import qualified Language.Haskell.LSP.Core as LSP
15+
16+import Development.IDE.Types.Logger
17+import Development.IDE.Core.Service
18+import Development.IDE.Types.Location
19+
20+import qualified Data.Set as S
21+import qualified Data.Text as T
22+
23+import Development.IDE.Core.FileStore
24+import Development.IDE.Core.OfInterest
25+
26+import qualified Network.URI as URI
27+
28+
29+textShow :: Show a => a -> T.Text
30+textShow = T.pack . show
31+
32+
33+setHandlersNotifications :: WithMessage -> LSP.Handlers -> IO LSP.Handlers
34+setHandlersNotifications WithMessage{..} x = return x{
35+ LSP.didOpenTextDocumentNotificationHandler = withNotification $ \ide (DidOpenTextDocumentParams item) -> do
36+ case URI.parseURI $ T.unpack $ getUri $ _uri (item :: TextDocumentItem) of
37+ Just uri
38+ | URI.uriScheme uri == "file:"
39+ -> handleDidOpenFile ide item
40+
41+ | otherwise
42+ -> logWarning (ideLogger ide) $ "Unknown scheme in URI: "
43+ <> textShow uri
44+
45+ _ -> logSeriousError (ideLogger ide) $ "Invalid URI in DidOpenTextDocument: "
46+ <> textShow (_uri (item :: TextDocumentItem))
47+
48+ ,LSP.didChangeTextDocumentNotificationHandler = withNotification $ \ide (DidChangeTextDocumentParams docId _) -> do
49+ let uri = _uri (docId :: VersionedTextDocumentIdentifier)
50+
51+ case uriToFilePath' uri of
52+ Just (toNormalizedFilePath -> filePath) -> do
53+ onFileModified ide filePath
54+ logInfo (ideLogger ide)
55+ $ "Updated text document: " <> textShow (fromNormalizedFilePath filePath)
56+
57+ Nothing ->
58+ logSeriousError (ideLogger ide)
59+ $ "Invalid file path: " <> textShow (_uri (docId :: VersionedTextDocumentIdentifier))
60+
61+ ,LSP.didCloseTextDocumentNotificationHandler = withNotification $ \ide (DidCloseTextDocumentParams (TextDocumentIdentifier uri)) ->
62+ case URI.parseURI $ T.unpack $ getUri uri of
63+ Just uri'
64+ | URI.uriScheme uri' == "file:" -> do
65+ Just fp <- pure $ toNormalizedFilePath <$> uriToFilePath' uri
66+ handleDidCloseFile ide fp
67+ | otherwise -> logWarning (ideLogger ide) $ "Unknown scheme in URI: " <> textShow uri
68+
69+ _ -> logSeriousError (ideLogger ide)
70+ $ "Invalid URI in DidCloseTextDocument: "
71+ <> textShow uri
72+
73+ }
74+ where
75+ -- Note that the state changes here are not atomic.
76+ -- When we have parallel compilation we could manage the state
77+ -- changes in STM so that we can atomically change the state.
78+ -- Internally it should be done via the IO oracle. See PROD-2808.
79+ handleDidOpenFile ide (TextDocumentItem uri _ _ _) = do
80+ Just filePath <- pure $ toNormalizedFilePath <$> uriToFilePath' uri
81+ onFileModified ide filePath
82+ modifyFilesOfInterest ide (S.insert filePath)
83+ logInfo (ideLogger ide) $ "Opened text document: " <> textShow filePath
84+
85+ handleDidCloseFile ide filePath = do
86+ logInfo (ideLogger ide) $ "Closed text document: " <> textShow (fromNormalizedFilePath filePath)
87+ onFileModified ide filePath
88+ modifyFilesOfInterest ide (S.delete filePath)
89+
90+
91+-- | Manages the file store (caching compilation results and unsaved content).
92+onFileModified
93+ :: IdeState
94+ -> NormalizedFilePath
95+ -> IO ()
96+onFileModified service fp = do
97+ logDebug (ideLogger service) $ "File modified " <> T.pack (show fp)
98+ -- if we get here then we must be using the LSP framework, in which case we don't
99+ -- need to bother sending file modifications, other than to force the database to rerun
100+ setBufferModified service fp Nothing
src/Development/IDE/LSP/Server.hs+9
@@ -7,6 +7,7 @@
77module Development.IDE.LSP.Server
88 ( runServer
99 , Handlers(..)
10+ , WithMessage(..)
1011 ) where
1112
1213
@@ -33,6 +34,14 @@ import qualified Language.Haskell.LSP.Control as LSP
3334import qualified Language.Haskell.LSP.Core as LSP
3435import qualified Language.Haskell.LSP.Messages as LSP
3536import qualified Language.Haskell.LSP.Types as LSP
37+import Development.IDE.Core.Service
38+
39+
40+data WithMessage = WithMessage
41+ {withResponse :: forall m req resp . (ResponseMessage resp -> LSP.FromServerMessage) -> (IdeState -> req -> IO resp) -> Maybe (LSP.Handler (RequestMessage m req resp))
42+ ,withNotification :: forall m req . (IdeState -> req -> IO ()) -> Maybe (LSP.Handler (NotificationMessage m req))
43+ }
44+
3645
3746------------------------------------------------------------------------
3847-- Server execution
test/Demo.hs+1−1
@@ -58,7 +58,7 @@ main = do
5858
5959 if "--lsp" `elem` args then do
6060 hPutStrLn stderr "Starting IDE server"
61− runLanguageServer logger $ \event vfs -> do
61+ runLanguageServer $ \event vfs -> do
6262 hPutStrLn stderr "Server started"
6363 initialise (mainRule >> action kick) event logger options vfs
6464 else do