packages feed

lsp 2.3.0.0 → 2.4.0.0

raw patch · 11 files changed

+451/−416 lines, 11 filesdep ~aesondep ~asyncdep ~attoparsecPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, async, attoparsec, bytestring, co-log-core, containers, data-default, directory, exceptions, filepath, hashable, lens, lens-aeson, mtl, prettyprinter, random, row-types, text, text-rope, transformers, unliftio-core, unordered-containers

API changes (from Hackage documentation)

- Language.LSP.VFS: PosPrefixInfo :: !Text -> !Text -> !Text -> !Position -> PosPrefixInfo
- Language.LSP.VFS: [$sel:cursorPos:PosPrefixInfo] :: PosPrefixInfo -> !Position
- Language.LSP.VFS: [$sel:fullLine:PosPrefixInfo] :: PosPrefixInfo -> !Text
- Language.LSP.VFS: [$sel:prefixModule:PosPrefixInfo] :: PosPrefixInfo -> !Text
- Language.LSP.VFS: [$sel:prefixText:PosPrefixInfo] :: PosPrefixInfo -> !Text
- Language.LSP.VFS: data PosPrefixInfo
- Language.LSP.VFS: getCompletionPrefix :: Monad m => Position -> VirtualFile -> m (Maybe PosPrefixInfo)
- Language.LSP.VFS: instance GHC.Classes.Eq Language.LSP.VFS.PosPrefixInfo
- Language.LSP.VFS: instance GHC.Show.Show Language.LSP.VFS.PosPrefixInfo
- Language.LSP.VFS: instance Language.LSP.VFS.HasFile_text Language.LSP.VFS.VirtualFile Data.Text.Utf16.Rope.Rope
+ Language.LSP.Server: [optSupportClientInitiatedProgress] :: Options -> Bool
+ Language.LSP.VFS: instance Language.LSP.VFS.HasFile_text Language.LSP.VFS.VirtualFile Data.Text.Utf16.Rope.Mixed.Rope
- Language.LSP.Server: Options :: Maybe TextDocumentSyncOptions -> Maybe [Char] -> Maybe [Char] -> Maybe [Char] -> Maybe [Char] -> Maybe [CodeActionKind] -> Maybe (NonEmpty Char) -> Maybe [Text] -> Maybe (Rec (("name" .== Text) .+ ("version" .== Maybe Text))) -> Options
+ Language.LSP.Server: Options :: Maybe TextDocumentSyncOptions -> Maybe [Char] -> Maybe [Char] -> Maybe [Char] -> Maybe [Char] -> Maybe [CodeActionKind] -> Maybe (NonEmpty Char) -> Maybe [Text] -> Maybe (Rec (("name" .== Text) .+ ("version" .== Maybe Text))) -> Bool -> Options
- Language.LSP.Server: registerCapability :: forall f t (m :: Method ClientToServer t) config. MonadLsp config f => SClientMethod m -> RegistrationOptions m -> Handler f m -> f (Maybe (RegistrationToken m))
+ Language.LSP.Server: registerCapability :: forall f t (m :: Method ClientToServer t) config. MonadLsp config f => LogAction f (WithSeverity LspCoreLog) -> SClientMethod m -> RegistrationOptions m -> Handler f m -> f (Maybe (RegistrationToken m))
- Language.LSP.Server: withIndefiniteProgress :: MonadLsp c m => Text -> ProgressCancellable -> m a -> m a
+ Language.LSP.Server: withIndefiniteProgress :: MonadLsp c m => Text -> Maybe ProgressToken -> ProgressCancellable -> ((Text -> m ()) -> m a) -> m a
- Language.LSP.Server: withProgress :: MonadLsp c m => Text -> ProgressCancellable -> ((ProgressAmount -> m ()) -> m a) -> m a
+ Language.LSP.Server: withProgress :: MonadLsp c m => Text -> Maybe ProgressToken -> ProgressCancellable -> ((ProgressAmount -> m ()) -> m a) -> m a

Files

ChangeLog.md view
@@ -1,5 +1,19 @@ # Revision history for lsp +## 2.4.0.0++- Server-created progress now will not send reports until and unless the client +  confirms the progress token creation.+- Progress helper functions now can take a progress token provided by the client, +  so client-initiated progress can now be supported properly.+- The server options now allow the user to say whether the server should advertise+  support for client-initiated progress or not.+- The server now dynamically registers for `workspace/didChangeConfiguration` +  notifications, to ensure that newer clients continue to send them.+- Removed `getCompletionPrefix` from the `VFS` module. This is specific to completing+  Haskell identifiers and doesn't belong here. It has already been moved to `ghcide`+  some time ago.+ ## 2.3.0.0  - Fix inference of server capabilities for newer methods (except notebook methods).
example/Reactor.hs view
@@ -1,11 +1,8 @@ {-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeInType #-} -- So we can keep using the old prettyprinter modules (which have a better -- compatibility range) for now.@@ -221,11 +218,16 @@                let regOpts = LSP.CodeLensRegistrationOptions (LSP.InR LSP.Null) Nothing (Just False) -              void $ registerCapability LSP.SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do-                logger <& "Processing a textDocument/codeLens request" `WithSeverity` Info-                let cmd = LSP.Command "Say hello" "lsp-hello-command" Nothing-                    rsp = [LSP.CodeLens (LSP.mkRange 0 0 0 100) (Just cmd) Nothing]-                responder (Right $ LSP.InL rsp)+              void+                $ registerCapability+                  mempty+                  LSP.SMethod_TextDocumentCodeLens+                  regOpts+                $ \_req responder -> do+                  logger <& "Processing a textDocument/codeLens request" `WithSeverity` Info+                  let cmd = LSP.Command "Say hello" "lsp-hello-command" Nothing+                      rsp = [LSP.CodeLens (LSP.mkRange 0 0 0 100) (Just cmd) Nothing]+                  responder (Right $ LSP.InL rsp)     , notificationHandler LSP.SMethod_TextDocumentDidOpen $ \msg -> do         let doc = msg ^. LSP.params . LSP.textDocument . LSP.uri             fileName = LSP.uriToFilePath doc@@ -313,7 +315,7 @@          logger <& ("The arguments are: " <> T.pack (show margs)) `WithSeverity` Debug         responder (Right $ LSP.InL (J.Object mempty)) -- respond to the request-        void $ withProgress "Executing some long running command" Cancellable $ \update ->+        void $ withProgress "Executing some long running command" (req ^. LSP.params . LSP.workDoneToken) Cancellable $ \update ->           forM [(0 :: LSP.UInt) .. 10] $ \i -> do             update (ProgressAmount (Just (i * 10)) (Just "Doing stuff"))             liftIO $ threadDelay (1 * 1000000)
example/Simple.hs view
@@ -21,7 +21,7 @@           Right (InL (MessageActionItem "Turn on")) -> do             let regOpts = CodeLensRegistrationOptions (InR Null) Nothing (Just False) -            _ <- registerCapability SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do+            _ <- registerCapability mempty SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do               let cmd = Command "Say hello" "lsp-hello-command" Nothing                   rsp = [CodeLens (mkRange 0 0 0 100) (Just cmd) Nothing]               responder $ Right $ InL rsp
lsp.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.2 name:               lsp-version:            2.3.0.0+version:            2.4.0.0 synopsis:           Haskell library for the Microsoft Language Server Protocol description:   An implementation of the types, and basic message server to@@ -28,10 +28,8 @@  library   hs-source-dirs:     src-  default-language:   Haskell2010-  default-extensions: ImportQualifiedPost+  default-language:   GHC2021   ghc-options:        -Wall -fprint-explicit-kinds-   reexported-modules:     , Language.LSP.Protocol.Types     , Language.LSP.Protocol.Lens@@ -51,45 +49,44 @@    ghc-options:        -Wall   build-depends:-    , aeson                 >=1.0.0.0-    , async                 >=2.0-    , attoparsec-    , base                  >=4.11    && <5-    , bytestring-    , co-log-core           >=0.3.1.0-    , containers-    , data-default-    , directory-    , exceptions-    , filepath-    , hashable-    , lens-aeson-    , lens                  >=4.15.2+    , aeson                 >=2      && <2.3+    , async                 ^>=2.2+    , attoparsec            ^>=0.14+    , base                  >=4.11   && <5+    , bytestring            >=0.10   && <0.13+    , co-log-core           ^>=0.3+    , containers            ^>=0.6+    , data-default          ^>=0.7+    , directory             ^>=1.3+    , exceptions            ^>=0.10+    , filepath              >=1.4 && < 1.6+    , hashable              ^>=1.4+    , lens                  >=5.1    && <5.3+    , lens-aeson            ^>=1.2     , lsp-types             ^>=2.1-    , mtl                   <2.4-    , prettyprinter-    , random-    , row-types+    , mtl                   >=2.2    && <2.4+    , prettyprinter         ^>=1.7+    , random                ^>=1.2+    , row-types             ^>=1.0     , sorted-list           ^>=0.2.1     , stm                   ^>=2.5-    , text-    , text-rope-    , transformers          >=0.5.6   && <0.7-    , unliftio-core         >=0.2.0.0-    , unordered-containers+    , text                  >=1      && <2.2+    , text-rope             ^>=0.2+    , transformers          >=0.5    && <0.7+    , unliftio-core         ^>=0.2+    , unordered-containers  ^>=0.2     , uuid                  >=1.3  executable lsp-demo-reactor-server-  main-is:          Reactor.hs-  hs-source-dirs:   example-  default-language: Haskell2010-  default-extensions: ImportQualifiedPost-  ghc-options:      -Wall -Wno-unticked-promoted-constructors+  main-is:            Reactor.hs+  hs-source-dirs:     example+  default-language:   GHC2021+  ghc-options:        -Wall -Wno-unticked-promoted-constructors   build-depends:     , aeson     , base     , co-log-core-    , lens           >=4.15.2+    , lens     , lsp     , prettyprinter     , stm@@ -100,11 +97,10 @@     buildable: False  executable lsp-demo-simple-server-  main-is:          Simple.hs-  hs-source-dirs:   example-  default-language: Haskell2010-  default-extensions: ImportQualifiedPost-  ghc-options:      -Wall -Wno-unticked-promoted-constructors+  main-is:            Simple.hs+  hs-source-dirs:     example+  default-language:   GHC2021+  ghc-options:        -Wall -Wno-unticked-promoted-constructors   build-depends:     , base     , lsp@@ -133,12 +129,11 @@     , hspec     , lsp     , row-types-    , sorted-list           >=0.2.1 && <0.2.2+    , sorted-list     , text     , text-rope     , unordered-containers    build-tool-depends: hspec-discover:hspec-discover   ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall-  default-language:   Haskell2010-  default-extensions: ImportQualifiedPost+  default-language:   GHC2021
src/Language/LSP/Diagnostics.hs view
@@ -1,6 +1,4 @@ {-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}  {- 
src/Language/LSP/Server.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ExplicitNamespaces #-}  module Language.LSP.Server (   module Language.LSP.Server.Control,
src/Language/LSP/Server/Control.hs view
@@ -1,7 +1,5 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-} -- So we can keep using the old prettyprinter modules (which have a better -- compatibility range) for now. {-# OPTIONS_GHC -Wno-deprecations #-}
src/Language/LSP/Server/Core.hs view
@@ -1,20 +1,13 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE BinaryLiterals #-} {-# LANGUAGE DerivingVia #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RoleAnnotations #-}-{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilyDependencies #-} {-# LANGUAGE TypeInType #-}-{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE CUSKs #-} {-# OPTIONS_GHC -Wno-unticked-promoted-constructors #-} {-# OPTIONS_GHC -fprint-explicit-kinds #-} @@ -26,7 +19,9 @@   WithSeverity (..),   (<&),  )+import Control.Applicative import Control.Concurrent.Async+import Control.Concurrent.MVar import Control.Concurrent.STM import Control.Exception qualified as E import Control.Lens (at, (^.), (^?), _Just)@@ -68,7 +63,7 @@ import Language.LSP.Protocol.Utils.Misc (prettyJSON) import Language.LSP.Protocol.Utils.SMethodMap (SMethodMap) import Language.LSP.Protocol.Utils.SMethodMap qualified as SMethodMap-import Language.LSP.VFS+import Language.LSP.VFS hiding (end) import Prettyprinter import System.Random hiding (next) @@ -87,8 +82,10 @@   | ConfigurationNotSupported   | BadConfigurationResponse ResponseError   | WrongConfigSections [J.Value]-  deriving (Show)+  | forall m. CantRegister (SMethod m) +deriving instance (Show LspCoreLog)+ instance Pretty LspCoreLog where   pretty (NewConfig config) = "LSP: set new config:" <+> prettyJSON config   pretty (ConfigurationNotSupported) = "LSP: not requesting configuration since the client does not support workspace/configuration"@@ -101,6 +98,7 @@       ]   pretty (BadConfigurationResponse err) = "LSP: error when requesting configuration: " <+> pretty err   pretty (WrongConfigSections sections) = "LSP: expected only one configuration section, got: " <+> (prettyJSON $ J.toJSON sections)+  pretty (CantRegister m) = "LSP: can't register dynamically for:" <+> pretty m  newtype LspT config m a = LspT {unLspT :: ReaderT (LanguageContextEnv config) m a}   deriving (Functor, Applicative, Monad, MonadCatch, MonadIO, MonadMask, MonadThrow, MonadTrans, MonadUnliftIO, MonadFix)@@ -284,6 +282,8 @@   -- If you set `executeCommandHandler`, you **must** set this.   , optServerInfo :: Maybe (Rec ("name" .== Text .+ "version" .== Maybe Text))   -- ^ Information about the server that can be advertised to the client.+  , optSupportClientInitiatedProgress :: Bool+  -- ^ Whether or not to support client-initiated progress.   }  instance Default Options where@@ -298,6 +298,7 @@       Nothing       Nothing       Nothing+      False  defaultOptions :: Options defaultOptions = def@@ -552,30 +553,27 @@ registerCapability ::   forall f t (m :: Method ClientToServer t) config.   MonadLsp config f =>+  LogAction f (WithSeverity LspCoreLog) ->   SClientMethod m ->   RegistrationOptions m ->   Handler f m ->   f (Maybe (RegistrationToken m))-registerCapability method regOpts f = do-  clientCaps <- resClientCapabilities <$> getLspEnv+registerCapability logger method regOpts f = do   handlers <- resHandlers <$> getLspEnv   let alreadyStaticallyRegistered = case splitClientMethod method of         IsClientNot -> SMethodMap.member method $ notHandlers handlers         IsClientReq -> SMethodMap.member method $ reqHandlers handlers         IsClientEither -> error "Cannot register capability for custom methods"-  go clientCaps alreadyStaticallyRegistered+  go alreadyStaticallyRegistered  where   -- If the server has already registered statically, don't dynamically register   -- as per the spec-  go _clientCaps True = pure Nothing-  go clientCaps False-    -- First, check to see if the client supports dynamic registration on this method-    | dynamicRegistrationSupported method clientCaps = do-        uuid <- liftIO $ UUID.toText <$> getStdRandom random-        let registration = L.TRegistration uuid method (Just regOpts)-            params = L.RegistrationParams [toUntypedRegistration registration]-            regId = RegistrationId uuid-        rio <- askUnliftIO+  go True = pure Nothing+  go False = do+    rio <- askUnliftIO+    mtoken <- trySendRegistration logger method regOpts+    case mtoken of+      Just token@(RegistrationToken _ regId) -> do         ~() <- case splitClientMethod method of           IsClientNot -> modifyState resRegistrationsNot $ \oldRegs ->             let pair = Pair regId (ClientMessageHandler (unliftIO rio . f))@@ -585,12 +583,34 @@              in SMethodMap.insert method pair oldRegs           IsClientEither -> error "Cannot register capability for custom methods" -        -- TODO: handle the scenario where this returns an error-        _ <- sendRequest SMethod_ClientRegisterCapability params $ \_res -> pure ()+        pure $ Just token+      Nothing -> pure Nothing -        pure (Just (RegistrationToken method regId))-    | otherwise = pure Nothing+trySendRegistration ::+  forall f t (m :: Method ClientToServer t) config.+  MonadLsp config f =>+  LogAction f (WithSeverity LspCoreLog) ->+  SClientMethod m ->+  RegistrationOptions m ->+  f (Maybe (RegistrationToken m))+trySendRegistration logger method regOpts = do+  clientCaps <- resClientCapabilities <$> getLspEnv+  -- First, check to see if the client supports dynamic registration on this method+  if dynamicRegistrationSupported method clientCaps+    then do+      uuid <- liftIO $ UUID.toText <$> getStdRandom random+      let registration = L.TRegistration uuid method (Just regOpts)+          params = L.RegistrationParams [toUntypedRegistration registration]+          regId = RegistrationId uuid +      -- TODO: handle the scenario where this returns an error+      _ <- sendRequest SMethod_ClientRegisterCapability params $ \_res -> pure ()++      pure (Just $ RegistrationToken method regId)+    else do+      logger <& (CantRegister SMethod_WorkspaceDidChangeConfiguration) `WithSeverity` Warning+      pure Nothing+ {- | Sends a @client/unregisterCapability@ request and removes the handler  for that associated registration. -}@@ -625,98 +645,163 @@      in (L.ProgressToken $ L.InL cur, next) {-# INLINE getNewProgressId #-} -withProgressBase :: MonadLsp c m => Bool -> Text -> ProgressCancellable -> ((ProgressAmount -> m ()) -> m a) -> m a-withProgressBase indefinite title cancellable f = do-  progId <- getNewProgressId+{- | The progress states we can be in.+See Note [Progress states]+-}+data ProgressState = ProgressInitial | ProgressStarted ProgressToken | ProgressEnded -  let initialPercentage-        | indefinite = Nothing-        | otherwise = Just 0-      cancellable' = case cancellable of-        Cancellable -> True-        NotCancellable -> False+withProgressBase ::+  forall c m a.+  MonadLsp c m =>+  Bool ->+  Text ->+  Maybe ProgressToken ->+  ProgressCancellable ->+  ((ProgressAmount -> m ()) -> m a) ->+  m a+withProgressBase indefinite title clientToken cancellable f = do+  progressState <- liftIO $ newMVar ProgressInitial -  -- Create progress token-  -- FIXME  : This needs to wait until the request returns before-  -- continuing!!!-  _ <- sendRequest-    SMethod_WindowWorkDoneProgressCreate-    (WorkDoneProgressCreateParams progId)-    $ \res -> do-      case res of-        -- An error occurred when the client was setting it up-        -- No need to do anything then, as per the spec-        Left _err -> pure ()-        Right _ -> pure ()+  -- Until we start the progress reporting, track the current latest progress in an MVar, so when+  -- we do start we can start at the right point.+  let initialPercentage = if indefinite then Nothing else Just 0+  initialProgress <- liftIO $ newMVar (ProgressAmount initialPercentage Nothing) -  -- Send the begin and done notifications via 'bracket_' so that they are always fired-  res <- withRunInIO $ \runInBase ->-    E.bracket_-      -- Send begin notification-      ( runInBase $-          sendNotification SMethod_Progress $-            ProgressParams progId $-              J.toJSON $-                WorkDoneProgressBegin L.AString title (Just cancellable') Nothing initialPercentage-      )-      -- Send end notification-      ( runInBase $-          sendNotification SMethod_Progress $-            ProgressParams progId $-              J.toJSON $-                (WorkDoneProgressEnd L.AString Nothing)-      )-      $ do-        -- Run f asynchronously-        aid <- async $ runInBase $ f (updater progId)-        runInBase $ storeProgress progId aid-        wait aid+  let+    sendProgressReport :: (J.ToJSON r) => ProgressToken -> r -> m ()+    sendProgressReport token report = sendNotification SMethod_Progress $ ProgressParams token $ J.toJSON report -  -- Delete the progress cancellation from the map-  -- If we don't do this then it's easy to leak things as the map contains any IO action.-  deleteProgress progId+    -- See Note [Progress states]+    tryStart :: ProgressToken -> m ()+    tryStart t = withRunInIO $ \runInBase -> modifyMVar_ progressState $ \case+      -- Can start if we are in the initial state, otherwise not+      ProgressInitial -> withMVar initialProgress $ \(ProgressAmount pct msg) -> do+        let+          cancellable' = case cancellable of+            Cancellable -> Just True+            NotCancellable -> Just False+        runInBase $ sendProgressReport t $ WorkDoneProgressBegin L.AString title cancellable' msg pct+        pure (ProgressStarted t)+      s -> pure s+    -- See Note [Progress states]+    tryUpdate :: ProgressAmount -> m ()+    tryUpdate (ProgressAmount pct msg) = withRunInIO $ \runInBase -> withMVar progressState $ \case+      -- If the progress has not started yet, then record the latest progress percentage+      ProgressInitial -> modifyMVar_ initialProgress $ \(ProgressAmount oldPct oldMsg) -> do+        let+          -- Update the percentage if the new one is not nothing+          newPct = pct <|> oldPct+          -- Update the message if the new one is not nothing+          newMsg = msg <|> oldMsg+        pure $ ProgressAmount newPct newMsg+      -- Just send the update, we don't need to worry about updating initialProgress any more+      ProgressStarted t -> runInBase $ sendProgressReport t $ WorkDoneProgressReport L.AString Nothing msg pct+      _ -> pure ()+    -- See Note [Progress states]+    tryEnd :: m ()+    tryEnd = withRunInIO $ \runInBase -> modifyMVar_ progressState $ \case+      -- Don't send an end message unless we successfully started+      ProgressStarted t -> do+        runInBase $ sendProgressReport t $ WorkDoneProgressEnd L.AString Nothing+        pure ProgressEnded+      -- But in all cases we still want to transition state+      _ -> pure ProgressEnded -  return res- where-  updater progId (ProgressAmount percentage msg) = do-    sendNotification SMethod_Progress $-      ProgressParams progId $-        J.toJSON $-          WorkDoneProgressReport L.AString Nothing msg percentage+    -- The progress token is also used as the cancellation ID+    -- See Note [Request cancellation]+    createAndStart :: m ProgressToken+    createAndStart =+      case clientToken of+        -- See Note [Client- versus server-initiated progress]+        -- Client-initiated progress+        Just t -> tryStart t >> pure t+        -- Try server-initiated progress+        Nothing -> do+          t <- getNewProgressId+          clientCaps <- getClientCapabilities -clientSupportsProgress :: L.ClientCapabilities -> Bool-clientSupportsProgress caps = fromMaybe False $ caps ^? L.window . _Just . L.workDoneProgress . _Just-{-# INLINE clientSupportsProgress #-}+          -- If we don't have a progress token from the client and+          -- the client doesn't support server-initiated progress then+          -- there's nothing to do: we can't report progress.+          -- But we still need to return our internal token to use for+          -- cancellation+          when (clientSupportsServerInitiatedProgress clientCaps)+            $ void+            $+            -- Server-initiated progress+            -- See Note [Client- versus server-initiated progress]+            sendRequest+              SMethod_WindowWorkDoneProgressCreate+              (WorkDoneProgressCreateParams t)+            $ \case+              -- Successfully registered the token, we can now use it.+              -- So we go ahead and start. We do this as soon as we get the+              -- token back so the client gets feedback ASAP+              Right _ -> tryStart t+              -- The client sent us an error, we can't use the token. So we remain+              -- in ProgressInitial and don't send any progress updates ever+              -- TODO: log the error+              Left _err -> pure () -{- | Wrapper for reporting progress to the client during a long running- task.- 'withProgress' @title cancellable f@ starts a new progress reporting- session, and finishes it once f is completed.- f is provided with an update function that allows it to report on- the progress during the session.- If @cancellable@ is 'Cancellable', @f@ will be thrown a- 'ProgressCancelledException' if the user cancels the action in- progress.--}-withProgress :: MonadLsp c m => Text -> ProgressCancellable -> ((ProgressAmount -> m ()) -> m a) -> m a-withProgress title cancellable f = do-  clientCaps <- getClientCapabilities-  if clientSupportsProgress clientCaps-    then withProgressBase False title cancellable f-    else f (const $ return ())+          pure t -{- | Same as 'withProgress', but for processes that do not report the- precentage complete.+    end :: ProgressToken -> m ()+    end cancellationId = do+      tryEnd+      -- Delete the progress cancellation from the map+      -- If we don't do this then it's easy to leak things as the map contains any IO action.+      deleteProgress cancellationId - @since 0.10.0.0+  -- Send the begin and done notifications via 'bracket' so that they are always fired+  withRunInIO $ \runInBase ->+    E.bracket (runInBase createAndStart) (runInBase . end) $ \cancellationId -> do+      -- Run f asynchronously+      aid <- async $ runInBase $ f tryUpdate+      -- Always store the thread ID so we can cancel, see Note [Request cancellation]+      runInBase $ storeProgress cancellationId aid+      wait aid++clientSupportsServerInitiatedProgress :: L.ClientCapabilities -> Bool+clientSupportsServerInitiatedProgress caps = fromMaybe False $ caps ^? L.window . _Just . L.workDoneProgress . _Just+{-# INLINE clientSupportsServerInitiatedProgress #-}++{- |+Wrapper for reporting progress to the client during a long running task. -}-withIndefiniteProgress :: MonadLsp c m => Text -> ProgressCancellable -> m a -> m a-withIndefiniteProgress title cancellable f = do-  clientCaps <- getClientCapabilities-  if clientSupportsProgress clientCaps-    then withProgressBase True title cancellable (const f)-    else f+withProgress ::+  MonadLsp c m =>+  -- | The title of the progress operation+  Text ->+  -- | The progress token provided by the client in the method params, if any+  Maybe ProgressToken ->+  -- | Whether or not this operation is cancellable. If true, the user will be+  -- shown a button to allow cancellation. Note that requests can still be cancelled+  -- even if this is not set.+  ProgressCancellable ->+  -- | An update function to pass progress updates to+  ((ProgressAmount -> m ()) -> m a) ->+  m a+withProgress title clientToken cancellable f = withProgressBase False title clientToken cancellable f +{- |+Same as 'withProgress', but for processes that do not report the precentage complete.+-}+withIndefiniteProgress ::+  MonadLsp c m =>+  -- | The title of the progress operation+  Text ->+  -- | The progress token provided by the client in the method params, if any+  Maybe ProgressToken ->+  -- | Whether or not this operation is cancellable. If true, the user will be+  -- shown a button to allow cancellation. Note that requests can still be cancelled+  -- even if this is not set.+  ProgressCancellable ->+  -- | An update function to pass progress updates to+  ((Text -> m ()) -> m a) ->+  m a+withIndefiniteProgress title clientToken cancellable f =+  withProgressBase True title clientToken cancellable (\update -> f (\msg -> update (ProgressAmount Nothing (Just msg))))+ -- ---------------------------------------------------------------------  {- | Aggregate all diagnostics pertaining to a particular version of a document,@@ -865,4 +950,42 @@ try to parse the entire config object. This hopefully lets us handle a variety of sensible cases where the client sends us mostly our config, either wrapped in our section or not.+-}++{- Note [Progress states]+Creating and using progress actually requires a small state machine.+The states are:+- ProgressInitial: we haven't got a progress token+- ProgressStarted: we have got a progress token and started the progress+- ProgressEnded: we have ended the progress++Notably,+1. We can't send updates except in ProgressStarted+2. We can't start the progress until we get the token back+   - This means that we may have to wait to send the start report, we can't necessarily+     send it immediately!+3. We can end if we haven't started (by just transitioning state), but we shouldn't+   send an end report.++We can have concurrent updates to the state, since we sometimes transiton states+in response to the client. In particular, for server-initiated progress, we have+to wait for the client to confirm the token until we can enter ProgressStarted.+-}++{- Note [Client- versus server-initiated progress]+The protocol supports both client- and server-initiated progress. Client-initiated progress+is simpler: the client gives you a progress token, and then you use that to report progress.+Server-initiated progress is more complex: you need to send a request to the client to tell+them about the token you want to use, and only after that can you send updates using it.+-}++{- Note [Request cancellation]+Request cancellation is a bit strange.++We need to in fact assume that all requests are cancellable, see+https://github.com/microsoft/language-server-protocol/issues/1159.++The 'cancellable' property that we can set when making progress reports just+affects whether the client should show a 'Cancel' button to the user in the UI.+The client can still always choose to cancel for another reason. -}
src/Language/LSP/Server/Processing.hs view
@@ -1,15 +1,8 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecursiveDo #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeInType #-} -- So we can keep using the old prettyprinter modules (which have a better -- compatibility range) for now.@@ -56,7 +49,6 @@ import Data.List import Data.List.NonEmpty (NonEmpty (..)) import Data.Map.Strict qualified as Map-import Data.Maybe import Data.Monoid import Data.Row import Data.String (fromString)@@ -201,42 +193,93 @@  static option. -} inferServerCapabilities :: ClientCapabilities -> Options -> Handlers m -> ServerCapabilities-inferServerCapabilities clientCaps o h =+inferServerCapabilities _clientCaps o h =   ServerCapabilities     { _textDocumentSync = sync-    , _hoverProvider = supportedBool SMethod_TextDocumentHover+    , _hoverProvider =+        supported' SMethod_TextDocumentHover $+          InR $+            HoverOptions clientInitiatedProgress     , _completionProvider = completionProvider     , _inlayHintProvider = inlayProvider-    , _declarationProvider = supportedBool SMethod_TextDocumentDeclaration+    , _declarationProvider =+        supported' SMethod_TextDocumentDeclaration $+          InR $+            InL $+              DeclarationOptions clientInitiatedProgress     , _signatureHelpProvider = signatureHelpProvider-    , _definitionProvider = supportedBool SMethod_TextDocumentDefinition-    , _typeDefinitionProvider = supportedBool SMethod_TextDocumentTypeDefinition-    , _implementationProvider = supportedBool SMethod_TextDocumentImplementation-    , _referencesProvider = supportedBool SMethod_TextDocumentReferences-    , _documentHighlightProvider = supportedBool SMethod_TextDocumentDocumentHighlight-    , _documentSymbolProvider = supportedBool SMethod_TextDocumentDocumentSymbol+    , _definitionProvider =+        supported' SMethod_TextDocumentDefinition $+          InR $+            DefinitionOptions clientInitiatedProgress+    , _typeDefinitionProvider =+        supported' SMethod_TextDocumentTypeDefinition $+          InR $+            InL $+              TypeDefinitionOptions clientInitiatedProgress+    , _implementationProvider =+        supported' SMethod_TextDocumentImplementation $+          InR $+            InL $+              ImplementationOptions clientInitiatedProgress+    , _referencesProvider =+        supported' SMethod_TextDocumentReferences $+          InR $+            ReferenceOptions clientInitiatedProgress+    , _documentHighlightProvider =+        supported' SMethod_TextDocumentDocumentHighlight $+          InR $+            DocumentHighlightOptions clientInitiatedProgress+    , _documentSymbolProvider =+        supported' SMethod_TextDocumentDocumentSymbol $+          InR $+            DocumentSymbolOptions clientInitiatedProgress Nothing     , _codeActionProvider = codeActionProvider     , _codeLensProvider =         supported' SMethod_TextDocumentCodeLens $-          CodeLensOptions-            (Just False)-            (supported SMethod_CodeLensResolve)-    , _documentFormattingProvider = supportedBool SMethod_TextDocumentFormatting-    , _documentRangeFormattingProvider = supportedBool SMethod_TextDocumentRangeFormatting+          CodeLensOptions clientInitiatedProgress (supported SMethod_CodeLensResolve)+    , _documentFormattingProvider =+        supported' SMethod_TextDocumentFormatting $+          InR $+            DocumentFormattingOptions clientInitiatedProgress+    , _documentRangeFormattingProvider =+        supported' SMethod_TextDocumentRangeFormatting $+          InR $+            DocumentRangeFormattingOptions clientInitiatedProgress     , _documentOnTypeFormattingProvider = documentOnTypeFormattingProvider-    , _renameProvider = renameProvider+    , _renameProvider =+        supported' SMethod_TextDocumentRename $+          InR $+            RenameOptions clientInitiatedProgress (supported SMethod_TextDocumentPrepareRename)     , _documentLinkProvider =         supported' SMethod_TextDocumentDocumentLink $-          DocumentLinkOptions-            (Just False)-            (supported SMethod_DocumentLinkResolve)-    , _colorProvider = supportedBool SMethod_TextDocumentDocumentColor-    , _foldingRangeProvider = supportedBool SMethod_TextDocumentFoldingRange+          DocumentLinkOptions clientInitiatedProgress (supported SMethod_DocumentLinkResolve)+    , _colorProvider =+        supported' SMethod_TextDocumentDocumentColor $+          InR $+            InL $+              DocumentColorOptions clientInitiatedProgress+    , _foldingRangeProvider =+        supported' SMethod_TextDocumentFoldingRange $+          InR $+            InL $+              FoldingRangeOptions clientInitiatedProgress     , _executeCommandProvider = executeCommandProvider-    , _selectionRangeProvider = supportedBool SMethod_TextDocumentSelectionRange-    , _callHierarchyProvider = supportedBool SMethod_TextDocumentPrepareCallHierarchy+    , _selectionRangeProvider =+        supported' SMethod_TextDocumentSelectionRange $+          InR $+            InL $+              SelectionRangeOptions clientInitiatedProgress+    , _callHierarchyProvider =+        supported' SMethod_TextDocumentPrepareCallHierarchy $+          InR $+            InL $+              CallHierarchyOptions clientInitiatedProgress     , _semanticTokensProvider = semanticTokensProvider-    , _workspaceSymbolProvider = supportedBool SMethod_WorkspaceSymbol+    , _workspaceSymbolProvider =+        supported' SMethod_WorkspaceSymbol $+          InR $+            WorkspaceSymbolOptions clientInitiatedProgress (supported SMethod_WorkspaceSymbolResolve)     , _workspace = Just workspace     , _experimental = Nothing :: Maybe Value     , -- The only encoding the VFS supports is the legacy UTF16 option at the moment@@ -245,30 +288,28 @@         supported' SMethod_TextDocumentLinkedEditingRange $           InR $             InL $-              LinkedEditingRangeOptions{_workDoneProgress = Nothing}+              LinkedEditingRangeOptions clientInitiatedProgress     , _monikerProvider =         supported' SMethod_TextDocumentMoniker $           InR $             InL $-              MonikerOptions{_workDoneProgress = Nothing}+              MonikerOptions clientInitiatedProgress     , _typeHierarchyProvider =         supported' SMethod_TextDocumentPrepareTypeHierarchy $           InR $             InL $-              TypeHierarchyOptions{_workDoneProgress = Nothing}+              TypeHierarchyOptions clientInitiatedProgress     , _inlineValueProvider =         supported' SMethod_TextDocumentInlineValue $           InR $             InL $-              InlineValueOptions{_workDoneProgress = Nothing}+              InlineValueOptions clientInitiatedProgress     , _diagnosticProvider = diagnosticProvider     , -- TODO: super unclear what to do about notebooks in general       _notebookDocumentSync = Nothing     }  where-  -- \| For when we just return a simple @true@/@false@ to indicate if we-  -- support the capability-  supportedBool = Just . InL . supported_b+  clientInitiatedProgress = Just (optSupportClientInitiatedProgress o)    supported' m b     | supported_b m = Just b@@ -286,56 +327,40 @@   singleton :: a -> [a]   singleton x = [x] -  completionProvider-    | supported_b SMethod_TextDocumentCompletion =-        Just $-          CompletionOptions-            { _triggerCharacters = map T.singleton <$> optCompletionTriggerCharacters o-            , _allCommitCharacters = map T.singleton <$> optCompletionAllCommitCharacters o-            , _resolveProvider = supported SMethod_CompletionItemResolve-            , _completionItem = Nothing-            , _workDoneProgress = Nothing-            }-    | otherwise = Nothing--  inlayProvider-    | supported_b SMethod_TextDocumentInlayHint =-        Just $-          InR $-            InL-              InlayHintOptions-                { _workDoneProgress = Nothing-                , _resolveProvider = supported SMethod_InlayHintResolve-                }-    | otherwise = Nothing--  clientSupportsCodeActionKinds =-    isJust $-      clientCaps ^? L.textDocument . _Just . L.codeAction . _Just . L.codeActionLiteralSupport . _Just+  completionProvider =+    supported' SMethod_TextDocumentCompletion $+      CompletionOptions+        { _triggerCharacters = map T.singleton <$> optCompletionTriggerCharacters o+        , _allCommitCharacters = map T.singleton <$> optCompletionAllCommitCharacters o+        , _resolveProvider = supported SMethod_CompletionItemResolve+        , _completionItem = Nothing+        , _workDoneProgress = clientInitiatedProgress+        } -  codeActionProvider-    | supported_b SMethod_TextDocumentCodeAction =-        Just $-          InR $-            CodeActionOptions-              { _workDoneProgress = Nothing-              , _codeActionKinds = codeActionKinds (optCodeActionKinds o)-              , _resolveProvider = supported SMethod_CodeActionResolve-              }-    | otherwise = Just (InL False)+  inlayProvider =+    supported' SMethod_TextDocumentInlayHint $+      InR $+        InL+          InlayHintOptions+            { _workDoneProgress = clientInitiatedProgress+            , _resolveProvider = supported SMethod_InlayHintResolve+            } -  codeActionKinds (Just ks)-    | clientSupportsCodeActionKinds = Just ks-  codeActionKinds _ = Nothing+  codeActionProvider =+    supported' SMethod_TextDocumentCodeAction $+      InR $+        CodeActionOptions+          { _workDoneProgress = clientInitiatedProgress+          , _codeActionKinds = optCodeActionKinds o+          , _resolveProvider = supported SMethod_CodeActionResolve+          } -  signatureHelpProvider-    | supported_b SMethod_TextDocumentSignatureHelp =-        Just $-          SignatureHelpOptions-            Nothing-            (map T.singleton <$> optSignatureHelpTriggerCharacters o)-            (map T.singleton <$> optSignatureHelpRetriggerCharacters o)-    | otherwise = Nothing+  signatureHelpProvider =+    supported' SMethod_TextDocumentSignatureHelp $+      SignatureHelpOptions+        clientInitiatedProgress+        (map T.singleton <$> optSignatureHelpTriggerCharacters o)+        (map T.singleton <$> optSignatureHelpRetriggerCharacters o)    documentOnTypeFormattingProvider     | supported_b SMethod_TextDocumentOnTypeFormatting@@ -347,32 +372,18 @@         error "documentOnTypeFormattingTriggerCharacters needs to be set if a documentOnTypeFormattingHandler is set"     | otherwise = Nothing -  executeCommandProvider-    | supported_b SMethod_WorkspaceExecuteCommand-    , Just cmds <- optExecuteCommandCommands o =-        Just (ExecuteCommandOptions Nothing cmds)-    | supported_b SMethod_WorkspaceExecuteCommand-    , Nothing <- optExecuteCommandCommands o =-        error "executeCommandCommands needs to be set if a executeCommandHandler is set"-    | otherwise = Nothing--  clientSupportsPrepareRename =-    fromMaybe False $-      clientCaps ^? L.textDocument . _Just . L.rename . _Just . L.prepareSupport . _Just--  renameProvider-    | clientSupportsPrepareRename-    , supported_b SMethod_TextDocumentRename-    , supported_b SMethod_TextDocumentPrepareRename =-        Just $-          InR . RenameOptions Nothing . Just $-            True-    | supported_b SMethod_TextDocumentRename = Just (InL True)-    | otherwise = Just (InL False)+  executeCommandProvider =+    supported' SMethod_WorkspaceExecuteCommand $+      case optExecuteCommandCommands o of+        Just cmds -> ExecuteCommandOptions clientInitiatedProgress cmds+        Nothing -> error "executeCommandCommands needs to be set if a executeCommandHandler is set"    -- Always provide the default legend   -- TODO: allow user-provided legend via 'Options', or at least user-provided types-  semanticTokensProvider = Just $ InL $ SemanticTokensOptions Nothing defaultSemanticTokensLegend semanticTokenRangeProvider semanticTokenFullProvider+  semanticTokensProvider =+    Just $+      InL $+        SemanticTokensOptions clientInitiatedProgress defaultSemanticTokensLegend semanticTokenRangeProvider semanticTokenFullProvider   semanticTokenRangeProvider     | supported_b SMethod_TextDocumentSemanticTokensRange = Just $ InL True     | otherwise = Nothing@@ -394,7 +405,7 @@     supported' SMethod_TextDocumentDiagnostic $       InL $         DiagnosticOptions-          { _workDoneProgress = Nothing+          { _workDoneProgress = clientInitiatedProgress           , _identifier = Nothing           , -- TODO: this is a conservative but maybe inaccurate, unclear how much it matters             _interFileDependencies = True@@ -410,7 +421,7 @@     SMethod_WorkspaceDidChangeWorkspaceFolders -> handle' logger (Just updateWorkspaceFolders) m msg     SMethod_WorkspaceDidChangeConfiguration -> handle' logger (Just $ handleDidChangeConfiguration logger) m msg     -- See Note [LSP configuration]-    SMethod_Initialized -> handle' logger (Just $ \_ -> requestConfigUpdate (cmap (fmap LspCore) logger)) m msg+    SMethod_Initialized -> handle' logger (Just $ \_ -> initialDynamicRegistrations logger >> requestConfigUpdate (cmap (fmap LspCore) logger)) m msg     SMethod_TextDocumentDidOpen -> handle' logger (Just $ vfsFunc logger openVFS) m msg     SMethod_TextDocumentDidChange -> handle' logger (Just $ vfsFunc logger changeFromClientVFS) m msg     SMethod_TextDocumentDidClose -> handle' logger (Just $ vfsFunc logger closeVFS) m msg@@ -512,6 +523,19 @@ shutdownRequestHandler :: Handler IO Method_Shutdown shutdownRequestHandler _req k = do   k $ Right Null++initialDynamicRegistrations :: (m ~ LspM config) => LogAction m (WithSeverity LspProcessingLog) -> m ()+initialDynamicRegistrations logger = do+  section <- LspT $ asks resConfigSection+  -- We need to register for `workspace/didChangeConfiguration` dynamically in order to+  -- ensure we receive notifications. See+  -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration+  -- https://github.com/microsoft/language-server-protocol/issues/1888+  void $+    trySendRegistration+      (cmap (fmap LspCore) logger)+      SMethod_WorkspaceDidChangeConfiguration+      (DidChangeConfigurationRegistrationOptions (Just $ InL section))  {- | Try to find the configuration section in an object that might represent "all" the settings.  The heuristic we use is to look for a property with the right name, and use that if we find
src/Language/LSP/VFS.hs view
@@ -1,16 +1,10 @@ {-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeInType #-}-{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} -- So we can keep using the old prettyprinter modules (which have a better -- compatibility range) for now.@@ -56,8 +50,6 @@    -- * manipulating the file contents   rangeLinesFromVfs,-  PosPrefixInfo (..),-  getCompletionPrefix,    -- * for tests   applyChanges,@@ -69,22 +61,20 @@ import Control.Lens hiding (parts, (<.>)) import Control.Monad import Control.Monad.State-import Data.Char (isAlphaNum, isUpper) import Data.Foldable (traverse_) import Data.Hashable import Data.Int (Int32) import Data.List import Data.Map.Strict qualified as Map-import Data.Maybe import Data.Ord import Data.Row import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Text.Prettyprint.Doc hiding (line)-import Data.Text.Rope qualified as URope-import Data.Text.Utf16.Rope (Rope)-import Data.Text.Utf16.Rope qualified as Rope+import Data.Text.Utf16.Lines as Utf16 (Position (..))+import Data.Text.Utf16.Rope.Mixed (Rope)+import Data.Text.Utf16.Rope.Mixed qualified as Rope import Language.LSP.Protocol.Lens qualified as J import Language.LSP.Protocol.Message qualified as J import Language.LSP.Protocol.Types qualified as J@@ -115,7 +105,7 @@   deriving (Show)  data VfsLog-  = SplitInsideCodePoint Rope.Position Rope+  = SplitInsideCodePoint Utf16.Position Rope   | URINotFound J.NormalizedUri   | Opening J.NormalizedUri   | Closing J.NormalizedUri@@ -350,7 +340,7 @@ applyChange logger str (J.TextDocumentContentChangeEvent (J.InL e))   | J.Range (J.Position sl sc) (J.Position fl fc) <- e .! #range   , txt <- e .! #text =-      changeChars logger str (Rope.Position (fromIntegral sl) (fromIntegral sc)) (Rope.Position (fromIntegral fl) (fromIntegral fc)) txt+      changeChars logger str (Utf16.Position (fromIntegral sl) (fromIntegral sc)) (Utf16.Position (fromIntegral fl) (fromIntegral fc)) txt applyChange _ _ (J.TextDocumentContentChangeEvent (J.InR e)) =   pure $ Rope.fromText $ e .! #text @@ -360,11 +350,11 @@  the given range with the new text. If the given positions lie within  a code point then this does nothing (returns the original 'Rope') and logs. -}-changeChars :: (Monad m) => LogAction m (WithSeverity VfsLog) -> Rope -> Rope.Position -> Rope.Position -> Text -> m Rope+changeChars :: (Monad m) => LogAction m (WithSeverity VfsLog) -> Rope -> Utf16.Position -> Utf16.Position -> Text -> m Rope changeChars logger str start finish new = do-  case Rope.splitAtPosition finish str of+  case Rope.utf16SplitAtPosition finish str of     Nothing -> logger <& SplitInsideCodePoint finish str `WithSeverity` Warning >> pure str-    Just (before, after) -> case Rope.splitAtPosition start before of+    Just (before, after) -> case Rope.utf16SplitAtPosition start before of       Nothing -> logger <& SplitInsideCodePoint start before `WithSeverity` Warning >> pure str       Just (before', _) -> pure $ mconcat [before', Rope.fromText new, after] @@ -402,11 +392,14 @@ - We then split the line at the given position, and check how long the prefix is, which takes linear time in the length of the (single) line. -We also may need to convert the line back and forth between ropes with different indexing. Again-this is linear time in the length of the line.- So the overall process is logarithmic in the number of lines, and linear in the length of the specific line. Which is okay-ish, so long as we don't have very long lines.++We are not able to use the `Rope.splitAtPosition`+Because when column index out of range or when the column indexing at the newline char.+The prefix result would wrap over the line and having the same result (nextLineNum, 0).+We would not be able to distinguish them. When the first case should return `Nothing`,+second case should return a `Just (CurrentLineNum, columnNumberConverted)`. -}  {- | Extracts a specific line from a 'Rope.Rope'.@@ -415,41 +408,12 @@ extractLine :: Rope.Rope -> Word -> Maybe Rope.Rope extractLine rope l = do   -- Check for the line being out of bounds-  let lastLine = Rope.posLine $ Rope.lengthAsPosition rope+  let lastLine = Utf16.posLine $ Rope.utf16LengthAsPosition rope   guard $ l <= lastLine-   let (_, suffix) = Rope.splitAtLine l rope       (prefix, _) = Rope.splitAtLine 1 suffix   pure prefix -{- | Translate a code-point offset into a code-unit offset.- Linear in the length of the rope.--}-codePointOffsetToCodeUnitOffset :: URope.Rope -> Word -> Maybe Word-codePointOffsetToCodeUnitOffset rope offset = do-  -- Check for the position being out of bounds-  guard $ offset <= URope.length rope-  -- Split at the given position in *code points*-  let (prefix, _) = URope.splitAt offset rope-      -- Convert the prefix to a rope using *code units*-      utf16Prefix = Rope.fromText $ URope.toText prefix-  -- Get the length of the prefix in *code units*-  pure $ Rope.length utf16Prefix--{- | Translate a UTF-16 code-unit offset into a code-point offset.- Linear in the length of the rope.--}-codeUnitOffsetToCodePointOffset :: Rope.Rope -> Word -> Maybe Word-codeUnitOffsetToCodePointOffset rope offset = do-  -- Check for the position being out of bounds-  guard $ offset <= Rope.length rope-  -- Split at the given position in *code units*-  (prefix, _) <- Rope.splitAt offset rope-  -- Convert the prefix to a rope using *code points*-  let utfPrefix = URope.fromText $ Rope.toText prefix-  -- Get the length of the prefix in *code points*-  pure $ URope.length utfPrefix- {- | Given a virtual file, translate a 'CodePointPosition' in that file into a 'J.Position' in that file.   Will return 'Nothing' if the requested position is out of bounds of the document.@@ -458,15 +422,12 @@  the position. -} codePointPositionToPosition :: VirtualFile -> CodePointPosition -> Maybe J.Position-codePointPositionToPosition vFile (CodePointPosition l cpc) = do+codePointPositionToPosition vFile (CodePointPosition l c) = do   -- See Note [Converting between code points and code units]   let text = _file_text vFile-  utf16Line <- extractLine text (fromIntegral l)-  -- Convert the line a rope using *code points*-  let utfLine = URope.fromText $ Rope.toText utf16Line--  cuc <- codePointOffsetToCodeUnitOffset utfLine (fromIntegral cpc)-  pure $ J.Position l (fromIntegral cuc)+  lineRope <- extractLine text $ fromIntegral l+  guard $ c <= fromIntegral (Rope.charLength lineRope)+  return $ J.Position l (fromIntegral $ Rope.utf16Length $ fst $ Rope.charSplitAt (fromIntegral c) lineRope)  {- | Given a virtual file, translate a 'CodePointRange' in that file into a 'J.Range' in that file. @@ -487,13 +448,12 @@  the position. -} positionToCodePointPosition :: VirtualFile -> J.Position -> Maybe CodePointPosition-positionToCodePointPosition vFile (J.Position l cuc) = do+positionToCodePointPosition vFile (J.Position l c) = do   -- See Note [Converting between code points and code units]   let text = _file_text vFile-  utf16Line <- extractLine text (fromIntegral l)--  cpc <- codeUnitOffsetToCodePointOffset utf16Line (fromIntegral cuc)-  pure $ CodePointPosition l (fromIntegral cpc)+  lineRope <- extractLine text $ fromIntegral l+  guard $ c <= fromIntegral (Rope.utf16Length lineRope)+  CodePointPosition l . fromIntegral . Rope.charLength . fst <$> Rope.utf16SplitAt (fromIntegral c) lineRope  {- | Given a virtual file, translate a 'J.Range' in that file into a 'CodePointRange' in that file. @@ -506,64 +466,9 @@ rangeToCodePointRange vFile (J.Range b e) =   CodePointRange <$> positionToCodePointPosition vFile b <*> positionToCodePointPosition vFile e --- ------------------------------------------------------------------------- TODO:AZ:move this to somewhere sane---- | Describes the line at the current cursor position-data PosPrefixInfo = PosPrefixInfo-  { fullLine :: !T.Text-  -- ^ The full contents of the line the cursor is at-  , prefixModule :: !T.Text-  -- ^ If any, the module name that was typed right before the cursor position.-  --  For example, if the user has typed "Data.Maybe.from", then this property-  --  will be "Data.Maybe"-  , prefixText :: !T.Text-  -- ^ The word right before the cursor position, after removing the module part.-  -- For example if the user has typed "Data.Maybe.from",-  -- then this property will be "from"-  , cursorPos :: !J.Position-  -- ^ The cursor position-  }-  deriving (Show, Eq)--getCompletionPrefix :: (Monad m) => J.Position -> VirtualFile -> m (Maybe PosPrefixInfo)-getCompletionPrefix pos@(J.Position l c) (VirtualFile _ _ ropetext) =-  return $ Just $ fromMaybe (PosPrefixInfo "" "" "" pos) $ do-    -- Maybe monad-    let lastMaybe [] = Nothing-        lastMaybe xs = Just $ last xs--    let curRope = fst $ Rope.splitAtLine 1 $ snd $ Rope.splitAtLine (fromIntegral l) ropetext-    beforePos <- Rope.toText . fst <$> Rope.splitAt (fromIntegral c) curRope-    curWord <--      if-          | T.null beforePos -> Just ""-          | T.last beforePos == ' ' -> Just "" -- don't count abc as the curword in 'abc '-          | otherwise -> lastMaybe (T.words beforePos)--    let parts =-          T.split (== '.') $-            T.takeWhileEnd (\x -> isAlphaNum x || x `elem` ("._'" :: String)) curWord-    case reverse parts of-      [] -> Nothing-      (x : xs) -> do-        let modParts =-              dropWhile (not . isUpper . T.head) $-                reverse $-                  filter (not . T.null) xs-            modName = T.intercalate "." modParts-        -- curRope is already a single line, but it may include an enclosing '\n'-        let curLine = T.dropWhileEnd (== '\n') $ Rope.toText curRope-        return $ PosPrefixInfo curLine modName x pos---- ---------------------------------------------------------------------- rangeLinesFromVfs :: VirtualFile -> J.Range -> T.Text rangeLinesFromVfs (VirtualFile _ _ ropetext) (J.Range (J.Position lf _cf) (J.Position lt _ct)) = r  where   (_, s1) = Rope.splitAtLine (fromIntegral lf) ropetext   (s2, _) = Rope.splitAtLine (fromIntegral (lt - lf)) s1   r = Rope.toText s2---- ---------------------------------------------------------------------
test/VspSpec.hs view
@@ -6,7 +6,7 @@ import Data.Row import Data.String import Data.Text qualified as T-import Data.Text.Utf16.Rope qualified as Rope+import Data.Text.Utf16.Rope.Mixed qualified as Rope import Language.LSP.Protocol.Types qualified as J import Language.LSP.VFS @@ -278,27 +278,3 @@       codePointPositionToPosition vfile (CodePointPosition 2 1) `shouldBe` Nothing       -- Greater line than max line       codePointPositionToPosition vfile (CodePointPosition 3 0) `shouldBe` Nothing--    -- -----------------------------------    it "getCompletionPrefix" $ do-      let-        orig =-          T.unlines-            [ "{-# ings #-}"-            , "import Data.List"-            ]-      pp4 <- getCompletionPrefix (J.Position 0 4) (vfsFromText orig)-      pp4 `shouldBe` Just (PosPrefixInfo "{-# ings #-}" "" "" (J.Position 0 4))--      pp5 <- getCompletionPrefix (J.Position 0 5) (vfsFromText orig)-      pp5 `shouldBe` Just (PosPrefixInfo "{-# ings #-}" "" "i" (J.Position 0 5))--      pp6 <- getCompletionPrefix (J.Position 0 6) (vfsFromText orig)-      pp6 `shouldBe` Just (PosPrefixInfo "{-# ings #-}" "" "in" (J.Position 0 6))--      pp14 <- getCompletionPrefix (J.Position 1 14) (vfsFromText orig)-      pp14 `shouldBe` Just (PosPrefixInfo "import Data.List" "Data" "Li" (J.Position 1 14))--      pp00 <- getCompletionPrefix (J.Position 0 0) (vfsFromText orig)-      pp00 `shouldBe` Just (PosPrefixInfo "{-# ings #-}" "" "" (J.Position 0 0))