From c4d3a3d3632c2779d3de7707954ce183e07c8841 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 23 Apr 2026 06:46:59 -0400 Subject: [PATCH] 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. (cherry picked from commit ecb8402197f1883feec1c7a9f9d02a39912ae04a) --- runtime/lua/vim/lsp/buf.lua | 43 +++++++--- test/functional/plugin/lsp/buf_spec.lua | 103 ++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 9 deletions(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index fb0b99f6d9..9fae0be48d 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1342,6 +1342,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 @@ -1363,6 +1381,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) @@ -1373,15 +1398,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 @@ -1393,7 +1414,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) @@ -1404,6 +1424,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 a49110bee0..b363fca614 100644 --- a/test/functional/plugin/lsp/buf_spec.lua +++ b/test/functional/plugin/lsp/buf_spec.lua @@ -969,6 +969,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()