From 0ccacb9a7e26e1e06d186f2200db9f0c799b5b8c Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Mon, 15 Dec 2025 19:31:40 +0100 Subject: [PATCH 1/5] feat: add experimental support for Ty language server - Introduced OPENCODE_EXPERIMENTAL_LSP_TY flag to conditionally enable Ty language server functionality. - Implemented Ty language server with support for Python file extensions and virtual environment detection. --- packages/opencode/src/flag/flag.ts | 3 +- packages/opencode/src/lsp/index.ts | 6 ++++ packages/opencode/src/lsp/server.ts | 54 +++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index d7a24708a070..4c2d2dc0cc3a 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -24,7 +24,8 @@ export namespace Flag { truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA") export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH") export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS") - + export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY") + function truthy(key: string) { const value = process.env[key]?.toLowerCase() return value === "true" || value === "1" diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 764c91fcc873..c360234d6bc9 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -9,6 +9,7 @@ import z from "zod" import { Config } from "../config/config" import { spawn } from "child_process" import { Instance } from "../project/instance" +import { Flag } from "@/flag/flag" export namespace LSP { const log = Log.create({ service: "lsp" }) @@ -204,6 +205,11 @@ export namespace LSP { for (const server of Object.values(s.servers)) { if (server.extensions.length && !server.extensions.includes(extension)) continue + if (extension === ".py" || extension === ".pyi") { + if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY && !server.experimental) continue // If experimental flag is enabled and server is not experimental, skip + if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY && server.experimental) continue // If experimental flag is disabled and server is experimental, skip + } + const root = await server.root(file) if (!root) continue if (s.broken.has(root + server.id)) continue diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index e3e3fdf7de7a..adb7183555b1 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -50,6 +50,7 @@ export namespace LSPServer { global?: boolean root: RootFunction spawn(root: string): Promise + experimental?: boolean, } export const Deno: Info = { @@ -354,6 +355,59 @@ export namespace LSPServer { }, } + export const Ty: Info = { + id: "ty", + extensions: [".py", ".pyi"], + root: NearestRoot(["pyproject.toml", "ty.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), + async spawn(root) { + let binary = Bun.which("ty") + + const initialization: Record = {} + + const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter( + (p): p is string => p !== undefined, + ) + for (const venvPath of potentialVenvPaths) { + const isWindows = process.platform === "win32" + const potentialPythonPath = isWindows + ? path.join(venvPath, "Scripts", "python.exe") + : path.join(venvPath, "bin", "python") + if (await Bun.file(potentialPythonPath).exists()) { + initialization["pythonPath"] = potentialPythonPath + break + } + } + + if(!binary) { + for (const venvPath of potentialVenvPaths) { + const isWindows = process.platform === "win32" + const potentialTyPath = isWindows + ? path.join(venvPath, "Scripts", "ty.exe") + : path.join(venvPath, "bin", "ty") + if (await Bun.file(potentialTyPath).exists()) { + binary = potentialTyPath + break + } + } + } + + if(!binary) { + log.error("ty not found, please install ty first") + return + } + + const proc = spawn(binary, ["server"], { + cwd: root, + }) + + return { + process: proc, + initialization, + } + }, + experimental: true, + } + export const Pyright: Info = { id: "pyright", extensions: [".py", ".pyi"], From 948e8f9b6ffe07bc802cd24eca3dc6d2c68a6940 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Mon, 15 Dec 2025 23:29:05 +0100 Subject: [PATCH 2/5] feat: implement filtering of experimental LSP servers based on OPENCODE_EXPERIMENTAL_LSP_TY flag cleaner pattern - Added a function to conditionally enable or disable LSP servers (pyright and ty) based on the experimental flag. --- packages/opencode/src/lsp/index.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index c360234d6bc9..7feefc90babd 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -61,6 +61,22 @@ export namespace LSP { }) export type DocumentSymbol = z.infer + const filterExperimentalServers = (servers: Record) => { + if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { + // If experimental flag is enabled, disable pyright + if(servers["pyright"]) { + log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled") + delete servers["pyright"] + } + } else { + // If experimental flag is disabled, disable ty + if(servers["ty"]) { + log.info("LSP server ty is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is disabled") + delete servers["ty"] + } + } + } + const state = Instance.state( async () => { const clients: LSPClient.Info[] = [] @@ -80,6 +96,9 @@ export namespace LSP { for (const server of Object.values(LSPServer)) { servers[server.id] = server } + + filterExperimentalServers(servers) + for (const [name, item] of Object.entries(cfg.lsp ?? {})) { const existing = servers[name] if (item.disabled) { @@ -205,10 +224,6 @@ export namespace LSP { for (const server of Object.values(s.servers)) { if (server.extensions.length && !server.extensions.includes(extension)) continue - if (extension === ".py" || extension === ".pyi") { - if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY && !server.experimental) continue // If experimental flag is enabled and server is not experimental, skip - if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY && server.experimental) continue // If experimental flag is disabled and server is experimental, skip - } const root = await server.root(file) if (!root) continue From 9bc58f3380ef5db96e09a70f4c36c13a0d56465a Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Wed, 17 Dec 2025 13:01:10 +0100 Subject: [PATCH 3/5] feat: Remove log for when TY is diabled as this should be the default --- packages/opencode/src/lsp/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 7feefc90babd..9fd0ec643bf9 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -71,7 +71,6 @@ export namespace LSP { } else { // If experimental flag is disabled, disable ty if(servers["ty"]) { - log.info("LSP server ty is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is disabled") delete servers["ty"] } } From df5179dadfd7c2364f7599d55dc64fc33fae0ddd Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 18 Dec 2025 19:31:25 +0100 Subject: [PATCH 4/5] refactor: remove experimental flag from LSPServer configuration --- packages/opencode/src/lsp/server.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 0d3383fe6fd9..7cce8d2bd3c9 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -51,7 +51,6 @@ export namespace LSPServer { global?: boolean root: RootFunction spawn(root: string): Promise - experimental?: boolean, } export const Deno: Info = { @@ -412,7 +411,6 @@ export namespace LSPServer { initialization, } }, - experimental: true, } export const Pyright: Info = { From 83abc80d410dab5eb7baacaabb151785bcc680e1 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 18 Dec 2025 20:32:13 +0100 Subject: [PATCH 5/5] feat: add experimental flag for LSP server initialization --- packages/opencode/src/lsp/server.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 7cce8d2bd3c9..82d767188d7b 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -366,6 +366,10 @@ export namespace LSPServer { extensions: [".py", ".pyi"], root: NearestRoot(["pyproject.toml", "ty.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), async spawn(root) { + if(!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { + return undefined + } + let binary = Bun.which("ty") const initialization: Record = {}