Sign in

    lsp

    LSP (Language Server Protocol) implementation for MoonBit

    lsp
    language-server
    jsonrpc
    Download zip
    Author
    Version
    0.1.3
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    44

    #mizchi/lsp

    LSP (Language Server Protocol) implementation for MoonBit.

    #Packages

    PackageDescription
    mizchi/lsp/jsonJSON parser and serializer
    mizchi/lsp/jsonrpcJSON-RPC 2.0 codec
    mizchi/lsp/typesLSP type definitions (auto-generated)
    mizchi/lsp/serverLSP server framework
    mizchi/lsp/clientLSP client framework
    mizchi/lsp/fsFilesystem abstraction (real/in-memory)

    #Installation

    moon add mizchi/lsp

    #Usage

    #LSP Client

    Connect to an external LSP server and receive diagnostics.

    // Import packages
    // "mizchi/lsp/client"
    // "mizchi/lsp/json"

    // 1. Implement Transport trait for your platform

    ///|
    pub struct MyTransport {
    // Platform-specific fields (process handle, stdio, etc.)
    }

    ///|
    pub impl @client.Transport for MyTransport with fn read(self) {
    // Read from LSP server stdout
    // Return @client.ReadResult::Data(data) or ::Eof or ::Error(msg)
    }

    ///|
    pub impl @client.Transport for MyTransport with fn write(self, data) {
    // Write to LSP server stdin
    }

    // 2. Create client and connect

    ///|
    fn main {
    let config = @client.ClientConfig::{
    root_uri: "file:///path/to/project",
    process_id: 1234,
    }
    let client = @client.Client::new(config)
    let transport = MyTransport::new()

    // Register notification handler for diagnostics
    client.on_notification("textDocument/publishDiagnostics", fn(params) {
    match params {
    Some(@json.JsonValue::Object(obj)) =>
    // Handle diagnostics
    println(obj.to_json().stringify())
    _ => ()
    }
    })

    // Initialize
    client.initialize(transport, fn(resp) { println("Initialized!") })

    // Process server responses
    client.tick(transport)

    // Send initialized notification
    client.send_initialized(transport)

    // Open a document
    client.open_document(
    transport, "file:///path/to/file.mbt", "moonbit", 1, "fn main { }",
    )

    // Keep processing messages
    while client.tick(transport) {
    // Handle incoming notifications
    }

    // Shutdown
    client.shutdown(transport, fn(_) { })
    client.tick(transport)
    client.exit(transport)
    }

    #LSP Server

    Build an LSP server that responds to client requests.

    // Import packages
    // "mizchi/lsp/server"
    // "mizchi/lsp/jsonrpc"

    // 1. Implement IoHandler trait for your platform

    ///|
    pub struct MyIoHandler {
    // Platform-specific fields (stdio, socket, etc.)
    }

    ///|
    pub impl @server.IoHandler for MyIoHandler with fn read(self) {
    // Read from client
    // Return @server.ReadResult::Data(data) or ::Eof or ::Error(msg)
    }

    ///|
    pub impl @server.IoHandler for MyIoHandler with fn write(self, data) {
    // Write to client stdout
    }

    ///|
    pub impl @server.IoHandler for MyIoHandler with fn write_error(self, data) {
    // Write to client stderr
    }

    // 2. Create server and register handlers

    ///|
    fn main {
    let config = @server.ServerConfig::{ name: "my-lsp-server", version: "0.1.0" }
    let server = @server.Server::new(config)
    let io = MyIoHandler::new()

    // Register request handlers
    server.on_request("initialize", fn(req) {
    let capabilities = {
    ...
    }
    @jsonrpc.Response::ok(req.id, capabilities)
    })

    server.on_request("textDocument/hover", fn(req) {
    // Handle hover request
    @jsonrpc.Response::ok(req.id, hover_result)
    })

    // Register notification handlers
    server.on_notification("textDocument/didOpen", fn(req) {
    // Handle document open
    })

    // Run the server
    server.run(io)
    }

    #Testing with Stub Implementations

    Both client and server provide stub implementations for testing.

    ///|
    test "client with stub transport" {
    let config = @client.ClientConfig::{ root_uri: "file:///test", process_id: 1 }
    let client = @client.Client::new(config)
    let transport = @client.StubTransport::new()

    // Send request
    client.initialize(transport, fn(_) { })

    // Verify output
    let output = transport.get_output()
    assert_true(output[0].contains("initialize"))

    // Simulate server response
    let response = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}"
    transport.push_input(
    "Content-Length: \{response.length()}\r\n\r\n\{response}",
    )

    // Process
    client.tick(transport)
    }

    ///|
    test "server with stub io" {
    let config = @server.ServerConfig::{ name: "test", version: "0.1.0" }
    let server = @server.Server::new(config)
    let io = @server.StubIoHandler::new()

    server.on_request("test/ping", fn(req) {
    @jsonrpc.Response::ok(req.id, @json.JsonValue::String("pong"))
    })

    // Feed a request
    let message = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"test/ping\"}"
    server.feed(io, "Content-Length: \{message.length()}\r\n\r\n\{message}")

    // Check response
    let output = io.get_output()
    assert_true(output[0].contains("pong"))
    }

    #Filesystem Abstraction

    The fs package provides a FileSystem trait for platform-independent file operations.

    // "mizchi/lsp/fs"

    // Use in-memory filesystem for testing/WASM
    let fs = @fs.MemoryFs::new()

    // Basic file operations
    fs.write_file("/project/main.mbt", "fn main {}")
    fs.read_file("/project/main.mbt") // FsResult::Ok("fn main {}")
    fs.exists("/project/main.mbt") // true
    fs.is_file("/project/main.mbt") // true

    // Directory operations
    fs.create_dir("/project/src")
    fs.read_dir("/project") // Array of DirEntry

    // Implement FileSystem trait for real filesystem
    pub struct RealFs { }

    pub impl @fs.FileSystem for RealFs with read_file(self, path) {
    // Platform-specific file read
    }

    pub impl @fs.FileSystem for RealFs with write_file(self, path, content) {
    // Platform-specific file write
    }
    // ... other methods

    #DocumentStore

    Manage open documents backed by a filesystem.

    let fs = @fs.MemoryFs::new()
    let store = @fs.DocumentStore::new(fs)

    // Open document from filesystem
    store.open("file:///project/main.mbt", "moonbit", 1, None)

    // Open document with content (skip filesystem read)
    store.open("file:///new.mbt", "moonbit", 1, Some("fn test {}"))

    // Update and save
    store.update("file:///new.mbt", 2, "fn test { let x = 1 }")
    store.save("file:///new.mbt") // Write back to filesystem

    // List open documents
    store.list_open() // ["file:///project/main.mbt", "file:///new.mbt"]

    #Platform-specific Implementation

    The Transport (client), IoHandler (server), and FileSystem (fs) traits abstract platform-specific operations:

    PlatformTransport/IoHandlerFileSystem
    NativeProcess spawn + stdioOS file system
    JS/Nodechild_process.spawn via FFINode fs via FFI
    WASMStubTransport/StubIoHandlerMemoryFs

    #JSON-RPC Low-level API

    // Encode request
    let request = @jsonrpc.Request::new(
    "textDocument/hover",
    params=Some(params_json),
    id=Some(@jsonrpc.RequestId::Number(1)),
    )
    let encoded = @jsonrpc.encode_request(request)

    // Decode response
    let decoder = @jsonrpc.Decoder::new()
    decoder.push(data)
    match decoder.decode() {
    Some(result) => {
    match result.message {
    @jsonrpc.Message::Response(resp) => { ... }
    @jsonrpc.Message::Request(req) => { ... }
    }
    }
    None => ()
    }

    #LSP Types

    Auto-generated from LSP specification. See types/generated.mbt for all types.

    // Position, Range, Location

    ///|
    let pos = @types.Position::{ line: 0, character: 5 }

    ///|
    let range = @types.Range::{ start: pos, end: pos }

    // Diagnostic

    ///|
    let diag = @types.Diagnostic::{
    range,
    message: "Error message",
    severity: Some(@types.DiagnosticSeverity::Error),
    // ...
    }

    #Development

    # Run tests moon test # Generate LSP types from specification cd codegen && pnpm install && pnpm run generate # Format code moon fmt

    #License

    Apache-2.0

    Source Files