|
|
@@ -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"
|
|
|
-
|
|
|
-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()
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+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 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()
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- 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("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(
|
|
|
- "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("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(
|
|
|
- "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 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 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("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(
|
|
|
- "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("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(
|
|
|
- "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("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(
|
|
|
- "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)
|
|
|
- }),
|
|
|
+ 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(
|
|
|
- "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)
|