| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- import { beforeEach, describe, expect, test } from "bun:test"
- import path from "path"
- import { pathToFileURL } from "url"
- import { tmpdir } from "../fixture/fixture"
- import { LSPClient } from "@/lsp/client"
- import * as LSPServer from "@/lsp/server"
- import { Instance } from "../../src/project/instance"
- import { WithInstance } from "../../src/project/with-instance"
- import * as Log from "@opencode-ai/core/util/log"
- function spawnFakeServer() {
- const { spawn } = require("child_process")
- const serverPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
- return {
- process: spawn(process.execPath, [serverPath], {
- stdio: "pipe",
- }),
- }
- }
- describe("LSPClient interop", () => {
- beforeEach(async () => {
- await Log.init({ print: true })
- })
- test("handles workspace/workspaceFolders request", async () => {
- const handle = spawnFakeServer() as any
- const client = await WithInstance.provide({
- directory: process.cwd(),
- fn: () =>
- LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: process.cwd(),
- directory: process.cwd(),
- }),
- })
- await client.connection.sendNotification("test/trigger", {
- method: "workspace/workspaceFolders",
- })
- await new Promise((resolve) => setTimeout(resolve, 100))
- expect(client.connection).toBeDefined()
- await client.shutdown()
- })
- test("handles client/registerCapability request", async () => {
- const handle = spawnFakeServer() as any
- const client = await WithInstance.provide({
- directory: process.cwd(),
- fn: () =>
- LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: process.cwd(),
- directory: process.cwd(),
- }),
- })
- await client.connection.sendNotification("test/trigger", {
- method: "client/registerCapability",
- })
- await new Promise((resolve) => setTimeout(resolve, 100))
- expect(client.connection).toBeDefined()
- await client.shutdown()
- })
- test("handles client/unregisterCapability request", async () => {
- const handle = spawnFakeServer() as any
- const client = await WithInstance.provide({
- directory: process.cwd(),
- fn: () =>
- LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: process.cwd(),
- directory: process.cwd(),
- }),
- })
- await client.connection.sendNotification("test/trigger", {
- method: "client/unregisterCapability",
- })
- await new Promise((resolve) => setTimeout(resolve, 100))
- expect(client.connection).toBeDefined()
- await client.shutdown()
- })
- test("initialize does not overclaim unsupported diagnostics capabilities", async () => {
- const handle = spawnFakeServer() as any
- const client = await WithInstance.provide({
- directory: process.cwd(),
- fn: () =>
- LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: process.cwd(),
- directory: process.cwd(),
- }),
- })
- const params = await client.connection.sendRequest<any>("test/get-initialize-params", {})
- expect(params.capabilities.workspace.diagnostics.refreshSupport).toBe(false)
- expect(params.capabilities.textDocument.publishDiagnostics.versionSupport).toBe(false)
- await client.shutdown()
- })
- test("workspace/configuration returns one result per requested item", async () => {
- const handle = spawnFakeServer() as any
- const initialization = {
- alpha: {
- beta: 1,
- },
- gamma: true,
- }
- const client = await WithInstance.provide({
- directory: process.cwd(),
- fn: () =>
- LSPClient.create({
- serverID: "fake",
- server: {
- ...(handle as unknown as LSPServer.Handle),
- initialization,
- },
- root: process.cwd(),
- directory: process.cwd(),
- }),
- })
- const response = await client.connection.sendRequest<any[]>("test/request-configuration", {
- items: [{ section: "alpha" }, { section: "alpha.beta" }, { section: "missing" }, {}],
- })
- expect(response).toEqual([{ beta: 1 }, 1, null, initialization])
- await client.shutdown()
- })
- test("sends ranged didChange for incremental sync servers", async () => {
- const handle = spawnFakeServer() as any
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "client.ts")
- await Bun.write(file, "first\n")
- await WithInstance.provide({
- directory: tmp.path,
- fn: async () => {
- const client = await LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: tmp.path,
- directory: tmp.path,
- })
- await client.notify.open({ path: file })
- await Bun.write(file, "second\nthird\n")
- await client.notify.open({ path: file })
- const change = await client.connection.sendRequest<{
- textDocument: { version: number }
- contentChanges: {
- range?: { start: { line: number; character: number }; end: { line: number; character: number } }
- text: string
- }[]
- }>("test/get-last-change", {})
- expect(change.textDocument.version).toBe(1)
- expect(change.contentChanges).toEqual([
- {
- range: {
- start: { line: 0, character: 0 },
- end: { line: 1, character: 0 },
- },
- text: "second\nthird\n",
- },
- ])
- await client.shutdown()
- },
- })
- })
- test("document mode falls back to push diagnostics", async () => {
- const handle = spawnFakeServer() as any
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "client.ts")
- await Bun.write(file, "const x = 1\n")
- await WithInstance.provide({
- directory: tmp.path,
- fn: async () => {
- const client = await LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: tmp.path,
- directory: tmp.path,
- })
- const version = await client.notify.open({ path: file })
- const wait = client.waitForDiagnostics({ path: file, version, mode: "document" })
- await client.connection.sendNotification("test/publish-diagnostics", {
- uri: pathToFileURL(file).href,
- version,
- diagnostics: [
- {
- range: {
- start: { line: 0, character: 0 },
- end: { line: 0, character: 5 },
- },
- message: "push diagnostic",
- severity: 1,
- },
- ],
- })
- await wait
- const diagnostics = client.diagnostics.get(file) ?? []
- expect(diagnostics).toHaveLength(1)
- expect(diagnostics[0]?.message).toBe("push diagnostic")
- const count = await client.connection.sendRequest("test/get-diagnostic-request-count", {})
- expect(count).toBe(0)
- await client.shutdown()
- },
- })
- })
- test("document mode accepts matching push diagnostics published before waiting", async () => {
- const handle = spawnFakeServer() as any
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "client.ts")
- await Bun.write(file, "const x = 1\n")
- await WithInstance.provide({
- directory: tmp.path,
- fn: async () => {
- const client = await LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: tmp.path,
- directory: tmp.path,
- })
- const version = await client.notify.open({ path: file })
- await client.connection.sendNotification("test/publish-diagnostics", {
- uri: pathToFileURL(file).href,
- version,
- diagnostics: [
- {
- range: {
- start: { line: 0, character: 0 },
- end: { line: 0, character: 5 },
- },
- message: "push diagnostic",
- severity: 1,
- },
- ],
- })
- for (let i = 0; i < 20 && (client.diagnostics.get(file)?.length ?? 0) === 0; i++) {
- await new Promise((resolve) => setTimeout(resolve, 25))
- }
- expect(client.diagnostics.get(file)?.[0]?.message).toBe("push diagnostic")
- const started = Date.now()
- await client.waitForDiagnostics({ path: file, version, mode: "document" })
- expect(Date.now() - started).toBeLessThan(1_000)
- await client.shutdown()
- },
- })
- })
- test("document mode waits for pull diagnostics", async () => {
- const handle = spawnFakeServer() as any
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "client.cs")
- await Bun.write(file, "class C {}\n")
- await WithInstance.provide({
- directory: tmp.path,
- fn: async () => {
- const client = await LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: tmp.path,
- directory: tmp.path,
- })
- await client.connection.sendRequest("test/configure-pull-diagnostics", {
- registerOn: "didOpen",
- registrations: [{ identifier: "DocumentCompilerSemantic" }],
- documentDiagnosticsByIdentifier: {
- DocumentCompilerSemantic: [
- {
- range: {
- start: { line: 0, character: 0 },
- end: { line: 0, character: 5 },
- },
- message: "pull diagnostic",
- severity: 1,
- },
- ],
- },
- })
- const version = await client.notify.open({ path: file })
- await client.waitForDiagnostics({ path: file, version, mode: "document" })
- const diagnostics = client.diagnostics.get(file) ?? []
- expect(diagnostics).toHaveLength(1)
- expect(diagnostics[0]?.message).toBe("pull diagnostic")
- const count = await client.connection.sendRequest("test/get-diagnostic-request-count", {})
- expect(count).toBeGreaterThan(0)
- await client.shutdown()
- },
- })
- })
- test("document mode does not wait for the slowest pull identifier after current-file diagnostics arrive", async () => {
- const handle = spawnFakeServer() as any
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "client.cs")
- await Bun.write(file, "class C {}\n")
- await WithInstance.provide({
- directory: tmp.path,
- fn: async () => {
- const client = await LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: tmp.path,
- directory: tmp.path,
- })
- await client.connection.sendRequest("test/configure-pull-diagnostics", {
- registrations: [{ identifier: "fast" }, { identifier: "slow" }],
- documentDiagnosticsByIdentifier: {
- fast: [
- {
- range: {
- start: { line: 0, character: 0 },
- end: { line: 0, character: 5 },
- },
- message: "fast diagnostic",
- severity: 1,
- },
- ],
- slow: [],
- },
- documentDelayMsByIdentifier: {
- slow: 2_500,
- },
- })
- const version = await client.notify.open({ path: file })
- await client.connection.sendRequest("test/register-configured-pull-diagnostics", {})
- await new Promise((resolve) => setTimeout(resolve, 100))
- const started = Date.now()
- await client.waitForDiagnostics({ path: file, version, mode: "document" })
- expect(Date.now() - started).toBeLessThan(1_000)
- expect(client.diagnostics.get(file)?.[0]?.message).toBe("fast diagnostic")
- expect(await client.connection.sendRequest("test/get-diagnostic-request-count", {})).toBeGreaterThan(1)
- await client.shutdown()
- },
- })
- })
- test("full mode includes workspace pull diagnostics", async () => {
- const handle = spawnFakeServer() as any
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "client.cs")
- const related = path.join(tmp.path, "other.cs")
- await Bun.write(file, "class C {}\n")
- await Bun.write(related, "class D {}\n")
- await WithInstance.provide({
- directory: tmp.path,
- fn: async () => {
- const client = await LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: tmp.path,
- directory: tmp.path,
- })
- await client.connection.sendRequest("test/configure-pull-diagnostics", {
- registerOn: "didOpen",
- registrations: [
- { identifier: "DocumentCompilerSemantic" },
- { identifier: "WorkspaceDocumentsAndProject", workspaceDiagnostics: true },
- ],
- documentDiagnosticsByIdentifier: {
- DocumentCompilerSemantic: [
- {
- range: {
- start: { line: 0, character: 0 },
- end: { line: 0, character: 5 },
- },
- message: "current file",
- severity: 1,
- },
- ],
- },
- workspaceDiagnosticsByIdentifier: {
- WorkspaceDocumentsAndProject: [
- {
- uri: pathToFileURL(related).href,
- items: [
- {
- range: {
- start: { line: 0, character: 0 },
- end: { line: 0, character: 5 },
- },
- message: "workspace file",
- severity: 1,
- },
- ],
- },
- ],
- },
- })
- const version = await client.notify.open({ path: file })
- await client.waitForDiagnostics({ path: file, version, mode: "full" })
- expect(client.diagnostics.get(file)?.[0]?.message).toBe("current file")
- expect(client.diagnostics.get(related)?.[0]?.message).toBe("workspace file")
- await client.shutdown()
- },
- })
- })
- test("full mode treats an empty workspace pull response as handled", async () => {
- const handle = spawnFakeServer() as any
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "client.cs")
- await Bun.write(file, "class C {}\n")
- await WithInstance.provide({
- directory: tmp.path,
- fn: async () => {
- const client = await LSPClient.create({
- serverID: "fake",
- server: handle as unknown as LSPServer.Handle,
- root: tmp.path,
- directory: tmp.path,
- })
- await client.connection.sendRequest("test/configure-pull-diagnostics", {
- registerOn: "didOpen",
- registrations: [{ identifier: "WorkspaceDocumentsAndProject", workspaceDiagnostics: true }],
- workspaceDiagnosticsByIdentifier: {
- WorkspaceDocumentsAndProject: [],
- },
- })
- const version = await client.notify.open({ path: file })
- const started = Date.now()
- await client.waitForDiagnostics({ path: file, version, mode: "full" })
- expect(Date.now() - started).toBeLessThan(1_000)
- await client.shutdown()
- },
- })
- })
- })
|