From 9bbb31dd8882b8f78cb718983a0372395ac63fe8 Mon Sep 17 00:00:00 2001 From: Kevin McBride <681929+krmcbride@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:58:15 -0700 Subject: [PATCH 1/5] feat: complete implementation of getDiagnostics tool --- lua/claudecode/tools/get_diagnostics.lua | 90 ++++++++++++--- tests/unit/tools/get_diagnostics_spec.lua | 131 +++++++++++++++++++--- 2 files changed, 190 insertions(+), 31 deletions(-) diff --git a/lua/claudecode/tools/get_diagnostics.lua b/lua/claudecode/tools/get_diagnostics.lua index afa6fdc..abfd050 100644 --- a/lua/claudecode/tools/get_diagnostics.lua +++ b/lua/claudecode/tools/get_diagnostics.lua @@ -1,13 +1,28 @@ --- Tool implementation for getting diagnostics. +local schema = { + description = "Get Neovim LSP diagnostics (errors, warnings) from open buffers", + inputSchema = { + type = "object", + properties = { + uri = { + type = "string", + description = "Optional file URI to get diagnostics for. If not provided, gets diagnostics for all open files.", + }, + }, + additionalProperties = false, + ["$schema"] = "http://json-schema.org/draft-07/schema#", + }, +} + --- Handles the getDiagnostics tool invocation. -- Retrieves diagnostics from Neovim's diagnostic system. --- @param _params table The input parameters for the tool (currently unused). +-- @param params table The input parameters for the tool. +-- @field params.uri string|nil Optional file URI to get diagnostics for. -- @return table A table containing the list of diagnostics. -- @error table A table with code, message, and data for JSON-RPC error if failed. -local function handler(_params) -- Prefix unused params with underscore +local function handler(params) if not vim.lsp or not vim.diagnostic or not vim.diagnostic.get then - -- This tool is internal, so returning an error might be too strong. -- Returning an empty list or a specific status could be an alternative. -- For now, let's align with the error pattern for consistency if the feature is unavailable. error({ @@ -17,29 +32,78 @@ local function handler(_params) -- Prefix unused params with underscore }) end - local all_diagnostics = vim.diagnostic.get(0) -- Get for all buffers + local log_module_ok, log = pcall(require, "claudecode.logger") + if not log_module_ok then + return { + code = -32603, -- Internal error + message = "Internal error", + data = "Failed to load logger module", + } + end + + log.debug("getDiagnostics handler called with params: " .. vim.inspect(params)) + + -- Extract the uri parameter + local diagnostics + + if not params.uri then + -- Get diagnostics for all buffers + log.debug("Getting diagnostics for all open buffers") + diagnostics = vim.diagnostic.get() + else + -- Remove file:// prefix if present + local uri = params.uri + local filepath = uri + if uri:sub(1, 7) == "file://" then + filepath = uri:sub(8) -- Remove "file://" prefix + end + + -- Get buffer number for the specific file + local bufnr = vim.fn.bufnr(filepath) + if bufnr == -1 then + -- File is not open in any buffer, throw an error + log.debug("File buffer must be open to get diagnostics: " .. filepath) + error({ + code = -32001, + message = "File not open in buffer", + data = "File must be open in Neovim to retrieve diagnostics: " .. filepath, + }) + else + -- Get diagnostics for the specific buffer + log.debug("Getting diagnostics for bufnr: " .. bufnr) + diagnostics = vim.diagnostic.get(bufnr) + end + end local formatted_diagnostics = {} - for _, diagnostic in ipairs(all_diagnostics) do + for _, diagnostic in ipairs(diagnostics) do local file_path = vim.api.nvim_buf_get_name(diagnostic.bufnr) -- Ensure we only include diagnostics with valid file paths if file_path and file_path ~= "" then table.insert(formatted_diagnostics, { - file = file_path, - line = diagnostic.lnum, -- 0-indexed from vim.diagnostic.get - character = diagnostic.col, -- 0-indexed from vim.diagnostic.get - severity = diagnostic.severity, -- e.g., vim.diagnostic.severity.ERROR - message = diagnostic.message, - source = diagnostic.source, + type = "text", + -- json encode this + text = vim.fn.json_encode({ + -- Use the file path and diagnostic information + filePath = file_path, + -- Convert line and column to 1-indexed + line = diagnostic.lnum + 1, + character = diagnostic.col + 1, + severity = diagnostic.severity, -- e.g., vim.diagnostic.severity.ERROR + message = diagnostic.message, + source = diagnostic.source, + }), }) end end - return { diagnostics = formatted_diagnostics } + return { + content = formatted_diagnostics, + } end return { name = "getDiagnostics", - schema = nil, -- Internal tool + schema = schema, handler = handler, } diff --git a/tests/unit/tools/get_diagnostics_spec.lua b/tests/unit/tools/get_diagnostics_spec.lua index 5472690..7d61ff6 100644 --- a/tests/unit/tools/get_diagnostics_spec.lua +++ b/tests/unit/tools/get_diagnostics_spec.lua @@ -5,12 +5,23 @@ describe("Tool: get_diagnostics", function() before_each(function() package.loaded["claudecode.tools.get_diagnostics"] = nil + package.loaded["claudecode.logger"] = nil + + -- Mock the logger module + package.loaded["claudecode.logger"] = { + debug = function() end, + error = function() end, + info = function() end, + warn = function() end, + } + get_diagnostics_handler = require("claudecode.tools.get_diagnostics").handler _G.vim = _G.vim or {} _G.vim.lsp = _G.vim.lsp or {} -- Ensure vim.lsp exists for the check _G.vim.diagnostic = _G.vim.diagnostic or {} _G.vim.api = _G.vim.api or {} + _G.vim.fn = _G.vim.fn or {} -- Default mocks _G.vim.diagnostic.get = spy.new(function() @@ -19,12 +30,25 @@ describe("Tool: get_diagnostics", function() _G.vim.api.nvim_buf_get_name = spy.new(function(bufnr) return "/path/to/file_for_buf_" .. tostring(bufnr) .. ".lua" end) + _G.vim.fn.json_encode = spy.new(function(obj) + return vim.inspect(obj) -- Use vim.inspect as a simple serialization + end) + _G.vim.fn.bufnr = spy.new(function(filepath) + -- Mock buffer lookup + if filepath == "/test/file.lua" then + return 1 + end + return -1 -- File not open + end) end) after_each(function() package.loaded["claudecode.tools.get_diagnostics"] = nil + package.loaded["claudecode.logger"] = nil _G.vim.diagnostic.get = nil _G.vim.api.nvim_buf_get_name = nil + _G.vim.fn.json_encode = nil + _G.vim.fn.bufnr = nil -- Note: We don't nullify _G.vim.lsp or _G.vim.diagnostic entirely -- as they are checked for existence. end) @@ -33,14 +57,14 @@ describe("Tool: get_diagnostics", function() local success, result = pcall(get_diagnostics_handler, {}) expect(success).to_be_true() expect(result).to_be_table() - expect(result.diagnostics).to_be_table() - expect(#result.diagnostics).to_be(0) - assert.spy(_G.vim.diagnostic.get).was_called_with(0) + expect(result.content).to_be_table() + expect(#result.content).to_be(0) + assert.spy(_G.vim.diagnostic.get).was_called_with() end) it("should return formatted diagnostics if available", function() local mock_diagnostics = { - { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, + { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, { bufnr = 2, lnum = 20, col = 15, severity = 2, message = "Warning message 2", source = "linter2" }, } _G.vim.diagnostic.get = spy.new(function() @@ -49,19 +73,24 @@ describe("Tool: get_diagnostics", function() local success, result = pcall(get_diagnostics_handler, {}) expect(success).to_be_true() - expect(result.diagnostics).to_be_table() - expect(#result.diagnostics).to_be(2) + expect(result.content).to_be_table() + expect(#result.content).to_be(2) - expect(result.diagnostics[1].file).to_be("/path/to/file_for_buf_1.lua") - expect(result.diagnostics[1].line).to_be(10) - expect(result.diagnostics[1].character).to_be(5) - expect(result.diagnostics[1].severity).to_be(1) - expect(result.diagnostics[1].message).to_be("Error message 1") - expect(result.diagnostics[1].source).to_be("linter1") + -- Check that results are MCP content items + expect(result.content[1].type).to_be("text") + expect(result.content[2].type).to_be("text") - expect(result.diagnostics[2].file).to_be("/path/to/file_for_buf_2.lua") - expect(result.diagnostics[2].severity).to_be(2) - expect(result.diagnostics[2].message).to_be("Warning message 2") + -- Verify JSON encoding was called with correct structure + assert.spy(_G.vim.fn.json_encode).was_called(2) + + -- Check the first diagnostic was encoded with 1-indexed values + local first_call_args = _G.vim.fn.json_encode.calls[1].vals[1] + expect(first_call_args.filePath).to_be("/path/to/file_for_buf_1.lua") + expect(first_call_args.line).to_be(11) -- 10 + 1 for 1-indexing + expect(first_call_args.character).to_be(6) -- 5 + 1 for 1-indexing + expect(first_call_args.severity).to_be(1) + expect(first_call_args.message).to_be("Error message 1") + expect(first_call_args.source).to_be("linter1") assert.spy(_G.vim.api.nvim_buf_get_name).was_called_with(1) assert.spy(_G.vim.api.nvim_buf_get_name).was_called_with(2) @@ -69,7 +98,7 @@ describe("Tool: get_diagnostics", function() it("should filter out diagnostics with no file path", function() local mock_diagnostics = { - { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, + { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, { bufnr = 99, lnum = 20, col = 15, severity = 2, message = "Warning message 2", source = "linter2" }, -- This one will have no path } _G.vim.diagnostic.get = spy.new(function() @@ -87,8 +116,12 @@ describe("Tool: get_diagnostics", function() local success, result = pcall(get_diagnostics_handler, {}) expect(success).to_be_true() - expect(#result.diagnostics).to_be(1) - expect(result.diagnostics[1].file).to_be("/path/to/file1.lua") + expect(#result.content).to_be(1) + + -- Verify only the diagnostic with a file path was included + assert.spy(_G.vim.fn.json_encode).was_called(1) + local encoded_args = _G.vim.fn.json_encode.calls[1].vals[1] + expect(encoded_args.filePath).to_be("/path/to/file1.lua") end) it("should error if vim.diagnostic.get is not available", function() @@ -120,4 +153,66 @@ describe("Tool: get_diagnostics", function() expect(success).to_be_false() expect(err.code).to_be(-32000) end) + + it("should filter diagnostics by URI when provided", function() + local mock_diagnostics = { + { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error in file1", source = "linter1" }, + } + _G.vim.diagnostic.get = spy.new(function(bufnr) + if bufnr == 1 then + return mock_diagnostics + end + return {} + end) + _G.vim.api.nvim_buf_get_name = spy.new(function(bufnr) + if bufnr == 1 then + return "/test/file.lua" + end + return "" + end) + + local success, result = pcall(get_diagnostics_handler, { uri = "file:///test/file.lua" }) + expect(success).to_be_true() + expect(#result.content).to_be(1) + + -- Should have called vim.diagnostic.get with specific buffer + assert.spy(_G.vim.diagnostic.get).was_called_with(1) + assert.spy(_G.vim.fn.bufnr).was_called_with("/test/file.lua") + end) + + it("should error for URI of unopened file", function() + _G.vim.fn.bufnr = spy.new(function() + return -1 -- File not open + end) + + local success, err = pcall(get_diagnostics_handler, { uri = "file:///unknown/file.lua" }) + expect(success).to_be_false() + expect(err).to_be_table() + expect(err.code).to_be(-32001) + expect(err.message).to_be("File not open in buffer") + assert_contains(err.data, "File must be open in Neovim to retrieve diagnostics: /unknown/file.lua") + + -- Should have checked for buffer but not called vim.diagnostic.get + assert.spy(_G.vim.fn.bufnr).was_called_with("/unknown/file.lua") + assert.spy(_G.vim.diagnostic.get).was_not_called() + end) + + it("should handle URI without file:// prefix", function() + _G.vim.fn.bufnr = spy.new(function(filepath) + if filepath == "/test/file.lua" then + return 1 + end + return -1 + end) + _G.vim.diagnostic.get = spy.new(function() + return {} + end) + + local success, result = pcall(get_diagnostics_handler, { uri = "/test/file.lua" }) + expect(success).to_be_true() + + -- Should have used the path directly + assert.spy(_G.vim.fn.bufnr).was_called_with("/test/file.lua") + assert.spy(_G.vim.diagnostic.get).was_called_with(1) + end) end) From c9cbe553b5d248c3d9458d8e37daf1159b5e9af0 Mon Sep 17 00:00:00 2001 From: Kevin McBride <681929+krmcbride@users.noreply.github.com> Date: Fri, 13 Jun 2025 17:06:48 -0700 Subject: [PATCH 2/5] refactor(getDiagnostics): remove pcall from logger import --- lua/claudecode/tools/get_diagnostics.lua | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lua/claudecode/tools/get_diagnostics.lua b/lua/claudecode/tools/get_diagnostics.lua index abfd050..b8a02f4 100644 --- a/lua/claudecode/tools/get_diagnostics.lua +++ b/lua/claudecode/tools/get_diagnostics.lua @@ -32,24 +32,17 @@ local function handler(params) }) end - local log_module_ok, log = pcall(require, "claudecode.logger") - if not log_module_ok then - return { - code = -32603, -- Internal error - message = "Internal error", - data = "Failed to load logger module", - } - end + local logger = require("claudecode.logger") - log.debug("getDiagnostics handler called with params: " .. vim.inspect(params)) + logger.debug("getDiagnostics handler called with params: " .. vim.inspect(params)) -- Extract the uri parameter local diagnostics if not params.uri then -- Get diagnostics for all buffers - log.debug("Getting diagnostics for all open buffers") diagnostics = vim.diagnostic.get() + logger.debug("Getting diagnostics for all open buffers") else -- Remove file:// prefix if present local uri = params.uri @@ -62,7 +55,7 @@ local function handler(params) local bufnr = vim.fn.bufnr(filepath) if bufnr == -1 then -- File is not open in any buffer, throw an error - log.debug("File buffer must be open to get diagnostics: " .. filepath) + logger.debug("File buffer must be open to get diagnostics: " .. filepath) error({ code = -32001, message = "File not open in buffer", @@ -70,7 +63,7 @@ local function handler(params) }) else -- Get diagnostics for the specific buffer - log.debug("Getting diagnostics for bufnr: " .. bufnr) + logger.debug("Getting diagnostics for bufnr: " .. bufnr) diagnostics = vim.diagnostic.get(bufnr) end end From 5cab3d353da082cb0d6bf333e01f9c73f80efee5 Mon Sep 17 00:00:00 2001 From: Kevin McBride <681929+krmcbride@users.noreply.github.com> Date: Fri, 13 Jun 2025 17:09:14 -0700 Subject: [PATCH 3/5] refactor(getDiagnostics): use alternate json encode We're also passing nil explicitly to get diagnostics from all buffers --- lua/claudecode/tools/get_diagnostics.lua | 4 ++-- tests/unit/tools/get_diagnostics_spec.lua | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lua/claudecode/tools/get_diagnostics.lua b/lua/claudecode/tools/get_diagnostics.lua index b8a02f4..06e28df 100644 --- a/lua/claudecode/tools/get_diagnostics.lua +++ b/lua/claudecode/tools/get_diagnostics.lua @@ -41,8 +41,8 @@ local function handler(params) if not params.uri then -- Get diagnostics for all buffers - diagnostics = vim.diagnostic.get() logger.debug("Getting diagnostics for all open buffers") + diagnostics = vim.diagnostic.get(nil) else -- Remove file:// prefix if present local uri = params.uri @@ -76,7 +76,7 @@ local function handler(params) table.insert(formatted_diagnostics, { type = "text", -- json encode this - text = vim.fn.json_encode({ + text = vim.json.encode({ -- Use the file path and diagnostic information filePath = file_path, -- Convert line and column to 1-indexed diff --git a/tests/unit/tools/get_diagnostics_spec.lua b/tests/unit/tools/get_diagnostics_spec.lua index 7d61ff6..488d07e 100644 --- a/tests/unit/tools/get_diagnostics_spec.lua +++ b/tests/unit/tools/get_diagnostics_spec.lua @@ -30,7 +30,7 @@ describe("Tool: get_diagnostics", function() _G.vim.api.nvim_buf_get_name = spy.new(function(bufnr) return "/path/to/file_for_buf_" .. tostring(bufnr) .. ".lua" end) - _G.vim.fn.json_encode = spy.new(function(obj) + _G.vim.json.encode = spy.new(function(obj) return vim.inspect(obj) -- Use vim.inspect as a simple serialization end) _G.vim.fn.bufnr = spy.new(function(filepath) @@ -47,7 +47,7 @@ describe("Tool: get_diagnostics", function() package.loaded["claudecode.logger"] = nil _G.vim.diagnostic.get = nil _G.vim.api.nvim_buf_get_name = nil - _G.vim.fn.json_encode = nil + _G.vim.json.encode = nil _G.vim.fn.bufnr = nil -- Note: We don't nullify _G.vim.lsp or _G.vim.diagnostic entirely -- as they are checked for existence. @@ -59,7 +59,7 @@ describe("Tool: get_diagnostics", function() expect(result).to_be_table() expect(result.content).to_be_table() expect(#result.content).to_be(0) - assert.spy(_G.vim.diagnostic.get).was_called_with() + assert.spy(_G.vim.diagnostic.get).was_called_with(nil) end) it("should return formatted diagnostics if available", function() @@ -81,10 +81,10 @@ describe("Tool: get_diagnostics", function() expect(result.content[2].type).to_be("text") -- Verify JSON encoding was called with correct structure - assert.spy(_G.vim.fn.json_encode).was_called(2) + assert.spy(_G.vim.json.encode).was_called(2) -- Check the first diagnostic was encoded with 1-indexed values - local first_call_args = _G.vim.fn.json_encode.calls[1].vals[1] + local first_call_args = _G.vim.json.encode.calls[1].vals[1] expect(first_call_args.filePath).to_be("/path/to/file_for_buf_1.lua") expect(first_call_args.line).to_be(11) -- 10 + 1 for 1-indexing expect(first_call_args.character).to_be(6) -- 5 + 1 for 1-indexing @@ -119,8 +119,8 @@ describe("Tool: get_diagnostics", function() expect(#result.content).to_be(1) -- Verify only the diagnostic with a file path was included - assert.spy(_G.vim.fn.json_encode).was_called(1) - local encoded_args = _G.vim.fn.json_encode.calls[1].vals[1] + assert.spy(_G.vim.json.encode).was_called(1) + local encoded_args = _G.vim.json.encode.calls[1].vals[1] expect(encoded_args.filePath).to_be("/path/to/file1.lua") end) From 548fd1abfd0756d7a92718a4bc50598be536fbbc Mon Sep 17 00:00:00 2001 From: Kevin McBride <681929+krmcbride@users.noreply.github.com> Date: Fri, 13 Jun 2025 17:10:32 -0700 Subject: [PATCH 4/5] refactor(getDiagnostics): tweak file scheme handling, assume URI input --- lua/claudecode/tools/get_diagnostics.lua | 7 ++--- tests/unit/tools/get_diagnostics_spec.lua | 34 +++++++++-------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/lua/claudecode/tools/get_diagnostics.lua b/lua/claudecode/tools/get_diagnostics.lua index 06e28df..6dabf40 100644 --- a/lua/claudecode/tools/get_diagnostics.lua +++ b/lua/claudecode/tools/get_diagnostics.lua @@ -44,12 +44,9 @@ local function handler(params) logger.debug("Getting diagnostics for all open buffers") diagnostics = vim.diagnostic.get(nil) else - -- Remove file:// prefix if present local uri = params.uri - local filepath = uri - if uri:sub(1, 7) == "file://" then - filepath = uri:sub(8) -- Remove "file://" prefix - end + -- Strips the file:// scheme + local filepath = vim.uri_to_fname(uri) -- Get buffer number for the specific file local bufnr = vim.fn.bufnr(filepath) diff --git a/tests/unit/tools/get_diagnostics_spec.lua b/tests/unit/tools/get_diagnostics_spec.lua index 488d07e..6fa9df6 100644 --- a/tests/unit/tools/get_diagnostics_spec.lua +++ b/tests/unit/tools/get_diagnostics_spec.lua @@ -40,6 +40,14 @@ describe("Tool: get_diagnostics", function() end return -1 -- File not open end) + _G.vim.uri_to_fname = spy.new(function(uri) + -- Realistic mock that matches vim.uri_to_fname behavior + if uri:sub(1, 7) == "file://" then + return uri:sub(8) + end + -- Real vim.uri_to_fname throws an error for URIs without proper scheme + error("URI must contain a scheme: " .. uri) + end) end) after_each(function() @@ -49,6 +57,7 @@ describe("Tool: get_diagnostics", function() _G.vim.api.nvim_buf_get_name = nil _G.vim.json.encode = nil _G.vim.fn.bufnr = nil + _G.vim.uri_to_fname = nil -- Note: We don't nullify _G.vim.lsp or _G.vim.diagnostic entirely -- as they are checked for existence. end) @@ -175,7 +184,8 @@ describe("Tool: get_diagnostics", function() expect(success).to_be_true() expect(#result.content).to_be(1) - -- Should have called vim.diagnostic.get with specific buffer + -- Should have used vim.uri_to_fname to convert URI to file path + assert.spy(_G.vim.uri_to_fname).was_called_with("file:///test/file.lua") assert.spy(_G.vim.diagnostic.get).was_called_with(1) assert.spy(_G.vim.fn.bufnr).was_called_with("/test/file.lua") end) @@ -192,27 +202,9 @@ describe("Tool: get_diagnostics", function() expect(err.message).to_be("File not open in buffer") assert_contains(err.data, "File must be open in Neovim to retrieve diagnostics: /unknown/file.lua") - -- Should have checked for buffer but not called vim.diagnostic.get + -- Should have used vim.uri_to_fname and checked for buffer but not called vim.diagnostic.get + assert.spy(_G.vim.uri_to_fname).was_called_with("file:///unknown/file.lua") assert.spy(_G.vim.fn.bufnr).was_called_with("/unknown/file.lua") assert.spy(_G.vim.diagnostic.get).was_not_called() end) - - it("should handle URI without file:// prefix", function() - _G.vim.fn.bufnr = spy.new(function(filepath) - if filepath == "/test/file.lua" then - return 1 - end - return -1 - end) - _G.vim.diagnostic.get = spy.new(function() - return {} - end) - - local success, result = pcall(get_diagnostics_handler, { uri = "/test/file.lua" }) - expect(success).to_be_true() - - -- Should have used the path directly - assert.spy(_G.vim.fn.bufnr).was_called_with("/test/file.lua") - assert.spy(_G.vim.diagnostic.get).was_called_with(1) - end) end) From 56842aaa87f79340a56c050640edd0d82aeb4b62 Mon Sep 17 00:00:00 2001 From: Kevin McBride <681929+krmcbride@users.noreply.github.com> Date: Fri, 13 Jun 2025 18:10:25 -0700 Subject: [PATCH 5/5] fix(getDiagnostics): remove Neovim context from description --- lua/claudecode/tools/get_diagnostics.lua | 12 ++++++++---- tests/unit/tools/get_diagnostics_spec.lua | 12 ++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lua/claudecode/tools/get_diagnostics.lua b/lua/claudecode/tools/get_diagnostics.lua index 6dabf40..387612c 100644 --- a/lua/claudecode/tools/get_diagnostics.lua +++ b/lua/claudecode/tools/get_diagnostics.lua @@ -1,7 +1,11 @@ --- Tool implementation for getting diagnostics. +-- NOTE: Its important we don't tip off Claude that we're dealing with Neovim LSP diagnostics because it may adjust +-- line and col numbers by 1 on its own (since it knows nvim LSP diagnostics are 0-indexed). By calling these +-- "editor diagnostics" and converting to 1-indexed ourselves we (hopefully) avoid incorrect line and column numbers +-- in Claude's responses. local schema = { - description = "Get Neovim LSP diagnostics (errors, warnings) from open buffers", + description = "Get language diagnostics (errors, warnings) from the editor", inputSchema = { type = "object", properties = { @@ -28,7 +32,7 @@ local function handler(params) error({ code = -32000, message = "Feature unavailable", - data = "LSP or vim.diagnostic.get not available in this Neovim version/configuration.", + data = "Diagnostics not available in this editor version/configuration.", }) end @@ -55,8 +59,8 @@ local function handler(params) logger.debug("File buffer must be open to get diagnostics: " .. filepath) error({ code = -32001, - message = "File not open in buffer", - data = "File must be open in Neovim to retrieve diagnostics: " .. filepath, + message = "File not open", + data = "File must be open to retrieve diagnostics: " .. filepath, }) else -- Get diagnostics for the specific buffer diff --git a/tests/unit/tools/get_diagnostics_spec.lua b/tests/unit/tools/get_diagnostics_spec.lua index 6fa9df6..a927d90 100644 --- a/tests/unit/tools/get_diagnostics_spec.lua +++ b/tests/unit/tools/get_diagnostics_spec.lua @@ -73,7 +73,7 @@ describe("Tool: get_diagnostics", function() it("should return formatted diagnostics if available", function() local mock_diagnostics = { - { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, + { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, { bufnr = 2, lnum = 20, col = 15, severity = 2, message = "Warning message 2", source = "linter2" }, } _G.vim.diagnostic.get = spy.new(function() @@ -95,7 +95,7 @@ describe("Tool: get_diagnostics", function() -- Check the first diagnostic was encoded with 1-indexed values local first_call_args = _G.vim.json.encode.calls[1].vals[1] expect(first_call_args.filePath).to_be("/path/to/file_for_buf_1.lua") - expect(first_call_args.line).to_be(11) -- 10 + 1 for 1-indexing + expect(first_call_args.line).to_be(11) -- 10 + 1 for 1-indexing expect(first_call_args.character).to_be(6) -- 5 + 1 for 1-indexing expect(first_call_args.severity).to_be(1) expect(first_call_args.message).to_be("Error message 1") @@ -107,7 +107,7 @@ describe("Tool: get_diagnostics", function() it("should filter out diagnostics with no file path", function() local mock_diagnostics = { - { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, + { bufnr = 1, lnum = 10, col = 5, severity = 1, message = "Error message 1", source = "linter1" }, { bufnr = 99, lnum = 20, col = 15, severity = 2, message = "Warning message 2", source = "linter2" }, -- This one will have no path } _G.vim.diagnostic.get = spy.new(function() @@ -140,7 +140,7 @@ describe("Tool: get_diagnostics", function() expect(err).to_be_table() expect(err.code).to_be(-32000) assert_contains(err.message, "Feature unavailable") - assert_contains(err.data, "LSP or vim.diagnostic.get not available") + assert_contains(err.data, "Diagnostics not available in this editor version/configuration.") end) it("should error if vim.diagnostic is not available", function() @@ -199,8 +199,8 @@ describe("Tool: get_diagnostics", function() expect(success).to_be_false() expect(err).to_be_table() expect(err.code).to_be(-32001) - expect(err.message).to_be("File not open in buffer") - assert_contains(err.data, "File must be open in Neovim to retrieve diagnostics: /unknown/file.lua") + expect(err.message).to_be("File not open") + assert_contains(err.data, "File must be open to retrieve diagnostics: /unknown/file.lua") -- Should have used vim.uri_to_fname and checked for buffer but not called vim.diagnostic.get assert.spy(_G.vim.uri_to_fname).was_called_with("file:///unknown/file.lua")