refactor(lsp): remove async facade exports (#22321)

This commit is contained in:
Kit Langton
2026-04-13 12:47:52 -04:00
committed by GitHub
parent 663e798e76
commit 21d7a85e76
6 changed files with 159 additions and 181 deletions
+47 -47
View File
@@ -1,55 +1,55 @@
import { describe, expect, spyOn, test } from "bun:test"
import { describe, expect, spyOn } from "bun:test"
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 { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
describe("lsp.spawn", () => {
test("does not spawn builtin LSP for files outside instance", async () => {
await using tmp = await tmpdir()
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
it.live("does not spawn builtin LSP for files outside instance", () =>
provideTmpdirInstance((dir) =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
try {
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Lsp.LSP.touchFile(path.join(tmp.path, "..", "outside.ts"))
await Lsp.LSP.hover({
file: path.join(tmp.path, "..", "hover.ts"),
line: 0,
character: 0,
})
},
})
try {
yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
yield* lsp.hover({
file: path.join(dir, "..", "hover.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(0)
} finally {
spy.mockRestore()
}
}),
),
),
)
expect(spy).toHaveBeenCalledTimes(0)
} finally {
spy.mockRestore()
await Instance.disposeAll()
}
})
it.live("would spawn builtin LSP for files inside instance", () =>
provideTmpdirInstance((dir) =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
test("would spawn builtin LSP for files inside instance", async () => {
await using tmp = await tmpdir()
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
try {
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Lsp.LSP.hover({
file: path.join(tmp.path, "src", "inside.ts"),
line: 0,
character: 0,
})
},
})
expect(spy).toHaveBeenCalledTimes(1)
} finally {
spy.mockRestore()
await Instance.disposeAll()
}
})
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(1)
} finally {
spy.mockRestore()
}
}),
),
),
)
})
+96 -91
View File
@@ -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 * as Lsp from "../../src/lsp/index"
import { Effect, Layer } from "effect"
import { LSP } from "../../src/lsp"
import { LSPServer } from "../../src/lsp/server"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
function withInstance(fn: (dir: string) => Promise<void>) {
return async () => {
await using tmp = await tmpdir()
try {
await Instance.provide({
directory: tmp.path,
fn: () => fn(tmp.path),
})
} finally {
await Instance.disposeAll()
}
}
}
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
describe("LSP service lifecycle", () => {
let spawnSpy: ReturnType<typeof spyOn>
@@ -30,97 +20,112 @@ describe("LSP service lifecycle", () => {
spawnSpy.mockRestore()
})
test(
"init() completes without error",
withInstance(async () => {
await Lsp.LSP.init()
}),
it.live("init() completes without error", () => provideTmpdirInstance(() => LSP.Service.use((lsp) => lsp.init())))
it.live("status() returns empty array initially", () =>
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(
"status() returns empty array initially",
withInstance(async () => {
const result = await Lsp.LSP.status()
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(0)
}),
it.live("diagnostics() returns empty object initially", () =>
provideTmpdirInstance(() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.diagnostics()
expect(typeof result).toBe("object")
expect(Object.keys(result).length).toBe(0)
}),
),
),
)
test(
"diagnostics() returns empty object initially",
withInstance(async () => {
const result = await Lsp.LSP.diagnostics()
expect(typeof result).toBe("object")
expect(Object.keys(result).length).toBe(0)
}),
it.live("hasClients() returns true for .ts files in instance", () =>
provideTmpdirInstance((dir) =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
expect(result).toBe(true)
}),
),
),
)
test(
"hasClients() returns true for .ts files in instance",
withInstance(async (dir) => {
const result = await Lsp.LSP.hasClients(path.join(dir, "test.ts"))
expect(result).toBe(true)
}),
it.live("hasClients() returns false for files outside instance", () =>
provideTmpdirInstance((dir) =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.hasClients(path.join(dir, "..", "outside.ts"))
expect(typeof result).toBe("boolean")
}),
),
),
)
test(
"hasClients() returns false for files outside instance",
withInstance(async (dir) => {
const result = await Lsp.LSP.hasClients(path.join(dir, "..", "outside.ts"))
// hasClients checks servers but doesn't check containsPath — getClients does
// So hasClients may return true even for outside files (it checks extension + root)
// The guard is in getClients, not hasClients
expect(typeof result).toBe("boolean")
}),
it.live("workspaceSymbol() returns empty array with no clients", () =>
provideTmpdirInstance(() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.workspaceSymbol("test")
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(0)
}),
),
),
)
test(
"workspaceSymbol() returns empty array with no clients",
withInstance(async () => {
const result = await Lsp.LSP.workspaceSymbol("test")
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(0)
}),
it.live("definition() returns empty array for unknown file", () =>
provideTmpdirInstance((dir) =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.definition({
file: path.join(dir, "nonexistent.ts"),
line: 0,
character: 0,
})
expect(Array.isArray(result)).toBe(true)
}),
),
),
)
test(
"definition() returns empty array for unknown file",
withInstance(async (dir) => {
const result = await Lsp.LSP.definition({
file: path.join(dir, "nonexistent.ts"),
line: 0,
character: 0,
})
expect(Array.isArray(result)).toBe(true)
}),
it.live("references() returns empty array for unknown file", () =>
provideTmpdirInstance((dir) =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.references({
file: path.join(dir, "nonexistent.ts"),
line: 0,
character: 0,
})
expect(Array.isArray(result)).toBe(true)
}),
),
),
)
test(
"references() returns empty array for unknown file",
withInstance(async (dir) => {
const result = await Lsp.LSP.references({
file: path.join(dir, "nonexistent.ts"),
line: 0,
character: 0,
})
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
}),
it.live("multiple init() calls are idempotent", () =>
provideTmpdirInstance(() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
yield* lsp.init()
yield* lsp.init()
yield* lsp.init()
}),
),
),
)
})
describe("LSP.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 } },
message: "Type 'string' is not assignable to type 'number'",
severity: 1,
@@ -129,7 +134,7 @@ describe("LSP.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 } },
message: "Unused variable",
severity: 2,
@@ -138,7 +143,7 @@ describe("LSP.Diagnostic", () => {
})
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 } },
message: "Something wrong",
} as any)