diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 4cdb549096a..05a44b7c1fc 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -37,6 +37,7 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX = number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX") export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT") export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY") + export const OPENCODE_EXPERIMENTAL_LSP_RUFF = truthy("OPENCODE_EXPERIMENTAL_LSP_RUFF") export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 0fd3b69dfcd..7ebb68a462e 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -62,17 +62,26 @@ export namespace LSP { export type DocumentSymbol = z.infer const filterExperimentalServers = (servers: Record) => { + const disable = (id: string, reason?: string) => { + if (!servers[id]) return + if (reason) log.info(reason) + delete servers[id] + } + 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"]) { - delete servers["ty"] - } + disable("pyright", "LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled") + } + + if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { + disable("ty") + } + + if (Flag.OPENCODE_EXPERIMENTAL_LSP_RUFF) { + disable("pyright", "LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_RUFF is enabled") + } + + if (!Flag.OPENCODE_EXPERIMENTAL_LSP_RUFF) { + disable("ruff") } } diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 24da77edcfe..e3e7fd291a5 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -502,6 +502,57 @@ export namespace LSPServer { }, } + export const Ruff: Info = { + id: "ruff", + extensions: [".py", ".pyi"], + root: NearestRoot([ + "pyproject.toml", + "ruff.toml", + ".ruff.toml", + "setup.py", + "setup.cfg", + "requirements.txt", + "Pipfile", + ]), + async spawn(root) { + if (!Flag.OPENCODE_EXPERIMENTAL_LSP_RUFF) { + return undefined + } + + const venvs = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter( + (p): p is string => p !== undefined, + ) + + const binary = await (async () => { + const direct = Bun.which("ruff") + if (direct) return direct + + for (const venv of venvs) { + const windows = process.platform === "win32" + const candidate = windows ? path.join(venv, "Scripts", "ruff.exe") : path.join(venv, "bin", "ruff") + if (await Bun.file(candidate).exists()) { + return candidate + } + } + + return undefined + })() + + if (!binary) { + log.error("ruff not found, please install ruff first") + return + } + + const proc = spawn(binary, ["server"], { + cwd: root, + }) + + return { + process: proc, + } + }, + } + export const Pyright: Info = { id: "pyright", extensions: [".py", ".pyi"],