convert lsp client to effect
This commit is contained in:
parent
1c453bd4e1
commit
9d11b62ff0
5 changed files with 66 additions and 27 deletions
|
|
@ -11,6 +11,7 @@ import { Effect, Schema } from "effect"
|
|||
import type * as LSPServer from "./server"
|
||||
import { withTimeout } from "../util/timeout"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
|
|
@ -30,7 +31,7 @@ const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
|
|||
const log = Log.create({ service: "lsp.client" })
|
||||
const busRuntime = makeRuntime(Bus.Service, Bus.layer)
|
||||
|
||||
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
|
||||
export type Info = NonNullable<Effect.Success<ReturnType<typeof create>>>
|
||||
|
||||
export type Diagnostic = VSCodeDiagnostic
|
||||
|
||||
|
|
@ -138,13 +139,18 @@ function shouldSeedDiagnosticsOnFirstPush(serverID: string) {
|
|||
return serverID === "typescript"
|
||||
}
|
||||
|
||||
export async function create(input: {
|
||||
export const create = Effect.fn("LSPClient.create")(function* (input: {
|
||||
serverID: string
|
||||
server: LSPServer.Handle
|
||||
root: string
|
||||
directory: string
|
||||
instance: InstanceContext
|
||||
}) {
|
||||
const appFs = yield* AppFileSystem.Service
|
||||
// Bridge: read file content through AppFileSystem (so the simulated backend
|
||||
// can satisfy file reads from the in-memory FS instead of real disk).
|
||||
const readText = (p: string) => Effect.runPromise(appFs.readFileString(p) as Effect.Effect<string, Error>)
|
||||
return yield* Effect.promise(async () => {
|
||||
const logger = log.clone().tag("serverID", input.serverID)
|
||||
logger.info("starting client")
|
||||
const instance = input.instance
|
||||
|
|
@ -596,7 +602,7 @@ export async function create(input: {
|
|||
request.path = Filesystem.normalizePath(
|
||||
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
|
||||
)
|
||||
const text = await Filesystem.readText(request.path)
|
||||
const text = await readText(request.path)
|
||||
const extension = path.extname(request.path)
|
||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
||||
|
||||
|
|
@ -702,6 +708,7 @@ export async function create(input: {
|
|||
logger.info("initialized")
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export * as LSPClient from "./client"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import { InstanceState } from "@/effect/instance-state"
|
|||
import { containsPath } from "@/project/instance-context"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
||||
|
|
@ -150,6 +153,7 @@ export const makeLayer = (supported: LSPServer.Info[]) =>
|
|||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
const appFs = yield* AppFileSystem.Service
|
||||
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("LSP.state")(function* (ctx) {
|
||||
|
|
@ -238,13 +242,15 @@ export const makeLayer = (supported: LSPServer.Info[]) =>
|
|||
if (!handle) return undefined
|
||||
log.info("spawned lsp server", { serverID: server.id, root })
|
||||
|
||||
const client = await LSPClient.create({
|
||||
serverID: server.id,
|
||||
server: handle,
|
||||
root,
|
||||
directory: ctx.directory,
|
||||
instance: ctx,
|
||||
}).catch(async (err) => {
|
||||
const client = await Effect.runPromise(
|
||||
LSPClient.create({
|
||||
serverID: server.id,
|
||||
server: handle,
|
||||
root,
|
||||
directory: ctx.directory,
|
||||
instance: ctx,
|
||||
}).pipe(Effect.provideService(AppFileSystem.Service, appFs)),
|
||||
).catch(async (err) => {
|
||||
s.broken.add(key)
|
||||
await Process.stop(handle.process)
|
||||
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
|
||||
|
|
@ -297,7 +303,9 @@ export const makeLayer = (supported: LSPServer.Info[]) =>
|
|||
if (!client) continue
|
||||
|
||||
result.push(client)
|
||||
await Bus.publish(ctx, Event.Updated, {})
|
||||
void busRuntime.runPromise((bus) =>
|
||||
bus.publish(Event.Updated, {}).pipe(Effect.provideService(InstanceRef, ctx)),
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
@ -508,7 +516,11 @@ export const makeLayer = (supported: LSPServer.Info[]) =>
|
|||
|
||||
export const layer = makeLayer(builtinServers)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
)
|
||||
|
||||
export * as Diagnostic from "./diagnostic"
|
||||
|
||||
|
|
|
|||
|
|
@ -335,6 +335,9 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
|
|||
files: {
|
||||
".git/HEAD": "ref: refs/heads/main\n",
|
||||
".git/config": '[core]\n\trepositoryformatversion = 0\n\tbare = false\n[branch "main"]\n',
|
||||
// Enable built-in LSP servers (e.g. the simulated typescript stub) so
|
||||
// tools like `edit` / `write` get diagnostics in the simulated chain.
|
||||
"opencode.json": JSON.stringify({ $schema: "https://opencode.ai/config.json", lsp: true }, null, 2) + "\n",
|
||||
},
|
||||
}),
|
||||
// SimulationFileSystem.layer no longer provides FileSystem.FileSystem; satisfy
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { tmpdir, withTestInstance } from "../fixture/fixture"
|
|||
import { LSPClient } from "@/lsp/client"
|
||||
import * as LSPServer from "@/lsp/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
|
||||
function spawnFakeServer() {
|
||||
const { spawn } = require("child_process")
|
||||
|
|
@ -16,6 +18,12 @@ function spawnFakeServer() {
|
|||
}
|
||||
}
|
||||
|
||||
// LSPClient.create is an Effect that yields AppFileSystem so the production
|
||||
// real-fs layer or a simulated one can satisfy file reads. Tests use the
|
||||
// default real-disk layer.
|
||||
const createClient = (input: Parameters<typeof LSPClient.create>[0]) =>
|
||||
Effect.runPromise(LSPClient.create(input).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
|
||||
describe("LSPClient interop", () => {
|
||||
beforeEach(async () => {
|
||||
await Log.init({ print: true })
|
||||
|
|
@ -27,7 +35,7 @@ describe("LSPClient interop", () => {
|
|||
const client = await withTestInstance({
|
||||
directory: process.cwd(),
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
|
|
@ -51,7 +59,7 @@ describe("LSPClient interop", () => {
|
|||
const client = await withTestInstance({
|
||||
directory: process.cwd(),
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
|
|
@ -75,7 +83,7 @@ describe("LSPClient interop", () => {
|
|||
const client = await withTestInstance({
|
||||
directory: process.cwd(),
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
|
|
@ -99,7 +107,7 @@ describe("LSPClient interop", () => {
|
|||
const client = await withTestInstance({
|
||||
directory: process.cwd(),
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
|
|
@ -127,7 +135,7 @@ describe("LSPClient interop", () => {
|
|||
const client = await withTestInstance({
|
||||
directory: process.cwd(),
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
createClient({
|
||||
serverID: "fake",
|
||||
server: {
|
||||
...(handle as unknown as LSPServer.Handle),
|
||||
|
|
@ -157,7 +165,7 @@ describe("LSPClient interop", () => {
|
|||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
const client = await createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
|
|
@ -201,7 +209,7 @@ describe("LSPClient interop", () => {
|
|||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
const client = await createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
|
|
@ -248,7 +256,7 @@ describe("LSPClient interop", () => {
|
|||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
const client = await createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
|
|
@ -296,7 +304,7 @@ describe("LSPClient interop", () => {
|
|||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
const client = await createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
|
|
@ -345,7 +353,7 @@ describe("LSPClient interop", () => {
|
|||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
const client = await createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
|
|
@ -399,7 +407,7 @@ describe("LSPClient interop", () => {
|
|||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
const client = await createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
|
|
@ -464,7 +472,7 @@ describe("LSPClient interop", () => {
|
|||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
const client = await createClient({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Config } from "@/config/config"
|
|||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import * as LSPServer from "@/lsp/server"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { awaitWithTimeout, testEffect } from "../lib/effect"
|
||||
|
|
@ -13,14 +14,22 @@ import { awaitWithTimeout, testEffect } from "../lib/effect"
|
|||
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
const experimentalTyIt = testEffect(
|
||||
Layer.mergeAll(
|
||||
LSP.layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(RuntimeFlags.layer({ experimentalLspTy: true }))),
|
||||
LSP.layer.pipe(
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalLspTy: true })),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
),
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
),
|
||||
)
|
||||
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
|
||||
const disabledDownloadIt = testEffect(
|
||||
Layer.mergeAll(
|
||||
LSP.layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(RuntimeFlags.layer({ disableLspDownload: true }))),
|
||||
LSP.layer.pipe(
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ disableLspDownload: true })),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
),
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue