Skip to content

Commit 127a305

Browse files
ssjolearyclaude
andcommitted
feat: implement editor/getDiagnostics protocol support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2507c33 commit 127a305

3 files changed

Lines changed: 198 additions & 0 deletions

File tree

lua/eca/editor.lua

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
local M = {}
2+
3+
local SEVERITY = {
4+
[vim.diagnostic.severity.ERROR] = "error",
5+
[vim.diagnostic.severity.WARN] = "warning",
6+
[vim.diagnostic.severity.INFO] = "information",
7+
[vim.diagnostic.severity.HINT] = "hint",
8+
}
9+
10+
local function uri_to_bufnr(uri)
11+
if not uri or uri == "" then return -1 end
12+
local path = vim.uri_to_fname(uri)
13+
return vim.fn.bufnr(path)
14+
end
15+
16+
local function get_diagnostics(params)
17+
local uri = params.uri
18+
local bufnr = uri_to_bufnr(uri)
19+
if bufnr == -1 then return { diagnostics = {} } end
20+
local raw = vim.diagnostic.get(bufnr)
21+
local diags = {}
22+
for _, d in ipairs(raw) do
23+
table.insert(diags, {
24+
uri = uri,
25+
message = d.message,
26+
severity = SEVERITY[d.severity] or "information",
27+
range = {
28+
start = { line = d.lnum, character = d.col },
29+
["end"] = { line = d.end_lnum or d.lnum, character = d.end_col or d.col },
30+
},
31+
source = d.source,
32+
code = d.code,
33+
})
34+
end
35+
return { diagnostics = diags }
36+
end
37+
38+
function M.handle_request(message)
39+
if message.method == "editor/getDiagnostics" then
40+
return get_diagnostics(message.params or {})
41+
end
42+
return {}
43+
end
44+
45+
return M

lua/eca/server.lua

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ function M.new(opts)
3737
require("eca.observer").notify(message)
3838
end)
3939
end,
40+
on_request = function(_, message)
41+
return require("eca.editor").handle_request(message)
42+
end,
4043
cwd = vim.fn.getcwd(),
4144
workspace_folders = {
4245
{
@@ -52,6 +55,7 @@ function M.new(opts)
5255
on_initialize = opts.on_initialize,
5356
on_stop = opts.on_stop,
5457
on_notification = opts.on_notification,
58+
on_request = opts.on_request,
5559
messages = {},
5660
pending_requests = {},
5761
initialized = false,
@@ -201,6 +205,7 @@ function M:initialize()
201205
capabilities = {
202206
codeAssistant = {
203207
chat = true,
208+
editor = { diagnostics = true },
204209
},
205210
},
206211
workspaceFolders = vim.deepcopy(self.workspace_folders),
@@ -255,13 +260,38 @@ function M:handle_message(message)
255260
else
256261
callback(nil, message.result)
257262
end
263+
elseif message.id and message.method then
264+
if self.on_request then
265+
local id = message.id
266+
vim.schedule(function()
267+
local ok, result = pcall(self.on_request, self, message)
268+
if not ok then
269+
Logger.error("on_request error: " .. tostring(result))
270+
self:send_response(id, {})
271+
return
272+
end
273+
local ok2, err = pcall(self.send_response, self, id, result)
274+
if not ok2 then
275+
Logger.error("send_response error: " .. tostring(err))
276+
end
277+
end)
278+
end
258279
elseif message.method and not message.id then
259280
if self.on_notification then
260281
self:on_notification(message)
261282
end
262283
end
263284
end
264285

286+
---@param id integer|string
287+
---@param result table
288+
function M:send_response(id, result)
289+
local message = { jsonrpc = "2.0", id = id, result = result }
290+
local json = vim.json.encode(message)
291+
local content = string.format("Content-Length: %d\r\n\r\n%s", #json, json)
292+
self.process:write(content)
293+
end
294+
265295
---@return integer
266296
function M:get_next_id()
267297
self.next_id = self.next_id + 1

tests/test_editor_diagnostics.lua

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
local MiniTest = require("mini.test")
2+
local eq = MiniTest.expect.equality
3+
local child = MiniTest.new_child_neovim()
4+
5+
local T = MiniTest.new_set({
6+
hooks = {
7+
pre_case = function()
8+
child.restart({ "-u", "scripts/minimal_init.lua" })
9+
end,
10+
post_once = child.stop,
11+
},
12+
})
13+
14+
T["editor"] = MiniTest.new_set()
15+
16+
T["editor"]["returns empty diagnostics when buffer not found"] = function()
17+
local count = child.lua_get([[
18+
require("eca.editor").handle_request({
19+
method = "editor/getDiagnostics",
20+
params = { uri = "file:///nonexistent/file.lua" },
21+
}).diagnostics
22+
]])
23+
eq(#count, 0)
24+
end
25+
26+
T["editor"]["maps severity levels correctly"] = function()
27+
child.lua([[
28+
local bufnr = vim.api.nvim_create_buf(false, true)
29+
vim.api.nvim_buf_set_name(bufnr, "/tmp/eca_test_sev.lua")
30+
local ns = vim.api.nvim_create_namespace("eca_sev")
31+
vim.diagnostic.set(ns, bufnr, {
32+
{ lnum=0, col=0, end_lnum=0, end_col=1, message="err", severity=vim.diagnostic.severity.ERROR },
33+
{ lnum=1, col=0, end_lnum=1, end_col=1, message="warn", severity=vim.diagnostic.severity.WARN },
34+
{ lnum=2, col=0, end_lnum=2, end_col=1, message="info", severity=vim.diagnostic.severity.INFO },
35+
{ lnum=3, col=0, end_lnum=3, end_col=1, message="hint", severity=vim.diagnostic.severity.HINT },
36+
})
37+
local res = require("eca.editor").handle_request({
38+
method = "editor/getDiagnostics",
39+
params = { uri = "file:///tmp/eca_test_sev.lua" },
40+
})
41+
_G.sev_result = {}
42+
for _, d in ipairs(res.diagnostics) do
43+
table.insert(_G.sev_result, d.severity)
44+
end
45+
vim.api.nvim_buf_delete(bufnr, { force = true })
46+
]])
47+
eq(child.lua_get("_G.sev_result[1]"), "error")
48+
eq(child.lua_get("_G.sev_result[2]"), "warning")
49+
eq(child.lua_get("_G.sev_result[3]"), "information")
50+
eq(child.lua_get("_G.sev_result[4]"), "hint")
51+
end
52+
53+
T["editor"]["includes range and message fields"] = function()
54+
child.lua([[
55+
local bufnr = vim.api.nvim_create_buf(false, true)
56+
vim.api.nvim_buf_set_name(bufnr, "/tmp/eca_test_fields.lua")
57+
local ns = vim.api.nvim_create_namespace("eca_fields")
58+
vim.diagnostic.set(ns, bufnr, {
59+
{ lnum=5, col=3, end_lnum=5, end_col=10, message="test error", severity=vim.diagnostic.severity.ERROR, source="myls" },
60+
})
61+
local res = require("eca.editor").handle_request({
62+
method = "editor/getDiagnostics",
63+
params = { uri = "file:///tmp/eca_test_fields.lua" },
64+
})
65+
_G.field_result = res.diagnostics[1]
66+
vim.api.nvim_buf_delete(bufnr, { force = true })
67+
]])
68+
eq(child.lua_get("_G.field_result.message"), "test error")
69+
eq(child.lua_get("_G.field_result.range.start.line"), 5)
70+
eq(child.lua_get("_G.field_result.range.start.character"), 3)
71+
eq(child.lua_get([[_G.field_result.range["end"].line]]), 5)
72+
eq(child.lua_get([[_G.field_result.range["end"].character]]), 10)
73+
end
74+
75+
T["editor"]["falls back to information for unknown severity"] = function()
76+
child.lua([[
77+
local bufnr = vim.api.nvim_create_buf(false, true)
78+
vim.api.nvim_buf_set_name(bufnr, "/tmp/eca_test_fallback.lua")
79+
local ns = vim.api.nvim_create_namespace("eca_fallback")
80+
vim.diagnostic.set(ns, bufnr, {
81+
{ lnum=0, col=0, end_lnum=0, end_col=1, message="x", severity=99 },
82+
})
83+
local res = require("eca.editor").handle_request({
84+
method = "editor/getDiagnostics",
85+
params = { uri = "file:///tmp/eca_test_fallback.lua" },
86+
})
87+
_G.fallback_sev = res.diagnostics[1] and res.diagnostics[1].severity
88+
vim.api.nvim_buf_delete(bufnr, { force = true })
89+
]])
90+
eq(child.lua_get("_G.fallback_sev"), "information")
91+
end
92+
93+
T["editor"]["uri field is present on each diagnostic"] = function()
94+
child.lua([[
95+
local bufnr = vim.api.nvim_create_buf(false, true)
96+
vim.api.nvim_buf_set_name(bufnr, "/tmp/eca_test_uri.lua")
97+
local ns = vim.api.nvim_create_namespace("eca_uri")
98+
vim.diagnostic.set(ns, bufnr, {
99+
{ lnum=0, col=0, end_lnum=0, end_col=1, message="e1", severity=vim.diagnostic.severity.ERROR },
100+
{ lnum=1, col=0, end_lnum=1, end_col=1, message="e2", severity=vim.diagnostic.severity.WARN },
101+
})
102+
local res = require("eca.editor").handle_request({
103+
method = "editor/getDiagnostics",
104+
params = { uri = "file:///tmp/eca_test_uri.lua" },
105+
})
106+
_G.uri_result = {}
107+
for _, d in ipairs(res.diagnostics) do
108+
table.insert(_G.uri_result, d.uri)
109+
end
110+
vim.api.nvim_buf_delete(bufnr, { force = true })
111+
]])
112+
eq(child.lua_get("_G.uri_result[1]"), "file:///tmp/eca_test_uri.lua")
113+
eq(child.lua_get("_G.uri_result[2]"), "file:///tmp/eca_test_uri.lua")
114+
end
115+
116+
T["editor"]["returns empty table for unknown method"] = function()
117+
local count = child.lua_get([[
118+
vim.tbl_count(require("eca.editor").handle_request({ method = "editor/unknown", params = {} }))
119+
]])
120+
eq(count, 0)
121+
end
122+
123+
return T

0 commit comments

Comments
 (0)