mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-17 18:12:42 +00:00
feat: support pull diagnostics in the LSP client (C#, Kotlin, etc) (#23771)
This commit is contained in:
@@ -1,7 +1,23 @@
|
||||
// Simple JSON-RPC 2.0 LSP-like fake server over stdio
|
||||
// Implements a minimal LSP handshake and triggers a request upon notification
|
||||
|
||||
let nextId = 1
|
||||
let readBuffer = Buffer.alloc(0)
|
||||
let lastChange = null
|
||||
let initializeParams = null
|
||||
let diagnosticRequestCount = 0
|
||||
let registeredCapability = false
|
||||
const pendingClientRequests = new Map()
|
||||
let pullConfig = {
|
||||
delayMs: 0,
|
||||
registerOn: undefined,
|
||||
registrations: [],
|
||||
documentDiagnostics: [],
|
||||
documentDiagnosticsByIdentifier: {},
|
||||
documentDelayMsByIdentifier: {},
|
||||
workspaceDiagnostics: [],
|
||||
workspaceDiagnosticsByIdentifier: {},
|
||||
workspaceDelayMsByIdentifier: {},
|
||||
}
|
||||
|
||||
function encode(message) {
|
||||
const json = JSON.stringify(message)
|
||||
@@ -14,29 +30,19 @@ function decodeFrames(buffer) {
|
||||
let idx
|
||||
while ((idx = buffer.indexOf("\r\n\r\n")) !== -1) {
|
||||
const header = buffer.slice(0, idx).toString("utf8")
|
||||
const m = /Content-Length:\s*(\d+)/i.exec(header)
|
||||
const len = m ? parseInt(m[1], 10) : 0
|
||||
const match = /Content-Length:\s*(\d+)/i.exec(header)
|
||||
const length = match ? parseInt(match[1], 10) : 0
|
||||
const bodyStart = idx + 4
|
||||
const bodyEnd = bodyStart + len
|
||||
const bodyEnd = bodyStart + length
|
||||
if (buffer.length < bodyEnd) break
|
||||
const body = buffer.slice(bodyStart, bodyEnd).toString("utf8")
|
||||
results.push(body)
|
||||
results.push(buffer.slice(bodyStart, bodyEnd).toString("utf8"))
|
||||
buffer = buffer.slice(bodyEnd)
|
||||
}
|
||||
return { messages: results, rest: buffer }
|
||||
}
|
||||
|
||||
let readBuffer = Buffer.alloc(0)
|
||||
|
||||
process.stdin.on("data", (chunk) => {
|
||||
readBuffer = Buffer.concat([readBuffer, chunk])
|
||||
const { messages, rest } = decodeFrames(readBuffer)
|
||||
readBuffer = rest
|
||||
for (const m of messages) handle(m)
|
||||
})
|
||||
|
||||
function send(msg) {
|
||||
process.stdout.write(encode(msg))
|
||||
function send(message) {
|
||||
process.stdout.write(encode(message))
|
||||
}
|
||||
|
||||
function sendRequest(method, params) {
|
||||
@@ -45,6 +51,50 @@ function sendRequest(method, params) {
|
||||
return id
|
||||
}
|
||||
|
||||
function sendResponse(id, result) {
|
||||
send({ jsonrpc: "2.0", id, result })
|
||||
}
|
||||
|
||||
function sendNotification(method, params) {
|
||||
send({ jsonrpc: "2.0", method, params })
|
||||
}
|
||||
|
||||
function maybeRegister(method) {
|
||||
if (pullConfig.registerOn !== method || registeredCapability) return
|
||||
registeredCapability = true
|
||||
sendRequest("client/registerCapability", {
|
||||
registrations: pullConfig.registrations.map((registration, index) => ({
|
||||
id: registration.id ?? `pull-${index}`,
|
||||
method: registration.method ?? "textDocument/diagnostic",
|
||||
registerOptions: registration.registerOptions ?? registration,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
function delayed(id, result, delayMs = pullConfig.delayMs) {
|
||||
if (!delayMs) {
|
||||
sendResponse(id, result)
|
||||
return
|
||||
}
|
||||
setTimeout(() => sendResponse(id, result), delayMs)
|
||||
}
|
||||
|
||||
function diagnosticsForIdentifier(identifier) {
|
||||
return pullConfig.documentDiagnosticsByIdentifier[identifier] ?? pullConfig.documentDiagnostics
|
||||
}
|
||||
|
||||
function workspaceDiagnosticsForIdentifier(identifier) {
|
||||
return pullConfig.workspaceDiagnosticsByIdentifier[identifier] ?? pullConfig.workspaceDiagnostics
|
||||
}
|
||||
|
||||
function documentDelayForIdentifier(identifier) {
|
||||
return pullConfig.documentDelayMsByIdentifier[identifier] ?? pullConfig.delayMs
|
||||
}
|
||||
|
||||
function workspaceDelayForIdentifier(identifier) {
|
||||
return pullConfig.workspaceDelayMsByIdentifier[identifier] ?? pullConfig.delayMs
|
||||
}
|
||||
|
||||
function handle(raw) {
|
||||
let data
|
||||
try {
|
||||
@@ -52,24 +102,148 @@ function handle(raw) {
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof data.method === "undefined" && typeof data.id !== "undefined") {
|
||||
const pending = pendingClientRequests.get(data.id)
|
||||
if (!pending) return
|
||||
pendingClientRequests.delete(data.id)
|
||||
sendResponse(pending, data.result ?? null)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "initialize") {
|
||||
send({ jsonrpc: "2.0", id: data.id, result: { capabilities: {} } })
|
||||
initializeParams = data.params
|
||||
sendResponse(data.id, {
|
||||
capabilities: {
|
||||
textDocumentSync: {
|
||||
change: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (data.method === "initialized") {
|
||||
|
||||
if (data.method === "test/get-initialize-params") {
|
||||
sendResponse(data.id, initializeParams)
|
||||
return
|
||||
}
|
||||
if (data.method === "workspace/didChangeConfiguration") {
|
||||
|
||||
if (data.method === "test/request-configuration") {
|
||||
const id = sendRequest("workspace/configuration", data.params)
|
||||
pendingClientRequests.set(id, data.id)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "initialized" || data.method === "workspace/didChangeConfiguration") {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "textDocument/didOpen") {
|
||||
maybeRegister("didOpen")
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "textDocument/didChange") {
|
||||
lastChange = data.params
|
||||
maybeRegister("didChange")
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "test/trigger") {
|
||||
const method = data.params && data.params.method
|
||||
if (method === "client/registerCapability") {
|
||||
sendRequest(method, {
|
||||
registrations: [
|
||||
{
|
||||
id: "test-diagnostic-registration",
|
||||
method: "textDocument/diagnostic",
|
||||
registerOptions: { identifier: "syntax" },
|
||||
},
|
||||
],
|
||||
})
|
||||
return
|
||||
}
|
||||
if (method === "client/unregisterCapability") {
|
||||
sendRequest(method, {
|
||||
unregisterations: [{ id: "test-diagnostic-registration", method: "textDocument/diagnostic" }],
|
||||
})
|
||||
return
|
||||
}
|
||||
if (method) sendRequest(method, {})
|
||||
return
|
||||
}
|
||||
if (typeof data.id !== "undefined") {
|
||||
// Respond OK to any request from client to keep transport flowing
|
||||
send({ jsonrpc: "2.0", id: data.id, result: null })
|
||||
|
||||
if (data.method === "test/configure-pull-diagnostics") {
|
||||
pullConfig = {
|
||||
delayMs: data.params?.delayMs ?? 0,
|
||||
registerOn: data.params?.registerOn,
|
||||
registrations: data.params?.registrations ?? [],
|
||||
documentDiagnostics: data.params?.documentDiagnostics ?? [],
|
||||
documentDiagnosticsByIdentifier: data.params?.documentDiagnosticsByIdentifier ?? {},
|
||||
documentDelayMsByIdentifier: data.params?.documentDelayMsByIdentifier ?? {},
|
||||
workspaceDiagnostics: data.params?.workspaceDiagnostics ?? [],
|
||||
workspaceDiagnosticsByIdentifier: data.params?.workspaceDiagnosticsByIdentifier ?? {},
|
||||
workspaceDelayMsByIdentifier: data.params?.workspaceDelayMsByIdentifier ?? {},
|
||||
}
|
||||
registeredCapability = false
|
||||
sendResponse(data.id, null)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "test/register-configured-pull-diagnostics") {
|
||||
maybeRegister(undefined)
|
||||
sendResponse(data.id, null)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "test/publish-diagnostics") {
|
||||
sendNotification("textDocument/publishDiagnostics", data.params)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "test/get-last-change") {
|
||||
sendResponse(data.id, lastChange)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "test/get-diagnostic-request-count") {
|
||||
sendResponse(data.id, diagnosticRequestCount)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "textDocument/diagnostic") {
|
||||
diagnosticRequestCount += 1
|
||||
delayed(
|
||||
data.id,
|
||||
{
|
||||
kind: "full",
|
||||
items: diagnosticsForIdentifier(data.params?.identifier ?? ""),
|
||||
},
|
||||
documentDelayForIdentifier(data.params?.identifier ?? ""),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.method === "workspace/diagnostic") {
|
||||
diagnosticRequestCount += 1
|
||||
delayed(
|
||||
data.id,
|
||||
{
|
||||
items: workspaceDiagnosticsForIdentifier(data.params?.identifier ?? ""),
|
||||
},
|
||||
workspaceDelayForIdentifier(data.params?.identifier ?? ""),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof data.id !== "undefined") {
|
||||
sendResponse(data.id, null)
|
||||
}
|
||||
}
|
||||
|
||||
process.stdin.on("data", (chunk) => {
|
||||
readBuffer = Buffer.concat([readBuffer, chunk])
|
||||
const { messages, rest } = decodeFrames(readBuffer)
|
||||
readBuffer = rest
|
||||
for (const message of messages) handle(message)
|
||||
})
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { describe, expect, test, beforeEach } from "bun:test"
|
||||
import { beforeEach, describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { LSPClient } from "../../src/lsp"
|
||||
import { LSPServer } from "../../src/lsp"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Log } from "../../src/util"
|
||||
|
||||
// Minimal fake LSP server that speaks JSON-RPC over stdio
|
||||
function spawnFakeServer() {
|
||||
const { spawn } = require("child_process")
|
||||
const serverPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
|
||||
@@ -39,10 +40,8 @@ describe("LSPClient interop", () => {
|
||||
method: "workspace/workspaceFolders",
|
||||
})
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect(client.connection).toBeDefined()
|
||||
|
||||
await client.shutdown()
|
||||
})
|
||||
|
||||
@@ -64,10 +63,8 @@ describe("LSPClient interop", () => {
|
||||
method: "client/registerCapability",
|
||||
})
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect(client.connection).toBeDefined()
|
||||
|
||||
await client.shutdown()
|
||||
})
|
||||
|
||||
@@ -89,10 +86,397 @@ describe("LSPClient interop", () => {
|
||||
method: "client/unregisterCapability",
|
||||
})
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
|
||||
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 Instance.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 Instance.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 Instance.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 Instance.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 Instance.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 Instance.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 Instance.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 Instance.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 Instance.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()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user