1local vim = vim 2local validate = vim.validate 3local vfn = vim.fn 4local util = require 'vim.lsp.util' 5 6local M = {} 7 8---@private 9--- Returns nil if {status} is false or nil, otherwise returns the rest of the 10--- arguments. 11local function ok_or_nil(status, ...) 12 if not status then return end 13 return ... 14end 15 16---@private 17--- Swallows errors. 18--- 19---@param fn Function to run 20---@param ... Function arguments 21---@returns Result of `fn(...)` if there are no errors, otherwise nil. 22--- Returns nil if errors occur during {fn}, otherwise returns 23local function npcall(fn, ...) 24 return ok_or_nil(pcall(fn, ...)) 25end 26 27---@private 28--- Sends an async request to all active clients attached to the current 29--- buffer. 30--- 31---@param method (string) LSP method name 32---@param params (optional, table) Parameters to send to the server 33---@param handler (optional, functionnil) See |lsp-handler|. Follows |lsp-handler-resolution| 34-- 35---@returns 2-tuple: 36--- - Map of client-id:request-id pairs for all successful requests. 37--- - Function which can be used to cancel all the requests. You could instead 38--- iterate all clients and call their `cancel_request()` methods. 39--- 40---@see |vim.lsp.buf_request()| 41local function request(method, params, handler) 42 validate { 43 method = {method, 's'}; 44 handler = {handler, 'f', true}; 45 } 46 return vim.lsp.buf_request(0, method, params, handler) 47end 48 49--- Checks whether the language servers attached to the current buffer are 50--- ready. 51--- 52---@returns `true` if server responds. 53function M.server_ready() 54 return not not vim.lsp.buf_notify(0, "window/progress", {}) 55end 56 57--- Displays hover information about the symbol under the cursor in a floating 58--- window. Calling the function twice will jump into the floating window. 59function M.hover() 60 local params = util.make_position_params() 61 request('textDocument/hover', params) 62end 63 64--- Jumps to the declaration of the symbol under the cursor. 65---@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. 66--- 67function M.declaration() 68 local params = util.make_position_params() 69 request('textDocument/declaration', params) 70end 71 72--- Jumps to the definition of the symbol under the cursor. 73--- 74function M.definition() 75 local params = util.make_position_params() 76 request('textDocument/definition', params) 77end 78 79--- Jumps to the definition of the type of the symbol under the cursor. 80--- 81function M.type_definition() 82 local params = util.make_position_params() 83 request('textDocument/typeDefinition', params) 84end 85 86--- Lists all the implementations for the symbol under the cursor in the 87--- quickfix window. 88function M.implementation() 89 local params = util.make_position_params() 90 request('textDocument/implementation', params) 91end 92 93--- Displays signature information about the symbol under the cursor in a 94--- floating window. 95function M.signature_help() 96 local params = util.make_position_params() 97 request('textDocument/signatureHelp', params) 98end 99 100--- Retrieves the completion items at the current cursor position. Can only be 101--- called in Insert mode. 102--- 103---@param context (context support not yet implemented) Additional information 104--- about the context in which a completion was triggered (how it was triggered, 105--- and by which trigger character, if applicable) 106--- 107---@see |vim.lsp.protocol.constants.CompletionTriggerKind| 108function M.completion(context) 109 local params = util.make_position_params() 110 params.context = context 111 return request('textDocument/completion', params) 112end 113 114---@private 115--- If there is more than one client that supports the given method, 116--- asks the user to select one. 117-- 118---@returns The client that the user selected or nil 119local function select_client(method, on_choice) 120 validate { 121 on_choice = { on_choice, 'function', false }, 122 } 123 local clients = vim.tbl_values(vim.lsp.buf_get_clients()) 124 clients = vim.tbl_filter(function(client) 125 return client.supports_method(method) 126 end, clients) 127 -- better UX when choices are always in the same order (between restarts) 128 table.sort(clients, function(a, b) 129 return a.name < b.name 130 end) 131 132 if #clients > 1 then 133 vim.ui.select(clients, { 134 prompt = 'Select a language server:', 135 format_item = function(client) 136 return client.name 137 end, 138 }, on_choice) 139 elseif #clients < 1 then 140 on_choice(nil) 141 else 142 on_choice(clients[1]) 143 end 144end 145 146--- Formats the current buffer. 147--- 148---@param options (optional, table) Can be used to specify FormattingOptions. 149--- Some unspecified options will be automatically derived from the current 150--- Neovim options. 151-- 152---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting 153function M.formatting(options) 154 local params = util.make_formatting_params(options) 155 local bufnr = vim.api.nvim_get_current_buf() 156 select_client('textDocument/formatting', function(client) 157 if client == nil then 158 return 159 end 160 161 return client.request('textDocument/formatting', params, nil, bufnr) 162 end) 163end 164 165--- Performs |vim.lsp.buf.formatting()| synchronously. 166--- 167--- Useful for running on save, to make sure buffer is formatted prior to being 168--- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example: 169--- 170--- <pre> 171--- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync() 172--- </pre> 173--- 174---@param options Table with valid `FormattingOptions` entries 175---@param timeout_ms (number) Request timeout 176---@see |vim.lsp.buf.formatting_seq_sync| 177function M.formatting_sync(options, timeout_ms) 178 local params = util.make_formatting_params(options) 179 local bufnr = vim.api.nvim_get_current_buf() 180 select_client('textDocument/formatting', function(client) 181 if client == nil then 182 return 183 end 184 185 local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr) 186 if result and result.result then 187 util.apply_text_edits(result.result, bufnr) 188 elseif err then 189 vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN) 190 end 191 end) 192end 193 194--- Formats the current buffer by sequentially requesting formatting from attached clients. 195--- 196--- Useful when multiple clients with formatting capability are attached. 197--- 198--- Since it's synchronous, can be used for running on save, to make sure buffer is formatted 199--- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method. 200--- Example: 201--- <pre> 202--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]] 203--- </pre> 204--- 205---@param options (optional, table) `FormattingOptions` entries 206---@param timeout_ms (optional, number) Request timeout 207---@param order (optional, table) List of client names. Formatting is requested from clients 208---in the following order: first all clients that are not in the `order` list, then 209---the remaining clients in the order as they occur in the `order` list. 210function M.formatting_seq_sync(options, timeout_ms, order) 211 local clients = vim.tbl_values(vim.lsp.buf_get_clients()); 212 local bufnr = vim.api.nvim_get_current_buf() 213 214 -- sort the clients according to `order` 215 for _, client_name in pairs(order or {}) do 216 -- if the client exists, move to the end of the list 217 for i, client in pairs(clients) do 218 if client.name == client_name then 219 table.insert(clients, table.remove(clients, i)) 220 break 221 end 222 end 223 end 224 225 -- loop through the clients and make synchronous formatting requests 226 for _, client in pairs(clients) do 227 if client.resolved_capabilities.document_formatting then 228 local params = util.make_formatting_params(options) 229 local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) 230 if result and result.result then 231 util.apply_text_edits(result.result, bufnr) 232 elseif err then 233 vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) 234 end 235 end 236 end 237end 238 239--- Formats a given range. 240--- 241---@param options Table with valid `FormattingOptions` entries. 242---@param start_pos ({number, number}, optional) mark-indexed position. 243---Defaults to the start of the last visual selection. 244---@param end_pos ({number, number}, optional) mark-indexed position. 245---Defaults to the end of the last visual selection. 246function M.range_formatting(options, start_pos, end_pos) 247 local params = util.make_given_range_params(start_pos, end_pos) 248 params.options = util.make_formatting_params(options).options 249 select_client('textDocument/rangeFormatting', function(client) 250 if client == nil then 251 return 252 end 253 254 return client.request('textDocument/rangeFormatting', params) 255 end) 256end 257 258--- Renames all references to the symbol under the cursor. 259--- 260---@param new_name (string) If not provided, the user will be prompted for a new 261---name using |vim.ui.input()|. 262function M.rename(new_name) 263 local opts = { 264 prompt = "New Name: " 265 } 266 267 ---@private 268 local function on_confirm(input) 269 if not (input and #input > 0) then return end 270 local params = util.make_position_params() 271 params.newName = input 272 request('textDocument/rename', params) 273 end 274 275 ---@private 276 local function prepare_rename(err, result) 277 if err == nil and result == nil then 278 vim.notify('nothing to rename', vim.log.levels.INFO) 279 return 280 end 281 if result and result.placeholder then 282 opts.default = result.placeholder 283 if not new_name then npcall(vim.ui.input, opts, on_confirm) end 284 elseif result and result.start and result['end'] and 285 result.start.line == result['end'].line then 286 local line = vfn.getline(result.start.line+1) 287 local start_char = result.start.character+1 288 local end_char = result['end'].character 289 opts.default = string.sub(line, start_char, end_char) 290 if not new_name then npcall(vim.ui.input, opts, on_confirm) end 291 else 292 -- fallback to guessing symbol using <cword> 293 -- 294 -- this can happen if the language server does not support prepareRename, 295 -- returns an unexpected response, or requests for "default behavior" 296 -- 297 -- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename 298 opts.default = vfn.expand('<cword>') 299 if not new_name then npcall(vim.ui.input, opts, on_confirm) end 300 end 301 if new_name then on_confirm(new_name) end 302 end 303 request('textDocument/prepareRename', util.make_position_params(), prepare_rename) 304end 305 306--- Lists all the references to the symbol under the cursor in the quickfix window. 307--- 308---@param context (table) Context for the request 309---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references 310function M.references(context) 311 validate { context = { context, 't', true } } 312 local params = util.make_position_params() 313 params.context = context or { 314 includeDeclaration = true; 315 } 316 request('textDocument/references', params) 317end 318 319--- Lists all symbols in the current buffer in the quickfix window. 320--- 321function M.document_symbol() 322 local params = { textDocument = util.make_text_document_params() } 323 request('textDocument/documentSymbol', params) 324end 325 326---@private 327local function pick_call_hierarchy_item(call_hierarchy_items) 328 if not call_hierarchy_items then return end 329 if #call_hierarchy_items == 1 then 330 return call_hierarchy_items[1] 331 end 332 local items = {} 333 for i, item in pairs(call_hierarchy_items) do 334 local entry = item.detail or item.name 335 table.insert(items, string.format("%d. %s", i, entry)) 336 end 337 local choice = vim.fn.inputlist(items) 338 if choice < 1 or choice > #items then 339 return 340 end 341 return choice 342end 343 344---@private 345local function call_hierarchy(method) 346 local params = util.make_position_params() 347 request('textDocument/prepareCallHierarchy', params, function(err, result, ctx) 348 if err then 349 vim.notify(err.message, vim.log.levels.WARN) 350 return 351 end 352 local call_hierarchy_item = pick_call_hierarchy_item(result) 353 local client = vim.lsp.get_client_by_id(ctx.client_id) 354 if client then 355 client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr) 356 else 357 vim.notify(string.format( 358 'Client with id=%d disappeared during call hierarchy request', ctx.client_id), 359 vim.log.levels.WARN 360 ) 361 end 362 end) 363end 364 365--- Lists all the call sites of the symbol under the cursor in the 366--- |quickfix| window. If the symbol can resolve to multiple 367--- items, the user can pick one in the |inputlist|. 368function M.incoming_calls() 369 call_hierarchy('callHierarchy/incomingCalls') 370end 371 372--- Lists all the items that are called by the symbol under the 373--- cursor in the |quickfix| window. If the symbol can resolve to 374--- multiple items, the user can pick one in the |inputlist|. 375function M.outgoing_calls() 376 call_hierarchy('callHierarchy/outgoingCalls') 377end 378 379--- List workspace folders. 380--- 381function M.list_workspace_folders() 382 local workspace_folders = {} 383 for _, client in pairs(vim.lsp.buf_get_clients()) do 384 for _, folder in pairs(client.workspace_folders or {}) do 385 table.insert(workspace_folders, folder.name) 386 end 387 end 388 return workspace_folders 389end 390 391--- Add the folder at path to the workspace folders. If {path} is 392--- not provided, the user will be prompted for a path using |input()|. 393function M.add_workspace_folder(workspace_folder) 394 workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'), 'dir') 395 vim.api.nvim_command("redraw") 396 if not (workspace_folder and #workspace_folder > 0) then return end 397 if vim.fn.isdirectory(workspace_folder) == 0 then 398 print(workspace_folder, " is not a valid directory") 399 return 400 end 401 local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}}) 402 for _, client in pairs(vim.lsp.buf_get_clients()) do 403 local found = false 404 for _, folder in pairs(client.workspace_folders or {}) do 405 if folder.name == workspace_folder then 406 found = true 407 print(workspace_folder, "is already part of this workspace") 408 break 409 end 410 end 411 if not found then 412 vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params) 413 if not client.workspace_folders then 414 client.workspace_folders = {} 415 end 416 table.insert(client.workspace_folders, params.event.added[1]) 417 end 418 end 419end 420 421--- Remove the folder at path from the workspace folders. If 422--- {path} is not provided, the user will be prompted for 423--- a path using |input()|. 424function M.remove_workspace_folder(workspace_folder) 425 workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h')) 426 vim.api.nvim_command("redraw") 427 if not (workspace_folder and #workspace_folder > 0) then return end 428 local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}) 429 for _, client in pairs(vim.lsp.buf_get_clients()) do 430 for idx, folder in pairs(client.workspace_folders) do 431 if folder.name == workspace_folder then 432 vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params) 433 client.workspace_folders[idx] = nil 434 return 435 end 436 end 437 end 438 print(workspace_folder, "is not currently part of the workspace") 439end 440 441--- Lists all symbols in the current workspace in the quickfix window. 442--- 443--- The list is filtered against {query}; if the argument is omitted from the 444--- call, the user is prompted to enter a string on the command line. An empty 445--- string means no filtering is done. 446--- 447---@param query (string, optional) 448function M.workspace_symbol(query) 449 query = query or npcall(vfn.input, "Query: ") 450 local params = {query = query} 451 request('workspace/symbol', params) 452end 453 454--- Send request to the server to resolve document highlights for the current 455--- text document position. This request can be triggered by a key mapping or 456--- by events such as `CursorHold`, eg: 457--- 458--- <pre> 459--- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight() 460--- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight() 461--- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references() 462--- </pre> 463--- 464--- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups 465--- to be defined or you won't be able to see the actual highlights. 466--- |LspReferenceText| 467--- |LspReferenceRead| 468--- |LspReferenceWrite| 469function M.document_highlight() 470 local params = util.make_position_params() 471 request('textDocument/documentHighlight', params) 472end 473 474--- Removes document highlights from current buffer. 475--- 476function M.clear_references() 477 util.buf_clear_references() 478end 479 480 481---@private 482-- 483--- This is not public because the main extension point is 484--- vim.ui.select which can be overridden independently. 485--- 486--- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects 487--- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results 488--- from multiple clients to have 1 single UI prompt for the user, yet we still 489--- need to be able to link a `CodeAction|Command` to the right client for 490--- `codeAction/resolve` 491local function on_code_action_results(results, ctx) 492 local action_tuples = {} 493 for client_id, result in pairs(results) do 494 for _, action in pairs(result.result or {}) do 495 table.insert(action_tuples, { client_id, action }) 496 end 497 end 498 if #action_tuples == 0 then 499 vim.notify('No code actions available', vim.log.levels.INFO) 500 return 501 end 502 503 ---@private 504 local function apply_action(action, client) 505 if action.edit then 506 util.apply_workspace_edit(action.edit) 507 end 508 if action.command then 509 local command = type(action.command) == 'table' and action.command or action 510 local fn = client.commands[command.command] or vim.lsp.commands[command.command] 511 if fn then 512 local enriched_ctx = vim.deepcopy(ctx) 513 enriched_ctx.client_id = client.id 514 fn(command, enriched_ctx) 515 else 516 M.execute_command(command) 517 end 518 end 519 end 520 521 ---@private 522 local function on_user_choice(action_tuple) 523 if not action_tuple then 524 return 525 end 526 -- textDocument/codeAction can return either Command[] or CodeAction[] 527 -- 528 -- CodeAction 529 -- ... 530 -- edit?: WorkspaceEdit -- <- must be applied before command 531 -- command?: Command 532 -- 533 -- Command: 534 -- title: string 535 -- command: string 536 -- arguments?: any[] 537 -- 538 local client = vim.lsp.get_client_by_id(action_tuple[1]) 539 local action = action_tuple[2] 540 if not action.edit 541 and client 542 and type(client.resolved_capabilities.code_action) == 'table' 543 and client.resolved_capabilities.code_action.resolveProvider then 544 545 client.request('codeAction/resolve', action, function(err, resolved_action) 546 if err then 547 vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR) 548 return 549 end 550 apply_action(resolved_action, client) 551 end) 552 else 553 apply_action(action, client) 554 end 555 end 556 557 vim.ui.select(action_tuples, { 558 prompt = 'Code actions:', 559 kind = 'codeaction', 560 format_item = function(action_tuple) 561 local title = action_tuple[2].title:gsub('\r\n', '\\r\\n') 562 return title:gsub('\n', '\\n') 563 end, 564 }, on_user_choice) 565end 566 567 568--- Requests code actions from all clients and calls the handler exactly once 569--- with all aggregated results 570---@private 571local function code_action_request(params) 572 local bufnr = vim.api.nvim_get_current_buf() 573 local method = 'textDocument/codeAction' 574 vim.lsp.buf_request_all(bufnr, method, params, function(results) 575 on_code_action_results(results, { bufnr = bufnr, method = method, params = params }) 576 end) 577end 578 579--- Selects a code action available at the current 580--- cursor position. 581--- 582---@param context table|nil `CodeActionContext` of the LSP specification: 583--- - diagnostics: (table|nil) 584--- LSP `Diagnostic[]`. Inferred from the current 585--- position if not provided. 586--- - only: (string|nil) 587--- LSP `CodeActionKind` used to filter the code actions. 588--- Most language servers support values like `refactor` 589--- or `quickfix`. 590---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction 591function M.code_action(context) 592 validate { context = { context, 't', true } } 593 context = context or {} 594 if not context.diagnostics then 595 context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics() 596 end 597 local params = util.make_range_params() 598 params.context = context 599 code_action_request(params) 600end 601 602--- Performs |vim.lsp.buf.code_action()| for a given range. 603--- 604--- 605---@param context table|nil `CodeActionContext` of the LSP specification: 606--- - diagnostics: (table|nil) 607--- LSP `Diagnostic[]`. Inferred from the current 608--- position if not provided. 609--- - only: (string|nil) 610--- LSP `CodeActionKind` used to filter the code actions. 611--- Most language servers support values like `refactor` 612--- or `quickfix`. 613---@param start_pos ({number, number}, optional) mark-indexed position. 614---Defaults to the start of the last visual selection. 615---@param end_pos ({number, number}, optional) mark-indexed position. 616---Defaults to the end of the last visual selection. 617function M.range_code_action(context, start_pos, end_pos) 618 validate { context = { context, 't', true } } 619 context = context or {} 620 if not context.diagnostics then 621 context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics() 622 end 623 local params = util.make_given_range_params(start_pos, end_pos) 624 params.context = context 625 code_action_request(params) 626end 627 628--- Executes an LSP server command. 629--- 630---@param command A valid `ExecuteCommandParams` object 631---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand 632function M.execute_command(command) 633 validate { 634 command = { command.command, 's' }, 635 arguments = { command.arguments, 't', true } 636 } 637 request('workspace/executeCommand', command) 638end 639 640return M 641-- vim:sw=2 ts=2 et 642