mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	lsp: Add support for call hierarchies (#12556)
* LSP: Add support for call hierarchies * LSP: Add support for call hierarchies * LSP: Add support for call hierarchies * LSP: Jump to call location Jump to the call site instead of jumping to the definition of the caller/callee. * LSP: add tests for the call hierarchy callbacks * Fix linting error Co-authored-by: Cédric Barreteau <>
This commit is contained in:
		| @@ -837,6 +837,16 @@ workspace_symbol({query})                     *vim.lsp.buf.workspace_symbol()* | ||||
|                 enter a string on the command line. An empty string means no | ||||
|                 filtering is done. | ||||
|  | ||||
| incoming_calls()                               *vim.lsp.buf.incoming_calls()* | ||||
|                 Lists all the call sites of the symbol under the cursor in the | ||||
|                 |quickfix| window. If the symbol can resolve to multiple | ||||
|                 items, the user can pick one in the |inputlist|. | ||||
|  | ||||
| outgoing_calls()                               *vim.lsp.buf.outgoing_calls()* | ||||
|                 Lists all the items that are called by the symbol under the | ||||
|                 cursor in the |quickfix| window. If the symbol can resolve to | ||||
|                 multiple items, the user can pick one in the |inputlist|. | ||||
|  | ||||
|  | ||||
| ============================================================================== | ||||
| Lua module: vim.lsp.callbacks                                  *lsp-callbacks* | ||||
|   | ||||
| @@ -511,6 +511,7 @@ function lsp.start_client(config) | ||||
|       or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition') | ||||
|       or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') | ||||
|       or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') | ||||
|       or (not client.resolved_capabilities.call_hierarchy and method == 'textDocument/prepareCallHierarchy') | ||||
|     then | ||||
|       callback(unsupported_method(method), method, nil, client_id, bufnr) | ||||
|       return | ||||
|   | ||||
| @@ -143,6 +143,38 @@ function M.document_symbol() | ||||
|   request('textDocument/documentSymbol', params) | ||||
| end | ||||
|  | ||||
| local function pick_call_hierarchy_item(call_hierarchy_items) | ||||
|   if not call_hierarchy_items then return end | ||||
|   if #call_hierarchy_items == 1 then | ||||
|     return call_hierarchy_items[1] | ||||
|   end | ||||
|   local items = {} | ||||
|   for i, item in ipairs(call_hierarchy_items) do | ||||
|     local entry = item.detail or item.name | ||||
|     table.insert(items, string.format("%d. %s", i, entry)) | ||||
|   end | ||||
|   local choice = vim.fn.inputlist(items) | ||||
|   if choice < 1 or choice > #items then | ||||
|     return | ||||
|   end | ||||
|   return choice | ||||
| end | ||||
|  | ||||
| function M.incoming_calls() | ||||
|   local params = util.make_position_params() | ||||
|   request('textDocument/prepareCallHierarchy', params, function(_, _, result) | ||||
|     local call_hierarchy_item = pick_call_hierarchy_item(result) | ||||
|     vim.lsp.buf_request(0, 'callHierarchy/incomingCalls', { item = call_hierarchy_item }) | ||||
|   end) | ||||
| end | ||||
|  | ||||
| function M.outgoing_calls() | ||||
|   local params = util.make_position_params() | ||||
|   request('textDocument/prepareCallHierarchy', params, function(_, _, result) | ||||
|     local call_hierarchy_item = pick_call_hierarchy_item(result) | ||||
|     vim.lsp.buf_request(0, 'callHierarchy/outgoingCalls', { item = call_hierarchy_item }) | ||||
|   end) | ||||
| end | ||||
|  | ||||
| --- Lists all symbols in the current workspace in the quickfix window. | ||||
| --- | ||||
|   | ||||
| @@ -214,6 +214,33 @@ M['textDocument/documentHighlight'] = function(_, _, result, _) | ||||
|   util.buf_highlight_references(bufnr, result) | ||||
| end | ||||
|  | ||||
| -- direction is "from" for incoming calls and "to" for outgoing calls | ||||
| local make_call_hierarchy_callback = function(direction) | ||||
|   -- result is a CallHierarchy{Incoming,Outgoing}Call[] | ||||
|   return function(_, _, result) | ||||
|     if not result then return end | ||||
|     local items = {} | ||||
|     for _, call_hierarchy_call in pairs(result) do | ||||
|       local call_hierarchy_item = call_hierarchy_call[direction] | ||||
|       for _, range in pairs(call_hierarchy_call.fromRanges) do | ||||
|         table.insert(items, { | ||||
|           filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)), | ||||
|           text = call_hierarchy_item.name, | ||||
|           lnum = range.start.line + 1, | ||||
|           col = range.start.character + 1, | ||||
|         }) | ||||
|       end | ||||
|     end | ||||
|     util.set_qflist(items) | ||||
|     api.nvim_command("copen") | ||||
|     api.nvim_command("wincmd p") | ||||
|   end | ||||
| end | ||||
|  | ||||
| M['callHierarchy/incomingCalls'] = make_call_hierarchy_callback('from') | ||||
|  | ||||
| M['callHierarchy/outgoingCalls'] = make_call_hierarchy_callback('to') | ||||
|  | ||||
| M['window/logMessage'] = function(_, _, result, client_id) | ||||
|   local message_type = result.type | ||||
|   local message = result.message | ||||
|   | ||||
| @@ -713,6 +713,9 @@ function protocol.make_client_capabilities() | ||||
|       }; | ||||
|       applyEdit = true; | ||||
|     }; | ||||
|     callHierarchy = { | ||||
|       dynamicRegistration = false; | ||||
|     }; | ||||
|     experimental = nil; | ||||
|   } | ||||
| end | ||||
| @@ -912,6 +915,7 @@ function protocol.resolve_capabilities(server_capabilities) | ||||
|   general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false | ||||
|   general_properties.document_formatting = server_capabilities.documentFormattingProvider or false | ||||
|   general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false | ||||
|   general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false | ||||
|  | ||||
|   if server_capabilities.codeActionProvider == nil then | ||||
|     general_properties.code_action = false | ||||
|   | ||||
| @@ -1497,4 +1497,147 @@ describe('LSP', function() | ||||
|     it('with softtabstop = 0', function() test_tabstop(2, 0) end) | ||||
|     it('with softtabstop = -1', function() test_tabstop(3, -1) end) | ||||
|   end) | ||||
|  | ||||
|   describe('vim.lsp.buf.outgoing_calls', function() | ||||
|     it('does nothing for an empty response', function() | ||||
|       local qflist_count = exec_lua([=[ | ||||
|         require'vim.lsp.callbacks'['callHierarchy/outgoingCalls']() | ||||
|         return #vim.fn.getqflist() | ||||
|       ]=]) | ||||
|       eq(0, qflist_count) | ||||
|     end) | ||||
|  | ||||
|     it('opens the quickfix list with the right caller', function() | ||||
|       local qflist = exec_lua([=[ | ||||
|         local rust_analyzer_response = { { | ||||
|           fromRanges = { { | ||||
|             ['end'] = { | ||||
|               character = 7, | ||||
|               line = 3 | ||||
|             }, | ||||
|             start = { | ||||
|               character = 4, | ||||
|               line = 3 | ||||
|             } | ||||
|           } }, | ||||
|           to = { | ||||
|             detail = "fn foo()", | ||||
|             kind = 12, | ||||
|             name = "foo", | ||||
|             range = { | ||||
|               ['end'] = { | ||||
|                 character = 11, | ||||
|                 line = 0 | ||||
|               }, | ||||
|               start = { | ||||
|                 character = 0, | ||||
|                 line = 0 | ||||
|               } | ||||
|             }, | ||||
|             selectionRange = { | ||||
|               ['end'] = { | ||||
|                 character = 6, | ||||
|                 line = 0 | ||||
|               }, | ||||
|               start = { | ||||
|               character = 3, | ||||
|               line = 0 | ||||
|               } | ||||
|             }, | ||||
|             uri = "file:///src/main.rs" | ||||
|           } | ||||
|         } } | ||||
|         local callback = require'vim.lsp.callbacks'['callHierarchy/outgoingCalls'] | ||||
|         callback(nil, nil, rust_analyzer_response) | ||||
|         return vim.fn.getqflist() | ||||
|       ]=]) | ||||
|  | ||||
|       local expected = { { | ||||
|         bufnr = 2, | ||||
|         col = 5, | ||||
|         lnum = 4, | ||||
|         module = "", | ||||
|         nr = 0, | ||||
|         pattern = "", | ||||
|         text = "foo", | ||||
|         type = "", | ||||
|         valid = 1, | ||||
|         vcol = 0 | ||||
|       } } | ||||
|  | ||||
|       eq(expected, qflist) | ||||
|     end) | ||||
|   end) | ||||
|  | ||||
|   describe('vim.lsp.buf.incoming_calls', function() | ||||
|     it('does nothing for an empty response', function() | ||||
|       local qflist_count = exec_lua([=[ | ||||
|         require'vim.lsp.callbacks'['callHierarchy/incomingCalls']() | ||||
|         return #vim.fn.getqflist() | ||||
|       ]=]) | ||||
|       eq(0, qflist_count) | ||||
|     end) | ||||
|  | ||||
|     it('opens the quickfix list with the right callee', function() | ||||
|       local qflist = exec_lua([=[ | ||||
|         local rust_analyzer_response = { { | ||||
|           from = { | ||||
|             detail = "fn main()", | ||||
|             kind = 12, | ||||
|             name = "main", | ||||
|             range = { | ||||
|               ['end'] = { | ||||
|                 character = 1, | ||||
|                 line = 4 | ||||
|               }, | ||||
|               start = { | ||||
|                 character = 0, | ||||
|                 line = 2 | ||||
|               } | ||||
|             }, | ||||
|             selectionRange = { | ||||
|               ['end'] = { | ||||
|                 character = 7, | ||||
|                 line = 2 | ||||
|               }, | ||||
|               start = { | ||||
|                 character = 3, | ||||
|                 line = 2 | ||||
|               } | ||||
|             }, | ||||
|             uri = "file:///src/main.rs" | ||||
|           }, | ||||
|           fromRanges = { { | ||||
|             ['end'] = { | ||||
|               character = 7, | ||||
|               line = 3 | ||||
|             }, | ||||
|             start = { | ||||
|               character = 4, | ||||
|               line = 3 | ||||
|             } | ||||
|           } } | ||||
|         } } | ||||
|  | ||||
|         local callback = require'vim.lsp.callbacks'['callHierarchy/incomingCalls'] | ||||
|         callback(nil, nil, rust_analyzer_response) | ||||
|         return vim.fn.getqflist() | ||||
|       ]=]) | ||||
|  | ||||
|       local expected = { { | ||||
|         bufnr = 2, | ||||
|         col = 5, | ||||
|         lnum = 4, | ||||
|         module = "", | ||||
|         nr = 0, | ||||
|         pattern = "", | ||||
|         text = "main", | ||||
|         type = "", | ||||
|         valid = 1, | ||||
|         vcol = 0 | ||||
|       } } | ||||
|  | ||||
|       eq(expected, qflist) | ||||
|     end) | ||||
|   end) | ||||
| end) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 cbarrete
					cbarrete