tower-lsp-boilerplate

repository·main·Indexed 43 days ago

https://github.com/iwanabethatguy/tower-lsp-boilerplate

A boilerplate for building a Rust-based Language Server Protocol (LSP) implementation using the tower-lsp library, demonstrated via a custom language called l-lang. It includes a VSCode extension client and implements features such as semantic tokens, inlay hints, diagnostics, code completion, and navigation (Go to Definition, Find References, Rename).

Tokens
2.7K
Snippets
8
Records
14
Agent score
55%

What's inside tower-lsp-boilerplate

  1. LSP Features provided by the server

    main

    This boilerplate implements a full suite of LSP features for l-lang:

    • Semantic Tokens: Role-based syntax highlighting.
    • Inlay Hints: Type annotations for variables.
    • Diagnostics: Real-time syntactic and semantic error reporting.
    • Code Completion: Context-aware symbol suggestions.
    • Go to Definition: Navigation to symbol declarations.
    • Find References: Locating all usages of a symbol.
    • Rename: Refactoring symbols across the codebase.
    • Format: Code formatting support.
  2. L-lang language overview and features

    main

    L-lang is a simple, statically-typed programming language used in this boilerplate. It supports:

    • Struct definitions: Defining types with specific field types.
    • Functions: Typed parameters and return types.
    • Variable bindings: Using let with type inference.
    • Arithmetic: +, -, *, /.
    • Field access: Accessing struct members.
    • Struct literals: Instantiating structs.
    • Basic types: int, bool, string.
    struct Point {
        x: int,
        y: int,
    }
    
    fn add_points(a: Point, b: Point) -> Point {
        return Point { x: a.x + b.x, y: a.y + b.y };
    }
    
    fn main() {
        let p1 = Point { x: 10, y: 20 };
        let result = add_points(p1, p2);
        return result;
    }
  3. Develop and debug the LSP using VSCode

    main

    To develop and debug the language server within VSCode, follow these steps:

    1. Install dependencies: pnpm i
    2. Build the Rust server: cargo build
    3. Open the project in VSCode: code .
    4. Launch the debugger: Press <kbd>F5</kbd> or go to the Debug panel and click Launch Client.
    5. Test the implementation: In the new VSCode instance that opens, open examples/test.l to verify syntax highlighting and LSP features.

    Troubleshooting: If you encounter the error Cannot find module '/xxx/xxx/dist/extension.js', run tsc -b manually to build the TypeScript project.

    pnpm i
    cargo build
    code .
  4. Preview and test the extension locally as a VSIX

    main

    To package the extension and install it as a local VSCode extension:

    1. Ensure all dependencies are installed and l-language-server is in your PATH.
    2. Package the extension: pnpm run package.
    3. Install the generated .vsix file: code --install-extension l-language-server-${version}.vsix (replace ${version} with the actual version found in your file system).
    4. Restart VSCode and create a minimal .l file to test the effects.
    pnpm run package
    code --install-extension l-language-server-${version}.vsix
  5. Implement semantic tokens for l-lang

    main

    The server provides semantic tokens via semantic_tokens_full and semantic_tokens_range. It maps l-lang symbols to LSP semantic token types using a delta-encoding scheme. The supported token types are:

    IndexToken TypeSymbolKind Mapping
    0FUNCTIONSymbolKind::Function
    1VARIABLESymbolKind::Variable
    2PARAMETERSymbolKind::Parameter
    3STRUCTSymbolKind::Struct
    4PROPERTYSymbolKind::Field

    Tokens are calculated by iterating over symbol spans and reference spans in the CompileResult and converting byte offsets to line/character positions using the document's Rope.

  6. Initialize and run the tower-lsp server

    main

    The language server is initialized using LspService::build with a Backend implementation. The server communicates via stdin and stdout. The main function sets up the LspService, creates a Backend instance with empty DashMaps for document and semantic data, and starts the server loop using Server::new(...).serve(service).await.

    #[tokio::main]
    async fn main() {
        env_logger::init();
    
        let stdin = tokio::io::stdin();
        let stdout = tokio::io::stdout();
    
        let (service, socket) = LspService::build(|client| Backend {
            client,
            semanticast_map: DashMap::new(),
            document_map: DashMap::new(),
        })
        .finish();
    
        Server::new(stdin, stdout, socket).serve(service).await;
    }
  7. Configure the L Language Server executable path

    main

    The extension determines which executable to run for the language server using the SERVER_PATH environment variable. If SERVER_PATH is not set, it defaults to running the command l-language-server.

    To use a custom server binary, set the SERVER_PATH environment variable in your shell or development environment before launching VS Code.

    # Example: setting a custom path to the server binary
    export SERVER_PATH=/path/to/your/custom-l-language-server
    code .
  8. Configure Rolldown for the Language Client

    main

    The project uses rolldown to bundle the VSCode extension client. The configuration targets a Node.js platform, uses CommonJS (cjs) format, and treats vscode as an external dependency.

    Key configuration details:

    • Input: client/src/extension.ts
    • Output File: dist/extension.js
    • External Dependencies: vscode must be marked as external to prevent bundling the VSCode API.
    • Environment-based behavior:
      • If NODE_ENV is production, the output is minified and sourcemaps are disabled.
      • If NODE_ENV is not production, sourcemaps are enabled and minification is disabled.
    • Plugins: A problem-matcher plugin is conditionally included during watch mode to log build start and finish events.
    import { defineConfig } from "rolldown";
    
    const production = process.env.NODE_ENV === "production";
    
    const problemMatcherPlugin = {
      name: "problem-matcher",
      buildStart() {
        console.log("[watch] build started");
      },
      buildEnd() {
        console.log("[watch] build finished");
      },
    };
    
    export default defineConfig(({ watch }) => ({
      input: "client/src/extension.ts",
      platform: "node",
      external: ["vscode"],
      plugins: [watch && problemMatcherPlugin].filter(Boolean),
      output: {
        file: "dist/extension.js",
        format: "cjs",
        sourcemap: !production,
        minify: production,
      },
    }));
  9. Implement inlay hints for variable types

    main
    The inlay_hint method provides type information for variables. For SymbolKind::Variable, it generates an InlayHintLabel. If the variable's type is a Struct, it provides a clickable Location to the struct definition; otherwise, it provides a simple string label (e.g., : type_name).
  10. Get hover information for symbols

    main

    The hover method provides Markdown-formatted information about the symbol at the current position. It uses the semanticast_map to identify the symbol kind and type information.

    • Functions/Structs: Returns a code block like ` ```l fn name

    ``` `.

    • Variables/Parameters/Fields: Returns a code block containing the name and the formatted type, e.g., ` ```l let name: type_str

    ``` `.