From 9f11c559cdda78c520161c13ac9dca9a79e8a007 Mon Sep 17 00:00:00 2001 From: "Can H. Tartanoglu" Date: Wed, 8 Jul 2026 17:17:49 +0200 Subject: [PATCH] Add built-in Pkl LSP support --- packages/core/src/v1/config/lsp.ts | 1 + packages/opencode/src/lsp/language.ts | 1 + packages/opencode/src/lsp/server.ts | 20 +++++++++++++++ packages/opencode/test/config/lsp.test.ts | 5 ++++ packages/opencode/test/lsp/index.test.ts | 30 +++++++++++++++++++++++ packages/web/src/content/docs/lsp.mdx | 1 + 6 files changed, 58 insertions(+) diff --git a/packages/core/src/v1/config/lsp.ts b/packages/core/src/v1/config/lsp.ts index 89a58b5b2c9b..a3dd5c7a82ec 100644 --- a/packages/core/src/v1/config/lsp.ts +++ b/packages/core/src/v1/config/lsp.ts @@ -55,6 +55,7 @@ export const builtinServerIds = [ "gleam", "clojure-lsp", "nixd", + "pkl", "tinymist", "haskell-language-server", "julials", diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts index 07a2e97231e8..2b70ebd4e961 100644 --- a/packages/opencode/src/lsp/language.ts +++ b/packages/opencode/src/lsp/language.ts @@ -65,6 +65,7 @@ export const LANGUAGE_EXTENSIONS: Record = { ".pm": "perl", ".pm6": "perl6", ".php": "php", + ".pkl": "pkl", ".ps1": "powershell", ".psm1": "powershell", ".pug": "jade", diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 2be68aa5a92d..3553667a3f92 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1864,6 +1864,26 @@ export const Nixd: Info = { }, } +export const Pkl: Info = { + id: "pkl", + extensions: [".pkl"], + root: NearestRoot(["PklProject", "pkl.project", "pkl.toml", "flake.nix", ".git"]), + async spawn(root) { + const pklLsp = which("pkl-lsp") + if (!pklLsp) { + return + } + return { + process: spawn(pklLsp, ["--stdio"], { + cwd: root, + env: { + ...process.env, + }, + }), + } + }, +} + export const Tinymist: Info = { id: "tinymist", extensions: [".typ", ".typc"], diff --git a/packages/opencode/test/config/lsp.test.ts b/packages/opencode/test/config/lsp.test.ts index 3d85e4cf75cc..3ae7fb566bfc 100644 --- a/packages/opencode/test/config/lsp.test.ts +++ b/packages/opencode/test/config/lsp.test.ts @@ -22,6 +22,11 @@ describe("ConfigLSPV1.Info refinement", () => { expect(decodeEffect(input)).toEqual(input) }) + test("pkl builtin with no extensions passes", () => { + const input = { pkl: { command: ["pkl-lsp", "--stdio"] } } + expect(decodeEffect(input)).toEqual(input) + }) + test("custom server WITH extensions passes", () => { const input = { "my-lsp": { command: ["my-lsp-bin"], extensions: [".ml"] }, diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index 57a7b59d1cfe..6b8c077cc780 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -7,6 +7,7 @@ import { Config } from "@/config/config" import { RuntimeFlags } from "@/effect/runtime-flags" import { LSP } from "@/lsp/lsp" import * as LSPServer from "@/lsp/server" +import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { TestInstance } from "../fixture/fixture" import { awaitWithTimeout, testEffect } from "../lib/effect" @@ -26,6 +27,12 @@ const disabledDownloadIt = testEffect( ) describe("lsp.spawn", () => { + it.effect("maps PKL files to the pkl language id", () => + Effect.sync(() => { + expect(LANGUAGE_EXTENSIONS[".pkl"]).toBe("pkl") + }), + ) + it.instance( "does not spawn builtin LSP for files outside instance", () => @@ -228,4 +235,27 @@ describe("lsp.spawn", () => { ), { config: { lsp: true } }, ) + + it.instance( + "would spawn builtin PKL LSP for PKL files when lsp is true", + () => + LSP.Service.use((lsp) => + Effect.gen(function* () { + const dir = (yield* TestInstance).directory + const spy = spyOn(LSPServer.Pkl, "spawn").mockResolvedValue(undefined) + + try { + yield* lsp.hover({ + file: path.join(dir, "config.pkl"), + line: 0, + character: 0, + }) + expect(spy).toHaveBeenCalledTimes(1) + } finally { + spy.mockRestore() + } + }), + ), + { config: { lsp: true } }, + ) }) diff --git a/packages/web/src/content/docs/lsp.mdx b/packages/web/src/content/docs/lsp.mdx index b9428e5b2d30..0e183afaa60f 100644 --- a/packages/web/src/content/docs/lsp.mdx +++ b/packages/web/src/content/docs/lsp.mdx @@ -34,6 +34,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | ocaml-lsp | .ml, .mli | `ocamllsp` command available | | oxlint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue, .astro, .svelte | `oxlint` dependency in project | | php intelephense | .php | Auto-installs for PHP projects | +| pkl | .pkl | `pkl-lsp` command available | | prisma | .prisma | `prisma` command available | | pyright | .py, .pyi | `pyright` dependency installed | | razor | .razor, .cshtml | `.NET SDK` and VS Code C# extension installed |