From 219a6f153f744113369a43e323fbf3de050a88b7 Mon Sep 17 00:00:00 2001 From: Sam Gammon Date: Mon, 12 Oct 2026 19:15:25 -0700 Subject: [PATCH] fix: point at the kotlin extension's first-run setup instead of a reload failure JetBrains.kotlin-server activates but registers none of its server commands until its region, terms and data-sharing prompts are answered, so every sync reported "Kotlin LSP did not reload". Detect that state from its intellij.region and intellij.dataSharing settings and the missing server commands, show one notice per window with a button to its setup page, and say the same when Debug with Elide would attach to its debugger. Signed-off-by: Sam Gammon --- packages/core/src/index.ts | 1 + packages/core/src/kotlinSetup.ts | 29 +++++++++++++ packages/core/test/kotlinSetup.test.ts | 23 ++++++++++ packages/vscode/src/debug.ts | 9 +++- packages/vscode/src/jetbrains.ts | 58 +++++++++++++++++++++++++- packages/vscode/src/projects.ts | 7 +++- 6 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/kotlinSetup.ts create mode 100644 packages/core/test/kotlinSetup.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d3fd613..d0bb58e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,4 +38,5 @@ export * from "./libraries.js"; export * from "./jdk.js"; export * from "./model.js"; export * from "./kotlinLsp.js"; +export * from "./kotlinSetup.js"; export * from "./workspace.js"; diff --git a/packages/core/src/kotlinSetup.ts b/packages/core/src/kotlinSetup.ts new file mode 100644 index 0000000..e6f84de --- /dev/null +++ b/packages/core/src/kotlinSetup.ts @@ -0,0 +1,29 @@ +/** + * First-run setup of JetBrains.kotlin-server: until a region and a data-sharing choice are recorded (and its terms + * accepted), the extension activates but never starts its language server or debug adapter, so none of its + * `jetbrains.kotlin.*` commands are registered. The choices are stored as the application-scoped settings below, + * whose default `not_set` means unanswered; the extension reads only the user (global) value. + */ +export const KOTLIN_REGION_SETTING = "intellij.region"; +export const KOTLIN_DATA_SHARING_SETTING = "intellij.dataSharing"; +/** Opens the Kotlin extension's setup page (region, terms, data sharing). */ +export const KOTLIN_SETUP_COMMAND = "jetbrains.showLicenseSetup"; + +const UNSET = "not_set"; + +export type KotlinSetupStep = "region" | "data sharing"; + +/** The setup questions still unanswered, given the user values of the region and data-sharing settings. */ +export function unansweredKotlinSetupSteps(region: unknown, dataSharing: unknown): KotlinSetupStep[] { + const answered = (v: unknown) => typeof v === "string" && v !== "" && v !== UNSET; + const steps: KotlinSetupStep[] = []; + if (!answered(region)) steps.push("region"); + if (!answered(dataSharing)) steps.push("data sharing"); + return steps; +} + +/** User-facing explanation for a Kotlin extension held back by its setup; `steps` may be empty (terms pending). */ +export function kotlinSetupPendingMessage(steps: readonly KotlinSetupStep[]): string { + const which = steps.length > 0 ? ` (${steps.join(", ")})` : ""; + return `the Kotlin extension is waiting for its first-run setup${which}; its language server and debugger start once you complete it.`; +} diff --git a/packages/core/test/kotlinSetup.test.ts b/packages/core/test/kotlinSetup.test.ts new file mode 100644 index 0000000..49b47e2 --- /dev/null +++ b/packages/core/test/kotlinSetup.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "bun:test"; +import { kotlinSetupPendingMessage, unansweredKotlinSetupSteps } from "../src/index.js"; + +describe("unansweredKotlinSetupSteps", () => { + test("missing or `not_set` values are unanswered", () => { + expect(unansweredKotlinSetupSteps(undefined, undefined)).toEqual(["region", "data sharing"]); + expect(unansweredKotlinSetupSteps("not_set", "not_set")).toEqual(["region", "data sharing"]); + expect(unansweredKotlinSetupSteps("europe", "not_set")).toEqual(["data sharing"]); + expect(unansweredKotlinSetupSteps("", "none")).toEqual(["region"]); + }); + + test("any recorded choice counts as answered", () => { + expect(unansweredKotlinSetupSteps("americas", "none")).toEqual([]); + expect(unansweredKotlinSetupSteps("europe", "full")).toEqual([]); + }); +}); + +describe("kotlinSetupPendingMessage", () => { + test("names the open questions, if known", () => { + expect(kotlinSetupPendingMessage(["region", "data sharing"])).toContain("first-run setup (region, data sharing)"); + expect(kotlinSetupPendingMessage([])).toContain("first-run setup;"); + }); +}); diff --git a/packages/vscode/src/debug.ts b/packages/vscode/src/debug.ts index fd2ed7a..a8da9ef 100644 --- a/packages/vscode/src/debug.ts +++ b/packages/vscode/src/debug.ts @@ -14,7 +14,7 @@ import { } from "@elide/ide-core"; import * as vscode from "vscode"; import { configuredInvocation, readConfig, type ElideConfig } from "./config.js"; -import { jetBrainsDebuggerType } from "./jetbrains.js"; +import { jetBrainsDebuggerType, notifyKotlinSetupPending, pendingKotlinSetup } from "./jetbrains.js"; import type { ElideUi } from "./output.js"; import type { ElideWorkspace } from "./projects.js"; @@ -95,6 +95,13 @@ export async function resolveAttachType(settings: ElideConfig): Promise types.has(t)); } +/** The Kotlin extension is active but holds its language server back until its first-run setup is complete. */ +export class KotlinSetupPendingError extends Error { + constructor(readonly steps: readonly KotlinSetupStep[]) { + super(kotlinSetupPendingMessage(steps)); + } +} + +/** + * The first-run setup steps the Kotlin extension still waits on, or `undefined` when it is not held back by them. + * + * The region and data-sharing answers are read from its settings. Its accepted terms live in its private state, so + * an active extension that registered its setup command but none of its server commands counts as waiting too + * (with no step named): it registers those commands only once the server is allowed to start. + */ +export async function pendingKotlinSetup(): Promise { + const ext = jetBrainsExtension(); + if (!ext) return undefined; + const config = vscode.workspace.getConfiguration(); + const steps = unansweredKotlinSetupSteps(config.inspect(KOTLIN_REGION_SETTING)?.globalValue, config.inspect(KOTLIN_DATA_SHARING_SETTING)?.globalValue); + if (!ext.isActive) return steps.length > 0 ? steps : undefined; + const available = new Set(await vscode.commands.getCommands(true)); + if (RELOAD_COMMANDS.some((c) => available.has(c))) return undefined; + return steps.length > 0 || available.has(KOTLIN_SETUP_COMMAND) ? steps : undefined; +} + +let setupNoticeShown = false; + +/** + * Tell the user the Kotlin extension waits on its setup, offering to open it. Background callers (every sync) pass + * `once` so the notice appears once per window; a command the user just ran always explains why it did nothing. + */ +export function notifyKotlinSetupPending(steps: readonly KotlinSetupStep[], { once = false } = {}): void { + if (once && setupNoticeShown) return; + setupNoticeShown = true; + void vscode.window.showWarningMessage(`Elide: ${kotlinSetupPendingMessage(steps)}`, "Open Kotlin Setup").then(async (pick) => { + if (pick !== "Open Kotlin Setup") return; + const available = await vscode.commands.getCommands(true); + if (available.includes(KOTLIN_SETUP_COMMAND)) await vscode.commands.executeCommand(KOTLIN_SETUP_COMMAND); + else await vscode.commands.executeCommand("workbench.action.openSettings", "intellij.region intellij.dataSharing"); + }); +} + /** * Ask a running Kotlin LSP to pick up the regenerated `workspace.json`. * * Returns the command used, or `undefined` when the JetBrains extension is not active yet (the server reads the * file when it starts on the first Kotlin document, so nothing needs to happen). The command's own promise is not * awaited: `jetbrains.kotlin.reloadWorkspace` resolves only once the server finished re-importing (and has been - * observed never to resolve while an import is already running); rejections are logged by the caller. + * observed never to resolve while an import is already running); rejections are logged by the caller. Throws + * {@link KotlinSetupPendingError} while the extension waits on its first-run setup: the server imports + * `workspace.json` when it starts afterwards. */ export async function reloadKotlinLsp(): Promise { const ext = jetBrainsExtension(); if (!ext?.isActive) return undefined; const available = new Set(await vscode.commands.getCommands(true)); const command = RELOAD_COMMANDS.find((c) => available.has(c)); + if (!command) { + const steps = await pendingKotlinSetup(); + if (steps) throw new KotlinSetupPendingError(steps); + } if (!command) throw new Error(`Kotlin LSP exposes none of ${RELOAD_COMMANDS.join(", ")}`); const pending = Promise.resolve(vscode.commands.executeCommand(command)); // Surface a rejection to the caller only if it happens promptly; a long-running reload is not an error. diff --git a/packages/vscode/src/projects.ts b/packages/vscode/src/projects.ts index 5c0e4c1..b6c8493 100644 --- a/packages/vscode/src/projects.ts +++ b/packages/vscode/src/projects.ts @@ -23,7 +23,7 @@ import { } from "@elide/ide-core"; import * as vscode from "vscode"; import { readConfig } from "./config.js"; -import { reloadKotlinLsp } from "./jetbrains.js"; +import { KotlinSetupPendingError, notifyKotlinSetupPending, reloadKotlinLsp } from "./jetbrains.js"; import type { ElideUi } from "./output.js"; export interface ElideProject { @@ -430,6 +430,11 @@ export class ElideWorkspace implements vscode.Disposable { void reloadKotlinLsp().then( (used) => this.ui.log(used ? `Kotlin LSP reload requested via ${used}` : "Kotlin LSP not running yet; it will import workspace.json on start"), (e) => { + if (e instanceof KotlinSetupPendingError) { + this.ui.log(`Kotlin LSP not started: ${e.message}`); + notifyKotlinSetupPending(e.steps, { once: true }); + return; + } this.ui.log(`Kotlin LSP reload failed: ${e instanceof Error ? e.message : String(e)}`); void vscode.window.showWarningMessage("Elide: Kotlin LSP did not reload; run 'IntelliJ: Restart Language Server'."); },