Skip to content

LSP Phase 1: flag-gated ksrpc transport pipe (stub server)#41

Merged
monkopedia-reviewer merged 1 commit into
mainfrom
lsp-phase1-pipe
Jun 3, 2026
Merged

LSP Phase 1: flag-gated ksrpc transport pipe (stub server)#41
monkopedia-reviewer merged 1 commit into
mainfrom
lsp-phase1-pipe

Conversation

@monkopedia-coder
Copy link
Copy Markdown
Collaborator

First phase of the LSP epic (#35). Proves the LSP message pipe end-to-end with a stub server — no real Kotlin LSP engine yet. Additive + flag-gated: with lspEnabled OFF the editor behaves byte-for-byte as today.

protocol/

  • Add com.monkopedia.lsp:lsp-ksrpc:1.0.0 (as api, since the @KsService LSP interfaces appear in the public KonstructionService surface). Built against ksrpc 1.1.0 / Kotlin 2.3.20 = our versions.
  • KonstructionService gains the nested LSP sub-service:
    @KsMethod("/lsp")
    suspend fun lsp(client: KsrpcLanguageClient): KsrpcLanguageServer
    Passing KsrpcLanguageClient as a param lets ksrpc multiplex the reverse publishDiagnostics channel onto the same WebSocket (no second connection, no connectAsLspServer). KonstructionService already extended RpcBidiService (required because lsp() both returns and accepts a sub-service), so the tier bump did not ripple into Konstructor/Workspace.

backend

  • StubLanguageServer (subclass DefaultLanguageServer): canned InitializeResult advertising hover + completion; on textDocument/didOpen pushes one canned publishDiagnostics (a single fake warning) back through the held KsrpcLanguageClient.
  • KonstructionServiceImpl.lsp(client) stashes the client and returns the stub, served through the existing ksrpc graph.

frontend (wasmJs) — behind the lspEnabled flag, default OFF

  • Add kodemirror lsp-client + lsp-ksrpc deps.
  • EditorLspClient (a KsrpcLanguageClient) routes server-pushed diagnostics into kodemirror's LSPClient.languageClient.
  • EditorPane builds LSPClient(server = lsp(EditorLspClient())) and appends languageServerSupport(...) to the editor extensions only when the flag is on and the session is ready — OFF leaves the extensions chain identical to before.
  • SettingsViewModel.lspEnabled (PersistedStateFlow, default false).

e2e

  • LspPipeTest: with the flag ON, opening a konstruction round-trips the canned publishDiagnostics back to the editor (count=1, asserted via a JS-bridge hook).

Verification

  • Whole project compiles (RpcBidiService bump broke nothing).
  • Full clean :e2e:test green — flag-OFF suite unchanged + flag-ON round-trip proven (LSP diagnostics received: count=1 uri=file:///0/0/content.csgs).
  • spotlessCheck clean; backend + protocol unit tests pass.

Part of #35; Fixes #36.

🤖 Generated with Claude Code

Prove the LSP message pipe end-to-end with a STUB server (no real Kotlin
LSP engine yet). Additive and flag-gated: with the lspEnabled flag OFF the
editor behaves byte-for-byte as before.

protocol/
- Add com.monkopedia.lsp:lsp-ksrpc:1.0.0 (api; the @KsService LSP interfaces
  appear in the public KonstructionService surface).
- KonstructionService gains a nested LSP sub-service:
    @KsMethod("/lsp") suspend fun lsp(client: KsrpcLanguageClient): KsrpcLanguageServer
  Passing KsrpcLanguageClient as a PARAM lets ksrpc multiplex the reverse
  publishDiagnostics channel onto the same WebSocket. KonstructionService
  already extends RpcBidiService (required because lsp() both returns and
  accepts a sub-service), so the tier bump did not ripple.

backend/
- StubLanguageServer (subclass DefaultLanguageServer): canned InitializeResult
  advertising hover + completion; on textDocument/didOpen pushes one canned
  publishDiagnostics back through the held KsrpcLanguageClient.
- KonstructionServiceImpl.lsp(client) stashes the client and returns the stub,
  served through the existing ksrpc graph.

frontend/ (wasmJs) — behind a persisted lspEnabled flag, default OFF
- Add kodemirror lsp-client + lsp-ksrpc deps.
- EditorLspClient (KsrpcLanguageClient) routes server-pushed diagnostics into
  kodemirror's LSPClient.languageClient.
- EditorPane builds LSPClient(server = lsp(EditorLspClient())) and appends
  languageServerSupport(...) to the extensions chain only when the flag is on
  and the session is ready — OFF leaves the extensions identical to before.
- SettingsViewModel.lspEnabled (PersistedStateFlow, default false).

e2e/
- LspPipeTest: with the flag ON, opening a konstruction round-trips the canned
  publishDiagnostics back to the editor (asserted via a JS-bridge hook).

Verified: whole project compiles; full clean :e2e:test green (flag-OFF
unchanged + flag-ON round-trip); spotlessCheck clean; backend/protocol unit
tests pass.

Part of #35; Fixes #36.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@monkopedia-reviewer monkopedia-reviewer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Monkopedia — I've reviewed this entire PR to merge standard and everything bot-verifiable is sound; the only thing I'm leaving for your judgment is the shape of Phase 1's LSP transport pipe before this foundational surface lands.

Reviewed & sound (vetted at tier-1 standard):

  • Flag-OFF safety is airtight. With lspEnabled default false, produceState returns null immediately, session's remember keys on lspExtension (null when OFF), and the if (lspExtension != null) append is skipped — the editor extensions chain is byte-for-byte identical to before. Real gating, not hidden UI. The default-OFF e2e suite passing green is the evidence.
  • ksrpc nested-sub-service usage is correct. KonstructionService already extended RpcBidiService, so the bidi requirement for lsp(client): KsrpcLanguageServer did not ripple to Konstructor/Workspace. Passed/returned @KsService types stay plain RpcService; backend stashes the client in StubLanguageServer and emits publishDiagnostics on it over the multiplexed reverse channel. No glaring leak (full session teardown is explicitly deferred to a later phase).
  • Round-trip test is meaningful (not a tautology): LspPipeTest creates a real konstruction, flips the flag ON, and waits for a diagnostic that can only appear via a genuine backend→editor push, asserting count >= 1.
  • CI green — independently verified: build-and-test passed (includes the :e2e:test gate with both flag-OFF suite and the new flag-ON round-trip).
  • Quality/scope: idiomatic, Apache-2022 headers on new files, scope limited to a stub (no real Kotlin engine creeping in).

One non-blocking nit (not gating): EditorPane's produceState uses catch (_: Throwable), where the codebase overwhelmingly uses catch (_: Exception) (40+ vs 2); Throwable also swallows CancellationException — benign here since the coroutine is torn down on re-key, but Exception would match house style. Flagging for a possible follow-up, not blocking this PR.

For your judgment: approve the foundational LSP transport pipe to land behind a default-off flag, as Phase 1's shape for epic #35?

Copy link
Copy Markdown
Collaborator

@monkopedia-reviewer monkopedia-reviewer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tier-3 signoff by Monkopedia: foundational LSP transport pipe lands behind the default-off lspEnabled flag. Reviewed to merge standard — flag-OFF airtight, ksrpc nested-sub-service usage correct (independently confirmed by the lsp-kotlin maintainer), meaningful round-trip test, CI green. The Throwable→Exception nit tracked as a Phase-2 follow-up.

@monkopedia-reviewer monkopedia-reviewer merged commit f783182 into main Jun 3, 2026
1 check passed
@monkopedia-reviewer monkopedia-reviewer deleted the lsp-phase1-pipe branch June 3, 2026 19:36
monkopedia-reviewer pushed a commit that referenced this pull request Jun 4, 2026
…json + stdio bridge (#42)

Replace the Phase 1 stub KsrpcLanguageServer with a REAL bridge to a JetBrains
kotlin-lsp (intellij-server --stdio) subprocess, producing genuine kcsg-aware
diagnostics for the open konstruction. Positions stay in wrapped-.kt space
(the ±3-line csgs↔kotlin translation is Phase 3 / #38).

Bridge (two legs):
- Frontend↔backend: the Phase 1 nested KonstructionService.lsp(client) is kept;
  the editor's KsrpcLanguageClient is stashed.
- Backend↔subprocess: KotlinLspProcess spawns one warm intellij-server, wraps its
  stdin/stdout with lsp-ksrpc's asLspConnection, and connectAsLspClient(forwarder)
  yields a subprocess-facing KsrpcLanguageServer stub. BridgeLanguageServer
  delegates each request to that stub.

Pull→push diagnostics: kotlin-lsp does NOT push publishDiagnostics; it answers
PULL textDocument/diagnostic. The editor client (Phase 1 design) listens for
pushes, so on didOpen the bridge polls the engine (retrying through the ~120s
cold index) and forwards items up as publishDiagnostics, rewriting the wrapped-.kt
URI back to the editor's content.csgs URI.

Per-konstruction setup: KonstructionLspWorkspace synthesizes a workspace dir with
(a) the wrapped .kt via KonstructionControllerImpl.copyContentToScript (compiler-
exact wrapping) and (b) a workspace.json matching the proven PoC recipe
(JAVA_MODULE + moduleSource + one project library pointing at lib.jar as a single
root_itself CLASSES root). Built with JSON object builders so the backend needs no
serialization compiler plugin.

Lifecycle gating (per the lsp-kotlin maintainer): LifecycleState is driven by hand
— initialized/shutdown/exit advance it, and awaitInitialized() gates the first
server→client emit. No blocking client-bound request from inside initialize().
On exit the poll is cancelled and the subprocess-facing stub is closed.

Config: KONSTRUCTOR_KOTLIN_LSP (or -Dkonstructor.kotlinLsp) points at the binary;
KONSTRUCTOR_KOTLIN_LSP_SYSTEM_PATH persists the warm index. Unset/missing binary ⇒
LSP stays off (bridge degrades to an inert server), never a crash. CI has no
binary, so this degrades to off and stays green.

Also: fix the #41 carry-over nit in EditorPane (catch Throwable → Exception).

Tests:
- KonstructionLspWorkspaceTest (CI-safe): asserts the synthesized workspace.json
  matches the strict PoC schema and the wrapped .kt reuses the compiler wrapping.
- BridgeLanguageServerIntegrationTest (LOCAL-only, double-gated on -Dintegration
  and KONSTRUCTOR_KOTLIN_LSP): drives the real bridge against the live engine.
- LspPipeTest updated: the canned-diagnostic stub is gone, so with no engine on CI
  the flag-ON path must establish cleanly and surface zero diagnostics.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LSP Phase 1: flag-gated ksrpc transport pipe (stub server)

2 participants