fix(lsp): add Push model support and fix JSON LSP diagnostics

- Add diagnosticsStore to capture Push model notifications
- Handle textDocument/publishDiagnostics notifications in processBuffer
- Fix workspace/configuration response for JSON LSP validation
- Add missing language mappings (json, html, css, sh, fish, md, tf)
- diagnostics() now tries Pull first, falls back to Push store
This commit is contained in:
YeonGyu-Kim
2025-12-08 09:38:00 +09:00
parent 4debb57402
commit b86346a79d
2 changed files with 46 additions and 8 deletions

View File

@@ -3,6 +3,7 @@ import { readFileSync } from "fs"
import { extname, resolve } from "path" import { extname, resolve } from "path"
import type { ResolvedServer } from "./config" import type { ResolvedServer } from "./config"
import { getLanguageId } from "./config" import { getLanguageId } from "./config"
import type { Diagnostic } from "./types"
interface ManagedClient { interface ManagedClient {
client: LSPClient client: LSPClient
@@ -155,6 +156,7 @@ export class LSPClient {
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[]>()
constructor( constructor(
private root: string, private root: string,
@@ -290,7 +292,11 @@ export class LSPClient {
try { try {
const msg = JSON.parse(content) const msg = JSON.parse(content)
if ("id" in msg && "method" in msg) { 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) this.handleServerRequest(msg.id, msg.method, msg.params)
} else if ("id" in msg && this.pending.has(msg.id)) { } else if ("id" in msg && this.pending.has(msg.id)) {
const handler = this.pending.get(msg.id)! const handler = this.pending.get(msg.id)!
@@ -347,9 +353,14 @@ export class LSPClient {
this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`) this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
} }
private handleServerRequest(id: number | string, method: string, _params?: unknown): void { private handleServerRequest(id: number | string, method: string, params?: unknown): void {
if (method === "workspace/configuration") { if (method === "workspace/configuration") {
this.respond(id, [{}]) 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") { } else if (method === "client/registerCapability") {
this.respond(id, null) this.respond(id, null)
} else if (method === "window/workDoneProgress/create") { } else if (method === "window/workDoneProgress/create") {
@@ -412,7 +423,9 @@ export class LSPClient {
...this.server.initialization, ...this.server.initialization,
}) })
this.notify("initialized") this.notify("initialized")
this.notify("workspace/didChangeConfiguration", { settings: {} }) this.notify("workspace/didChangeConfiguration", {
settings: { json: { validate: { enable: true } } },
})
await new Promise((r) => setTimeout(r, 300)) await new Promise((r) => setTimeout(r, 300))
} }
@@ -477,13 +490,23 @@ export class LSPClient {
return this.send("workspace/symbol", { query }) return this.send("workspace/symbol", { query })
} }
async diagnostics(filePath: string): Promise<unknown> { async diagnostics(filePath: string): Promise<{ items: Diagnostic[] }> {
const absPath = resolve(filePath) const absPath = resolve(filePath)
const uri = `file://${absPath}`
await this.openFile(absPath) await this.openFile(absPath)
await new Promise((r) => setTimeout(r, 500)) await new Promise((r) => setTimeout(r, 500))
return this.send("textDocument/diagnostic", {
textDocument: { uri: `file://${absPath}` }, try {
}) const result = await this.send("textDocument/diagnostic", {
textDocument: { uri },
})
if (result && typeof result === "object" && "items" in result) {
return result as { items: Diagnostic[] }
}
} catch {
}
return { items: this.diagnosticsStore.get(uri) ?? [] }
} }
async prepareRename(filePath: string, line: number, character: number): Promise<unknown> { async prepareRename(filePath: string, line: number, character: number): Promise<unknown> {
@@ -545,5 +568,6 @@ export class LSPClient {
this.proc?.kill() this.proc?.kill()
this.proc = null this.proc = null
this.processExited = true this.processExited = true
this.diagnosticsStore.clear()
} }
} }

View File

@@ -175,4 +175,18 @@ export const EXT_TO_LANG: Record<string, string> = {
".astro": "astro", ".astro": "astro",
".yaml": "yaml", ".yaml": "yaml",
".yml": "yaml", ".yml": "yaml",
".json": "json",
".jsonc": "jsonc",
".html": "html",
".htm": "html",
".css": "css",
".scss": "scss",
".less": "less",
".sh": "shellscript",
".bash": "shellscript",
".zsh": "shellscript",
".fish": "fish",
".md": "markdown",
".tf": "terraform",
".tfvars": "terraform",
} }