refactor(lsp): remove async facade exports (#22321)
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import { LSP } from "../../../lsp"
|
import { LSP } from "../../../lsp"
|
||||||
|
import { AppRuntime } from "../../../effect/app-runtime"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import { Log } from "../../../util/log"
|
import { Log } from "../../../util/log"
|
||||||
@@ -19,9 +21,16 @@ const DiagnosticsCommand = cmd({
|
|||||||
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
await LSP.touchFile(args.file, true)
|
const out = await AppRuntime.runPromise(
|
||||||
await sleep(1000)
|
LSP.Service.use((lsp) =>
|
||||||
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
|
Effect.gen(function* () {
|
||||||
|
yield* lsp.touchFile(args.file, true)
|
||||||
|
yield* Effect.sleep(1000)
|
||||||
|
return yield* lsp.diagnostics()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
process.stdout.write(JSON.stringify(out, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -33,7 +42,7 @@ export const SymbolsCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
using _ = Log.Default.time("symbols")
|
using _ = Log.Default.time("symbols")
|
||||||
const results = await LSP.workspaceSymbol(args.query)
|
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -46,7 +55,7 @@ export const DocumentSymbolsCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
using _ = Log.Default.time("document-symbols")
|
using _ = Log.Default.time("document-symbols")
|
||||||
const results = await LSP.documentSymbol(args.uri)
|
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Process } from "../util/process"
|
|||||||
import { spawn as lspspawn } from "./launch"
|
import { spawn as lspspawn } from "./launch"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
|
|
||||||
export namespace LSP {
|
export namespace LSP {
|
||||||
const log = Log.create({ service: "lsp" })
|
const log = Log.create({ service: "lsp" })
|
||||||
@@ -508,37 +507,6 @@ export namespace LSP {
|
|||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
||||||
|
|
||||||
export const init = async () => runPromise((svc) => svc.init())
|
|
||||||
|
|
||||||
export const status = async () => runPromise((svc) => svc.status())
|
|
||||||
|
|
||||||
export const hasClients = async (file: string) => runPromise((svc) => svc.hasClients(file))
|
|
||||||
|
|
||||||
export const touchFile = async (input: string, waitForDiagnostics?: boolean) =>
|
|
||||||
runPromise((svc) => svc.touchFile(input, waitForDiagnostics))
|
|
||||||
|
|
||||||
export const diagnostics = async () => runPromise((svc) => svc.diagnostics())
|
|
||||||
|
|
||||||
export const hover = async (input: LocInput) => runPromise((svc) => svc.hover(input))
|
|
||||||
|
|
||||||
export const definition = async (input: LocInput) => runPromise((svc) => svc.definition(input))
|
|
||||||
|
|
||||||
export const references = async (input: LocInput) => runPromise((svc) => svc.references(input))
|
|
||||||
|
|
||||||
export const implementation = async (input: LocInput) => runPromise((svc) => svc.implementation(input))
|
|
||||||
|
|
||||||
export const documentSymbol = async (uri: string) => runPromise((svc) => svc.documentSymbol(uri))
|
|
||||||
|
|
||||||
export const workspaceSymbol = async (query: string) => runPromise((svc) => svc.workspaceSymbol(query))
|
|
||||||
|
|
||||||
export const prepareCallHierarchy = async (input: LocInput) => runPromise((svc) => svc.prepareCallHierarchy(input))
|
|
||||||
|
|
||||||
export const incomingCalls = async (input: LocInput) => runPromise((svc) => svc.incomingCalls(input))
|
|
||||||
|
|
||||||
export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
|
|
||||||
|
|
||||||
export namespace Diagnostic {
|
export namespace Diagnostic {
|
||||||
const MAX_PER_FILE = 20
|
const MAX_PER_FILE = 20
|
||||||
|
|
||||||
|
|||||||
@@ -105,11 +105,6 @@ export const FileRoutes = lazy(() =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
/*
|
|
||||||
const query = c.req.valid("query").query
|
|
||||||
const result = await LSP.workspaceSymbol(query)
|
|
||||||
return c.json(result)
|
|
||||||
*/
|
|
||||||
return c.json([])
|
return c.json([])
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -256,7 +256,8 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(await LSP.status())
|
const items = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.status()))
|
||||||
|
return c.json(items)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
import { describe, expect, spyOn, test } from "bun:test"
|
import { describe, expect, spyOn } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import * as Lsp from "../../src/lsp/index"
|
import { Effect, Layer } from "effect"
|
||||||
|
import { LSP } from "../../src/lsp"
|
||||||
import { LSPServer } from "../../src/lsp/server"
|
import { LSPServer } from "../../src/lsp/server"
|
||||||
import { Instance } from "../../src/project/instance"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
|
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||||
|
|
||||||
describe("lsp.spawn", () => {
|
describe("lsp.spawn", () => {
|
||||||
test("does not spawn builtin LSP for files outside instance", async () => {
|
it.live("does not spawn builtin LSP for files outside instance", () =>
|
||||||
await using tmp = await tmpdir()
|
provideTmpdirInstance((dir) =>
|
||||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
LSP.Service.use((lsp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Instance.provide({
|
yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
|
||||||
directory: tmp.path,
|
yield* lsp.hover({
|
||||||
fn: async () => {
|
file: path.join(dir, "..", "hover.ts"),
|
||||||
await Lsp.LSP.touchFile(path.join(tmp.path, "..", "outside.ts"))
|
line: 0,
|
||||||
await Lsp.LSP.hover({
|
character: 0,
|
||||||
file: path.join(tmp.path, "..", "hover.ts"),
|
})
|
||||||
line: 0,
|
expect(spy).toHaveBeenCalledTimes(0)
|
||||||
character: 0,
|
} finally {
|
||||||
})
|
spy.mockRestore()
|
||||||
},
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(0)
|
it.live("would spawn builtin LSP for files inside instance", () =>
|
||||||
} finally {
|
provideTmpdirInstance((dir) =>
|
||||||
spy.mockRestore()
|
LSP.Service.use((lsp) =>
|
||||||
await Instance.disposeAll()
|
Effect.gen(function* () {
|
||||||
}
|
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||||
})
|
|
||||||
|
|
||||||
test("would spawn builtin LSP for files inside instance", async () => {
|
try {
|
||||||
await using tmp = await tmpdir()
|
yield* lsp.hover({
|
||||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
file: path.join(dir, "src", "inside.ts"),
|
||||||
|
line: 0,
|
||||||
try {
|
character: 0,
|
||||||
await Instance.provide({
|
})
|
||||||
directory: tmp.path,
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
fn: async () => {
|
} finally {
|
||||||
await Lsp.LSP.hover({
|
spy.mockRestore()
|
||||||
file: path.join(tmp.path, "src", "inside.ts"),
|
}
|
||||||
line: 0,
|
}),
|
||||||
character: 0,
|
),
|
||||||
})
|
),
|
||||||
},
|
)
|
||||||
})
|
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
|
||||||
} finally {
|
|
||||||
spy.mockRestore()
|
|
||||||
await Instance.disposeAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import * as Lsp from "../../src/lsp/index"
|
import { Effect, Layer } from "effect"
|
||||||
|
import { LSP } from "../../src/lsp"
|
||||||
import { LSPServer } from "../../src/lsp/server"
|
import { LSPServer } from "../../src/lsp/server"
|
||||||
import { Instance } from "../../src/project/instance"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
function withInstance(fn: (dir: string) => Promise<void>) {
|
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||||
return async () => {
|
|
||||||
await using tmp = await tmpdir()
|
|
||||||
try {
|
|
||||||
await Instance.provide({
|
|
||||||
directory: tmp.path,
|
|
||||||
fn: () => fn(tmp.path),
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
await Instance.disposeAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("LSP service lifecycle", () => {
|
describe("LSP service lifecycle", () => {
|
||||||
let spawnSpy: ReturnType<typeof spyOn>
|
let spawnSpy: ReturnType<typeof spyOn>
|
||||||
@@ -30,97 +20,112 @@ describe("LSP service lifecycle", () => {
|
|||||||
spawnSpy.mockRestore()
|
spawnSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
it.live("init() completes without error", () => provideTmpdirInstance(() => LSP.Service.use((lsp) => lsp.init())))
|
||||||
"init() completes without error",
|
|
||||||
withInstance(async () => {
|
it.live("status() returns empty array initially", () =>
|
||||||
await Lsp.LSP.init()
|
provideTmpdirInstance(() =>
|
||||||
}),
|
LSP.Service.use((lsp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const result = yield* lsp.status()
|
||||||
|
expect(Array.isArray(result)).toBe(true)
|
||||||
|
expect(result.length).toBe(0)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
it.live("diagnostics() returns empty object initially", () =>
|
||||||
"status() returns empty array initially",
|
provideTmpdirInstance(() =>
|
||||||
withInstance(async () => {
|
LSP.Service.use((lsp) =>
|
||||||
const result = await Lsp.LSP.status()
|
Effect.gen(function* () {
|
||||||
expect(Array.isArray(result)).toBe(true)
|
const result = yield* lsp.diagnostics()
|
||||||
expect(result.length).toBe(0)
|
expect(typeof result).toBe("object")
|
||||||
}),
|
expect(Object.keys(result).length).toBe(0)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
it.live("hasClients() returns true for .ts files in instance", () =>
|
||||||
"diagnostics() returns empty object initially",
|
provideTmpdirInstance((dir) =>
|
||||||
withInstance(async () => {
|
LSP.Service.use((lsp) =>
|
||||||
const result = await Lsp.LSP.diagnostics()
|
Effect.gen(function* () {
|
||||||
expect(typeof result).toBe("object")
|
const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
|
||||||
expect(Object.keys(result).length).toBe(0)
|
expect(result).toBe(true)
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
it.live("hasClients() returns false for files outside instance", () =>
|
||||||
"hasClients() returns true for .ts files in instance",
|
provideTmpdirInstance((dir) =>
|
||||||
withInstance(async (dir) => {
|
LSP.Service.use((lsp) =>
|
||||||
const result = await Lsp.LSP.hasClients(path.join(dir, "test.ts"))
|
Effect.gen(function* () {
|
||||||
expect(result).toBe(true)
|
const result = yield* lsp.hasClients(path.join(dir, "..", "outside.ts"))
|
||||||
}),
|
expect(typeof result).toBe("boolean")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
it.live("workspaceSymbol() returns empty array with no clients", () =>
|
||||||
"hasClients() returns false for files outside instance",
|
provideTmpdirInstance(() =>
|
||||||
withInstance(async (dir) => {
|
LSP.Service.use((lsp) =>
|
||||||
const result = await Lsp.LSP.hasClients(path.join(dir, "..", "outside.ts"))
|
Effect.gen(function* () {
|
||||||
// hasClients checks servers but doesn't check containsPath — getClients does
|
const result = yield* lsp.workspaceSymbol("test")
|
||||||
// So hasClients may return true even for outside files (it checks extension + root)
|
expect(Array.isArray(result)).toBe(true)
|
||||||
// The guard is in getClients, not hasClients
|
expect(result.length).toBe(0)
|
||||||
expect(typeof result).toBe("boolean")
|
}),
|
||||||
}),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
it.live("definition() returns empty array for unknown file", () =>
|
||||||
"workspaceSymbol() returns empty array with no clients",
|
provideTmpdirInstance((dir) =>
|
||||||
withInstance(async () => {
|
LSP.Service.use((lsp) =>
|
||||||
const result = await Lsp.LSP.workspaceSymbol("test")
|
Effect.gen(function* () {
|
||||||
expect(Array.isArray(result)).toBe(true)
|
const result = yield* lsp.definition({
|
||||||
expect(result.length).toBe(0)
|
file: path.join(dir, "nonexistent.ts"),
|
||||||
}),
|
line: 0,
|
||||||
|
character: 0,
|
||||||
|
})
|
||||||
|
expect(Array.isArray(result)).toBe(true)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
it.live("references() returns empty array for unknown file", () =>
|
||||||
"definition() returns empty array for unknown file",
|
provideTmpdirInstance((dir) =>
|
||||||
withInstance(async (dir) => {
|
LSP.Service.use((lsp) =>
|
||||||
const result = await Lsp.LSP.definition({
|
Effect.gen(function* () {
|
||||||
file: path.join(dir, "nonexistent.ts"),
|
const result = yield* lsp.references({
|
||||||
line: 0,
|
file: path.join(dir, "nonexistent.ts"),
|
||||||
character: 0,
|
line: 0,
|
||||||
})
|
character: 0,
|
||||||
expect(Array.isArray(result)).toBe(true)
|
})
|
||||||
}),
|
expect(Array.isArray(result)).toBe(true)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
it.live("multiple init() calls are idempotent", () =>
|
||||||
"references() returns empty array for unknown file",
|
provideTmpdirInstance(() =>
|
||||||
withInstance(async (dir) => {
|
LSP.Service.use((lsp) =>
|
||||||
const result = await Lsp.LSP.references({
|
Effect.gen(function* () {
|
||||||
file: path.join(dir, "nonexistent.ts"),
|
yield* lsp.init()
|
||||||
line: 0,
|
yield* lsp.init()
|
||||||
character: 0,
|
yield* lsp.init()
|
||||||
})
|
}),
|
||||||
expect(Array.isArray(result)).toBe(true)
|
),
|
||||||
}),
|
),
|
||||||
)
|
|
||||||
|
|
||||||
test(
|
|
||||||
"multiple init() calls are idempotent",
|
|
||||||
withInstance(async () => {
|
|
||||||
await Lsp.LSP.init()
|
|
||||||
await Lsp.LSP.init()
|
|
||||||
await Lsp.LSP.init()
|
|
||||||
// Should not throw or create duplicate state
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("LSP.Diagnostic", () => {
|
describe("LSP.Diagnostic", () => {
|
||||||
test("pretty() formats error diagnostic", () => {
|
test("pretty() formats error diagnostic", () => {
|
||||||
const result = Lsp.LSP.Diagnostic.pretty({
|
const result = LSP.Diagnostic.pretty({
|
||||||
range: { start: { line: 9, character: 4 }, end: { line: 9, character: 10 } },
|
range: { start: { line: 9, character: 4 }, end: { line: 9, character: 10 } },
|
||||||
message: "Type 'string' is not assignable to type 'number'",
|
message: "Type 'string' is not assignable to type 'number'",
|
||||||
severity: 1,
|
severity: 1,
|
||||||
@@ -129,7 +134,7 @@ describe("LSP.Diagnostic", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("pretty() formats warning diagnostic", () => {
|
test("pretty() formats warning diagnostic", () => {
|
||||||
const result = Lsp.LSP.Diagnostic.pretty({
|
const result = LSP.Diagnostic.pretty({
|
||||||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
|
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
|
||||||
message: "Unused variable",
|
message: "Unused variable",
|
||||||
severity: 2,
|
severity: 2,
|
||||||
@@ -138,7 +143,7 @@ describe("LSP.Diagnostic", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("pretty() defaults to ERROR when no severity", () => {
|
test("pretty() defaults to ERROR when no severity", () => {
|
||||||
const result = Lsp.LSP.Diagnostic.pretty({
|
const result = LSP.Diagnostic.pretty({
|
||||||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
||||||
message: "Something wrong",
|
message: "Something wrong",
|
||||||
} as any)
|
} as any)
|
||||||
|
|||||||
Reference in New Issue
Block a user