packages feed

lsp-client 0.3.0.0 → 0.4.0.0

raw patch · 8 files changed

+266/−215 lines, 8 filesdep +transformersdep −row-typesdep ~lspdep ~lsp-typessetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: transformers

Dependencies removed: row-types

Dependency ranges changed: lsp, lsp-types

API changes (from Hackage documentation)

- Control.Concurrent.STM.TVar.Extra: modifyTVarIO :: TVar a -> (a -> a) -> IO ()
- Control.Concurrent.STM.TVar.Extra: overTVar :: (a -> a) -> TVar a -> STM a
- Control.Concurrent.STM.TVar.Extra: overTVarIO :: (a -> a) -> TVar a -> IO a
- Control.Concurrent.STM.TVar.Extra: writeTVarIO :: TVar a -> a -> IO ()
- Language.LSP.Client.Exceptions: instance GHC.Classes.Eq Language.LSP.Client.Exceptions.SessionException
- Language.LSP.Client.Session: [initialized] :: SessionState -> TMVar InitializeResult
+ Language.LSP.Client.Session: class (Monad m) => MonadSession m
+ Language.LSP.Client.Session: getAllVersionedDocs :: MonadSession m => m [VersionedTextDocumentIdentifier]
+ Language.LSP.Client.Session: instance (Control.Monad.Trans.Class.MonadTrans t, Language.LSP.Client.Session.MonadSession m, GHC.Base.Monad (t m)) => Language.LSP.Client.Session.MonadSession (t m)
+ Language.LSP.Client.Session: instance Control.Monad.IO.Class.MonadIO m => Language.LSP.Client.Session.MonadSession (Language.LSP.Client.Session.SessionT m)
+ Language.LSP.Client.Session: liftSession :: forall a. MonadSession m => Session a -> m a
+ Language.LSP.Client.Session: type SessionT = ReaderT SessionState
- Language.LSP.Client: runSessionWithHandles :: Handle -> Handle -> Session a -> IO a
+ Language.LSP.Client: runSessionWithHandles :: MonadUnliftIO io => Handle -> Handle -> SessionT io a -> io a
- Language.LSP.Client.Exceptions: UnexpectedResponseError :: SomeLspId -> ResponseError -> SessionException
+ Language.LSP.Client.Exceptions: UnexpectedResponseError :: LspId m -> TResponseError m -> SessionException
- Language.LSP.Client.Session: SessionState :: TMVar InitializeResult -> TVar RequestMap -> TVar NotificationMap -> TVar Int32 -> TVar (HashMap Text SomeRegistration) -> ClientCapabilities -> TVar (HashSet ProgressToken) -> TQueue FromClientMessage -> TVar VFS -> FilePath -> SessionState
+ Language.LSP.Client.Session: SessionState :: TVar RequestMap -> TVar NotificationMap -> TVar Int32 -> TVar (HashMap Text SomeRegistration) -> ClientCapabilities -> TVar (HashSet ProgressToken) -> TQueue FromClientMessage -> TVar VFS -> FilePath -> SessionState
- Language.LSP.Client.Session: changeDoc :: TextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> Session ()
+ Language.LSP.Client.Session: changeDoc :: MonadSession m => TextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> m ()
- Language.LSP.Client.Session: clearNotificationCallback :: forall (m :: Method 'ServerToClient 'Notification). SMethod m -> Session ()
+ Language.LSP.Client.Session: clearNotificationCallback :: forall (method :: Method 'ServerToClient 'Notification) m. MonadSession m => SMethod method -> m ()
- Language.LSP.Client.Session: closeDoc :: TextDocumentIdentifier -> Session ()
+ Language.LSP.Client.Session: closeDoc :: MonadSession m => TextDocumentIdentifier -> m ()
- Language.LSP.Client.Session: createDoc :: FilePath -> Text -> Text -> Session TextDocumentIdentifier
+ Language.LSP.Client.Session: createDoc :: MonadSession m => FilePath -> LanguageKind -> Text -> m TextDocumentIdentifier
- Language.LSP.Client.Session: defaultSessionState :: VFS -> IO SessionState
+ Language.LSP.Client.Session: defaultSessionState :: MonadIO io => VFS -> io SessionState
- Language.LSP.Client.Session: documentContents :: TextDocumentIdentifier -> Session (Maybe Rope)
+ Language.LSP.Client.Session: documentContents :: MonadSession m => TextDocumentIdentifier -> m (Maybe Rope)
- Language.LSP.Client.Session: getDocUri :: FilePath -> Session Uri
+ Language.LSP.Client.Session: getDocUri :: MonadSession m => FilePath -> m Uri
- Language.LSP.Client.Session: getResponseResult :: TResponseMessage m -> MessageResult m
+ Language.LSP.Client.Session: getResponseResult :: Show (ErrorData m) => TResponseMessage m -> MessageResult m
- Language.LSP.Client.Session: getVersionedDoc :: TextDocumentIdentifier -> Session VersionedTextDocumentIdentifier
+ Language.LSP.Client.Session: getVersionedDoc :: MonadSession m => TextDocumentIdentifier -> m VersionedTextDocumentIdentifier
- Language.LSP.Client.Session: handleServerMessage :: FromServerMessage -> Session ()
+ Language.LSP.Client.Session: handleServerMessage :: MonadSession m => FromServerMessage -> m ()
- Language.LSP.Client.Session: initialize :: Session ()
+ Language.LSP.Client.Session: initialize :: MonadSession m => Maybe Value -> m InitializeResult
- Language.LSP.Client.Session: lspClientInfo :: Rec (("name" .== Text) .+ ("version" .== Maybe Text))
+ Language.LSP.Client.Session: lspClientInfo :: ClientInfo
- Language.LSP.Client.Session: openDoc :: FilePath -> Text -> Session TextDocumentIdentifier
+ Language.LSP.Client.Session: openDoc :: MonadSession m => FilePath -> LanguageKind -> m TextDocumentIdentifier
- Language.LSP.Client.Session: openDoc' :: FilePath -> Text -> Text -> Session TextDocumentIdentifier
+ Language.LSP.Client.Session: openDoc' :: MonadSession m => FilePath -> LanguageKind -> Text -> m TextDocumentIdentifier
- Language.LSP.Client.Session: receiveNotification :: forall (m :: Method 'ServerToClient 'Notification). TMessage m ~ TNotificationMessage m => SMethod m -> (TMessage m -> IO ()) -> Session ()
+ Language.LSP.Client.Session: receiveNotification :: forall (method :: Method 'ServerToClient 'Notification) m. (TMessage method ~ TNotificationMessage method, MonadSession m) => SMethod method -> (TMessage method -> IO ()) -> m ()
- Language.LSP.Client.Session: request :: forall (m :: Method 'ClientToServer 'Request). TMessage m ~ TRequestMessage m => SMethod m -> MessageParams m -> Session (TResponseMessage m)
+ Language.LSP.Client.Session: request :: forall (method :: Method 'ClientToServer 'Request) m. (TMessage method ~ TRequestMessage method, MonadSession m) => SMethod method -> MessageParams method -> m (TResponseMessage method)
- Language.LSP.Client.Session: sendMessage :: FromClientMessage -> Session ()
+ Language.LSP.Client.Session: sendMessage :: MonadSession m => FromClientMessage -> m ()
- Language.LSP.Client.Session: sendNotification :: forall (m :: Method 'ClientToServer 'Notification). TMessage m ~ TNotificationMessage m => SMethod m -> MessageParams m -> Session ()
+ Language.LSP.Client.Session: sendNotification :: forall (method :: Method 'ClientToServer 'Notification) m. (TMessage method ~ TNotificationMessage method, MonadSession m) => SMethod method -> MessageParams method -> m ()
- Language.LSP.Client.Session: sendRequest :: forall (m :: Method 'ClientToServer 'Request). TMessage m ~ TRequestMessage m => SMethod m -> MessageParams m -> (TResponseMessage m -> IO ()) -> Session (LspId m)
+ Language.LSP.Client.Session: sendRequest :: forall (method :: Method 'ClientToServer 'Request) m. (TMessage method ~ TRequestMessage method, MonadSession m) => SMethod method -> MessageParams method -> (TResponseMessage method -> IO ()) -> m (LspId method)
- Language.LSP.Client.Session: sendResponse :: forall (m :: Method 'ServerToClient 'Request). TRequestMessage m -> Either ResponseError (MessageResult m) -> Session ()
+ Language.LSP.Client.Session: sendResponse :: forall (method :: Method 'ServerToClient 'Request) m. MonadSession m => TRequestMessage method -> Either (TResponseError method) (MessageResult method) -> m ()
- Language.LSP.Client.Session: type Session = ReaderT SessionState IO
+ Language.LSP.Client.Session: type Session = SessionT IO

Files

CHANGELOG.md view
@@ -1,5 +1,14 @@ # Revision history for lsp-client +## 0.4.0.0  -- 2024-08-09++* Update to lsp-2.7 and lsp-types-2.3+* Introduce SessionT and MonadSession+* Lift all functions to MonadSession+* Session.initialize now takes LSP initialization options and returns the initialization result+* Session state no longer holds initialization result+* Add Session.getAllVersionedDocs+ ## 0.3.0.0  -- 2024-04-04  * Support lsp 2.4
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
lsp-client.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.0 name:                lsp-client-version:             0.3.0.0+version:             0.4.0.0 synopsis:            Haskell library for Language Server Protocol clients homepage:            https://github.com/ners/lsp-client/blob/master/README.md license:             Apache-2.0@@ -49,9 +49,8 @@         base >= 4.16 && < 5,         bytestring >= 0.9 && < 0.13,         lens >= 5 && < 6,-        lsp-types >= 2 && < 3,+        lsp-types ^>= 2.3,         unliftio >= 0.2 && < 0.3,-        row-types,  library     import:           common@@ -68,15 +67,17 @@         extra,         filepath,         generic-lens,-        lsp >= 2.4,+        lsp ^>= 2.7,         mtl,         stm,         text,         text-rope,-        unordered-containers,+        transformers,         unix-compat >= 0.7.1 && < 0.8,-    exposed-modules:+        unordered-containers,+    other-modules:         Control.Concurrent.STM.TVar.Extra,+    exposed-modules:         Language.LSP.Client,         Language.LSP.Client.Decoding,         Language.LSP.Client.Encoding,
src/Control/Concurrent/STM/TVar/Extra.hs view
@@ -4,7 +4,7 @@ import Prelude  overTVar :: (a -> a) -> TVar a -> STM a-overTVar f var = stateTVar var (\x -> (f x, f x))+overTVar f var = stateTVar var $ \x -> let y = f x in (y, y)  overTVarIO :: (a -> a) -> TVar a -> IO a overTVarIO = (atomically .) . overTVar
src/Language/LSP/Client.hs view
@@ -5,7 +5,6 @@  import Control.Concurrent.STM import Control.Monad (forever)-import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Reader (asks, runReaderT) import Data.ByteString.Lazy qualified as LazyByteString import Data.Dependent.Map qualified as DMap@@ -17,20 +16,21 @@ import Language.LSP.Protocol.Message qualified as LSP import Language.LSP.VFS (emptyVFS) import System.IO (Handle)-import UnliftIO (concurrently_, race)+import UnliftIO (MonadUnliftIO, concurrently_, liftIO, race) import Prelude  {- | Starts a new session, using the specified handles to communicate with the server. -} runSessionWithHandles-    :: Handle+    :: (MonadUnliftIO io)+    => Handle     -- ^ The input handle: messages sent from the server to the client will be read from here     -> Handle     -- ^ The output handle: messages sent by the client will be written here-    -> Session a+    -> SessionT io a     -- ^ Session actions-    -> IO a+    -> io a runSessionWithHandles input output action = do     initialState <- defaultSessionState emptyVFS     flip runReaderT initialState $ do
src/Language/LSP/Client/Exceptions.hs view
@@ -8,9 +8,10 @@ import Data.ByteString.Lazy.Char8 qualified as LazyByteString import Data.List (nub) import Language.LSP.Protocol.Message-    ( FromServerMessage-    , ResponseError-    , SomeLspId+    ( ErrorData+    , FromServerMessage+    , LspId+    , TResponseError     ) import Prelude @@ -22,11 +23,10 @@     | ReplayOutOfOrder FromServerMessage [FromServerMessage]     | UnexpectedDiagnostics     | IncorrectApplyEditRequest String-    | UnexpectedResponseError SomeLspId ResponseError+    | forall m. (Show (ErrorData m)) => UnexpectedResponseError (LspId m) (TResponseError m)     | UnexpectedServerTermination     | IllegalInitSequenceMessage FromServerMessage     | MessageSendError Value IOError-    deriving stock (Eq)  instance Exception SessionException 
src/Language/LSP/Client/Session.hs view
@@ -1,6 +1,7 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-name-shadowing #-} {-# OPTIONS_GHC -Wno-orphans #-}-{-# LANGUAGE CPP #-}  module Language.LSP.Client.Session where @@ -13,8 +14,10 @@ import Control.Lens.Extras (is) import Control.Monad (unless, when) import Control.Monad.IO.Class (MonadIO (liftIO))-import Control.Monad.Reader (ReaderT, asks)+import Control.Monad.Reader (ReaderT (runReaderT), ask, asks) import Control.Monad.State (StateT, execState)+import Control.Monad.Trans.Class (MonadTrans, lift)+import Data.Aeson (Value) import Data.Default (def) import Data.Foldable (foldl', foldr', forM_, toList) import Data.Function (on)@@ -28,40 +31,26 @@ import Data.List.Extra (groupOn) import Data.Map.Strict qualified as Map import Data.Maybe (fromJust, fromMaybe, mapMaybe)-import Data.Row import Data.Text (Text) import Data.Text qualified as Text import Data.Text.IO qualified as Text import Data.Text.Utf16.Rope.Mixed (Rope) import Language.LSP.Client.Decoding import Language.LSP.Client.Exceptions (SessionException (UnexpectedResponseError))-import Language.LSP.Protocol.Capabilities (fullCaps)-import Language.LSP.Protocol.Lens hiding (error, to)+import Language.LSP.Protocol.Capabilities (fullLatestClientCaps)+import Language.LSP.Protocol.Lens qualified as LSP import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types-import System.PosixCompat.Process (getProcessID) import Language.LSP.VFS-    ( VFS-    , VfsLog-    , VirtualFile (..)-    , changeFromClientVFS-    , changeFromServerVFS-    , closeVFS-    , lsp_version-    , openVFS-    , vfsMap-    , virtualFileVersion-    ) import System.Directory (canonicalizePath) import System.FilePath (isAbsolute, (</>)) import System.FilePath.Glob qualified as Glob+import System.PosixCompat.Process (getProcessID) import Prelude hiding (id) import Prelude qualified  data SessionState = SessionState-    { initialized :: TMVar InitializeResult-    -- ^ The response of the initialization handshake, if any.-    , pendingRequests :: TVar RequestMap+    { pendingRequests :: TVar RequestMap     -- ^ Response callbacks for sent requests waiting for a response. Once a response arrives the request is removed from this map.     , notificationHandlers :: TVar NotificationMap     -- ^ Notification callbacks that fire whenever a notification of their type is received.@@ -81,9 +70,8 @@     -- ^ The root of the project as sent to the server. Document URIs are relative to it. Not a `TVar` because it does not change during the session.     } -defaultSessionState :: VFS -> IO SessionState-defaultSessionState vfs' = do-    initialized <- newEmptyTMVarIO+defaultSessionState :: (MonadIO io) => VFS -> io SessionState+defaultSessionState vfs' = liftIO $ do     pendingRequests <- newTVarIO emptyRequestMap     notificationHandlers <- newTVarIO emptyNotificationMap     lastRequestId <- newTVarIO 0@@ -102,51 +90,55 @@ It is essentially an STM-backed `StateT`: despite it being `ReaderT`, it can still mutate `TVar` values. -}-type Session = ReaderT SessionState IO+type SessionT = ReaderT SessionState -documentChangeUri :: DocumentChange -> Uri-documentChangeUri (InL x) = x ^. textDocument . uri-documentChangeUri (InR (InL x)) = x ^. uri-documentChangeUri (InR (InR (InL x))) = x ^. oldUri-documentChangeUri (InR (InR (InR x))) = x ^. uri+type Session = SessionT IO --- eitherOf :: APrism' s a -> (a -> b) -> (s -> b) -> s -> b--- eitherOf p a b = either b a . matching p------ anyOf :: [APrism' s a] -> (a -> b) -> b -> s -> b--- anyOf [] _ b = const b--- anyOf (p : prisms) a b = eitherOf p a $ anyOf prisms a b+class (Monad m) => MonadSession m where+    liftSession :: forall a. Session a -> m a +instance {-# OVERLAPPING #-} (MonadIO m) => MonadSession (SessionT m) where+    liftSession a = liftIO . runReaderT a =<< ask++instance {-# OVERLAPPABLE #-} (MonadTrans t, MonadSession m, Monad (t m)) => MonadSession (t m) where+    liftSession = lift . liftSession++documentChangeUri :: DocumentChange -> Uri+documentChangeUri (InL x) = x ^. LSP.textDocument . LSP.uri+documentChangeUri (InR (InL x)) = x ^. LSP.uri+documentChangeUri (InR (InR (InL x))) = x ^. LSP.oldUri+documentChangeUri (InR (InR (InR x))) = x ^. LSP.uri+ {- | Fires whenever the client receives a message from the server. Updates the session state as needed. Note that this does not provide any business logic beyond updating the session state; you most likely want to use `sendRequest` and `receiveNotification` to register callbacks for specific messages. -}-handleServerMessage :: FromServerMessage -> Session ()+handleServerMessage :: (MonadSession m) => FromServerMessage -> m () handleServerMessage (FromServerMess SMethod_Progress req) =-    when (anyOf folded ($ req ^. params . value) [is _workDoneProgressBegin, is _workDoneProgressEnd])-        $ asks progressTokens-        >>= liftIO-        . flip modifyTVarIO (HashSet.insert $ req ^. params . token)+    liftSession . when (anyOf folded ($ req ^. LSP.params . LSP.value) [is _workDoneProgressBegin, is _workDoneProgressEnd]) $+        asks progressTokens+            >>= liftIO+                . flip modifyTVarIO (HashSet.insert $ req ^. LSP.params . LSP.token) handleServerMessage (FromServerMess SMethod_ClientRegisterCapability req) =-    asks serverCapabilities >>= liftIO . flip modifyTVarIO (HashMap.union (HashMap.fromList newRegs))+    liftSession $ asks serverCapabilities >>= liftIO . flip modifyTVarIO (HashMap.union (HashMap.fromList newRegs))   where-    regs = req ^.. params . registrations . traversed . to toSomeRegistration . _Just-    newRegs = (\sr@(SomeRegistration r) -> (r ^. id, sr)) <$> regs+    regs = req ^.. LSP.params . LSP.registrations . traversed . to toSomeRegistration . _Just+    newRegs = (\sr@(SomeRegistration r) -> (r ^. LSP.id, sr)) <$> regs handleServerMessage (FromServerMess SMethod_ClientUnregisterCapability req) =-    asks serverCapabilities >>= liftIO . flip modifyTVarIO (flip (foldr' HashMap.delete) unRegs)+    liftSession $ asks serverCapabilities >>= liftIO . flip modifyTVarIO (flip (foldr' HashMap.delete) unRegs)   where-    unRegs = (^. id) <$> req ^. params . unregisterations-handleServerMessage (FromServerMess SMethod_WorkspaceApplyEdit r) = do+    unRegs = (^. LSP.id) <$> req ^. LSP.params . LSP.unregisterations+handleServerMessage (FromServerMess SMethod_WorkspaceApplyEdit r) = liftSession $ do     -- First, prefer the versioned documentChanges field-    allChangeParams <- case r ^. params . edit . documentChanges of+    allChangeParams <- case r ^. LSP.params . LSP.edit . LSP.documentChanges of         Just cs -> do             mapM_ (checkIfNeedsOpened . documentChangeUri) cs             -- replace the user provided version numbers with the VFS ones + 1             -- (technically we should check that the user versions match the VFS ones)-            cs' <- traverseOf (traverse . _L . textDocument) bumpNewestVersion cs+            cs' <- traverseOf (traverse . _L . LSP.textDocument) bumpNewestVersion cs             return $ mapMaybe getParamsFromDocumentChange cs'         -- Then fall back to the changes field-        Nothing -> case r ^. params . edit . changes of+        Nothing -> case r ^. LSP.params . LSP.edit . LSP.changes of             Just cs -> do                 mapM_ checkIfNeedsOpened (Map.keys cs)                 concat <$> mapM (uncurry getChangeParams) (Map.toList cs)@@ -155,24 +147,22 @@      asks vfs >>= liftIO . flip modifyTVarIO (execState $ changeFromServerVFS logger r) -    let groupedParams = groupOn (view textDocument) allChangeParams+    let groupedParams = groupOn (view LSP.textDocument) allChangeParams         mergedParams = mergeParams <$> groupedParams      forM_ mergedParams $ sendNotification SMethod_TextDocumentDidChange      -- Update VFS to new document versions-    let sortedVersions = sortBy (compare `on` (^. textDocument . version)) <$> groupedParams-        latestVersions = view textDocument . last <$> sortedVersions+    let sortedVersions = sortBy (compare `on` (^. LSP.textDocument . LSP.version)) <$> groupedParams+        latestVersions = view LSP.textDocument . last <$> sortedVersions -    forM_ latestVersions $ \(VersionedTextDocumentIdentifier uri v) ->+    forM_ latestVersions $ \VersionedTextDocumentIdentifier{..} ->         asks vfs             >>= liftIO-            . flip-                modifyTVarIO-                ( \vfs -> do-                    let update (VirtualFile _ file_ver t) = VirtualFile v (file_ver + 1) t-                     in vfs & vfsMap . ix (toNormalizedUri uri) %~ update-                )+                . flip+                    modifyTVarIO+                    ( vfsMap . ix (toNormalizedUri _uri) %~ ((lsp_version .~ _version) . (file_version +~ 1))+                    )     sendResponse         r         $ Right@@ -183,7 +173,9 @@                 }   where     logger :: LogAction (StateT VFS Identity) (WithSeverity VfsLog)-    logger = LogAction $ \(WithSeverity msg sev) -> case sev of Error -> error $ show msg; _ -> pure ()+    logger = LogAction $ \WithSeverity{..} -> case getSeverity of Error -> error $ show getMsg; _ -> pure ()++    checkIfNeedsOpened :: Uri -> Session ()     checkIfNeedsOpened uri = do         isOpen <- asks vfs >>= liftIO . readTVarIO <&> has (vfsMap . ix (toNormalizedUri uri)) @@ -204,33 +196,43 @@      getParamsFromTextDocumentEdit :: TextDocumentEdit -> Maybe DidChangeTextDocumentParams     getParamsFromTextDocumentEdit (TextDocumentEdit docId edits) = do-        DidChangeTextDocumentParams <$> docId ^? _versionedTextDocumentIdentifier <*> pure (editToChangeEvent <$> edits)+        _textDocument <- docId ^? _versionedTextDocumentIdentifier+        let _contentChanges = editToChangeEvent <$> edits+        pure DidChangeTextDocumentParams{..}      editToChangeEvent :: TextEdit |? AnnotatedTextEdit -> TextDocumentContentChangeEvent-    editToChangeEvent (InR e) = TextDocumentContentChangeEvent $ InL $ #range .== (e ^. range) .+ #rangeLength .== Nothing .+ #text .== (e ^. newText)-    editToChangeEvent (InL e) = TextDocumentContentChangeEvent $ InL $ #range .== (e ^. range) .+ #rangeLength .== Nothing .+ #text .== (e ^. newText)+    editToChangeEvent (InR e) = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial{_rangeLength = Nothing, _range = e ^. LSP.range, _text = e ^. LSP.newText}+    editToChangeEvent (InL e) = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial{_rangeLength = Nothing, _range = e ^. LSP.range, _text = e ^. LSP.newText}      getParamsFromDocumentChange :: DocumentChange -> Maybe DidChangeTextDocumentParams     getParamsFromDocumentChange (InL textDocumentEdit) = getParamsFromTextDocumentEdit textDocumentEdit     getParamsFromDocumentChange _ = Nothing      bumpNewestVersion :: OptionalVersionedTextDocumentIdentifier -> Session OptionalVersionedTextDocumentIdentifier-    bumpNewestVersion (OptionalVersionedTextDocumentIdentifier uri (InL _)) = do-        nextVersion <- head <$> textDocumentVersions uri-        pure $ OptionalVersionedTextDocumentIdentifier uri $ InL nextVersion._version+    bumpNewestVersion OptionalVersionedTextDocumentIdentifier{_uri, _version = InL _} = do+        VersionedTextDocumentIdentifier{_version} <- head <$> textDocumentVersions _uri+        pure OptionalVersionedTextDocumentIdentifier{_version = InL _version, ..}     bumpNewestVersion i = pure i -    -- For a uri returns an infinite list of versions [n,n+1,n+2,...]+    -- For a uri returns an infinite list of versions [n+1,n+2,...]     -- where n is the current version     textDocumentVersions :: Uri -> Session [VersionedTextDocumentIdentifier]-    textDocumentVersions uri = do-        vfs <- asks vfs >>= liftIO . readTVarIO-        let curVer = fromMaybe 0 $ vfs ^? vfsMap . ix (toNormalizedUri uri) . lsp_version-        pure $ VersionedTextDocumentIdentifier uri <$> [curVer + 1 ..]+    textDocumentVersions _uri = do+        tail . iterate (LSP.version +~ 1) <$> getVersionedDoc TextDocumentIdentifier{_uri} +    textDocumentEdits :: Uri -> [TextEdit] -> Session [TextDocumentEdit]     textDocumentEdits uri edits = do-        vers <- textDocumentVersions uri-        pure $ zipWith (\v e -> TextDocumentEdit (review _versionedTextDocumentIdentifier v) [InL e]) vers edits+        versions <- textDocumentVersions uri+        pure $+            zipWith+                ( \v e ->+                    TextDocumentEdit+                        { _edits = [InL e]+                        , _textDocument = review _versionedTextDocumentIdentifier v+                        }+                )+                versions+                edits      getChangeParams uri edits = do         edits <- textDocumentEdits uri (reverse edits)@@ -238,73 +240,76 @@      mergeParams :: [DidChangeTextDocumentParams] -> DidChangeTextDocumentParams     mergeParams params =-        let events = concat $ toList $ toList . (^. contentChanges) <$> params-         in DidChangeTextDocumentParams (head params ^. textDocument) events-handleServerMessage (FromServerMess SMethod_WindowWorkDoneProgressCreate req) = sendResponse req $ Right Null+        DidChangeTextDocumentParams+            { _contentChanges = concat . toList $ toList . (^. LSP.contentChanges) <$> params+            , _textDocument = head params ^. LSP.textDocument+            }+handleServerMessage (FromServerMess SMethod_WindowWorkDoneProgressCreate req) = liftSession . sendResponse req $ Right Null handleServerMessage _ = pure ()  {- | Sends a request to the server, with a callback that fires when the response arrives. Multiple requests can be waiting at the same time. -} sendRequest-    :: forall (m :: Method 'ClientToServer 'Request)-     . (TMessage m ~ TRequestMessage m)-    => SMethod m-    -> MessageParams m-    -> (TResponseMessage m -> IO ())-    -> Session (LspId m)-sendRequest requestMethod params requestCallback = do-    reqId <- asks lastRequestId >>= liftIO . overTVarIO (+ 1) <&> IdInt-    asks pendingRequests >>= liftIO . flip modifyTVarIO (updateRequestMap reqId RequestCallback{..})-    sendMessage $ fromClientReq $ TRequestMessage "2.0" reqId requestMethod params-    pure reqId+    :: forall (method :: Method 'ClientToServer 'Request) m+     . (TMessage method ~ TRequestMessage method, MonadSession m)+    => SMethod method+    -> MessageParams method+    -> (TResponseMessage method -> IO ())+    -> m (LspId method)+sendRequest requestMethod _params requestCallback = liftSession $ do+    _id <- asks lastRequestId >>= liftIO . overTVarIO (+ 1) <&> IdInt+    asks pendingRequests >>= liftIO . flip modifyTVarIO (updateRequestMap _id RequestCallback{..})+    sendMessage $ fromClientReq TRequestMessage{_jsonrpc = "2.0", _method = requestMethod, ..}+    pure _id  {- | Send a response to the server. This is used internally to acknowledge server requests. Users of this library cannot register callbacks to server requests, so this function is probably of no use to them. -} sendResponse-    :: forall (m :: Method 'ServerToClient 'Request)-     . TRequestMessage m-    -> Either ResponseError (MessageResult m)-    -> Session ()-sendResponse req res = do-    sendMessage $ FromClientRsp req._method $ TResponseMessage req._jsonrpc (Just req._id) res+    :: forall (method :: Method 'ServerToClient 'Request) m+     . (MonadSession m)+    => TRequestMessage method+    -> Either (TResponseError method) (MessageResult method)+    -> m ()+sendResponse TRequestMessage{..} _result = liftSession . sendMessage $ FromClientRsp _method TResponseMessage{_id = Just _id, ..} +-- {_id = Just _id, ..}+ -- | Sends a request to the server and synchronously waits for its response. request-    :: forall (m :: Method 'ClientToServer 'Request)-     . (TMessage m ~ TRequestMessage m)-    => SMethod m-    -> MessageParams m-    -> Session (TResponseMessage m)-request method params = do+    :: forall (method :: Method 'ClientToServer 'Request) m+     . (TMessage method ~ TRequestMessage method, MonadSession m)+    => SMethod method+    -> MessageParams method+    -> m (TResponseMessage method)+request method params = liftSession $ do     done <- liftIO newEmptyMVar     void $ sendRequest method params $ putMVar done     liftIO $ takeMVar done  {- | Checks the response for errors and throws an exception if needed.- Returns the result if successful.+ Returns the result if successful.InitializeParams -}-getResponseResult :: TResponseMessage m -> MessageResult m-getResponseResult response = either err Prelude.id $ response ^. result+getResponseResult :: (Show (ErrorData m)) => TResponseMessage m -> MessageResult m+getResponseResult response = either err Prelude.id $ response ^. LSP.result   where-    lid = SomeLspId $ fromJust response._id-    err = throw . UnexpectedResponseError lid+    err = throw . UnexpectedResponseError (fromJust $ response ^. LSP.id)  -- | Sends a notification to the server. Updates the VFS if the notification is a document update. sendNotification-    :: forall (m :: Method 'ClientToServer 'Notification)-     . (TMessage m ~ TNotificationMessage m)-    => SMethod m-    -> MessageParams m-    -> Session ()-sendNotification m params = do+    :: forall (method :: Method 'ClientToServer 'Notification) m+     . (TMessage method ~ TNotificationMessage method, MonadSession m)+    => SMethod method+    -> MessageParams method+    -> m ()+sendNotification m params = liftSession $ do     let n = TNotificationMessage "2.0" m params     vfs <- asks vfs     case m of-        SMethod_TextDocumentDidOpen -> liftIO $ modifyTVarIO vfs (execState $ openVFS mempty n)-        SMethod_TextDocumentDidClose -> liftIO $ modifyTVarIO vfs (execState $ closeVFS mempty n)-        SMethod_TextDocumentDidChange -> liftIO $ modifyTVarIO vfs (execState $ changeFromClientVFS mempty n)+        SMethod_TextDocumentDidOpen -> liftIO . modifyTVarIO vfs . execState $ openVFS mempty n+        SMethod_TextDocumentDidClose -> liftIO . modifyTVarIO vfs . execState $ closeVFS mempty n+        SMethod_TextDocumentDidChange -> liftIO . modifyTVarIO vfs . execState $ changeFromClientVFS mempty n         _ -> pure ()     sendMessage $ fromClientNot n @@ -312,46 +317,49 @@ If multiple callbacks are registered for the same notification method, they will all be called. -} receiveNotification-    :: forall (m :: Method 'ServerToClient 'Notification)-     . (TMessage m ~ TNotificationMessage m)-    => SMethod m-    -> (TMessage m -> IO ())-    -> Session ()+    :: forall (method :: Method 'ServerToClient 'Notification) m+     . (TMessage method ~ TNotificationMessage method, MonadSession m)+    => SMethod method+    -> (TMessage method -> IO ())+    -> m () receiveNotification method notificationCallback =-    asks notificationHandlers-        >>= liftIO-        . flip-            modifyTVarIO-            ( appendNotificationCallback method NotificationCallback{..}-            )+    liftSession $+        asks notificationHandlers+            >>= liftIO+                . flip+                    modifyTVarIO+                    ( appendNotificationCallback method NotificationCallback{..}+                    )  {- | Clears the registered callback for the given notification method, if any. If multiple callbacks have been registered, this clears /all/ of them. -} clearNotificationCallback-    :: forall (m :: Method 'ServerToClient 'Notification)-     . SMethod m-    -> Session ()+    :: forall (method :: Method 'ServerToClient 'Notification) m+     . (MonadSession m)+    => SMethod method+    -> m () clearNotificationCallback method =-    asks notificationHandlers-        >>= liftIO-        . flip-            modifyTVarIO-            ( removeNotificationCallback method-            )+    liftSession $+        asks notificationHandlers+            >>= liftIO+                . flip+                    modifyTVarIO+                    ( removeNotificationCallback method+                    )  -- | Queues a message to be sent to the server at the client's earliest convenience.-sendMessage :: FromClientMessage -> Session ()-sendMessage msg = asks outgoing >>= liftIO . atomically . (`writeTQueue` msg)+sendMessage :: (MonadSession m) => FromClientMessage -> m ()+sendMessage msg = liftSession $ asks outgoing >>= liftIO . atomically . (`writeTQueue` msg) -lspClientInfo :: Rec ("name" .== Text .+ "version" .== Maybe Text)-lspClientInfo = #name .== "lsp-client" .+ #version .== Just CURRENT_PACKAGE_VERSION+lspClientInfo :: ClientInfo+lspClientInfo = ClientInfo{_name = "lsp-client", _version = Just CURRENT_PACKAGE_VERSION}  {- | Performs the initialisation handshake and synchronously waits for its completion. When the function completes, the session is initialised. -}-initialize :: Session ()-initialize = do+initialize :: (MonadSession m) => Maybe Value -> m InitializeResult+initialize options = liftSession $ do     pid <- liftIO getProcessID     response <-         request@@ -363,13 +371,13 @@                 , _locale = Nothing                 , _rootPath = Nothing                 , _rootUri = InR Null-                , _initializationOptions = Nothing-                , _capabilities = fullCaps-                , _trace = Just TraceValues_Off+                , _initializationOptions = options+                , _capabilities = fullLatestClientCaps+                , _trace = Just TraceValue_Off                 , _workspaceFolders = Nothing                 }-    asks initialized >>= liftIO . atomically . flip putTMVar (getResponseResult response)     sendNotification SMethod_Initialized InitializedParams+    pure $ getResponseResult response  {- | /Creates/ a new text document. This is different from 'openDoc'  as it sends a @workspace/didChangeWatchedFiles@ notification letting the server@@ -380,15 +388,16 @@  the server that one does exist. -} createDoc-    :: FilePath+    :: (MonadSession m)+    => FilePath     -- ^ The path to the document to open, __relative to the root directory__.-    -> Text-    -- ^ The text document's language identifier, e.g. @"haskell"@.+    -> LanguageKind+    -- ^ The text document's language     -> Text     -- ^ The content of the text document to create.-    -> Session TextDocumentIdentifier+    -> m TextDocumentIdentifier     -- ^ The identifier of the document just created.-createDoc file language contents = do+createDoc file language contents = liftSession $ do     serverCaps <- asks serverCapabilities >>= liftIO . readTVarIO     clientCaps <- asks clientCapabilities     rootDir <- asks rootDir@@ -399,84 +408,114 @@         regs :: [TRegistration 'Method_WorkspaceDidChangeWatchedFiles]         regs = concatMap pred $ HashMap.elems serverCaps         watchHits :: FileSystemWatcher -> Bool-        watchHits (FileSystemWatcher (GlobPattern (InL (Pattern pattern))) kind) =-            fileMatches (Text.unpack pattern) && maybe True containsCreate kind+        watchHits FileSystemWatcher{_globPattern = GlobPattern (InL (Pattern pattern)), _kind} =+            fileMatches (Text.unpack pattern) && maybe True containsCreate _kind         watchHits _ = False          fileMatches :: String -> Bool         fileMatches pattern = Glob.match (Glob.compile pattern) (if isAbsolute pattern then absFile else file)          regHits :: TRegistration 'Method_WorkspaceDidChangeWatchedFiles -> Bool-        regHits reg = any watchHits $ reg ^. registerOptions . _Just . watchers+        regHits reg = any watchHits $ reg ^. LSP.registerOptions . _Just . LSP.watchers          clientCapsSupports =             clientCaps-                ^? workspace+                ^? LSP.workspace                     . _Just-                    . didChangeWatchedFiles+                    . LSP.didChangeWatchedFiles                     . _Just-                    . dynamicRegistration+                    . LSP.dynamicRegistration                     . _Just-                    == Just True+                == Just True         shouldSend = clientCapsSupports && foldl' (\acc r -> acc || regHits r) False regs -    when shouldSend-        $ sendNotification SMethod_WorkspaceDidChangeWatchedFiles-        $ DidChangeWatchedFilesParams-            [FileEvent (filePathToUri $ rootDir </> file) FileChangeType_Created]+    when shouldSend $+        sendNotification+            SMethod_WorkspaceDidChangeWatchedFiles+            DidChangeWatchedFilesParams+                { _changes =+                    [ FileEvent+                        { _type_ = FileChangeType_Created+                        , _uri = filePathToUri $ rootDir </> file+                        }+                    ]+                }     openDoc' file language contents  {- | Opens a text document that /exists on disk/, and sends a  @textDocument/didOpen@ notification to the server. -}-openDoc :: FilePath -> Text -> Session TextDocumentIdentifier-openDoc file language = do+openDoc :: (MonadSession m) => FilePath -> LanguageKind -> m TextDocumentIdentifier+openDoc file language = liftSession $ do     rootDir <- asks rootDir-    let fp = rootDir </> file-    contents <- liftIO $ Text.readFile fp+    contents <- liftIO . Text.readFile $ rootDir </> file     openDoc' file language 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 -> Text -> Text -> Session TextDocumentIdentifier-openDoc' file language contents = do+openDoc' :: (MonadSession m) => FilePath -> LanguageKind -> Text -> m TextDocumentIdentifier+openDoc' file language contents = liftSession $ do     rootDir <- asks rootDir-    let fp = rootDir </> file-        uri = filePathToUri fp-        item = TextDocumentItem uri language 0 contents-    sendNotification SMethod_TextDocumentDidOpen (DidOpenTextDocumentParams item)-    pure $ TextDocumentIdentifier uri+    let _uri = filePathToUri $ rootDir </> file+    sendNotification+        SMethod_TextDocumentDidOpen+        DidOpenTextDocumentParams+            { _textDocument =+                TextDocumentItem+                    { _text = contents+                    , _languageId = language+                    , _version = 0+                    , _uri+                    }+            }+    pure TextDocumentIdentifier{..}  -- | Closes a text document and sends a @textDocument/didClose@ notification to the server.-closeDoc :: TextDocumentIdentifier -> Session ()-closeDoc docId = do-    let params = DidCloseTextDocumentParams (TextDocumentIdentifier (docId ^. uri))-    sendNotification SMethod_TextDocumentDidClose params+closeDoc :: (MonadSession m) => TextDocumentIdentifier -> m ()+closeDoc docId =+    liftSession $+        sendNotification+            SMethod_TextDocumentDidClose+            DidCloseTextDocumentParams+                { _textDocument =+                    TextDocumentIdentifier+                        { _uri = docId ^. LSP.uri+                        }+                }  -- | Changes a text document and sends a @textDocument/didChange@ notification to the server.-changeDoc :: TextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> Session ()-changeDoc docId changes = do-    verDoc <- getVersionedDoc docId-    let params = DidChangeTextDocumentParams (verDoc & version +~ 1) changes-    sendNotification SMethod_TextDocumentDidChange params+changeDoc :: (MonadSession m) => TextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> m ()+changeDoc docId _contentChanges = liftSession $ do+    _textDocument <- getVersionedDoc docId <&> LSP.version +~ 1+    sendNotification SMethod_TextDocumentDidChange DidChangeTextDocumentParams{..}  -- | Gets the Uri for the file relative to the session's root directory.-getDocUri :: FilePath -> Session Uri-getDocUri file = do+getDocUri :: (MonadSession m) => FilePath -> m Uri+getDocUri file = liftSession $ do     rootDir <- asks rootDir-    let fp = rootDir </> file-    return $ filePathToUri fp+    pure . filePathToUri $ rootDir </> file  -- | The current text contents of a document.-documentContents :: TextDocumentIdentifier -> Session (Maybe Rope)-documentContents (TextDocumentIdentifier uri) = do+documentContents :: (MonadSession m) => TextDocumentIdentifier -> m (Maybe Rope)+documentContents TextDocumentIdentifier{_uri} = liftSession $ do     vfs <- asks vfs >>= liftIO . readTVarIO-    pure $ vfs ^? vfsMap . ix (toNormalizedUri uri) . to _file_text+    pure $ vfs ^? vfsMap . ix (toNormalizedUri _uri) . to _file_text  -- | Adds the current version to the document, as tracked by the session.-getVersionedDoc :: TextDocumentIdentifier -> Session VersionedTextDocumentIdentifier-getVersionedDoc (TextDocumentIdentifier uri) = do+getVersionedDoc :: (MonadSession m) => TextDocumentIdentifier -> m VersionedTextDocumentIdentifier+getVersionedDoc TextDocumentIdentifier{_uri} = liftSession $ do     vfs <- asks vfs >>= liftIO . readTVarIO-    let ver = fromMaybe 0 $ vfs ^? vfsMap . ix (toNormalizedUri uri) . to virtualFileVersion-    pure $ VersionedTextDocumentIdentifier uri ver+    let _version = fromMaybe 0 $ vfs ^? vfsMap . ix (toNormalizedUri _uri) . to virtualFileVersion+    pure VersionedTextDocumentIdentifier{..}++-- | Get all the versioned documents tracked by the session.+getAllVersionedDocs :: (MonadSession m) => m [VersionedTextDocumentIdentifier]+getAllVersionedDocs = liftSession $ do+    vfs <- asks vfs >>= liftIO . readTVarIO+    pure $+        Map.toList (vfs ^. vfsMap) <&> \(nuri, vf) ->+            VersionedTextDocumentIdentifier+                { _uri = fromNormalizedUri nuri+                , _version = virtualFileVersion vf+                }
test/Language/LSP/ClientSpec.hs view
@@ -1,8 +1,7 @@ module Language.LSP.ClientSpec where  import Control.Arrow ((>>>))-import Control.Concurrent.STM.TVar.Extra (writeTVarIO)-import Control.Exception+import Control.Exception (AssertionFailed (..)) import Control.Lens ((^.)) import Control.Monad import Control.Monad.Extra (whenMaybeM, whileJustM, whileM)@@ -14,7 +13,6 @@ import Data.ByteString.Lazy qualified as LazyByteString import Data.Coerce (coerce) import Data.Maybe (fromJust)-import Data.Row import Data.String (IsString) import Data.Tuple.Extra (thd3) import Language.LSP.Client@@ -25,13 +23,12 @@ import Language.LSP.Protocol.Lens qualified as LSP import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types-import System.IO import System.Process (createPipe) import Test.Hspec hiding (shouldReturn) import Test.Hspec qualified as Hspec import Test.Hspec.QuickCheck import Test.QuickCheck-import UnliftIO (MonadIO (..), MonadUnliftIO, fromEither, newTVarIO, race, readTVarIO)+import UnliftIO import UnliftIO.Concurrent import Prelude hiding (log) @@ -39,9 +36,9 @@ shouldReturn a expected = a >>= liftIO . flip Hspec.shouldBe expected  withTimeout :: forall m a. (MonadUnliftIO m) => Int -> m a -> m a-withTimeout delay a = fromEither =<< race timeout a+withTimeout delay = fromEither <=< race t   where-    timeout = do+    t = do         threadDelay delay         pure $ AssertionFailed "Timeout exceeded" @@ -134,6 +131,9 @@ getAvailableContents :: Handle -> IO ByteString getAvailableContents h = whileJustM $ whenMaybeM (hReady h) (hGetSome h defaultChunkSize) +writeTVarIO :: TVar a -> a -> IO ()+writeTVarIO = (atomically .) . writeTVar+ spec :: Spec spec = do     prop "concurrently handles actions and server messages" $ again $ do@@ -176,7 +176,7 @@                 LSP.documentContents doc `shouldReturn` Just ""                 let content :: (IsString s) => s                     content = "foo\n\nbar"-                changeDoc doc [TextDocumentContentChangeEvent $ InR $ #text .== content]+                changeDoc doc [TextDocumentContentChangeEvent $ InR TextDocumentContentChangeWholeDocument{_text = content}]                 LSP.documentContents doc `shouldReturn` Just content                 closeDoc doc                 LSP.documentContents doc `shouldReturn` Nothing