From f487e5af019c7cd0f15ab9beb522c9358e8013e2 Mon Sep 17 00:00:00 2001 From: Jongwook Choi Date: Sat, 3 Feb 2024 17:47:56 -0500 Subject: fix(lsp): fix infinite loop on vim.lsp.tagfunc Problem: vim.lsp.tagfunc() causes an infinite loop. This is a bug happened while introducing deferred loading. Solution: Rename the private module to `vim.lsp._tagfunc`. --- runtime/lua/vim/lsp.lua | 4 +- runtime/lua/vim/lsp/_tagfunc.lua | 83 ++++++++++++++++++++++++++++++++++++++++ runtime/lua/vim/lsp/tagfunc.lua | 83 ---------------------------------------- 3 files changed, 85 insertions(+), 85 deletions(-) create mode 100644 runtime/lua/vim/lsp/_tagfunc.lua delete mode 100644 runtime/lua/vim/lsp/tagfunc.lua diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 5fa5a1db29..d8d47a8464 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -12,6 +12,7 @@ local lsp = vim._defer_require('vim.lsp', { _completion = ..., --- @module 'vim.lsp._completion' _dynamic = ..., --- @module 'vim.lsp._dynamic' _snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar' + _tagfunc = ..., --- @module 'vim.lsp._tagfunc' _watchfiles = ..., --- @module 'vim.lsp._watchfiles' buf = ..., --- @module 'vim.lsp.buf' codelens = ..., --- @module 'vim.lsp.codelens' @@ -22,7 +23,6 @@ local lsp = vim._defer_require('vim.lsp', { protocol = ..., --- @module 'vim.lsp.protocol' rpc = ..., --- @module 'vim.lsp.rpc' semantic_tokens = ..., --- @module 'vim.lsp.semantic_tokens' - tagfunc = ..., --- @module 'vim.lsp.tagfunc' util = ..., --- @module 'vim.lsp.util' }) @@ -2040,7 +2040,7 @@ end --- ---@return table[] tags A list of matching tags function lsp.tagfunc(pattern, flags) - return vim.lsp.tagfunc(pattern, flags) + return vim.lsp._tagfunc(pattern, flags) end ---Checks whether a client is stopped. diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua new file mode 100644 index 0000000000..4ad50e4a58 --- /dev/null +++ b/runtime/lua/vim/lsp/_tagfunc.lua @@ -0,0 +1,83 @@ +local lsp = vim.lsp +local util = lsp.util +local ms = lsp.protocol.Methods + +---@param name string +---@param range lsp.Range +---@param uri string +---@param offset_encoding string +---@return {name: string, filename: string, cmd: string, kind?: string} +local function mk_tag_item(name, range, uri, offset_encoding) + local bufnr = vim.uri_to_bufnr(uri) + -- This is get_line_byte_from_position is 0-indexed, call cursor expects a 1-indexed position + local byte = util._get_line_byte_from_position(bufnr, range.start, offset_encoding) + 1 + return { + name = name, + filename = vim.uri_to_fname(uri), + cmd = string.format([[/\%%%dl\%%%dc/]], range.start.line + 1, byte), + } +end + +---@param pattern string +---@return table[] +local function query_definition(pattern) + local params = util.make_position_params() + local results_by_client, err = lsp.buf_request_sync(0, ms.textDocument_definition, params, 1000) + if err then + return {} + end + local results = {} + local add = function(range, uri, offset_encoding) + table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding)) + end + for client_id, lsp_results in pairs(assert(results_by_client)) do + local client = lsp.get_client_by_id(client_id) + local offset_encoding = client and client.offset_encoding or 'utf-16' + local result = lsp_results.result or {} + if result.range then -- Location + add(result.range, result.uri) + else + result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]] + for _, item in pairs(result) do + if item.range then -- Location + add(item.range, item.uri, offset_encoding) + else -- LocationLink + add(item.targetSelectionRange, item.targetUri, offset_encoding) + end + end + end + end + return results +end + +---@param pattern string +---@return table[] +local function query_workspace_symbols(pattern) + local results_by_client, err = + lsp.buf_request_sync(0, ms.workspace_symbol, { query = pattern }, 1000) + if err then + return {} + end + local results = {} + for client_id, responses in pairs(assert(results_by_client)) do + local client = lsp.get_client_by_id(client_id) + local offset_encoding = client and client.offset_encoding or 'utf-16' + local symbols = responses.result --[[@as lsp.SymbolInformation[]|nil]] + for _, symbol in pairs(symbols or {}) do + local loc = symbol.location + local item = mk_tag_item(symbol.name, loc.range, loc.uri, offset_encoding) + item.kind = lsp.protocol.SymbolKind[symbol.kind] or 'Unknown' + table.insert(results, item) + end + end + return results +end + +local function tagfunc(pattern, flags) + local matches = string.match(flags, 'c') and query_definition(pattern) + or query_workspace_symbols(pattern) + -- fall back to tags if no matches + return #matches > 0 and matches or vim.NIL +end + +return tagfunc diff --git a/runtime/lua/vim/lsp/tagfunc.lua b/runtime/lua/vim/lsp/tagfunc.lua deleted file mode 100644 index 4ad50e4a58..0000000000 --- a/runtime/lua/vim/lsp/tagfunc.lua +++ /dev/null @@ -1,83 +0,0 @@ -local lsp = vim.lsp -local util = lsp.util -local ms = lsp.protocol.Methods - ----@param name string ----@param range lsp.Range ----@param uri string ----@param offset_encoding string ----@return {name: string, filename: string, cmd: string, kind?: string} -local function mk_tag_item(name, range, uri, offset_encoding) - local bufnr = vim.uri_to_bufnr(uri) - -- This is get_line_byte_from_position is 0-indexed, call cursor expects a 1-indexed position - local byte = util._get_line_byte_from_position(bufnr, range.start, offset_encoding) + 1 - return { - name = name, - filename = vim.uri_to_fname(uri), - cmd = string.format([[/\%%%dl\%%%dc/]], range.start.line + 1, byte), - } -end - ----@param pattern string ----@return table[] -local function query_definition(pattern) - local params = util.make_position_params() - local results_by_client, err = lsp.buf_request_sync(0, ms.textDocument_definition, params, 1000) - if err then - return {} - end - local results = {} - local add = function(range, uri, offset_encoding) - table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding)) - end - for client_id, lsp_results in pairs(assert(results_by_client)) do - local client = lsp.get_client_by_id(client_id) - local offset_encoding = client and client.offset_encoding or 'utf-16' - local result = lsp_results.result or {} - if result.range then -- Location - add(result.range, result.uri) - else - result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]] - for _, item in pairs(result) do - if item.range then -- Location - add(item.range, item.uri, offset_encoding) - else -- LocationLink - add(item.targetSelectionRange, item.targetUri, offset_encoding) - end - end - end - end - return results -end - ----@param pattern string ----@return table[] -local function query_workspace_symbols(pattern) - local results_by_client, err = - lsp.buf_request_sync(0, ms.workspace_symbol, { query = pattern }, 1000) - if err then - return {} - end - local results = {} - for client_id, responses in pairs(assert(results_by_client)) do - local client = lsp.get_client_by_id(client_id) - local offset_encoding = client and client.offset_encoding or 'utf-16' - local symbols = responses.result --[[@as lsp.SymbolInformation[]|nil]] - for _, symbol in pairs(symbols or {}) do - local loc = symbol.location - local item = mk_tag_item(symbol.name, loc.range, loc.uri, offset_encoding) - item.kind = lsp.protocol.SymbolKind[symbol.kind] or 'Unknown' - table.insert(results, item) - end - end - return results -end - -local function tagfunc(pattern, flags) - local matches = string.match(flags, 'c') and query_definition(pattern) - or query_workspace_symbols(pattern) - -- fall back to tags if no matches - return #matches > 0 and matches or vim.NIL -end - -return tagfunc -- cgit From b92b9be85d0a2e2e237e065b497b0ece95f7e6b4 Mon Sep 17 00:00:00 2001 From: Jongwook Choi Date: Sat, 3 Feb 2024 17:49:28 -0500 Subject: test(lsp): add test cases for vim.lsp.tagfunc Problem: There is no test case for vim.lsp.tagfunc; so CI was unable to catch the infinite loop bug (#27325). Solution: Add test cases for vim.lsp.tagfunc(). --- test/functional/plugin/lsp_spec.lua | 99 +++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index b602143443..ce76861b9a 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -4013,6 +4013,105 @@ describe('LSP', function() check_notify('both', true, true) end) end) + + describe('vim.lsp.tagfunc', function() + before_each(function() + clear() + ---@type lsp.Location[] + local mock_locations = { + { + range = { + ['start'] = { line = 5, character = 23 }, + ['end'] = { line = 10, character = 0 }, + }, + uri = 'test://buf', + }, + { + range = { + ['start'] = { line = 42, character = 10 }, + ['end'] = { line = 44, character = 0 }, + }, + uri = 'test://another-file', + }, + } + exec_lua(create_server_definition) + exec_lua( + [[ + _G.mock_locations = ... + _G.server = _create_server({ + ---@type lsp.ServerCapabilities + capabilities = { + definitionProvider = true, + workspaceSymbolProvider = true, + }, + handlers = { + ---@return lsp.Location[] + ['textDocument/definition'] = function() + return { _G.mock_locations[1] } + end, + ---@return lsp.WorkspaceSymbol[] + ['workspace/symbol'] = function(_, request) + assert(request.query == 'foobar') + return { + { + name = 'foobar', + kind = 13, ---@type lsp.SymbolKind + location = _G.mock_locations[1], + }, + { + name = 'vim.foobar', + kind = 12, ---@type lsp.SymbolKind + location = _G.mock_locations[2], + } + } + end, + }, + }) + _G.client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) + ]], + mock_locations + ) + end) + after_each(function() + exec_lua [[ + vim.lsp.stop_client(_G.client_id) + ]] + end) + + it('with flags=c, returns matching tags using textDocument/definition', function() + local result = exec_lua [[ + return vim.lsp.tagfunc('foobar', 'c') + ]] + eq({ + { + cmd = '/\\%6l\\%1c/', -- for location (5, 23) + filename = 'test://buf', + name = 'foobar', + }, + }, result) + end) + + it('without flags=c, returns all matching tags using workspace/symbol', function() + local result = exec_lua [[ + return vim.lsp.tagfunc('foobar', '') + ]] + eq({ + { + cmd = '/\\%6l\\%1c/', -- for location (5, 23) + filename = 'test://buf', + kind = 'Variable', + name = 'foobar', + }, + { + cmd = '/\\%43l\\%1c/', -- for location (42, 10) + filename = 'test://another-file', + kind = 'Function', + name = 'vim.foobar', + }, + }, result) + end) + end) + describe('cmd', function() it('can connect to lsp server via rpc.connect', function() local result = exec_lua [[ -- cgit