mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-05 00:23:10 +00:00
Refactor to support multiple instances inside single opencode process (#2360)
This release has a bunch of minor breaking changes if you are using opencode plugins or sdk 1. storage events have been removed (we might bring this back but had some issues) 2. concept of `app` is gone - there is a new concept called `project` and endpoints to list projects and get the current project 3. plugin receives `directory` which is cwd and `worktree` which is where the root of the project is if it's a git repo 4. the session.chat function has been renamed to session.prompt in sdk. it no longer requires model to be passed in (model is now an object) 5. every endpoint takes an optional `directory` parameter to operate as though opencode is running in that directory
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import path from "path"
|
||||
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
||||
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
||||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||
import { Bus } from "../bus"
|
||||
@@ -9,6 +8,7 @@ import z from "zod"
|
||||
import type { LSPServer } from "./server"
|
||||
import { NamedError } from "../util/error"
|
||||
import { withTimeout } from "../util/timeout"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export namespace LSPClient {
|
||||
const log = Log.create({ service: "lsp.client" })
|
||||
@@ -35,7 +35,6 @@ export namespace LSPClient {
|
||||
}
|
||||
|
||||
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
|
||||
const app = App.info()
|
||||
const l = log.clone().tag("serverID", input.serverID)
|
||||
l.info("starting client")
|
||||
|
||||
@@ -130,7 +129,7 @@ export namespace LSPClient {
|
||||
},
|
||||
notify: {
|
||||
async open(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
|
||||
const file = Bun.file(input.path)
|
||||
const text = await file.text()
|
||||
const extension = path.extname(input.path)
|
||||
@@ -169,7 +168,7 @@ export namespace LSPClient {
|
||||
return diagnostics
|
||||
},
|
||||
async waitForDiagnostics(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
|
||||
log.info("waiting for diagnostics", input)
|
||||
let unsub: () => void
|
||||
return await withTimeout(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
import { LSPClient } from "./client"
|
||||
import path from "path"
|
||||
@@ -6,6 +5,7 @@ import { LSPServer } from "./server"
|
||||
import { z } from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { spawn } from "child_process"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export namespace LSP {
|
||||
const log = Log.create({ service: "lsp" })
|
||||
@@ -53,8 +53,7 @@ export namespace LSP {
|
||||
})
|
||||
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
|
||||
|
||||
const state = App.state(
|
||||
"lsp",
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const clients: LSPClient.Info[] = []
|
||||
const servers: Record<string, LSPServer.Info> = {}
|
||||
@@ -71,9 +70,9 @@ export namespace LSP {
|
||||
}
|
||||
servers[name] = {
|
||||
...existing,
|
||||
root: existing?.root ?? (async (_file, app) => app.path.root),
|
||||
root: existing?.root ?? (async () => Instance.directory),
|
||||
extensions: item.extensions ?? existing.extensions,
|
||||
spawn: async (_app, root) => {
|
||||
spawn: async (root) => {
|
||||
return {
|
||||
process: spawn(item.command[0], item.command.slice(1), {
|
||||
cwd: root,
|
||||
@@ -117,7 +116,7 @@ export namespace LSP {
|
||||
const result: LSPClient.Info[] = []
|
||||
for (const server of Object.values(s.servers)) {
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
const root = await server.root(file, App.info())
|
||||
const root = await server.root(file)
|
||||
if (!root) continue
|
||||
if (s.broken.has(root + server.id)) continue
|
||||
|
||||
@@ -126,7 +125,7 @@ export namespace LSP {
|
||||
result.push(match)
|
||||
continue
|
||||
}
|
||||
const handle = await server.spawn(App.info(), root).catch((err) => {
|
||||
const handle = await server.spawn(root).catch((err) => {
|
||||
s.broken.add(root + server.id)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
||||
return undefined
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
|
||||
import type { App } from "../app/app"
|
||||
import path from "path"
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
@@ -7,6 +6,7 @@ import { BunProc } from "../bun"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "../flag/flag"
|
||||
|
||||
export namespace LSPServer {
|
||||
@@ -17,18 +17,18 @@ export namespace LSPServer {
|
||||
initialization?: Record<string, any>
|
||||
}
|
||||
|
||||
type RootFunction = (file: string, app: App.Info) => Promise<string | undefined>
|
||||
type RootFunction = (file: string) => Promise<string | undefined>
|
||||
|
||||
const NearestRoot = (patterns: string[]): RootFunction => {
|
||||
return async (file, app) => {
|
||||
return async (file) => {
|
||||
const files = Filesystem.up({
|
||||
targets: patterns,
|
||||
start: path.dirname(file),
|
||||
stop: app.path.root,
|
||||
stop: Instance.worktree,
|
||||
})
|
||||
const first = await files.next()
|
||||
await files.return()
|
||||
if (!first.value) return app.path.root
|
||||
if (!first.value) return Instance.worktree
|
||||
return path.dirname(first.value)
|
||||
}
|
||||
}
|
||||
@@ -38,15 +38,15 @@ export namespace LSPServer {
|
||||
extensions: string[]
|
||||
global?: boolean
|
||||
root: RootFunction
|
||||
spawn(app: App.Info, root: string): Promise<Handle | undefined>
|
||||
spawn(root: string): Promise<Handle | undefined>
|
||||
}
|
||||
|
||||
export const Typescript: Info = {
|
||||
id: "typescript",
|
||||
root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]),
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||
async spawn(app, root) {
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
|
||||
async spawn(root) {
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
|
||||
if (!tsserver) return
|
||||
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
||||
cwd: root,
|
||||
@@ -83,7 +83,7 @@ export namespace LSPServer {
|
||||
"nuxt.config.js",
|
||||
"vue.config.js",
|
||||
]),
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let binary = Bun.which("vue-language-server")
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
@@ -145,8 +145,8 @@ export namespace LSPServer {
|
||||
"package.json",
|
||||
]),
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
|
||||
async spawn(app, root) {
|
||||
const eslint = await Bun.resolve("eslint", app.path.cwd).catch(() => {})
|
||||
async spawn(root) {
|
||||
const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
|
||||
if (!eslint) return
|
||||
log.info("spawning eslint server")
|
||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
||||
@@ -194,13 +194,13 @@ export namespace LSPServer {
|
||||
|
||||
export const Gopls: Info = {
|
||||
id: "gopls",
|
||||
root: async (file, app) => {
|
||||
const work = await NearestRoot(["go.work"])(file, app)
|
||||
root: async (file) => {
|
||||
const work = await NearestRoot(["go.work"])(file)
|
||||
if (work) return work
|
||||
return NearestRoot(["go.mod", "go.sum"])(file, app)
|
||||
return NearestRoot(["go.mod", "go.sum"])(file)
|
||||
},
|
||||
extensions: [".go"],
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("gopls", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
@@ -238,7 +238,7 @@ export namespace LSPServer {
|
||||
id: "ruby-lsp",
|
||||
root: NearestRoot(["Gemfile"]),
|
||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("ruby-lsp", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
@@ -279,7 +279,7 @@ export namespace LSPServer {
|
||||
id: "pyright",
|
||||
extensions: [".py", ".pyi"],
|
||||
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let binary = Bun.which("pyright-langserver")
|
||||
const args = []
|
||||
if (!binary) {
|
||||
@@ -333,7 +333,7 @@ export namespace LSPServer {
|
||||
id: "elixir-ls",
|
||||
extensions: [".ex", ".exs"],
|
||||
root: NearestRoot(["mix.exs", "mix.lock"]),
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let binary = Bun.which("elixir-ls")
|
||||
if (!binary) {
|
||||
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
|
||||
@@ -389,7 +389,7 @@ export namespace LSPServer {
|
||||
id: "zls",
|
||||
extensions: [".zig", ".zon"],
|
||||
root: NearestRoot(["build.zig"]),
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("zls", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
@@ -495,7 +495,7 @@ export namespace LSPServer {
|
||||
id: "csharp",
|
||||
root: NearestRoot([".sln", ".csproj", "global.json"]),
|
||||
extensions: [".cs"],
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("csharp-ls", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
@@ -533,8 +533,8 @@ export namespace LSPServer {
|
||||
|
||||
export const RustAnalyzer: Info = {
|
||||
id: "rust",
|
||||
root: async (file, app) => {
|
||||
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(file, app)
|
||||
root: async (root) => {
|
||||
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
|
||||
if (crateRoot === undefined) {
|
||||
return undefined
|
||||
}
|
||||
@@ -557,13 +557,13 @@ export namespace LSPServer {
|
||||
currentDir = parentDir
|
||||
|
||||
// Stop if we've gone above the app root
|
||||
if (!currentDir.startsWith(app.path.root)) break
|
||||
if (!currentDir.startsWith(Instance.worktree)) break
|
||||
}
|
||||
|
||||
return crateRoot
|
||||
},
|
||||
extensions: [".rs"],
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
const bin = Bun.which("rust-analyzer")
|
||||
if (!bin) {
|
||||
log.info("rust-analyzer not found in path, please install it")
|
||||
@@ -581,7 +581,7 @@ export namespace LSPServer {
|
||||
id: "clangd",
|
||||
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
|
||||
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
|
||||
async spawn(_, root) {
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("clangd", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user