Skip to content
1 change: 1 addition & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
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")

function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
Expand Down
20 changes: 20 additions & 0 deletions packages/opencode/src/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand Down Expand Up @@ -60,6 +61,21 @@ export namespace LSP {
})
export type DocumentSymbol = z.infer<typeof DocumentSymbol>

const filterExperimentalServers = (servers: Record<string, LSPServer.Info>) => {
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"]
}
}
}

const state = Instance.state(
async () => {
const clients: LSPClient.Info[] = []
Expand All @@ -79,6 +95,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) {
Expand Down Expand Up @@ -204,6 +223,7 @@ export namespace LSP {

for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue

const root = await server.root(file)
if (!root) continue
if (s.broken.has(root + server.id)) continue
Expand Down
56 changes: 56 additions & 0 deletions packages/opencode/src/lsp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,62 @@ 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

isn't this spawning even when the var isn't set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh I thought the way the logic goes, the servers only get spawned at after being loaded to the Instance.state? Before then the server is removed when the var isn't set.

But then again i could just add a check there just to be 1000% guaranteed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just like how in index.ts

      if (cfg.lsp === false) {
        log.info("all LSPs are disabled")
        return {
          broken: new Set<string>(),
          servers,
          clients,
          spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
        }
      }

it can return just empty servers and spawning methods

if(!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
return undefined
}

let binary = Bun.which("ty")

const initialization: Record<string, string> = {}

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,
}
},
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Potential issue: The experimental: true property is added here but it is not actually used anywhere. The filterExperimentalServers function checks for specific server IDs ("ty", "pyright") rather than using this property. This could be intentional for now, but it is worth noting that this property is currently unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense bro-chacho

export const Pyright: Info = {
id: "pyright",
extensions: [".py", ".pyi"],
Expand Down
Loading