Skip to content

Commit 21b0b9a

Browse files
committed
feat: complete implementation of getDiagnostics tool
1 parent 09b231d commit 21b0b9a

File tree

2 files changed

+190
-31
lines changed

2 files changed

+190
-31
lines changed
Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
--- Tool implementation for getting diagnostics.
22

3+
local schema = {
4+
description = "Get Neovim LSP diagnostics (errors, warnings) from open buffers",
5+
inputSchema = {
6+
type = "object",
7+
properties = {
8+
uri = {
9+
type = "string",
10+
description = "Optional file URI to get diagnostics for. If not provided, gets diagnostics for all open files.",
11+
},
12+
},
13+
additionalProperties = false,
14+
["$schema"] = "http://json-schema.org/draft-07/schema#",
15+
},
16+
}
17+
318
--- Handles the getDiagnostics tool invocation.
419
-- Retrieves diagnostics from Neovim's diagnostic system.
5-
-- @param _params table The input parameters for the tool (currently unused).
20+
-- @param params table The input parameters for the tool.
21+
-- @field params.uri string|nil Optional file URI to get diagnostics for.
622
-- @return table A table containing the list of diagnostics.
723
-- @error table A table with code, message, and data for JSON-RPC error if failed.
8-
local function handler(_params) -- Prefix unused params with underscore
24+
local function handler(params)
925
if not vim.lsp or not vim.diagnostic or not vim.diagnostic.get then
10-
-- This tool is internal, so returning an error might be too strong.
1126
-- Returning an empty list or a specific status could be an alternative.
1227
-- For now, let's align with the error pattern for consistency if the feature is unavailable.
1328
error({
@@ -17,29 +32,78 @@ local function handler(_params) -- Prefix unused params with underscore
1732
})
1833
end
1934

20-
local all_diagnostics = vim.diagnostic.get(0) -- Get for all buffers
35+
local log_module_ok, log = pcall(require, "claudecode.logger")
36+
if not log_module_ok then
37+
return {
38+
code = -32603, -- Internal error
39+
message = "Internal error",
40+
data = "Failed to load logger module",
41+
}
42+
end
43+
44+
log.debug("getDiagnostics handler called with params: " .. vim.inspect(params))
45+
46+
-- Extract the uri parameter
47+
local diagnostics
48+
49+
if not params.uri then
50+
-- Get diagnostics for all buffers
51+
log.debug("Getting diagnostics for all open buffers")
52+
diagnostics = vim.diagnostic.get()
53+
else
54+
-- Remove file:// prefix if present
55+
local uri = params.uri
56+
local filepath = uri
57+
if uri:sub(1, 7) == "file://" then
58+
filepath = uri:sub(8) -- Remove "file://" prefix
59+
end
60+
61+
-- Get buffer number for the specific file
62+
local bufnr = vim.fn.bufnr(filepath)
63+
if bufnr == -1 then
64+
-- File is not open in any buffer, throw an error
65+
log.debug("File buffer must be open to get diagnostics: " .. filepath)
66+
error({
67+
code = -32001,
68+
message = "File not open in buffer",
69+
data = "File must be open in Neovim to retrieve diagnostics: " .. filepath,
70+
})
71+
else
72+
-- Get diagnostics for the specific buffer
73+
log.debug("Getting diagnostics for bufnr: " .. bufnr)
74+
diagnostics = vim.diagnostic.get(bufnr)
75+
end
76+
end
2177

2278
local formatted_diagnostics = {}
23-
for _, diagnostic in ipairs(all_diagnostics) do
79+
for _, diagnostic in ipairs(diagnostics) do
2480
local file_path = vim.api.nvim_buf_get_name(diagnostic.bufnr)
2581
-- Ensure we only include diagnostics with valid file paths
2682
if file_path and file_path ~= "" then
2783
table.insert(formatted_diagnostics, {
28-
file = file_path,
29-
line = diagnostic.lnum, -- 0-indexed from vim.diagnostic.get
30-
character = diagnostic.col, -- 0-indexed from vim.diagnostic.get
31-
severity = diagnostic.severity, -- e.g., vim.diagnostic.severity.ERROR
32-
message = diagnostic.message,
33-
source = diagnostic.source,
84+
type = "text",
85+
-- json encode this
86+
text = vim.fn.json_encode({
87+
-- Use the file path and diagnostic information
88+
filePath = file_path,
89+
-- Convert line and column to 1-indexed
90+
line = diagnostic.lnum + 1,
91+
character = diagnostic.col + 1,
92+
severity = diagnostic.severity, -- e.g., vim.diagnostic.severity.ERROR
93+
message = diagnostic.message,
94+
source = diagnostic.source,
95+
}),
3496
})
3597
end
3698
end
3799

38-
return { diagnostics = formatted_diagnostics }
100+
return {
101+
content = formatted_diagnostics,
102+
}
39103
end
40104

41105
return {
42106
name = "getDiagnostics",
43-
schema = nil, -- Internal tool
107+
schema = schema,
44108
handler = handler,
45109
}

tests/unit/tools/get_diagnostics_spec.lua

Lines changed: 113 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,23 @@ describe("Tool: get_diagnostics", function()
55

66
before_each(function()
77
package.loaded["claudecode.tools.get_diagnostics"] = nil
8+
package.loaded["claudecode.logger"] = nil
9+
10+
-- Mock the logger module
11+
package.loaded["claudecode.logger"] = {
12+
debug = function() end,
13+
error = function() end,
14+
info = function() end,
15+
warn = function() end,
16+
}
17+
818
get_diagnostics_handler = require("claudecode.tools.get_diagnostics").handler
919

1020
_G.vim = _G.vim or {}
1121
_G.vim.lsp = _G.vim.lsp or {} -- Ensure vim.lsp exists for the check
1222
_G.vim.diagnostic = _G.vim.diagnostic or {}
1323
_G.vim.api = _G.vim.api or {}
24+
_G.vim.fn = _G.vim.fn or {}
1425

1526
-- Default mocks
1627
_G.vim.diagnostic.get = spy.new(function()
@@ -19,12 +30,25 @@ describe("Tool: get_diagnostics", function()
1930
_G.vim.api.nvim_buf_get_name = spy.new(function(bufnr)
2031
return "/path/to/file_for_buf_" .. tostring(bufnr) .. ".lua"
2132
end)
33+
_G.vim.fn.json_encode = spy.new(function(obj)
34+
return vim.inspect(obj) -- Use vim.inspect as a simple serialization
35+
end)
36+
_G.vim.fn.bufnr = spy.new(function(filepath)
37+
-- Mock buffer lookup
38+
if filepath == "/test/file.lua" then
39+
return 1
40+
end
41+
return -1 -- File not open
42+
end)
2243
end)
2344

2445
after_each(function()
2546
package.loaded["claudecode.tools.get_diagnostics"] = nil
47+
package.loaded["claudecode.logger"] = nil
2648
_G.vim.diagnostic.get = nil
2749
_G.vim.api.nvim_buf_get_name = nil
50+
_G.vim.fn.json_encode = nil
51+
_G.vim.fn.bufnr = nil
2852
-- Note: We don't nullify _G.vim.lsp or _G.vim.diagnostic entirely
2953
-- as they are checked for existence.
3054
end)
@@ -33,14 +57,14 @@ describe("Tool: get_diagnostics", function()
3357
local success, result = pcall(get_diagnostics_handler, {})
3458
expect(success).to_be_true()
3559
expect(result).to_be_table()
36-
expect(result.diagnostics).to_be_table()
37-
expect(#result.diagnostics).to_be(0)
38-
assert.spy(_G.vim.diagnostic.get).was_called_with(0)
60+
expect(result.content).to_be_table()
61+
expect(#result.content).to_be(0)
62+
assert.spy(_G.vim.diagnostic.get).was_called_with()
3963
end)
4064

4165
it("should return formatted diagnostics if available", function()
4266
local mock_diagnostics = {
43-
{ bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" },
67+
{ bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" },
4468
{ bufnr = 2, lnum = 20, col = 15, severity = 2, message = "Warning message 2", source = "linter2" },
4569
}
4670
_G.vim.diagnostic.get = spy.new(function()
@@ -49,27 +73,32 @@ describe("Tool: get_diagnostics", function()
4973

5074
local success, result = pcall(get_diagnostics_handler, {})
5175
expect(success).to_be_true()
52-
expect(result.diagnostics).to_be_table()
53-
expect(#result.diagnostics).to_be(2)
76+
expect(result.content).to_be_table()
77+
expect(#result.content).to_be(2)
5478

55-
expect(result.diagnostics[1].file).to_be("/path/to/file_for_buf_1.lua")
56-
expect(result.diagnostics[1].line).to_be(10)
57-
expect(result.diagnostics[1].character).to_be(5)
58-
expect(result.diagnostics[1].severity).to_be(1)
59-
expect(result.diagnostics[1].message).to_be("Error message 1")
60-
expect(result.diagnostics[1].source).to_be("linter1")
79+
-- Check that results are MCP content items
80+
expect(result.content[1].type).to_be("text")
81+
expect(result.content[2].type).to_be("text")
6182

62-
expect(result.diagnostics[2].file).to_be("/path/to/file_for_buf_2.lua")
63-
expect(result.diagnostics[2].severity).to_be(2)
64-
expect(result.diagnostics[2].message).to_be("Warning message 2")
83+
-- Verify JSON encoding was called with correct structure
84+
assert.spy(_G.vim.fn.json_encode).was_called(2)
85+
86+
-- Check the first diagnostic was encoded with 1-indexed values
87+
local first_call_args = _G.vim.fn.json_encode.calls[1].vals[1]
88+
expect(first_call_args.filePath).to_be("/path/to/file_for_buf_1.lua")
89+
expect(first_call_args.line).to_be(11) -- 10 + 1 for 1-indexing
90+
expect(first_call_args.character).to_be(6) -- 5 + 1 for 1-indexing
91+
expect(first_call_args.severity).to_be(1)
92+
expect(first_call_args.message).to_be("Error message 1")
93+
expect(first_call_args.source).to_be("linter1")
6594

6695
assert.spy(_G.vim.api.nvim_buf_get_name).was_called_with(1)
6796
assert.spy(_G.vim.api.nvim_buf_get_name).was_called_with(2)
6897
end)
6998

7099
it("should filter out diagnostics with no file path", function()
71100
local mock_diagnostics = {
72-
{ bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" },
101+
{ bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" },
73102
{ bufnr = 99, lnum = 20, col = 15, severity = 2, message = "Warning message 2", source = "linter2" }, -- This one will have no path
74103
}
75104
_G.vim.diagnostic.get = spy.new(function()
@@ -87,8 +116,12 @@ describe("Tool: get_diagnostics", function()
87116

88117
local success, result = pcall(get_diagnostics_handler, {})
89118
expect(success).to_be_true()
90-
expect(#result.diagnostics).to_be(1)
91-
expect(result.diagnostics[1].file).to_be("/path/to/file1.lua")
119+
expect(#result.content).to_be(1)
120+
121+
-- Verify only the diagnostic with a file path was included
122+
assert.spy(_G.vim.fn.json_encode).was_called(1)
123+
local encoded_args = _G.vim.fn.json_encode.calls[1].vals[1]
124+
expect(encoded_args.filePath).to_be("/path/to/file1.lua")
92125
end)
93126

94127
it("should error if vim.diagnostic.get is not available", function()
@@ -120,4 +153,66 @@ describe("Tool: get_diagnostics", function()
120153
expect(success).to_be_false()
121154
expect(err.code).to_be(-32000)
122155
end)
156+
157+
it("should filter diagnostics by URI when provided", function()
158+
local mock_diagnostics = {
159+
{ bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error in file1", source = "linter1" },
160+
}
161+
_G.vim.diagnostic.get = spy.new(function(bufnr)
162+
if bufnr == 1 then
163+
return mock_diagnostics
164+
end
165+
return {}
166+
end)
167+
_G.vim.api.nvim_buf_get_name = spy.new(function(bufnr)
168+
if bufnr == 1 then
169+
return "/test/file.lua"
170+
end
171+
return ""
172+
end)
173+
174+
local success, result = pcall(get_diagnostics_handler, { uri = "file:///test/file.lua" })
175+
expect(success).to_be_true()
176+
expect(#result.content).to_be(1)
177+
178+
-- Should have called vim.diagnostic.get with specific buffer
179+
assert.spy(_G.vim.diagnostic.get).was_called_with(1)
180+
assert.spy(_G.vim.fn.bufnr).was_called_with("/test/file.lua")
181+
end)
182+
183+
it("should error for URI of unopened file", function()
184+
_G.vim.fn.bufnr = spy.new(function()
185+
return -1 -- File not open
186+
end)
187+
188+
local success, err = pcall(get_diagnostics_handler, { uri = "file:///unknown/file.lua" })
189+
expect(success).to_be_false()
190+
expect(err).to_be_table()
191+
expect(err.code).to_be(-32001)
192+
expect(err.message).to_be("File not open in buffer")
193+
assert_contains(err.data, "File must be open in Neovim to retrieve diagnostics: /unknown/file.lua")
194+
195+
-- Should have checked for buffer but not called vim.diagnostic.get
196+
assert.spy(_G.vim.fn.bufnr).was_called_with("/unknown/file.lua")
197+
assert.spy(_G.vim.diagnostic.get).was_not_called()
198+
end)
199+
200+
it("should handle URI without file:// prefix", function()
201+
_G.vim.fn.bufnr = spy.new(function(filepath)
202+
if filepath == "/test/file.lua" then
203+
return 1
204+
end
205+
return -1
206+
end)
207+
_G.vim.diagnostic.get = spy.new(function()
208+
return {}
209+
end)
210+
211+
local success, result = pcall(get_diagnostics_handler, { uri = "/test/file.lua" })
212+
expect(success).to_be_true()
213+
214+
-- Should have used the path directly
215+
assert.spy(_G.vim.fn.bufnr).was_called_with("/test/file.lua")
216+
assert.spy(_G.vim.diagnostic.get).was_called_with(1)
217+
end)
123218
end)

0 commit comments

Comments
 (0)