diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 2fad5e15e7..8b1f1609c6 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -56,7 +56,6 @@ export const Flag = { copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), OPENCODE_ENABLE_EXA: truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA"), OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"), - OPENCODE_EXPERIMENTAL_LSP_TY: truthy("OPENCODE_EXPERIMENTAL_LSP_TY"), OPENCODE_EXPERIMENTAL_LSP_TOOL: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL"), OPENCODE_EXPERIMENTAL_PLAN_MODE: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE"), OPENCODE_EXPERIMENTAL_SCOUT: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_SCOUT"), diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 5c640f9eb3..91f8205588 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -31,6 +31,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"), experimentalScout: enabledByExperimental("OPENCODE_EXPERIMENTAL_SCOUT"), experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"), + experimentalLspTy: bool("OPENCODE_EXPERIMENTAL_LSP_TY"), experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"), experimentalOxfmt: enabledByExperimental("OPENCODE_EXPERIMENTAL_OXFMT"), experimentalPlanMode: enabledByExperimental("OPENCODE_EXPERIMENTAL_PLAN_MODE"), diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 0249721c44..d74bbae933 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -6,13 +6,13 @@ import path from "path" import { pathToFileURL, fileURLToPath } from "url" import * as LSPServer from "./server" import { Config } from "@/config/config" -import { Flag } from "@opencode-ai/core/flag/flag" import { Process } from "@/util/process" import { spawn as lspspawn } from "./launch" import { Effect, Layer, Context, Schema } from "effect" 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" const log = Log.create({ service: "lsp" }) @@ -98,8 +98,8 @@ const kinds = [ SymbolKind.Enum, ] -const filterExperimentalServers = (servers: Record) => { - if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { +const filterExperimentalServers = (servers: Record, flags: RuntimeFlags.Info) => { + if (flags.experimentalLspTy) { if (servers["pyright"]) { log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled") delete servers["pyright"] @@ -143,6 +143,7 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const config = yield* Config.Service + const flags = yield* RuntimeFlags.Service const state = yield* InstanceState.make( Effect.fn("LSP.state")(function* (ctx) { @@ -157,7 +158,7 @@ export const layer = Layer.effect( servers[server.id] = server } - filterExperimentalServers(servers) + filterExperimentalServers(servers, flags) if (cfg.lsp !== true) { for (const [name, item] of Object.entries(cfg.lsp)) { @@ -217,7 +218,7 @@ export const layer = Layer.effect( async function schedule(server: LSPServer.Info, root: string, key: string) { const handle = await server - .spawn(root, ctx) + .spawn(root, ctx, flags) .then((value) => { if (!value) s.broken.add(key) return value @@ -498,7 +499,7 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer)) +export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer)) export * as Diagnostic from "./diagnostic" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index b8861d1f81..454fbc1dbd 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -14,6 +14,7 @@ import { which } from "../util/which" import { Module } from "@opencode-ai/core/util/module" import { spawn } from "./launch" import { Npm } from "@opencode-ai/core/npm" +import type { RuntimeFlags } from "@/effect/runtime-flags" const log = Log.create({ service: "lsp.server" }) const pathExists = async (p: string) => @@ -60,7 +61,7 @@ export interface Info { extensions: string[] global?: boolean root: RootFunction - spawn(root: string, ctx: InstanceContext): Promise + spawn(root: string, ctx: InstanceContext, flags: RuntimeFlags.Info): Promise } export const Deno: Info = { @@ -431,8 +432,8 @@ export const Ty: Info = { "Pipfile", "pyrightconfig.json", ]), - async spawn(root) { - if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { + async spawn(root, _ctx, flags) { + if (!flags.experimentalLspTy) { return undefined } diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index d5688964ae..8010038052 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -34,6 +34,7 @@ describe("RuntimeFlags", () => { expect(flags.enableQuestionTool).toBe(true) expect(flags.experimentalScout).toBe(true) expect(flags.experimentalBackgroundSubagents).toBe(true) + expect(flags.experimentalLspTy).toBe(false) expect(flags.experimentalLspTool).toBe(true) expect(flags.experimentalOxfmt).toBe(true) expect(flags.experimentalPlanMode).toBe(true) @@ -44,6 +45,20 @@ describe("RuntimeFlags", () => { }), ) + it.effect("defaultLayer parses OPENCODE_EXPERIMENTAL_LSP_TY", () => + Effect.gen(function* () { + const flags = yield* readFlags.pipe( + Effect.provide( + fromConfig({ + OPENCODE_EXPERIMENTAL_LSP_TY: "true", + }), + ), + ) + + expect(flags.experimentalLspTy).toBe(true) + }), + ) + it.effect("layer accepts partial test overrides and fills defaults from Config definitions", () => Effect.gen(function* () { const flags = yield* readFlags.pipe( diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index cd6648ae9d..eacd0df03c 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -1,6 +1,8 @@ import { describe, expect, spyOn } from "bun:test" import path from "path" import { Effect, Layer } from "effect" +import { Config } from "@/config/config" +import { RuntimeFlags } from "@/effect/runtime-flags" import { LSP } from "@/lsp/lsp" import * as LSPServer from "@/lsp/server" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" @@ -8,6 +10,12 @@ import { provideTmpdirInstance } from "../fixture/fixture" import { 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 }))), + CrossSpawnSpawner.defaultLayer, + ), +) describe("lsp.spawn", () => { it.live("does not spawn builtin LSP for files outside instance", () => @@ -106,4 +114,56 @@ describe("lsp.spawn", () => { }, ), ) + + it.live("uses pyright instead of ty by default", () => + provideTmpdirInstance( + (dir) => + LSP.Service.use((lsp) => + Effect.gen(function* () { + const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined) + const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined) + + try { + yield* lsp.hover({ + file: path.join(dir, "src", "inside.py"), + line: 0, + character: 0, + }) + expect(ty).toHaveBeenCalledTimes(0) + expect(pyright).toHaveBeenCalledTimes(1) + } finally { + ty.mockRestore() + pyright.mockRestore() + } + }), + ), + { config: { lsp: true } }, + ), + ) + + experimentalTyIt.live("uses ty instead of pyright when experimentalLspTy is enabled", () => + provideTmpdirInstance( + (dir) => + LSP.Service.use((lsp) => + Effect.gen(function* () { + const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined) + const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined) + + try { + yield* lsp.hover({ + file: path.join(dir, "src", "inside.py"), + line: 0, + character: 0, + }) + expect(ty).toHaveBeenCalledTimes(1) + expect(pyright).toHaveBeenCalledTimes(0) + } finally { + ty.mockRestore() + pyright.mockRestore() + } + }), + ), + { config: { lsp: true } }, + ), + ) })