tower-lsp-server

repository·main·Indexed 42 days ago

https://github.com/tower-lsp-community/tower-lsp-server

A Rust implementation of the Language Server Protocol (LSP) based on the Tower service abstraction. Version 0.24.0-rc.1 provides a structured framework for building language servers over stdio or TCP using the LanguageServer trait, LspService delegate, and Server transport manager. It includes a comprehensive JSON-RPC implementation for handling requests, notifications, and responses, and supports runtime-agnostic configurations for alternatives to tokio.

Tokens
12.1K
Snippets
29
Records
48
Agent score
50%

What's inside tower-lsp-server

  1. Understand LSP feature support in tower-lsp-server

    main

    The tower-lsp-server project tracks its support for the Language Server Protocol (LSP) specification. Support is categorized by the type of message and the direction of communication between the client and the server.

    Message Types and API Locations

    SymbolDescriptionAPI Location
    ↪️Request (Client $\to$ Server)LanguageServer trait
    ↪️Request (Server $\to$ Client)Client struct
    ➡️Notification (Client $\to$ Server)LanguageServer trait
    ⬅️Notification (Server $\to$ Client)Client struct

    Support Status

    • 🟢 Supported: Fully implemented.
    • 🟡 Partial support: Implemented with specific limitations (refer to footnotes in the full feature list).
    • 🔴 Unsupported: Not currently implemented.
  2. How tower-lsp-server works

    main

    The tower-lsp-server library provides a Language Server Protocol (LSP) implementation based on the Tower service abstraction. It is composed of three primary components:

    1. LanguageServer trait: This is where you define the specific behavior and logic of your language server (e.g., handling initialization, shutdown, or specific LSP requests).
    2. LspService delegate: An asynchronous delegate that wraps your LanguageServer implementation and handles the underlying protocol mechanics.
    3. Server: The component responsible for spawning the LspService and managing the transport layer (processing requests and responses) over stdio or TCP.
  3. Implement a minimal Language Server

    main

    To create a language server, implement the LanguageServer trait for your backend struct. You must provide implementations for initialize, initialized, and shutdown. Use the LspService::new constructor to create a service that instantiates your backend with a Client for sending messages back to the client. Finally, use Server::new with your transport handles (like stdin and stdout) to serve the service.

    use tower_lsp_server::jsonrpc::Result;
    use tower_lsp_server::gen_lsp_types::*;
    use tower_lsp_server::{Client, LanguageServer, LspService, Server};
    
    #[derive(Debug)]
    struct Backend {
        client: Client,
    }
    
    impl LanguageServer for Backend {
        async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
            Ok(InitializeResult::default())
        }
    
        async fn initialized(&self, _: InitializedParams) {
            self.client
                .log_message(MessageType::INFO, "server initialized!")
                .await;
        }
    
        async fn shutdown(&self) -> Result<()> {
            Ok(())
        }
    }
    
    #[tokio::main]
    async fn main() {
        let stdin = tokio::io::stdin();
        let stdout = tokio::io::stdout();
    
        let (service, socket) = LspService::new(|client| Backend { client });
        Server::new(stdin, stdout, socket).serve(service).await;
    }
  4. Use runtimes other than tokio

    main

    By default, tower-lsp-server is configured to use the tokio runtime. If you need to use a different asynchronous runtime, you must disable the default features and enable the runtime-agnostic feature in your Cargo.toml.

    [dependencies.tower-lsp-server]
    version = "*"
    default-features = false
    features = ["runtime-agnostic"]
  5. How LspService handles requests and lifecycle

    main

    The LspService implements tower::Service<Request>, acting as a bridge between the transport layer and your LanguageServer implementation.

    Lifecycle and State

    • Initialization: The service remains in an Initializing state until the initialize request is processed. During this time, poll_ready returns Poll::Pending.
    • Shutdown: Once the exit notification is received, the service enters the Exited state. Subsequent calls to poll_ready or call will return an ExitedError.
    • Cancellation: Pending requests can be canceled by the client issuing a $/cancelRequest notification. The service handles this by returning a JSON-RPC error response to the pending request.

    Request/Response Mapping

    • Requests: Incoming JSON-RPC requests result in an Option<Response>. If the handler returns a value, it is wrapped in Some(Response). If the handler is a notification (returns ()), the result is None.
    • Error Handling: If a client sends a request for a method that does not exist (and it is not a standard $/ extension method), the service automatically handles the MethodNotFound error logic.
  6. Use the Client handle to communicate with the language client

    main

    The Client struct is a lightweight, clonable handle used by the language server to send requests and notifications to the language client (e.g., an IDE or editor). It implements tower::Service, allowing it to be used with middleware.

    Key characteristics:

    • Cheap to clone: You can pass it across threads or into different tasks easily.
    • Lifecycle aware: Most requests and notifications will return an error if sent before the server has been initialized (JSON-RPC error code -32002).
    • Automatic ID management: It manages internal request IDs for JSON-RPC requests.
    // The Client is typically obtained during server setup via Client::new
    // and then cloned into various tasks or handlers.
    let client_clone = client.clone();
  7. How progress reporting works in tower-lsp-server

    main

    Progress reporting follows a builder pattern to emit $/progress notifications to the client. The workflow consists of three main stages:

    1. Creation: Use Client::progress(token, title) to create a Progress builder. By default, progress is unbounded and not cancellable.
    2. Configuration: Use builder methods to define the progress behavior:
      • .with_percentage(start_percentage: u32): Transitions the stream from Unbounded to Bounded (0-100%).
      • .with_cancel_button(): Transitions the stream from NotCancellable to Cancellable.
      • .with_message(message: M): Sets an initial detailed message.
    3. Execution: Call .begin().await on the builder. This sends the initial notification and returns an OngoingProgress handle.
    4. Updates: Use the OngoingProgress handle to call .report(...) or .report_with_message(...) to update the UI.
    5. Completion: Call .finish().await or .finish_with_message(message).await to signal the operation is complete.

    Note: Progress is not reported to the client until .begin() is called.

    // Conceptual workflow
    let progress = client.progress(token, "Indexing project")
        .with_percentage(0)
        .with_cancel_button()
        .with_message("Starting...");
    
    let ongoing = progress.begin().await;
    
    // Update progress
    ongoing.report_with_message("Scanning files...", 50, None).await;
    
    // Finish
    ongoing.finish().await;
  8. Run an LSP server using LspService and Server

    main

    To start the server, use LspService::new to wrap your backend implementation. This function takes a closure that receives a Client and returns your backend instance. Then, pass the resulting service and a socket to Server::new along with your transport streams (like stdin and stdout) and call .serve(service).await.

    #[tokio::main]
    async fn main() {
        let stdin = tokio::io::stdin();
        let stdout = tokio::io::stdout();
    
        // Create the service with a closure to initialize the backend with a Client
        let (service, socket) = LspService::new(|client| Backend { client });
    
        // Start the server using the provided transport and service
        Server::new(stdin, stdout, socket).serve(service).await;
    }
  9. Initialize an LspService

    main

    To create a language server, use LspService::build or LspService::build_with_lifecycle_methods. These return an LspServiceBuilder which you can customize before calling .finish() to obtain the LspService and a ClientSocket for server-to-client communication.

    • LspService::build(init): Registers all default methods from the LanguageServer trait. Use this for standard LSP servers.
    • LspService::build_with_lifecycle_methods(init): Registers only the essential LSP lifecycle methods (initialize, initialized, shutdown, exit, and $/cancelRequest). This is ideal for reducing binary size when implementing only a subset of LSP.

    The init argument is a closure that takes a Client and returns your server backend S (which must implement LanguageServer).

    use tower_lsp_server::{LanguageServer, LspService};
    
    struct MyServer;
    
    #[async_trait::async_trait]
    impl LanguageServer for MyServer {
        // ... implement required methods
    }
    
    let (service, socket) = LspService::build(|client| MyServer)
        .finish();
  10. Check Language Server Protocol (LSP) feature support

    main

    The tower-lsp-server project tracks support for various Language Server Protocol (LSP) methods across different versions. When building a language server, you should verify if the specific LSP methods your client requires are supported by the version of the library you are using.

    Supported methods are marked with a green circle (🟢), while unsupported methods are marked with a red circle (🔴). The status is categorized by the LSP specification version (e.g., 3.18.0, 3.17.0, etc.).

  11. LSP Method Support Reference (Versions 3.15.0 to 3.6.0)

    main

    This reference lists the availability of specific LSP methods for older versions of the protocol. Use this to determine if your required functionality (like progress reporting or workspace configuration) is implemented in the version of the server you are targeting.

    ### [3.15.0] - 2020-01-14
    
    | Method Name | Message Type | Supported |
    | :--- | :---: | :---: |
    | `$/progress` | ↔ | 🔴 |
    | `window/workDoneProgress/create` | ↪ | 🔴 |
    | `window/workDoneProgress/cancel` | ↔ | 🔴 |
    | `textDocument/selectionRange` | ↩ | 🟢 |
    
    ### [3.14.0] - 2018-12-13
    
    | Method Name | Message Type | Supported |
    | :--- | :---: | :---: |
    | `textDocument/declaration` | ↩ | 🟢 |
    
    ### [3.12.0] - 2018-08-23
    
    | Method Name | Message Type | Supported |
    | :--- | :---: | :---: |
    | `textDocument/prepareRename` | ↩ | 🟢 |
    
    ### [3.10.0] - 2018-07-23
    
    | Method Name | Message Type | Supported |
    | :--- | :---: | :---: |
    | `textDocument/foldingRange` | ↩ | 🟢 |
    
    ### [3.6.0] - 2018-02-22
    
    | Method Name | Message Type | Supported |
    | :--- | :---: | :---: |
    | `workspace/workspaceFolders` | ↪ | 🟢 |
    | `workspace/didChangeWorkspaceFolders` | ↔ | 🟢 |
    | `workspace/configuration` | ↪ | 🟢 |
    | `textDocument/typeDefinition` | ↩ | 🟢 |
    | `textDocument/implementation` | ↩ | 🟢 |
    | `textDocument/documentColor` | ↩ | 🟢 |
    | `textDocument/colorPresentation` | ↩ | 🟢 |