Migrate LSP client to vscode-jsonrpc for improved stability (#1095)
* refactor(lsp): migrate to vscode-jsonrpc for improved stability Replace custom JSON-RPC implementation with vscode-jsonrpc library. Use MessageConnection with StreamMessageReader/Writer. Implement Bun↔Node stream bridges for compatibility. Preserve all existing functionality (warmup, cleanup, capabilities). Net reduction of ~60 lines while improving protocol handling. * fix(lsp): clear timeout on successful response to prevent unhandled rejections --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -18,6 +18,7 @@
|
|||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
|
"vscode-jsonrpc": "^8.2.0",
|
||||||
"zod": "^4.1.8",
|
"zod": "^4.1.8",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -303,6 +304,8 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
|
"vscode-jsonrpc": "^8.2.0",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { spawn, type Subprocess } from "bun"
|
import { spawn, type Subprocess } from "bun"
|
||||||
|
import { Readable, Writable } from "node:stream"
|
||||||
import { readFileSync } from "fs"
|
import { readFileSync } from "fs"
|
||||||
import { extname, resolve } from "path"
|
import { extname, resolve } from "path"
|
||||||
import { pathToFileURL } from "node:url"
|
import { pathToFileURL } from "node:url"
|
||||||
|
import {
|
||||||
|
createMessageConnection,
|
||||||
|
StreamMessageReader,
|
||||||
|
StreamMessageWriter,
|
||||||
|
type MessageConnection,
|
||||||
|
} from "vscode-jsonrpc/node"
|
||||||
import { getLanguageId } from "./config"
|
import { getLanguageId } from "./config"
|
||||||
import type { Diagnostic, ResolvedServer } from "./types"
|
import type { Diagnostic, ResolvedServer } from "./types"
|
||||||
|
|
||||||
@@ -38,22 +45,18 @@ class LSPServerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Works on all platforms
|
|
||||||
process.on("exit", cleanup)
|
process.on("exit", cleanup)
|
||||||
|
|
||||||
// Ctrl+C - works on all platforms
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
cleanup()
|
cleanup()
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Kill signal - Unix/macOS
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
cleanup()
|
cleanup()
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ctrl+Break - Windows specific
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
process.on("SIGBREAK", () => {
|
process.on("SIGBREAK", () => {
|
||||||
cleanup()
|
cleanup()
|
||||||
@@ -209,13 +212,12 @@ export const lspManager = LSPServerManager.getInstance()
|
|||||||
|
|
||||||
export class LSPClient {
|
export class LSPClient {
|
||||||
private proc: Subprocess<"pipe", "pipe", "pipe"> | null = null
|
private proc: Subprocess<"pipe", "pipe", "pipe"> | null = null
|
||||||
private buffer: Uint8Array = new Uint8Array(0)
|
private connection: MessageConnection | null = null
|
||||||
private pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>()
|
|
||||||
private requestIdCounter = 0
|
|
||||||
private openedFiles = new Set<string>()
|
private openedFiles = new Set<string>()
|
||||||
private stderrBuffer: string[] = []
|
private stderrBuffer: string[] = []
|
||||||
private processExited = false
|
private processExited = false
|
||||||
private diagnosticsStore = new Map<string, Diagnostic[]>()
|
private diagnosticsStore = new Map<string, Diagnostic[]>()
|
||||||
|
private readonly REQUEST_TIMEOUT = 15000
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private root: string,
|
private root: string,
|
||||||
@@ -238,7 +240,6 @@ export class LSPClient {
|
|||||||
throw new Error(`Failed to spawn LSP server: ${this.server.command.join(" ")}`)
|
throw new Error(`Failed to spawn LSP server: ${this.server.command.join(" ")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startReading()
|
|
||||||
this.startStderrReading()
|
this.startStderrReading()
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
@@ -249,33 +250,66 @@ export class LSPClient {
|
|||||||
`LSP server exited immediately with code ${this.proc.exitCode}` + (stderr ? `\nstderr: ${stderr}` : "")
|
`LSP server exited immediately with code ${this.proc.exitCode}` + (stderr ? `\nstderr: ${stderr}` : "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private startReading(): void {
|
const stdoutReader = this.proc.stdout.getReader()
|
||||||
if (!this.proc) return
|
const nodeReadable = new Readable({
|
||||||
|
async read() {
|
||||||
const reader = this.proc.stdout.getReader()
|
try {
|
||||||
const read = async () => {
|
const { done, value } = await stdoutReader.read()
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) {
|
if (done) {
|
||||||
this.processExited = true
|
this.push(null)
|
||||||
this.rejectAllPending("LSP server stdout closed")
|
} else {
|
||||||
break
|
this.push(Buffer.from(value))
|
||||||
}
|
}
|
||||||
const newBuf = new Uint8Array(this.buffer.length + value.length)
|
} catch {
|
||||||
newBuf.set(this.buffer)
|
this.push(null)
|
||||||
newBuf.set(value, this.buffer.length)
|
|
||||||
this.buffer = newBuf
|
|
||||||
this.processBuffer()
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
},
|
||||||
this.processExited = true
|
})
|
||||||
this.rejectAllPending(`LSP stdout read error: ${err}`)
|
|
||||||
|
const stdin = this.proc.stdin
|
||||||
|
const nodeWritable = new Writable({
|
||||||
|
write(chunk, _encoding, callback) {
|
||||||
|
try {
|
||||||
|
stdin.write(chunk)
|
||||||
|
callback()
|
||||||
|
} catch (err) {
|
||||||
|
callback(err as Error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connection = createMessageConnection(
|
||||||
|
new StreamMessageReader(nodeReadable),
|
||||||
|
new StreamMessageWriter(nodeWritable)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.connection.onNotification("textDocument/publishDiagnostics", (params: { uri?: string; diagnostics?: Diagnostic[] }) => {
|
||||||
|
if (params.uri) {
|
||||||
|
this.diagnosticsStore.set(params.uri, params.diagnostics ?? [])
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
read()
|
|
||||||
|
this.connection.onRequest("workspace/configuration", (params: { items?: Array<{ section?: string }> }) => {
|
||||||
|
const items = params?.items ?? []
|
||||||
|
return items.map((item) => {
|
||||||
|
if (item.section === "json") return { validate: { enable: true } }
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connection.onRequest("client/registerCapability", () => null)
|
||||||
|
this.connection.onRequest("window/workDoneProgress/create", () => null)
|
||||||
|
|
||||||
|
this.connection.onClose(() => {
|
||||||
|
this.processExited = true
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connection.onError((error) => {
|
||||||
|
console.error("LSP connection error:", error)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connection.listen()
|
||||||
}
|
}
|
||||||
|
|
||||||
private startStderrReading(): void {
|
private startStderrReading(): void {
|
||||||
@@ -294,142 +328,49 @@ export class LSPClient {
|
|||||||
this.stderrBuffer.shift()
|
this.stderrBuffer.shift()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
read()
|
read()
|
||||||
}
|
}
|
||||||
|
|
||||||
private rejectAllPending(reason: string): void {
|
private async sendRequest<T>(method: string, params?: unknown): Promise<T> {
|
||||||
for (const [id, handler] of this.pending) {
|
if (!this.connection) throw new Error("LSP client not started")
|
||||||
handler.reject(new Error(reason))
|
|
||||||
this.pending.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private findSequence(haystack: Uint8Array, needle: number[]): number {
|
if (this.processExited || (this.proc && this.proc.exitCode !== null)) {
|
||||||
outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
|
|
||||||
for (let j = 0; j < needle.length; j++) {
|
|
||||||
if (haystack[i + j] !== needle[j]) continue outer
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
private processBuffer(): void {
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
const CONTENT_LENGTH = [67, 111, 110, 116, 101, 110, 116, 45, 76, 101, 110, 103, 116, 104, 58]
|
|
||||||
const CRLF_CRLF = [13, 10, 13, 10]
|
|
||||||
const LF_LF = [10, 10]
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const headerStart = this.findSequence(this.buffer, CONTENT_LENGTH)
|
|
||||||
if (headerStart === -1) break
|
|
||||||
if (headerStart > 0) this.buffer = this.buffer.slice(headerStart)
|
|
||||||
|
|
||||||
let headerEnd = this.findSequence(this.buffer, CRLF_CRLF)
|
|
||||||
let sepLen = 4
|
|
||||||
if (headerEnd === -1) {
|
|
||||||
headerEnd = this.findSequence(this.buffer, LF_LF)
|
|
||||||
sepLen = 2
|
|
||||||
}
|
|
||||||
if (headerEnd === -1) break
|
|
||||||
|
|
||||||
const header = decoder.decode(this.buffer.slice(0, headerEnd))
|
|
||||||
const match = header.match(/Content-Length:\s*(\d+)/i)
|
|
||||||
if (!match) break
|
|
||||||
|
|
||||||
const len = parseInt(match[1], 10)
|
|
||||||
const start = headerEnd + sepLen
|
|
||||||
const end = start + len
|
|
||||||
if (this.buffer.length < end) break
|
|
||||||
|
|
||||||
const content = decoder.decode(this.buffer.slice(start, end))
|
|
||||||
this.buffer = this.buffer.slice(end)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(content)
|
|
||||||
|
|
||||||
if ("method" in msg && !("id" in msg)) {
|
|
||||||
if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
|
|
||||||
this.diagnosticsStore.set(msg.params.uri, msg.params.diagnostics ?? [])
|
|
||||||
}
|
|
||||||
} else if ("id" in msg && "method" in msg) {
|
|
||||||
this.handleServerRequest(msg.id, msg.method, msg.params)
|
|
||||||
} else if ("id" in msg && this.pending.has(msg.id)) {
|
|
||||||
const handler = this.pending.get(msg.id)!
|
|
||||||
this.pending.delete(msg.id)
|
|
||||||
if ("error" in msg) {
|
|
||||||
handler.reject(new Error(msg.error.message))
|
|
||||||
} else {
|
|
||||||
handler.resolve(msg.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private send(method: string, params?: unknown): Promise<unknown> {
|
|
||||||
if (!this.proc) throw new Error("LSP client not started")
|
|
||||||
|
|
||||||
if (this.processExited || this.proc.exitCode !== null) {
|
|
||||||
const stderr = this.stderrBuffer.slice(-10).join("\n")
|
const stderr = this.stderrBuffer.slice(-10).join("\n")
|
||||||
throw new Error(`LSP server already exited (code: ${this.proc.exitCode})` + (stderr ? `\nstderr: ${stderr}` : ""))
|
throw new Error(`LSP server already exited (code: ${this.proc?.exitCode})` + (stderr ? `\nstderr: ${stderr}` : ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = ++this.requestIdCounter
|
let timeoutId: ReturnType<typeof setTimeout>
|
||||||
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
const header = `Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n`
|
timeoutId = setTimeout(() => {
|
||||||
this.proc.stdin.write(header + msg)
|
const stderr = this.stderrBuffer.slice(-5).join("\n")
|
||||||
|
reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
|
||||||
return new Promise((resolve, reject) => {
|
}, this.REQUEST_TIMEOUT)
|
||||||
this.pending.set(id, { resolve, reject })
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.pending.has(id)) {
|
|
||||||
this.pending.delete(id)
|
|
||||||
const stderr = this.stderrBuffer.slice(-5).join("\n")
|
|
||||||
reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
|
|
||||||
}
|
|
||||||
}, 15000)
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
private notify(method: string, params?: unknown): void {
|
const requestPromise = this.connection.sendRequest(method, params) as Promise<T>
|
||||||
if (!this.proc) return
|
|
||||||
if (this.processExited || this.proc.exitCode !== null) return
|
|
||||||
|
|
||||||
const msg = JSON.stringify({ jsonrpc: "2.0", method, params })
|
try {
|
||||||
this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
|
const result = await Promise.race([requestPromise, timeoutPromise])
|
||||||
}
|
clearTimeout(timeoutId!)
|
||||||
|
return result
|
||||||
private respond(id: number | string, result: unknown): void {
|
} catch (error) {
|
||||||
if (!this.proc) return
|
clearTimeout(timeoutId!)
|
||||||
if (this.processExited || this.proc.exitCode !== null) return
|
throw error
|
||||||
|
|
||||||
const msg = JSON.stringify({ jsonrpc: "2.0", id, result })
|
|
||||||
this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleServerRequest(id: number | string, method: string, params?: unknown): void {
|
|
||||||
if (method === "workspace/configuration") {
|
|
||||||
const items = (params as { items?: Array<{ section?: string }> })?.items ?? []
|
|
||||||
const result = items.map((item) => {
|
|
||||||
if (item.section === "json") return { validate: { enable: true } }
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
this.respond(id, result)
|
|
||||||
} else if (method === "client/registerCapability") {
|
|
||||||
this.respond(id, null)
|
|
||||||
} else if (method === "window/workDoneProgress/create") {
|
|
||||||
this.respond(id, null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sendNotification(method: string, params?: unknown): void {
|
||||||
|
if (!this.connection) return
|
||||||
|
if (this.processExited || (this.proc && this.proc.exitCode !== null)) return
|
||||||
|
|
||||||
|
this.connection.sendNotification(method, params)
|
||||||
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
const rootUri = pathToFileURL(this.root).href
|
const rootUri = pathToFileURL(this.root).href
|
||||||
await this.send("initialize", {
|
await this.sendRequest("initialize", {
|
||||||
processId: process.pid,
|
processId: process.pid,
|
||||||
rootUri,
|
rootUri,
|
||||||
rootPath: this.root,
|
rootPath: this.root,
|
||||||
@@ -481,8 +422,8 @@ export class LSPClient {
|
|||||||
},
|
},
|
||||||
...this.server.initialization,
|
...this.server.initialization,
|
||||||
})
|
})
|
||||||
this.notify("initialized")
|
this.sendNotification("initialized")
|
||||||
this.notify("workspace/didChangeConfiguration", {
|
this.sendNotification("workspace/didChangeConfiguration", {
|
||||||
settings: { json: { validate: { enable: true } } },
|
settings: { json: { validate: { enable: true } } },
|
||||||
})
|
})
|
||||||
await new Promise((r) => setTimeout(r, 300))
|
await new Promise((r) => setTimeout(r, 300))
|
||||||
@@ -496,7 +437,7 @@ export class LSPClient {
|
|||||||
const ext = extname(absPath)
|
const ext = extname(absPath)
|
||||||
const languageId = getLanguageId(ext)
|
const languageId = getLanguageId(ext)
|
||||||
|
|
||||||
this.notify("textDocument/didOpen", {
|
this.sendNotification("textDocument/didOpen", {
|
||||||
textDocument: {
|
textDocument: {
|
||||||
uri: pathToFileURL(absPath).href,
|
uri: pathToFileURL(absPath).href,
|
||||||
languageId,
|
languageId,
|
||||||
@@ -512,7 +453,7 @@ export class LSPClient {
|
|||||||
async definition(filePath: string, line: number, character: number): Promise<unknown> {
|
async definition(filePath: string, line: number, character: number): Promise<unknown> {
|
||||||
const absPath = resolve(filePath)
|
const absPath = resolve(filePath)
|
||||||
await this.openFile(absPath)
|
await this.openFile(absPath)
|
||||||
return this.send("textDocument/definition", {
|
return this.sendRequest("textDocument/definition", {
|
||||||
textDocument: { uri: pathToFileURL(absPath).href },
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
position: { line: line - 1, character },
|
position: { line: line - 1, character },
|
||||||
})
|
})
|
||||||
@@ -521,7 +462,7 @@ export class LSPClient {
|
|||||||
async references(filePath: string, line: number, character: number, includeDeclaration = true): Promise<unknown> {
|
async references(filePath: string, line: number, character: number, includeDeclaration = true): Promise<unknown> {
|
||||||
const absPath = resolve(filePath)
|
const absPath = resolve(filePath)
|
||||||
await this.openFile(absPath)
|
await this.openFile(absPath)
|
||||||
return this.send("textDocument/references", {
|
return this.sendRequest("textDocument/references", {
|
||||||
textDocument: { uri: pathToFileURL(absPath).href },
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
position: { line: line - 1, character },
|
position: { line: line - 1, character },
|
||||||
context: { includeDeclaration },
|
context: { includeDeclaration },
|
||||||
@@ -531,13 +472,13 @@ export class LSPClient {
|
|||||||
async documentSymbols(filePath: string): Promise<unknown> {
|
async documentSymbols(filePath: string): Promise<unknown> {
|
||||||
const absPath = resolve(filePath)
|
const absPath = resolve(filePath)
|
||||||
await this.openFile(absPath)
|
await this.openFile(absPath)
|
||||||
return this.send("textDocument/documentSymbol", {
|
return this.sendRequest("textDocument/documentSymbol", {
|
||||||
textDocument: { uri: pathToFileURL(absPath).href },
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async workspaceSymbols(query: string): Promise<unknown> {
|
async workspaceSymbols(query: string): Promise<unknown> {
|
||||||
return this.send("workspace/symbol", { query })
|
return this.sendRequest("workspace/symbol", { query })
|
||||||
}
|
}
|
||||||
|
|
||||||
async diagnostics(filePath: string): Promise<{ items: Diagnostic[] }> {
|
async diagnostics(filePath: string): Promise<{ items: Diagnostic[] }> {
|
||||||
@@ -547,14 +488,13 @@ export class LSPClient {
|
|||||||
await new Promise((r) => setTimeout(r, 500))
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.send("textDocument/diagnostic", {
|
const result = await this.sendRequest<{ items?: Diagnostic[] }>("textDocument/diagnostic", {
|
||||||
textDocument: { uri },
|
textDocument: { uri },
|
||||||
})
|
})
|
||||||
if (result && typeof result === "object" && "items" in result) {
|
if (result && typeof result === "object" && "items" in result) {
|
||||||
return result as { items: Diagnostic[] }
|
return result as { items: Diagnostic[] }
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
|
|
||||||
return { items: this.diagnosticsStore.get(uri) ?? [] }
|
return { items: this.diagnosticsStore.get(uri) ?? [] }
|
||||||
}
|
}
|
||||||
@@ -562,7 +502,7 @@ export class LSPClient {
|
|||||||
async prepareRename(filePath: string, line: number, character: number): Promise<unknown> {
|
async prepareRename(filePath: string, line: number, character: number): Promise<unknown> {
|
||||||
const absPath = resolve(filePath)
|
const absPath = resolve(filePath)
|
||||||
await this.openFile(absPath)
|
await this.openFile(absPath)
|
||||||
return this.send("textDocument/prepareRename", {
|
return this.sendRequest("textDocument/prepareRename", {
|
||||||
textDocument: { uri: pathToFileURL(absPath).href },
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
position: { line: line - 1, character },
|
position: { line: line - 1, character },
|
||||||
})
|
})
|
||||||
@@ -571,7 +511,7 @@ export class LSPClient {
|
|||||||
async rename(filePath: string, line: number, character: number, newName: string): Promise<unknown> {
|
async rename(filePath: string, line: number, character: number, newName: string): Promise<unknown> {
|
||||||
const absPath = resolve(filePath)
|
const absPath = resolve(filePath)
|
||||||
await this.openFile(absPath)
|
await this.openFile(absPath)
|
||||||
return this.send("textDocument/rename", {
|
return this.sendRequest("textDocument/rename", {
|
||||||
textDocument: { uri: pathToFileURL(absPath).href },
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
position: { line: line - 1, character },
|
position: { line: line - 1, character },
|
||||||
newName,
|
newName,
|
||||||
@@ -583,10 +523,13 @@ export class LSPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
try {
|
if (this.connection) {
|
||||||
this.notify("shutdown", {})
|
try {
|
||||||
this.notify("exit")
|
this.sendNotification("shutdown", {})
|
||||||
} catch {
|
this.sendNotification("exit")
|
||||||
|
} catch {}
|
||||||
|
this.connection.dispose()
|
||||||
|
this.connection = null
|
||||||
}
|
}
|
||||||
this.proc?.kill()
|
this.proc?.kill()
|
||||||
this.proc = null
|
this.proc = null
|
||||||
|
|||||||
Reference in New Issue
Block a user