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.
This commit is contained in:
Barrett Ruth
2026-04-23 06:46:59 -04:00
committed by GitHub
parent 19ef632dec
commit ecb8402197
2 changed files with 137 additions and 9 deletions

View File

@@ -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)