packages feed

lsp-test 0.10.3.0 → 0.11.0.0

raw patch · 8 files changed

+497/−198 lines, 8 filesdep +Globdep ~haskell-lspnew-component:exe:dummy-serverPVP ok

version bump matches the API change (PVP)

Dependencies added: Glob

Dependency ranges changed: haskell-lsp

API changes (from Hackage documentation)

- Language.Haskell.LSP.Test: openDoc' :: FilePath -> String -> Text -> Session TextDocumentIdentifier
+ Language.Haskell.LSP.Test: createDoc :: FilePath -> String -> Text -> Session TextDocumentIdentifier
+ Language.Haskell.LSP.Test: getRegisteredCapabilities :: Session [Registration]

Files

ChangeLog.md view
@@ -1,5 +1,12 @@ # Revision history for lsp-test +## 0.11.0.0 -- 2020-05-14++* Replace `openDoc'` with `createDoc` which now sends+  `workspace/didChangeWatchedFiles` notifications if the server has registered+  for it+* Add `getRegisteredCapabilities`+ ## 0.10.3.0 -- 2020-05-04  * Build with new haskell-lsp-0.22
README.md view
@@ -1,4 +1,4 @@-# lsp-test [![Actions Status](https://github.com/bubba/lsp-test/workflows/Haskell%20CI/badge.svg)](https://github.com/bubba/lsp-test/actions) [![Hackage](https://img.shields.io/hackage/v/lsp-test.svg)](https://hackage.haskell.org/package/lsp-test-0.1.0.0)+# lsp-test [![Actions Status](https://github.com/bubba/lsp-test/workflows/Haskell%20CI/badge.svg)](https://github.com/bubba/lsp-test/actions) [![Hackage](https://img.shields.io/hackage/v/lsp-test.svg)](https://hackage.haskell.org/package/lsp-test) lsp-test is a functional testing framework for Language Server Protocol servers.  ```haskell@@ -35,17 +35,19 @@ ```  Try out the example tests in the `example` directory with `cabal test`.-For more examples check the [Wiki](https://github.com/bubba/lsp-test/wiki/Introduction)+For more examples check the [Wiki](https://github.com/bubba/lsp-test/wiki/Introduction). -## Developing-The tests are integration tests, so make sure you have the following language servers installed and on your PATH:-### [haskell-ide-engine](https://github.com/haskell/haskell-ide-engine)-- Check out a relatively recent version of the repo, or see `.travis.yml` to get the exact commit used for CI.-- `stack install`-### [javascript-typescript-langserver](https://github.com/sourcegraph/javascript-typescript-langserver)-`npm i -g javascript-typescript-langserver`+Whilst writing your tests you may want to debug them to see what's going wrong.+You can set the `logMessages` and `logStdErr` options in `SessionConfig` to see what the server is up to.+There are also corresponding environment variables so you can turn them on from the command line:+```+LSP_TEST_LOG_MESSAGES=1 cabal test+``` -Then run the tests with `cabal test` or `stack test`.+## Developing+The tests for lsp-test use a dummy server found in `test/dummy-server/`.+Run the tests with `cabal test` or `stack test`.+Tip: If you want to filter the tests, use `cabal run test:tests -- -m "foo"`  ## Troubleshooting Seeing funny stuff when running lsp-test via stack? If your server is built upon Haskell tooling, [keep in mind that stack sets some environment variables related to GHC, and you may want to unset them.](https://github.com/alanz/haskell-ide-engine/blob/bfb16324d396da71000ef81d51acbebbdaa854ab/test/utils/TestUtils.hs#L290-L298)
lsp-test.cabal view
@@ -1,5 +1,5 @@ name:                lsp-test-version:             0.10.3.0+version:             0.11.0.0 synopsis:            Functional test framework for LSP servers. description:   A test framework for writing tests against@@ -7,14 +7,16 @@   @Language.Haskell.LSP.Test@ launches your server as a subprocess and allows you to simulate a session   down to the wire, and @Language.Haskell.LSP.Test@ can replay captured sessions from   <haskell-lsp https://hackage.haskell.org/package/haskell-lsp>.-  It's currently used for testing in <https://github.com/haskell/haskell-ide-engine haskell-ide-engine>.+  To see examples of it in action, check out <https://github.com/haskell/haskell-ide-engine haskell-ide-engine>,+  <https://github.com/haskell/haskell-language-server haskell-language-server> and+  <https://github.com/digital-asset/ghcide ghcide>. homepage:            https://github.com/bubba/lsp-test#readme license:             BSD3 license-file:        LICENSE author:              Luke Lau maintainer:          luke_lau@icloud.com bug-reports:         https://github.com/bubba/lsp-test/issues-copyright:           2019 Luke Lau+copyright:           2020 Luke Lau category:            Testing build-type:          Simple cabal-version:       2.0@@ -26,6 +28,11 @@   type:     git   location: https://github.com/bubba/lsp-test/ +Flag DummyServer+  Description: Build the dummy server executable used in testing+  Default:     False+  Manual:      True+ library   hs-source-dirs:      src   exposed-modules:     Language.Haskell.LSP.Test@@ -48,6 +55,7 @@                      , Diff                      , directory                      , filepath+                     , Glob ^>= 0.10                      , lens                      , mtl                      , parser-combinators >= 1.2@@ -69,6 +77,22 @@                        Language.Haskell.LSP.Test.Session   ghc-options:         -W +executable dummy-server+  main-is:             Main.hs+  hs-source-dirs:      test/dummy-server+  ghc-options:         -W+  build-depends:       base >= 4.10 && < 5+                     , haskell-lsp+                     , data-default+                     , aeson+                     , unordered-containers+                     , directory+                     , filepath+  default-language:    Haskell2010+  scope:               private+  if !flag(DummyServer)+    buildable:         False+ test-suite tests   type:                exitcode-stdio-1.0   main-is:             Test.hs@@ -83,4 +107,7 @@                      , aeson                      , unordered-containers                      , text+                     , directory+                     , filepath   default-language:    Haskell2010+  build-tool-depends: lsp-test:dummy-server
src/Language/Haskell/LSP/Test.hs view
@@ -41,8 +41,8 @@   -- ** Initialization   , initializeResponse   -- ** Documents+  , createDoc   , openDoc-  , openDoc'   , closeDoc   , changeDoc   , documentContents@@ -82,6 +82,8 @@   , applyEdit   -- ** Code lenses   , getCodeLenses+  -- ** Capabilities+  , getRegisteredCapabilities   ) where  import Control.Applicative.Combinators@@ -90,12 +92,13 @@ import Control.Monad.IO.Class import Control.Exception import Control.Lens hiding ((.=), List)+import qualified Data.Map.Strict as Map import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Aeson import Data.Default import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map as Map+import Data.List import Data.Maybe import Language.Haskell.LSP.Types import Language.Haskell.LSP.Types.Lens hiding@@ -114,6 +117,7 @@ import System.IO import System.Directory import System.FilePath+import qualified System.FilePath.Glob as Glob  -- | Starts a new session. --@@ -345,7 +349,61 @@ initializeResponse :: Session InitializeResponse initializeResponse = initRsp <$> ask >>= (liftIO . readMVar) --- | Opens a text document and sends a notification to the client.+-- | /Creates/ a new text document. This is different from 'openDoc'+-- as it sends a workspace/didChangeWatchedFiles notification letting the server+-- know that a file was created within the workspace, __provided that the server+-- has registered for it__, and the file matches any patterns the server+-- registered for.+-- It /does not/ actually create a file on disk, but is useful for convincing+-- the server that one does exist.+--+-- @since 11.0.0.0+createDoc :: FilePath -- ^ The path to the document to open, __relative to the root directory__.+          -> String -- ^ The text document's language identifier, e.g. @"haskell"@.+          -> T.Text -- ^ The content of the text document to create.+          -> Session TextDocumentIdentifier -- ^ The identifier of the document just created.+createDoc file languageId contents = do+  dynCaps <- curDynCaps <$> get+  rootDir <- asks rootDir+  caps <- asks sessionCapabilities+  absFile <- liftIO $ canonicalizePath (rootDir </> file)+  let regs = filter (\r -> r ^. method == WorkspaceDidChangeWatchedFiles) $+              Map.elems dynCaps+      watchHits :: FileSystemWatcher -> Bool+      watchHits (FileSystemWatcher pattern kind) =+        -- If WatchKind is exlcuded, defaults to all true as per spec+        fileMatches pattern && createHits (fromMaybe (WatchKind True True True) kind)++      fileMatches pattern = Glob.match (Glob.compile pattern) relOrAbs+        -- If the pattern is absolute then match against the absolute fp+        where relOrAbs+                | isAbsolute pattern = absFile+                | otherwise = file++      createHits (WatchKind create _ _) = create++      regHits :: Registration -> Bool+      regHits reg = isJust $ do+        opts <- reg ^. registerOptions+        fileWatchOpts <- case fromJSON opts :: Result DidChangeWatchedFilesRegistrationOptions of+          Success x -> Just x+          Error _ -> Nothing+        if foldl' (\acc w -> acc || watchHits w) False (fileWatchOpts ^. watchers)+          then Just ()+          else Nothing++      clientCapsSupports =+          caps ^? workspace . _Just . didChangeWatchedFiles . _Just . dynamicRegistration . _Just+            == Just True+      shouldSend = clientCapsSupports && foldl' (\acc r -> acc || regHits r) False regs++  when shouldSend $+    sendNotification WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $+      List [ FileEvent (filePathToUri file) FcCreated ]+  openDoc' file languageId contents++-- | Opens a text document that /exists on disk/, and sends a+-- textDocument/didOpen notification to the server. openDoc :: FilePath -> String -> Session TextDocumentIdentifier openDoc file languageId = do   context <- ask@@ -354,6 +412,7 @@   openDoc' file languageId contents  -- | This is a variant of `openDoc` that takes the file content as an argument.+-- Use this is the file exists /outside/ of the current workspace. openDoc' :: FilePath -> String -> T.Text -> Session TextDocumentIdentifier openDoc' file languageId contents = do   context <- ask@@ -363,13 +422,13 @@   sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams item)   pure $ TextDocumentIdentifier uri --- | Closes a text document and sends a notification to the client.+-- | Closes a text document and sends a textDocument/didOpen notification to the server. closeDoc :: TextDocumentIdentifier -> Session () closeDoc docId = do   let params = DidCloseTextDocumentParams (TextDocumentIdentifier (docId ^. uri))   sendNotification TextDocumentDidClose params --- | Changes a text document and sends a notification to the client+-- | Changes a text document and sends a textDocument/didOpen notification to the server. changeDoc :: TextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> Session () changeDoc docId changes = do   verDoc <- getVersionedDoc docId@@ -611,3 +670,10 @@     rsp <- request TextDocumentCodeLens (CodeLensParams tId Nothing) :: Session CodeLensResponse     case getResponseResult rsp of         List res -> pure res++-- | Returns a list of capabilities that the server has requested to /dynamically/+-- register during the 'Session'.+--+-- @since 0.11.0.0+getRegisteredCapabilities :: Session [Registration]+getRegisteredCapabilities = (Map.elems . curDynCaps) <$> get
src/Language/Haskell/LSP/Test/Parsing.hs view
@@ -75,7 +75,7 @@ satisfyMaybe pred = do    skipTimeout <- overridingTimeout <$> get-  timeoutId <- curTimeoutId <$> get+  timeoutId <- getCurTimeoutId   unless skipTimeout $ do     chan <- asks messageChan     timeout <- asks (messageTimeout . config)@@ -85,8 +85,7 @@    x <- Session await -  unless skipTimeout $-    modify $ \s -> s { curTimeoutId = timeoutId + 1 }+  unless skipTimeout (bumpTimeoutId timeoutId)    modify $ \s -> s { lastReceivedMessage = Just x } 
src/Language/Haskell/LSP/Test/Session.hs view
@@ -23,6 +23,8 @@   , sendMessage   , updateState   , withTimeout+  , getCurTimeoutId+  , bumpTimeoutId   , logMsg   , LogMsgType(..)   )@@ -61,6 +63,7 @@ import Language.Haskell.LSP.Types.Capabilities import Language.Haskell.LSP.Types import Language.Haskell.LSP.Types.Lens+import qualified Language.Haskell.LSP.Types.Lens as LSP import Language.Haskell.LSP.VFS import Language.Haskell.LSP.Test.Compat import Language.Haskell.LSP.Test.Decoding@@ -121,7 +124,9 @@   {     serverIn :: Handle   , rootDir :: FilePath-  , messageChan :: Chan SessionMessage+  , messageChan :: Chan SessionMessage -- ^ Where all messages come through+  -- Keep curTimeoutId in SessionContext, as its tied to messageChan+  , curTimeoutId :: MVar Int -- ^ The current timeout we are waiting on   , requestMap :: MVar RequestMap   , initRsp :: MVar InitializeResponse   , config :: SessionConfig@@ -139,16 +144,29 @@ instance Monad m => HasReader r (ConduitM a b (StateT s (ReaderT r m))) where   ask = lift $ lift Reader.ask +getCurTimeoutId :: (HasReader SessionContext m, MonadIO m) => m Int+getCurTimeoutId = asks curTimeoutId >>= liftIO . readMVar++-- Pass this the timeoutid you *were* waiting on+bumpTimeoutId :: (HasReader SessionContext m, MonadIO m) => Int -> m ()+bumpTimeoutId prev = do+  v <- asks curTimeoutId+  -- when updating the curtimeoutid, account for the fact that something else+  -- might have bumped the timeoutid in the meantime+  liftIO $ modifyMVar_ v (\x -> pure (max x (prev + 1)))+ data SessionState = SessionState   {     curReqId :: LspId   , vfs :: VFS   , curDiagnostics :: Map.Map NormalizedUri [Diagnostic]-  , curTimeoutId :: Int   , overridingTimeout :: Bool   -- ^ The last received message from the server.   -- Used for providing exception information   , lastReceivedMessage :: Maybe FromServerMessage+  , curDynCaps :: Map.Map T.Text Registration+  -- ^ The capabilities that the server has dynamically registered with us so+  -- far   }  class Monad m => HasState s m where@@ -166,15 +184,19 @@   get = Session (lift State.get)   put = Session . lift . State.put -instance Monad m => HasState s (ConduitM a b (StateT s m))+instance Monad m => HasState s (StateT s m) where+  get = State.get+  put = State.put++instance (Monad m, (HasState s m)) => HasState s (ConduitM a b m)  where-  get = lift State.get-  put = lift . State.put+  get = lift get+  put = lift . put -instance Monad m => HasState s (ConduitParser a (StateT s m))+instance (Monad m, (HasState s m)) => HasState s (ConduitParser a m)  where-  get = lift State.get-  put = lift . State.put+  get = lift get+  put = lift . put  runSession :: SessionContext -> SessionState -> Session a -> IO (a, SessionState) runSession context state (Session session) = runReaderT (runStateT conduit state) context@@ -200,7 +222,7 @@      watchdog :: ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()     watchdog = Conduit.awaitForever $ \msg -> do-      curId <- curTimeoutId <$> get+      curId <- getCurTimeoutId       case msg of         ServerMessage sMsg -> yield sMsg         TimeoutMessage tId -> when (curId == tId) $ lastReceivedMessage <$> get >>= throw . Timeout@@ -229,25 +251,28 @@    reqMap <- newMVar newRequestMap   messageChan <- newChan+  timeoutIdVar <- newMVar 0   initRsp <- newEmptyMVar    mainThreadId <- myThreadId -  let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config caps-      initState vfs = SessionState (IdInt 0) vfs-                                       mempty 0 False Nothing+  let context = SessionContext serverIn absRootDir messageChan timeoutIdVar reqMap initRsp config caps+      initState vfs = SessionState (IdInt 0) vfs mempty False Nothing mempty       runSession' ses = initVFS $ \vfs -> runSession context (initState vfs) ses -      errorHandler = throwTo mainThreadId :: SessionException -> IO()+      errorHandler = throwTo mainThreadId :: SessionException -> IO ()       serverListenerLauncher =         forkIO $ catch (serverHandler serverOut context) errorHandler       server = (Just serverIn, Just serverOut, Nothing, serverProc)-      serverAndListenerFinalizer tid =-        finally (timeout (messageTimeout config * 1000000)+      serverAndListenerFinalizer tid = do+        finally (timeout (messageTimeout config * 1^6)                          (runSession' exitServer))-                (cleanupProcess server >> killThread tid)+                -- Make sure to kill the listener first, before closing+                -- handles etc via cleanupProcess+                (killThread tid >> cleanupProcess server) -  (result, _) <- bracket serverListenerLauncher serverAndListenerFinalizer+  (result, _) <- bracket serverListenerLauncher+                         serverAndListenerFinalizer                          (const $ runSession' session)   return result @@ -258,12 +283,25 @@  updateState :: (MonadIO m, HasReader SessionContext m, HasState SessionState m)             => FromServerMessage -> m ()++-- Keep track of dynamic capability registration+updateState (ReqRegisterCapability req) = do+  let List newRegs = (\r -> (r ^. LSP.id, r)) <$> req ^. params . registrations+  modify $ \s ->+    s { curDynCaps = Map.union (Map.fromList newRegs) (curDynCaps s) }++updateState (ReqUnregisterCapability req) = do+  let List unRegs = (^. LSP.id) <$> req ^. params . unregistrations+  modify $ \s ->+    let newCurDynCaps = foldr' Map.delete (curDynCaps s) unRegs+    in s { curDynCaps = newCurDynCaps }+ updateState (NotPublishDiagnostics n) = do   let List diags = n ^. params . diagnostics       doc = n ^. params . uri-  modify (\s ->+  modify $ \s ->     let newDiags = Map.insert (toNormalizedUri doc) diags (curDiagnostics s)-      in s { curDiagnostics = newDiags })+      in s { curDiagnostics = newDiags }  updateState (ReqApplyWorkspaceEdit r) = do @@ -336,21 +374,20 @@   logMsg LogClient msg   liftIO $ B.hPut h (addHeader $ encode msg) --- | Execute a block f that will throw a 'Timeout' exception+-- | Execute a block f that will throw a 'Language.Haskell.LSP.Test.Exception.Timeout' exception -- after duration seconds. This will override the global timeout -- for waiting for messages to arrive defined in 'SessionConfig'. withTimeout :: Int -> Session a -> Session a withTimeout duration f = do   chan <- asks messageChan-  timeoutId <- curTimeoutId <$> get+  timeoutId <- getCurTimeoutId   modify $ \s -> s { overridingTimeout = True }   liftIO $ forkIO $ do     threadDelay (duration * 1000000)     writeChan chan (TimeoutMessage timeoutId)   res <- f-  modify $ \s -> s { curTimeoutId = timeoutId + 1,-                     overridingTimeout = False-                   }+  bumpTimeoutId timeoutId+  modify $ \s -> s { overridingTimeout = False }   return res  data LogMsgType = LogServer | LogClient@@ -375,5 +412,3 @@           | otherwise       = Cyan          showPretty = B.unpack . encodePretty--
test/Test.hs view
@@ -15,37 +15,38 @@ import           Control.Monad.IO.Class import           Control.Monad import           Control.Lens hiding (List)-import           GHC.Generics import           Language.Haskell.LSP.Messages import           Language.Haskell.LSP.Test-import           Language.Haskell.LSP.Test.Replay import           Language.Haskell.LSP.Types-import           Language.Haskell.LSP.Types.Lens as LSP hiding+import           Language.Haskell.LSP.Types.Lens hiding   (capabilities, message, rename, applyEdit)+import qualified Language.Haskell.LSP.Types.Lens as LSP import           Language.Haskell.LSP.Types.Capabilities as LSP+import           System.Directory+import           System.FilePath import           System.Timeout  {-# ANN module ("HLint: ignore Reduce duplication" :: String) #-} {-# ANN module ("HLint: ignore Unnecessary hiding" :: String) #-} -main = hspec $ do++main = findServer >>= \serverExe -> hspec $ do   describe "Session" $ do-    it "fails a test" $-      let session = runSession "hie" fullCaps "test/data/renamePass" $ do+    it "fails a test" $ do+      let session = runSession serverExe fullCaps "test/data/renamePass" $ do                       openDoc "Desktop/simple.hs" "haskell"-                      skipMany loggingNotification                       anyRequest         in session `shouldThrow` anySessionException-    it "initializeResponse" $ runSession "hie" fullCaps "test/data/renamePass" $ do+    it "initializeResponse" $ runSession serverExe fullCaps "test/data/renamePass" $ do       rsp <- initializeResponse-      liftIO $ rsp ^. result `shouldSatisfy` isLeft+      liftIO $ rsp ^. result `shouldSatisfy` isRight      it "runSessionWithConfig" $-      runSession "hie" didChangeCaps "test/data/renamePass" $ return ()+      runSession serverExe didChangeCaps "test/data/renamePass" $ return ()      describe "withTimeout" $ do       it "times out" $-        let sesh = runSession "hie" fullCaps "test/data/renamePass" $ do+        let sesh = runSession serverExe fullCaps "test/data/renamePass" $ do                     openDoc "Desktop/simple.hs" "haskell"                     -- won't receive a request - will timeout                     -- incoming logging requests shouldn't increase the@@ -56,15 +57,13 @@           in timeout 6000000 sesh `shouldThrow` anySessionException        it "doesn't time out" $-        let sesh = runSession "hie" fullCaps "test/data/renamePass" $ do+        let sesh = runSession serverExe fullCaps "test/data/renamePass" $ do                     openDoc "Desktop/simple.hs" "haskell"                     withTimeout 5 $ skipManyTill anyMessage publishDiagnosticsNotification           in void $ timeout 6000000 sesh -      it "further timeout messages are ignored" $ runSession "hie" fullCaps "test/data/renamePass" $ do+      it "further timeout messages are ignored" $ runSession serverExe fullCaps "test/data/renamePass" $ do         doc <- openDoc "Desktop/simple.hs" "haskell"-        -- warm up the cache-        getDocumentSymbols doc         -- shouldn't timeout         withTimeout 3 $ getDocumentSymbols doc         -- longer than the original timeout@@ -75,7 +74,7 @@        it "overrides global message timeout" $         let sesh =-              runSessionWithConfig (def { messageTimeout = 5 }) "hie" fullCaps "test/data/renamePass" $ do+              runSessionWithConfig (def { messageTimeout = 5 }) serverExe fullCaps "test/data/renamePass" $ do                 doc <- openDoc "Desktop/simple.hs" "haskell"                 -- shouldn't time out in here since we are overriding it                 withTimeout 10 $ liftIO $ threadDelay 7000000@@ -85,7 +84,7 @@        it "unoverrides global message timeout" $         let sesh =-              runSessionWithConfig (def { messageTimeout = 5 }) "hie" fullCaps "test/data/renamePass" $ do+              runSessionWithConfig (def { messageTimeout = 5 }) serverExe fullCaps "test/data/renamePass" $ do                 doc <- openDoc "Desktop/simple.hs" "haskell"                 -- shouldn't time out in here since we are overriding it                 withTimeout 10 $ liftIO $ threadDelay 7000000@@ -99,15 +98,15 @@      describe "SessionException" $ do       it "throw on time out" $-        let sesh = runSessionWithConfig (def {messageTimeout = 10}) "hie" fullCaps "test/data/renamePass" $ do+        let sesh = runSessionWithConfig (def {messageTimeout = 10}) serverExe fullCaps "test/data/renamePass" $ do                 skipMany loggingNotification                 _ <- message :: Session ApplyWorkspaceEditRequest                 return ()         in sesh `shouldThrow` anySessionException -      it "don't throw when no time out" $ runSessionWithConfig (def {messageTimeout = 5}) "hie" fullCaps "test/data/renamePass" $ do+      it "don't throw when no time out" $ runSessionWithConfig (def {messageTimeout = 5}) serverExe fullCaps "test/data/renamePass" $ do         loggingNotification-        liftIO $ threadDelay $ 10 * 1000000+        liftIO $ threadDelay $ 6 * 1000000         _ <- openDoc "Desktop/simple.hs" "haskell"         return () @@ -115,7 +114,7 @@         it "throws when there's an unexpected message" $           let selector (UnexpectedMessage "Publish diagnostics notification" (NotLogMessage _)) = True               selector _ = False-            in runSession "hie" fullCaps "test/data/renamePass" publishDiagnosticsNotification `shouldThrow` selector+            in runSession serverExe fullCaps "test/data/renamePass" publishDiagnosticsNotification `shouldThrow` selector         it "provides the correct types that were expected and received" $           let selector (UnexpectedMessage "ResponseMessage WorkspaceEdit" (RspDocumentSymbols _)) = True               selector _ = False@@ -124,40 +123,38 @@                 sendRequest TextDocumentDocumentSymbol (DocumentSymbolParams doc Nothing)                 skipMany anyNotification                 message :: Session RenameResponse -- the wrong type-            in runSession "hie" fullCaps "test/data/renamePass" sesh+            in runSession serverExe fullCaps "test/data/renamePass" sesh               `shouldThrow` selector -  describe "replaySession" $-    -- This is too fickle at the moment-    -- it "passes a test" $-    --   replaySession "hie" "test/data/renamePass"-    it "fails a test" $-      let selector (ReplayOutOfOrder _ _) = True-          selector _ = False-        in replaySession "hie" "test/data/renameFail" `shouldThrow` selector+  -- This is too fickle at the moment+  -- describe "replaySession" $+  --   it "passes a test" $+  --     replaySession serverExe "test/data/renamePass"+  --   it "fails a test" $+  --     let selector (ReplayOutOfOrder _ _) = True+  --         selector _ = False+  --       in replaySession serverExe "test/data/renameFail" `shouldThrow` selector -  describe "manual javascript session" $-    it "passes a test" $-      runSession "javascript-typescript-stdio" fullCaps "test/data/javascriptPass" $ do-        doc <- openDoc "test.js" "javascript"+  -- describe "manual javascript session" $+  --   it "passes a test" $+  --     runSession "javascript-typescript-stdio" fullCaps "test/data/javascriptPass" $ do+  --       doc <- openDoc "test.js" "javascript" -        noDiagnostics+  --       noDiagnostics -        Right (fooSymbol:_) <- getDocumentSymbols doc+  --       Right (fooSymbol:_) <- getDocumentSymbols doc -        liftIO $ do-          fooSymbol ^. name `shouldBe` "foo"-          fooSymbol ^. kind `shouldBe` SkFunction+  --       liftIO $ do+  --         fooSymbol ^. name `shouldBe` "foo"+  --         fooSymbol ^. kind `shouldBe` SkFunction    describe "text document VFS" $     it "sends back didChange notifications" $-      runSession "hie" def "test/data/refactor" $ do+      runSession serverExe def "test/data/refactor" $ do         doc <- openDoc "Main.hs" "haskell" -        let args = toJSON $ AOP (doc ^. uri)-                                (Position 1 14)-                                "Redundant bracket"-            reqParams = ExecuteCommandParams "applyrefact:applyOne" (Just (List [args])) Nothing+        let args = toJSON (doc ^. uri)+            reqParams = ExecuteCommandParams "doAnEdit" (Just (List [args])) Nothing         request_ WorkspaceExecuteCommand reqParams          editReq <- message :: Session ApplyWorkspaceEditRequest@@ -165,169 +162,154 @@           let (Just cs) = editReq ^. params . edit . changes               [(u, List es)] = HM.toList cs           u `shouldBe` doc ^. uri-          es `shouldBe` [TextEdit (Range (Position 1 0) (Position 1 18)) "main = return 42"]--        noDiagnostics-+          es `shouldBe` [TextEdit (Range (Position 0 0) (Position 0 5)) "howdy"]         contents <- documentContents doc-        liftIO $ contents `shouldBe` "main :: IO Int\nmain = return 42\n"+        liftIO $ contents `shouldBe` "howdy:: IO Int\nmain = return (42)\n"    describe "getDocumentEdit" $     it "automatically consumes applyedit requests" $-      runSession "hie" fullCaps "test/data/refactor" $ do+      runSession serverExe fullCaps "test/data/refactor" $ do         doc <- openDoc "Main.hs" "haskell" -        let args = toJSON $ AOP (doc ^. uri)-                                (Position 1 14)-                                "Redundant bracket"-            reqParams = ExecuteCommandParams "applyrefact:applyOne" (Just (List [args])) Nothing+        let args = toJSON (doc ^. uri)+            reqParams = ExecuteCommandParams "doAnEdit" (Just (List [args])) Nothing         request_ WorkspaceExecuteCommand reqParams         contents <- getDocumentEdit doc-        liftIO $ contents `shouldBe` "main :: IO Int\nmain = return 42\n"-        noDiagnostics+        liftIO $ contents `shouldBe` "howdy:: IO Int\nmain = return (42)\n"    describe "getCodeActions" $-    it "works" $ runSession "hie" fullCaps "test/data/refactor" $ do+    it "works" $ runSession serverExe fullCaps "test/data/refactor" $ do       doc <- openDoc "Main.hs" "haskell"       waitForDiagnostics       [CACodeAction action] <- getCodeActions doc (Range (Position 1 14) (Position 1 18))-      liftIO $ action ^. title `shouldBe` "Apply hint:Redundant bracket"+      liftIO $ action ^. title `shouldBe` "Delete this"    describe "getAllCodeActions" $-    it "works" $ runSession "hie" fullCaps "test/data/refactor" $ do+    it "works" $ runSession serverExe fullCaps "test/data/refactor" $ do       doc <- openDoc "Main.hs" "haskell"       _ <- waitForDiagnostics       actions <- getAllCodeActions doc       liftIO $ do         let [CACodeAction action] = actions-        action ^. title `shouldBe` "Apply hint:Redundant bracket"-        action ^. command . _Just . command `shouldSatisfy` T.isSuffixOf ":applyrefact:applyOne"+        action ^. title `shouldBe` "Delete this"+        action ^. command . _Just . command `shouldBe` "deleteThis"    describe "getDocumentSymbols" $-    it "works" $ runSession "hie" fullCaps "test/data/renamePass" $ do+    it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do       doc <- openDoc "Desktop/simple.hs" "haskell"        skipMany loggingNotification -      noDiagnostics-       Left (mainSymbol:_) <- getDocumentSymbols doc        liftIO $ do-        mainSymbol ^. name `shouldBe` "main"-        mainSymbol ^. kind `shouldBe` SkFunction-        mainSymbol ^. range `shouldBe` Range (Position 3 0) (Position 5 30)+        mainSymbol ^. name `shouldBe` "foo"+        mainSymbol ^. kind `shouldBe` SkObject+        mainSymbol ^. range `shouldBe` mkRange 0 0 3 6    describe "applyEdit" $ do-    it "increments the version" $ runSession "hie" docChangesCaps "test/data/renamePass" $ do+    it "increments the version" $ runSession serverExe docChangesCaps "test/data/renamePass" $ do       doc <- openDoc "Desktop/simple.hs" "haskell"       VersionedTextDocumentIdentifier _ (Just oldVersion) <- getVersionedDoc doc       let edit = TextEdit (Range (Position 1 1) (Position 1 3)) "foo"       VersionedTextDocumentIdentifier _ (Just newVersion) <- applyEdit doc edit       liftIO $ newVersion `shouldBe` oldVersion + 1-    it "changes the document contents" $ runSession "hie" fullCaps "test/data/renamePass" $ do+    it "changes the document contents" $ runSession serverExe fullCaps "test/data/renamePass" $ do       doc <- openDoc "Desktop/simple.hs" "haskell"       let edit = TextEdit (Range (Position 0 0) (Position 0 2)) "foo"       applyEdit doc edit       contents <- documentContents doc       liftIO $ contents `shouldSatisfy` T.isPrefixOf "foodule" -  describe "getCompletions" $-    it "works" $ runSession "hie" def "test/data/renamePass" $ do-      doc <- openDoc "Desktop/simple.hs" "haskell"+  -- describe "getCompletions" $+  --   it "works" $ runSession serverExe def "test/data/renamePass" $ do+  --     doc <- openDoc "Desktop/simple.hs" "haskell" -      -- wait for module to be loaded-      skipMany loggingNotification-      noDiagnostics-      noDiagnostics+  --     -- wait for module to be loaded+  --     skipMany loggingNotification+  --     noDiagnostics+  --     noDiagnostics -      comps <- getCompletions doc (Position 5 5)-      let item = head (filter (\x -> x ^. label == "interactWithUser") comps)-      liftIO $ do-        item ^. label `shouldBe` "interactWithUser"-        item ^. kind `shouldBe` Just CiFunction-        item ^. detail `shouldBe` Just "Items -> IO ()\nMain"+  --     comps <- getCompletions doc (Position 5 5)+  --     let item = head (filter (\x -> x ^. label == "interactWithUser") comps)+  --     liftIO $ do+  --       item ^. label `shouldBe` "interactWithUser"+  --       item ^. kind `shouldBe` Just CiFunction+  --       item ^. detail `shouldBe` Just "Items -> IO ()\nMain" -  describe "getReferences" $-    it "works" $ runSession "hie" fullCaps "test/data/renamePass" $ do-      doc <- openDoc "Desktop/simple.hs" "haskell"-      let pos = Position 40 3 -- interactWithUser-          uri = doc ^. LSP.uri-      refs <- getReferences doc pos True-      liftIO $ refs `shouldContain` map (Location uri) [-          mkRange 41 0 41 16-        , mkRange 75 6 75 22-        , mkRange 71 6 71 22-        ]+  -- describe "getReferences" $+  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do+  --     doc <- openDoc "Desktop/simple.hs" "haskell"+  --     let pos = Position 40 3 -- interactWithUser+  --         uri = doc ^. LSP.uri+  --     refs <- getReferences doc pos True+  --     liftIO $ refs `shouldContain` map (Location uri) [+  --         mkRange 41 0 41 16+  --       , mkRange 75 6 75 22+  --       , mkRange 71 6 71 22+  --       ] -  describe "getDefinitions" $-    it "works" $ runSession "hie" fullCaps "test/data/renamePass" $ do-      doc <- openDoc "Desktop/simple.hs" "haskell"-      let pos = Position 49 25 -- addItem-      defs <- getDefinitions doc pos-      liftIO $ defs `shouldBe` [Location (doc ^. uri) (mkRange 28 0 28 7)]+  -- describe "getDefinitions" $+  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do+  --     doc <- openDoc "Desktop/simple.hs" "haskell"+  --     let pos = Position 49 25 -- addItem+  --     defs <- getDefinitions doc pos+  --     liftIO $ defs `shouldBe` [Location (doc ^. uri) (mkRange 28 0 28 7)] -  describe "getTypeDefinitions" $-    it "works" $ runSession "hie" fullCaps "test/data/renamePass" $ do-      doc <- openDoc "Desktop/simple.hs" "haskell"-      let pos = Position 20 23  -- Quit value-      defs <- getTypeDefinitions doc pos-      liftIO $ defs `shouldBe` [Location (doc ^. uri) (mkRange 10 0 14 19)]  -- Type definition+  -- describe "getTypeDefinitions" $+  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do+  --     doc <- openDoc "Desktop/simple.hs" "haskell"+  --     let pos = Position 20 23  -- Quit value+  --     defs <- getTypeDefinitions doc pos+  --     liftIO $ defs `shouldBe` [Location (doc ^. uri) (mkRange 10 0 14 19)]  -- Type definition    describe "waitForDiagnosticsSource" $-    it "works" $ runSession "hie" fullCaps "test/data" $ do+    it "works" $ runSession serverExe fullCaps "test/data" $ do       openDoc "Error.hs" "haskell"-      [diag] <- waitForDiagnosticsSource "bios"+      [diag] <- waitForDiagnosticsSource "dummy-server"       liftIO $ do-        diag ^. severity `shouldBe` Just DsError-        diag ^. source `shouldBe` Just "bios"--  describe "rename" $ do-    it "works" $ pendingWith "HaRe not in hie-bios yet"-    it "works on javascript" $-      runSession "javascript-typescript-stdio" fullCaps "test/data/javascriptPass" $ do-        doc <- openDoc "test.js" "javascript"-        rename doc (Position 2 11) "bar"-        documentContents doc >>= liftIO . (`shouldContain` "function bar()") . T.unpack+        diag ^. severity `shouldBe` Just DsWarning+        diag ^. source `shouldBe` Just "dummy-server" -    -- runSession "hie" fullCaps "test/data" $ do-    --   doc <- openDoc "Rename.hs" "haskell"-    --   rename doc (Position 1 0) "bar"-    --   documentContents doc >>= liftIO . shouldBe "main = bar\nbar = return 42\n"+  -- describe "rename" $ do+  --   it "works" $ pendingWith "HaRe not in hie-bios yet"+  --   it "works on javascript" $+  --     runSession "javascript-typescript-stdio" fullCaps "test/data/javascriptPass" $ do+  --       doc <- openDoc "test.js" "javascript"+  --       rename doc (Position 2 11) "bar"+  --       documentContents doc >>= liftIO . (`shouldContain` "function bar()") . T.unpack    describe "getHover" $-    it "works" $ runSession "hie" fullCaps "test/data/renamePass" $ do+    it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do       doc <- openDoc "Desktop/simple.hs" "haskell"-      -- hover returns nothing until module is loaded-      skipManyTill loggingNotification $ count 2 noDiagnostics-      hover <- getHover doc (Position 45 9) -- putStrLn+      hover <- getHover doc (Position 45 9)       liftIO $ hover `shouldSatisfy` isJust -  describe "getHighlights" $-    it "works" $ runSession "hie" fullCaps "test/data/renamePass" $ do-      doc <- openDoc "Desktop/simple.hs" "haskell"-      skipManyTill loggingNotification $ count 2 noDiagnostics-      highlights <- getHighlights doc (Position 27 4) -- addItem-      liftIO $ length highlights `shouldBe` 4+  -- describe "getHighlights" $+  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do+  --     doc <- openDoc "Desktop/simple.hs" "haskell"+  --     skipManyTill loggingNotification $ count 2 noDiagnostics+  --     highlights <- getHighlights doc (Position 27 4) -- addItem+  --     liftIO $ length highlights `shouldBe` 4 -  describe "formatDoc" $-    it "works" $ runSession "hie" fullCaps "test/data" $ do-      doc <- openDoc "Format.hs" "haskell"-      oldContents <- documentContents doc-      formatDoc doc (FormattingOptions 4 True)-      documentContents doc >>= liftIO . (`shouldNotBe` oldContents)+  -- describe "formatDoc" $+  --   it "works" $ runSession serverExe fullCaps "test/data" $ do+  --     doc <- openDoc "Format.hs" "haskell"+  --     oldContents <- documentContents doc+  --     formatDoc doc (FormattingOptions 4 True)+  --     documentContents doc >>= liftIO . (`shouldNotBe` oldContents) -  describe "formatRange" $-    it "works" $ runSession "hie" fullCaps "test/data" $ do-      doc <- openDoc "Format.hs" "haskell"-      oldContents <- documentContents doc-      formatRange doc (FormattingOptions 4 True) (Range (Position 1 10) (Position 2 10))-      documentContents doc >>= liftIO . (`shouldNotBe` oldContents)+  -- describe "formatRange" $+  --   it "works" $ runSession serverExe fullCaps "test/data" $ do+  --     doc <- openDoc "Format.hs" "haskell"+  --     oldContents <- documentContents doc+  --     formatRange doc (FormattingOptions 4 True) (Range (Position 1 10) (Position 2 10))+  --     documentContents doc >>= liftIO . (`shouldNotBe` oldContents)    describe "closeDoc" $     it "works" $       let sesh =-            runSession "hie" fullCaps "test/data" $ do+            runSession serverExe fullCaps "test/data" $ do               doc <- openDoc "Format.hs" "haskell"               closeDoc doc               -- need to evaluate to throw@@ -335,7 +317,7 @@       in sesh `shouldThrow` anyException    describe "satisfy" $-    it "works" $ runSession "hie" fullCaps "test/data" $ do+    it "works" $ runSession serverExe fullCaps "test/data" $ do       openDoc "Format.hs" "haskell"       let pred (NotLogMessage _) = True           pred _ = False@@ -343,10 +325,60 @@    describe "ignoreLogNotifications" $     it "works" $-      runSessionWithConfig (defaultConfig { ignoreLogNotifications = True }) "hie"  fullCaps "test/data" $ do+      runSessionWithConfig (defaultConfig { ignoreLogNotifications = True }) serverExe  fullCaps "test/data" $ do         openDoc "Format.hs" "haskell"         void publishDiagnosticsNotification +  describe "dynamic capabilities" $ do+    it "keeps track" $ runSession serverExe fullCaps "test/data" $ do+      loggingNotification -- initialized log message++      createDoc ".register" "haskell" ""+      message :: Session RegisterCapabilityRequest++      doc <- createDoc "Foo.watch" "haskell" ""+      NotLogMessage msg <- loggingNotification+      liftIO $ msg ^. params . LSP.message `shouldBe` "got workspace/didChangeWatchedFiles"++      caps <- getRegisteredCapabilities+      liftIO $ caps `shouldBe`+        [ Registration "0" WorkspaceDidChangeWatchedFiles $ Just $ toJSON $+          DidChangeWatchedFilesRegistrationOptions $ List+          [ FileSystemWatcher "*.watch" (Just (WatchKind True True True)) ]+        ]++      -- now unregister it by sending a specific createDoc+      createDoc ".unregister" "haskell" ""+      message :: Session UnregisterCapabilityRequest++      createDoc "Bar.watch" "haskell" ""+      void $ sendRequest TextDocumentHover $ TextDocumentPositionParams doc (Position 0 0) Nothing+      count 0 $ loggingNotification+      void $ anyResponse++    it "handles absolute patterns" $ runSession serverExe fullCaps "" $ do+      curDir <- liftIO $ getCurrentDirectory++      loggingNotification -- initialized log message++      createDoc ".register.abs" "haskell" ""+      message :: Session RegisterCapabilityRequest++      doc <- createDoc (curDir </> "Foo.watch") "haskell" ""+      NotLogMessage msg <- loggingNotification+      liftIO $ msg ^. params . LSP.message `shouldBe` "got workspace/didChangeWatchedFiles"++      -- now unregister it by sending a specific createDoc+      createDoc ".unregister.abs" "haskell" ""+      message :: Session UnregisterCapabilityRequest++      createDoc (curDir </> "Bar.watch") "haskell" ""+      void $ sendRequest TextDocumentHover $ TextDocumentPositionParams doc (Position 0 0) Nothing+      count 0 $ loggingNotification+      void $ anyResponse+++mkRange :: Int -> Int -> Int -> Int -> Range mkRange sl sc el ec = Range (Position sl sc) (Position el ec)  didChangeCaps :: ClientCapabilities@@ -361,8 +393,24 @@     workspaceCaps = def { _workspaceEdit = Just editCaps }     editCaps = WorkspaceEditClientCapabilities (Just True) -data ApplyOneParams = AOP-  { file      :: Uri-  , start_pos :: Position-  , hintTitle :: String-  } deriving (Generic, ToJSON)++findExeRecursive :: FilePath -> FilePath -> IO (Maybe FilePath)+findExeRecursive exe dir = do+  me <- listToMaybe <$> findExecutablesInDirectories [dir] exe+  case me of+    Just e -> return (Just e)+    Nothing -> do+      subdirs <- (fmap (dir </>)) <$> listDirectory dir >>= filterM doesDirectoryExist+      foldM (\acc subdir -> case acc of+                              Just y -> pure $ Just y+                              Nothing -> findExeRecursive exe subdir)+            Nothing+            subdirs++-- | So we can find the dummy-server with cabal run+-- since it doesnt put build tools on the path (only cabal test)+findServer = do+  let serverName = "dummy-server"+  e <- findExecutable serverName+  e' <- findExeRecursive serverName "dist-newstyle"+  pure $ fromJust $ e <|> e'
+ test/dummy-server/Main.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE OverloadedStrings #-}+import Data.Aeson+import Data.Default+import Data.List (isSuffixOf)+import qualified Data.HashMap.Strict as HM+import Language.Haskell.LSP.Core+import Language.Haskell.LSP.Control+import Language.Haskell.LSP.Messages+import Language.Haskell.LSP.Types+import Control.Concurrent+import Control.Monad+import System.Directory+import System.FilePath++main = do+  lfvar <- newEmptyMVar+  let initCbs = InitializeCallbacks+        { onInitialConfiguration = const $ Right ()+        , onConfigurationChange = const $ Right ()+        , onStartup = \lf -> do+            putMVar lfvar lf++            return Nothing+        }+      options = def+        { executeCommandCommands = Just ["doAnEdit"]+        }+  run initCbs (handlers lfvar) options Nothing++handlers :: MVar (LspFuncs ()) -> Handlers+handlers lfvar = def+  { initializedHandler = pure $ \_ -> send $ NotLogMessage $ fmServerLogMessageNotification MtLog "initialized"+  , hoverHandler = pure $ \req -> send $+      RspHover $ makeResponseMessage req (Just (Hover (HoverContents (MarkupContent MkPlainText "hello")) Nothing))+  , documentSymbolHandler = pure $ \req -> send $+      RspDocumentSymbols $ makeResponseMessage req $ DSDocumentSymbols $+        List [ DocumentSymbol "foo"+                              Nothing+                              SkObject+                              Nothing+                              (mkRange 0 0 3 6)+                              (mkRange 0 0 3 6)+                              Nothing+             ]+  , didOpenTextDocumentNotificationHandler = pure $ \noti -> do+      let NotificationMessage _ _ (DidOpenTextDocumentParams doc) = noti+          TextDocumentItem uri _ _ _ = doc+          Just fp = uriToFilePath uri+          diag = Diagnostic (mkRange 0 0 0 1)+                            (Just DsWarning)+                            (Just (NumberValue 42))+                            (Just "dummy-server")+                            "Here's a warning"+                            Nothing+                            Nothing+      when (".hs" `isSuffixOf` fp) $ void $ forkIO $ do+        threadDelay (2 * 10^6)+        send $ NotPublishDiagnostics $+          fmServerPublishDiagnosticsNotification $ PublishDiagnosticsParams uri $ List [diag]++      -- also act as a registerer for workspace/didChangeWatchedFiles+      when (".register" `isSuffixOf` fp) $ do+        reqId <- readMVar lfvar >>= getNextReqId+        send $ ReqRegisterCapability $ fmServerRegisterCapabilityRequest reqId $+          RegistrationParams $ List $+            [ Registration "0" WorkspaceDidChangeWatchedFiles $ Just $ toJSON $+                DidChangeWatchedFilesRegistrationOptions $ List+                [ FileSystemWatcher "*.watch" (Just (WatchKind True True True)) ]+            ]+      when (".register.abs" `isSuffixOf` fp) $ do+        curDir <- getCurrentDirectory+        reqId <- readMVar lfvar >>= getNextReqId+        send $ ReqRegisterCapability $ fmServerRegisterCapabilityRequest reqId $+          RegistrationParams $ List $+            [ Registration "1" WorkspaceDidChangeWatchedFiles $ Just $ toJSON $+                DidChangeWatchedFilesRegistrationOptions $ List+                [ FileSystemWatcher (curDir </> "*.watch") (Just (WatchKind True True True)) ]+            ]++      -- also act as an unregisterer for workspace/didChangeWatchedFiles+      when (".unregister" `isSuffixOf` fp) $ do+        reqId <- readMVar lfvar >>= getNextReqId+        send $ ReqUnregisterCapability $ fmServerUnregisterCapabilityRequest reqId $+          UnregistrationParams $ List [ Unregistration "0" "workspace/didChangeWatchedFiles" ]+      when (".unregister.abs" `isSuffixOf` fp) $ do+        reqId <- readMVar lfvar >>= getNextReqId+        send $ ReqUnregisterCapability $ fmServerUnregisterCapabilityRequest reqId $+          UnregistrationParams $ List [ Unregistration "1" "workspace/didChangeWatchedFiles" ]+  , executeCommandHandler = pure $ \req -> do+      send $ RspExecuteCommand $ makeResponseMessage req Null+      reqId <- readMVar lfvar >>= getNextReqId+      let RequestMessage _ _ _ (ExecuteCommandParams "doAnEdit" (Just (List [val])) _) = req+          Success docUri = fromJSON val+          edit = List [TextEdit (mkRange 0 0 0 5) "howdy"]+      send $ ReqApplyWorkspaceEdit $ fmServerApplyWorkspaceEditRequest reqId $+        ApplyWorkspaceEditParams $ WorkspaceEdit (Just (HM.singleton docUri edit))+                                                 Nothing+  , codeActionHandler = pure $ \req -> do+      let RequestMessage _ _ _ params = req+          CodeActionParams _ _ cactx _ = params+          CodeActionContext diags _ = cactx+          caresults = fmap diag2caresult diags+          diag2caresult d = CACodeAction $+            CodeAction "Delete this"+                       Nothing+                       (Just (List [d]))+                       Nothing+                      (Just (Command "" "deleteThis" Nothing))+      send $ RspCodeAction $ makeResponseMessage req caresults+  , didChangeWatchedFilesNotificationHandler = pure $ \_ ->+      send $ NotLogMessage $ fmServerLogMessageNotification MtLog "got workspace/didChangeWatchedFiles"+  }+  where send msg = readMVar lfvar >>= \lf -> (sendFunc lf) msg++mkRange sl sc el ec = Range (Position sl sc) (Position el ec)