summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorBarrett Ruth <62671086+barrettruth@users.noreply.github.com>2026-04-23 06:46:59 -0400
committerGitHub <noreply@github.com>2026-04-23 06:46:59 -0400
commitecb8402197f1883feec1c7a9f9d02a39912ae04a (patch)
treee041e062a43fa92efc1a91fc790b3554351213d7
parent19ef632decf7e5d3d9bb70f347c88b92c138b45e (diff)
fix(lsp): filter code_action diagnostics to the cursor #38988
Problem: Cursor-position `vim.lsp.buf.code_action()` requests include all diagnostics on the current line, so unrelated same-line diagnostics affect the returned actions. Solution: Filter same-line diagnostics to the cursor position for cursor-position requests.
-rw-r--r--runtime/lua/vim/lsp/buf.lua43
-rw-r--r--test/functional/plugin/lsp/buf_spec.lua103
2 files changed, 137 insertions, 9 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index ad98356ecf..926d82c986 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -1309,6 +1309,24 @@ local function on_code_action_results(results, opts)
vim.ui.select(actions, select_opts, on_user_choice)
end
+---@param diagnostic vim.Diagnostic
+---@param bufnr integer
+---@param lnum integer
+---@param col integer
+---@return boolean
+local function diagnostic_contains_cursor(diagnostic, bufnr, lnum, col)
+ local start = vim.pos(bufnr, diagnostic.lnum, diagnostic.col)
+ local finish =
+ vim.pos(bufnr, diagnostic.end_lnum or diagnostic.lnum, diagnostic.end_col or diagnostic.col)
+ local cursor = vim.pos(bufnr, lnum, col)
+
+ if start == finish then
+ return cursor == start
+ end
+
+ return start <= cursor and cursor < finish
+end
+
--- Selects a code action (LSP: "textDocument/codeAction" request) available at cursor position.
---
---@param opts? vim.lsp.buf.code_action.Opts
@@ -1330,6 +1348,13 @@ function M.code_action(opts)
local mode = api.nvim_get_mode().mode
local bufnr = api.nvim_get_current_buf()
local win = api.nvim_get_current_win()
+ local range = opts.range
+ if range == nil and (mode == 'v' or mode == 'V') then
+ range = range_from_selection(bufnr, mode)
+ end
+ local cursor = api.nvim_win_get_cursor(win)
+ local lnum = cursor[1] - 1
+ local col = cursor[2]
local clients = lsp.get_clients({ bufnr = bufnr, method = 'textDocument/codeAction' })
if not next(clients) then
vim.notify(lsp._unsupported_method('textDocument/codeAction'), vim.log.levels.WARN)
@@ -1340,15 +1365,11 @@ function M.code_action(opts)
---@type lsp.CodeActionParams
local params
- if opts.range then
- assert(type(opts.range) == 'table', 'code_action range must be a table')
- local start = assert(opts.range.start, 'range must have a `start` property')
- local end_ = assert(opts.range['end'], 'range must have a `end` property')
+ if range then
+ assert(type(range) == 'table', 'code_action range must be a table')
+ local start = assert(range.start, 'range must have a `start` property')
+ local end_ = assert(range['end'], 'range must have a `end` property')
params = util.make_given_range_params(start, end_, bufnr, client.offset_encoding)
- elseif mode == 'v' or mode == 'V' then
- local range = range_from_selection(bufnr, mode)
- params =
- util.make_given_range_params(range.start, range['end'], bufnr, client.offset_encoding)
else
params = util.make_range_params(win, client.offset_encoding)
end
@@ -1360,7 +1381,6 @@ function M.code_action(opts)
else
local ns_push = lsp.diagnostic.get_namespace(client.id)
local diagnostics = {}
- local lnum = api.nvim_win_get_cursor(0)[1] - 1
client:_provider_foreach('textDocument/diagnostic', function(cap)
local ns_pull = lsp.diagnostic.get_namespace(client.id, true, cap.identifier)
@@ -1371,6 +1391,11 @@ function M.code_action(opts)
end)
vim.list_extend(diagnostics, vim.diagnostic.get(bufnr, { namespace = ns_push, lnum = lnum }))
+ if range == nil then
+ diagnostics = vim.tbl_filter(function(diagnostic)
+ return diagnostic_contains_cursor(diagnostic, bufnr, lnum, col)
+ end, diagnostics)
+ end
params.context = vim.tbl_extend('force', context, {
---@diagnostic disable-next-line: no-unknown
diagnostics = vim.tbl_map(function(d)
diff --git a/test/functional/plugin/lsp/buf_spec.lua b/test/functional/plugin/lsp/buf_spec.lua
index a2afbe56f7..4f88108419 100644
--- a/test/functional/plugin/lsp/buf_spec.lua
+++ b/test/functional/plugin/lsp/buf_spec.lua
@@ -968,6 +968,109 @@ describe('vim.lsp.buf', function()
}
end)
+ it('uses diagnostics at cursor position', function()
+ exec_lua(create_server_definition)
+ local severity = exec_lua(function()
+ return vim.diagnostic.severity.ERROR
+ end)
+ local messages = exec_lua(function(severity_)
+ local server = _G._create_server({
+ capabilities = {
+ codeActionProvider = true,
+ },
+ handlers = {
+ ['textDocument/codeAction'] = function(_, _, callback)
+ callback(nil, {})
+ end,
+ },
+ })
+
+ local bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'local first, second = 1, 2' })
+
+ local client_id = assert(vim.lsp.start({
+ name = 'dummy',
+ cmd = server.cmd,
+ }))
+
+ local expected_messages = 2 -- initialize, initialized
+ local function wait_for_messages()
+ assert(
+ vim.wait(200, function()
+ return #server.messages == expected_messages
+ end),
+ 'Timed out waiting for expected number of messages. Current messages seen so far: '
+ .. vim.inspect(server.messages)
+ )
+ end
+
+ wait_for_messages()
+
+ vim.api.nvim_win_set_cursor(0, { 1, 15 })
+
+ local ns = vim.lsp.diagnostic.get_namespace(client_id)
+ vim.diagnostic.set(ns, bufnr, {
+ {
+ lnum = 0,
+ col = 6,
+ end_lnum = 0,
+ end_col = 11,
+ message = 'first',
+ severity = severity_,
+ user_data = {
+ lsp = {
+ range = {
+ start = { line = 0, character = 6 },
+ ['end'] = { line = 0, character = 11 },
+ },
+ message = 'first',
+ severity = severity_,
+ },
+ },
+ },
+ {
+ lnum = 0,
+ col = 13,
+ end_lnum = 0,
+ end_col = 19,
+ message = 'second',
+ severity = severity_,
+ user_data = {
+ lsp = {
+ range = {
+ start = { line = 0, character = 13 },
+ ['end'] = { line = 0, character = 19 },
+ },
+ message = 'second',
+ severity = severity_,
+ },
+ },
+ },
+ })
+
+ vim.lsp.buf.code_action()
+
+ expected_messages = expected_messages + 1
+ wait_for_messages()
+
+ vim.lsp.get_client_by_id(client_id):stop()
+
+ return server.messages
+ end, severity)
+
+ eq('textDocument/codeAction', messages[3].method)
+ eq({
+ {
+ range = {
+ start = { line = 0, character = 13 },
+ ['end'] = { line = 0, character = 19 },
+ },
+ message = 'second',
+ severity = severity,
+ },
+ }, messages[3].params.context.diagnostics)
+ end)
+
it('fallback to command execution on resolve error', function()
exec_lua(create_server_definition)
local result = exec_lua(function()