haskell-lsp

repository·master·Indexed 19 days ago

https://github.com/haskell/lsp

A Haskell implementation of the Microsoft Language Server Protocol (LSP) 3.15 specification. The ecosystem consists of three packages: 'lsp', a core library for building servers with built-in JSON-RPC transport, Virtual File System (VFS), and capability management; 'lsp-types', providing type-safe definitions generated from the official LSP specification; and 'lsp-test', a functional testing framework for LSP servers featuring session replay, HSpec integration, and message parsing combinators.

Tokens
3.3K
Snippets
8
Records
14
Agent score
61%

What's inside haskell-lsp

  1. Overview of the lsp library

    master

    The lsp library is a framework designed for building Language Server Protocol (LSP) servers. It abstracts the complexities of the protocol by providing built-in support for:

    • JSON-RPC Transport: Handling the underlying communication layer.
    • Virtual File System (VFS): Managing and tracking document state in memory.
    • Request/Notification Handling: Responding to LSP requests and notifications via registered handlers.
    • Capability Management: Automatically setting server capabilities in the initialize request based on registered handlers, as well as supporting dynamic registration of capabilities.
    • Lifecycle & Progress: Managing cancellable requests and progress notifications.
    • Diagnostics: Handling the publishing and flushing of diagnostic information.

    It is part of a larger ecosystem including lsp-types (for protocol types) and lsp-test (for testing servers).

  2. Overview of the lsp ecosystem

    master

    The lsp project is a Haskell implementation of the Microsoft Language Server Protocol (LSP) 3.15 specification. It is distributed as three distinct packages to separate concerns:

    • lsp-types: Provides type-safe definitions that correspond to the TypeScript definitions in the LSP specification.
    • lsp: The core library for building language servers. It manages:
      • JSON-RPC transport.
      • Document state management via a Virtual File System (VFS).
      • Request and notification handling.
      • Server capability negotiation (including dynamic registration).
      • Cancellable requests and progress notifications.
      • Diagnostics publishing and flushing.
    • lsp-test: A functional testing framework specifically designed for testing LSP servers.
  3. Overview of lsp-types

    master
    The lsp-types library provides the Haskell data types required to implement the Microsoft Language Server Protocol (LSP). It is a core component of the lsp ecosystem, working alongside the lsp and lsp-test packages to provide a complete framework for building language servers.
  4. Debug lsp-test sessions

    master

    To see what the server is doing during a test, you can enable logging. You can configure this via the SessionConfig object using logMessages and logStdErr.

    Alternatively, you can enable logging from the command line using environment variables:

    • LSP_TEST_LOG_MESSAGES=1
    • LSP_TEST_LOG_STDERR=1
    LSP_TEST_LOG_MESSAGES=1 LSP_TEST_LOG_STDERR=1 cabal test
  5. Update the generated lsp-types code

    master

    The data types in lsp-types are generated from the official LSP specification. If you need to refresh or update the generated Haskell code, run the generator using cabal from within the lsp-types directory.

    cabal run generator
  6. Run a basic lsp-test session

    master

    To start a testing session, use runSession. You must provide the server executable name, client capabilities (e.g., fullLatestClientCaps), and the project directory path. Within the session block, you can perform actions like openDoc to simulate opening a file and use functions like getDocumentSymbols to inspect the server's state.

    import Language.LSP.Test
    main = runSession "hie" fullLatestClientCaps "proj/dir" $ do
      doc <- openDoc "Foo.hs" "haskell"
      skipMany anyNotification
      symbols <- getDocumentSymbols doc
  7. Test language servers with lsp-test

    master

    The lsp-test package provides a framework for functional testing of LSP servers. You can run sessions, perform actions like opening documents, and assert on server responses.

    Setting up a session

    Use runSession to initialize a session with a server name, capabilities, and a project directory.

    Unit tests with HSpec

    You can integrate lsp-test with HSpec to write assertions about diagnostics, symbols, or other LSP features.

    Replaying captured sessions

    If you have a captured session, you can replay it using replaySession.

    Parsing with combinators

    lsp-test provides combinators to skip or count specific notifications, requests, or responses during a session.

    Debugging tests

    To see server logs and standard error during testing, set the following environment variables:

    • LSP_TEST_LOG_MESSAGES=1
    • LSP_TEST_LOG_STDERR=1
    -- Setting up a session
    import Language.LSP.Test
    main = runSession "hie" fullCaps "proj/dir" $ do
      doc <- openDoc "Foo.hs" "haskell"
      skipMany anyNotification
      symbols <- getDocumentSymbols doc
    
    -- Unit tests with HSpec
    describe "diagnostics" $
      it "report errors" $ runSession "hie" fullCaps "test/data" $ do
        openDoc "Error.hs" "haskell"
        [diag] <- waitForDiagnosticsSource "ghcmod"
        liftIO $ do
          diag ^. severity `shouldBe` Just DsError
          diag ^. source `shouldBe` Just "ghcmod"
    
    -- Replaying captured session
    replaySession "hie" "test/data/renamePass"
    
    -- Parsing with combinators
    skipManyTill loggingNotification publishDiagnosticsNotification
    count 4 (message :: Session ApplyWorkspaceEditRequest)
    anyRequest <|> anyResponse
  8. Troubleshoot lsp-test with Stack

    master
    If you encounter unexpected behavior when running lsp-test via stack, it may be due to environment variables set by stack related to GHC. You may need to unset these variables to ensure the test environment matches your expectations.
  9. Troubleshoot stack environment issues

    master
    If you encounter unexpected behavior when running lsp-test via stack, it may be due to stack setting environment variables related to GHC. If your server relies on Haskell tooling, you may need to unset these environment variables to ensure a clean testing environment.
  10. Build a minimal language server with lsp

    master

    To build a language server, you define a set of Handlers for notifications and requests, then use runServer with a ServerDefinition.

    Key components of ServerDefinition include:

    • staticHandlers: A function that returns your handlers based on server capabilities.
    • doInitialize: A function to handle the initialization request and return the environment.
    • interpretHandler: Defines how to run your handler monad (e.g., using runLspT).
    • parseConfig: Logic to parse server configuration.

    Example of a minimal server implementing SMethod_Initialized and SMethod_TextDocumentHover:

    {-# LANGUAGE DuplicateRecordFields #-}
    {-# LANGUAGE LambdaCase #-}
    {-# LANGUAGE OverloadedStrings #-}
    
    import Control.Monad.IO.Class
    import Data.Text qualified as T
    import Language.LSP.Protocol.Message
    import Language.LSP.Protocol.Types
    import Language.LSP.Server
    
    handlers :: Handlers (LspM ())
    handlers =
      mconcat
        [ notificationHandler SMethod_Initialized $ \_not -> do
            let params =
                  ShowMessageRequestParams
                    MessageType_Info
                    "Turn on code lenses?"
                    (Just [MessageActionItem "Turn on", MessageActionItem "Don't"])
            _ <- sendRequest SMethod_WindowShowMessageRequest params $ \case
              Right (InL (MessageActionItem "Turn on")) -> do
                let regOpts = CodeLensRegistrationOptions (InR Null) Nothing (Just False)
    
                _ <- 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
                pure ()
              Right _ ->
                sendNotification SMethod_WindowShowMessage (ShowMessageParams MessageType_Info "Not turning on code lenses")
              Left err ->
                sendNotification SMethod_WindowShowMessage (ShowMessageParams MessageType_Error $ "Something went wrong!\n" <> T.pack (show err))
            pure ()
        , requestHandler SMethod_TextDocumentHover $ \req responder -> do
            let TRequestMessage _ _ _ (HoverParams _doc pos _workDone) = req
                Position _l _c' = pos
                rsp = Hover (InL ms) (Just range)
                ms = mkMarkdown "Hello world"
                range = Range pos pos
            responder (Right $ InL rsp)
        ]
    
    main :: IO Int
    main =
      runServer $
        ServerDefinition
          { parseConfig = const $ const $ Right ()
          , onConfigChange = const $ pure ()
          , defaultConfig = ()
          , configSection = "demo"
          , doInitialize = \env _req -> pure $ Right env
          , staticHandlers = \_caps -> handlers
          , interpretHandler = \env -> Iso (runLspT env) liftIO
          , options = defaultOptions
          }