Skip to content

Repository files navigation

lsp-kmp

A Kotlin Multiplatform implementation of the Language Server Protocol (LSP) and the Debug Adapter Protocol (DAP), modeled after eclipse-lsp4j/lsp4j.

Features

  • JSON-RPC 2.0 core — message models, request/response correlation, Content-Length framed transport (shared by LSP and DAP)
  • LSP model parity with lsp4j — all 365 model classes from lsp4j's Protocol.xtend, including client/server capabilities, completion, hover, diagnostics, semantic tokens, inlay hints, inline values, notebook documents, and pull diagnostics (LSP 3.18)
  • LSP servicesLanguageServer, TextDocumentService, WorkspaceService, LanguageClient interfaces mirroring lsp4j, wired to the wire by LanguageServerLauncher
  • DAP — debug adapter models (initialize, setBreakpoints, threads, stackTrace, scopes, variables, evaluate, …), DebugAdapter interface, and DebugAdapterLauncher
  • Multiplatform — JVM, Android, and every Kotlin/Native tier-1 target (macOS, iOS, tvOS, watchOS, Linux, Windows); all except JS/Wasm
  • Examples — a stdio-based language server and a TCP socket-based language server

Modules

Path Description
: (root) Library: cn.enaium.lsp (LSP + JSON-RPC) and cn.enaium.lsp.dap (DAP)
:examples:stdio Language server over stdin/stdout
:examples:socket Language server over a TCP socket

Package layout

cn.enaium.lsp
├── jsonrpc          # JSON-RPC 2.0 core (messages, server, transport, launcher)
├── model            # LSP data models (lsp4j parity)
├── dap              # Debug Adapter Protocol
│   ├── model        # DAP data models
│   ├── DebugAdapter
│   └── DebugAdapterLauncher
├── LanguageServer
├── LanguageServerLauncher
└── LanguageServerServices

Requirements

  • JDK 25 (jvmToolchain)
  • Gradle 9.7+ (wrapper included)

Usage

The library is not yet published to a Maven repository. Consume it as a local project dependency or wait for publication.

Writing a language server

Implement LanguageServer (only the methods you support need real bodies; every service method has a default), then run it over a MessageTransport:

class MyServer : LanguageServer {
    override fun initialize(params: InitializeParams): InitializeResult =
        InitializeResult(
            capabilities = ServerCapabilities(
                textDocumentSync = JsonPrimitive(TextDocumentSyncKind.Full),
                hoverProvider = JsonPrimitive(true),
            ),
            serverInfo = ServerInfo(name = "my-server", version = "1.0.0"),
        )

    override fun shutdown(): Any? = null
    override fun exit() { Runtime.getRuntime().halt(0) }

    override fun textDocumentService(): TextDocumentService? = MyTextDocumentService()
    override fun workspaceService(): WorkspaceService? = null
    override fun windowService(): WindowService? = null
}

class MyTextDocumentService : TextDocumentService {
    override fun hover(params: HoverParams): Hover? =
        Hover(contents = JsonPrimitive("Hello from ${params.textDocument.uri}"))
}

fun main() {
    // stdio transport: content-length framed messages on stdin/stdout
    val transport = StreamMessageTransport(System.`in`, System.out)
    val launcher = LanguageServerLauncher(transport, MyServer())
    launcher.listen()
}

Writing a debug adapter

class MyAdapter : DebugAdapter {
    override fun initialize(request: InitializeRequestArguments): Capabilities =
        Capabilities(supportsConfigurationDoneRequest = true)

    override fun launch(args: JsonElement?): JsonElement? = null
    override fun threads(): ThreadsResponseBody = ThreadsResponseBody(listOf(Thread(1, "main")))
    // ... implement the remaining commands your adapter supports
}

fun main() {
    val transport = StreamMessageTransport(System.`in`, System.out)
    val launcher = DebugAdapterLauncher(transport, MyAdapter())
    launcher.listen()
}

Custom transports

MessageTransport is a two-method interface (send / receive); StreamMessageTransport implements the standard Content-Length framing over any InputStream/OutputStream — including sockets, pipes, or WebSocket byte streams. Use the socket example as a reference for a TCP server.

Examples

Build and run:

# stdio language server (LSP client integration, e.g. VS Code language client)
./gradlew :examples:stdio:run

# socket language server on port 8080 (pass a port as the first argument)
./gradlew :examples:socket:run

Both examples accept the standard LSP handshake (initialize → requests → shutdownexit).

Testing

./gradlew :jvmTest        # JVM tests
./gradlew :macosArm64Test # native (macOS arm64) tests
./gradlew :linuxX64Test   # native (Linux x64) tests

Simulator-based tests (iOS/tvOS/watchOS simulators) require the corresponding simulator runtime installed via Xcode; they are skipped when no runtime is available. Apple x64 test tasks are disabled (Rosetta not required for compilation, only for running tests).

Dependencies are declared through a Gradle version catalog at gradle/libs.versions.toml. Android builds need an SDK location; provide it via ANDROID_HOME or a local local.properties (sdk.dir=...).

The test suite covers JSON-RPC dispatch/correlation, typed launcher round-trips, stream framing, end-to-end pipe communication, and LSP lifecycle wiring.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages