buf.lua (51759B)
1 --- @brief 2 --- The `vim.lsp.buf_…` functions perform operations for LSP clients attached to the current buffer. 3 4 local api = vim.api 5 local lsp = vim.lsp 6 local validate = vim.validate 7 local util = require('vim.lsp.util') 8 local npcall = vim.F.npcall 9 10 local M = {} 11 12 --- @param params? table 13 --- @return fun(client: vim.lsp.Client): lsp.TextDocumentPositionParams 14 local function client_positional_params(params) 15 local win = api.nvim_get_current_win() 16 return function(client) 17 local ret = util.make_position_params(win, client.offset_encoding) 18 if params then 19 ret = vim.tbl_extend('force', ret, params) 20 end 21 return ret 22 end 23 end 24 25 local hover_ns = api.nvim_create_namespace('nvim.lsp.hover_range') 26 local rename_ns = api.nvim_create_namespace('nvim.lsp.rename_range') 27 28 --- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts 29 --- @field silent? boolean 30 31 --- Displays hover information about the symbol under the cursor in a floating 32 --- window. The window will be dismissed on cursor move. 33 --- Calling the function twice will jump into the floating window 34 --- (thus by default, "KK" will open the hover window and focus it). 35 --- In the floating window, all commands and mappings are available as usual, 36 --- except that "q" dismisses the window. 37 --- You can scroll the contents the same as you would any other buffer. 38 --- 39 --- Note: to disable hover highlights, add the following to your config: 40 --- 41 --- ```lua 42 --- vim.api.nvim_create_autocmd('ColorScheme', { 43 --- callback = function() 44 --- vim.api.nvim_set_hl(0, 'LspReferenceTarget', {}) 45 --- end, 46 --- }) 47 --- ``` 48 --- @param config? vim.lsp.buf.hover.Opts 49 function M.hover(config) 50 validate('config', config, 'table', true) 51 52 config = config or {} 53 config.focus_id = 'textDocument/hover' 54 55 lsp.buf_request_all(0, 'textDocument/hover', client_positional_params(), function(results, ctx) 56 local bufnr = assert(ctx.bufnr) 57 if api.nvim_get_current_buf() ~= bufnr then 58 -- Ignore result since buffer changed. This happens for slow language servers. 59 return 60 end 61 62 -- Filter errors from results 63 local results1 = {} --- @type table<integer,lsp.Hover> 64 local empty_response = false 65 66 for client_id, resp in pairs(results) do 67 local err, result = resp.err, resp.result 68 if err then 69 lsp.log.error(err.code, err.message) 70 elseif result and result.contents then 71 -- Make sure the response is not empty 72 -- Five response shapes: 73 -- - MarkupContent: { kind="markdown", value="doc" } 74 -- - MarkedString-string: "doc" 75 -- - MarkedString-pair: { language="c", value="doc" } 76 -- - MarkedString[]-string: { "doc1", ... } 77 -- - MarkedString[]-pair: { { language="c", value="doc1" }, ... } 78 if 79 ( 80 type(result.contents) == 'table' 81 and #( 82 vim.tbl_get(result.contents, 'value') -- MarkupContent or MarkedString-pair 83 or vim.tbl_get(result.contents, 1, 'value') -- MarkedString[]-pair 84 or result.contents[1] -- MarkedString[]-string 85 or '' 86 ) 87 > 0 88 ) 89 or ( 90 type(result.contents) == 'string' and #result.contents > 0 -- MarkedString-string 91 ) 92 then 93 results1[client_id] = result 94 else 95 empty_response = true 96 end 97 end 98 end 99 100 if vim.tbl_isempty(results1) then 101 if config.silent ~= true then 102 if empty_response then 103 vim.notify('Empty hover response', vim.log.levels.INFO) 104 else 105 vim.notify('No information available', vim.log.levels.INFO) 106 end 107 end 108 return 109 end 110 111 local contents = {} --- @type string[] 112 113 local nresults = #vim.tbl_keys(results1) 114 115 local format = 'markdown' 116 117 for client_id, result in pairs(results1) do 118 local client = assert(lsp.get_client_by_id(client_id)) 119 if nresults > 1 then 120 -- Show client name if there are multiple clients 121 contents[#contents + 1] = string.format('# %s', client.name) 122 end 123 if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then 124 if #results1 == 1 then 125 format = 'plaintext' 126 contents = vim.split(result.contents.value or '', '\n', { trimempty = true }) 127 else 128 -- Surround plaintext with ``` to get correct formatting 129 contents[#contents + 1] = '```' 130 vim.list_extend( 131 contents, 132 vim.split(result.contents.value or '', '\n', { trimempty = true }) 133 ) 134 contents[#contents + 1] = '```' 135 end 136 else 137 vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) 138 end 139 local range = result.range 140 if range then 141 local start = range.start 142 local end_ = range['end'] 143 local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) 144 local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) 145 146 vim.hl.range( 147 bufnr, 148 hover_ns, 149 'LspReferenceTarget', 150 { start.line, start_idx }, 151 { end_.line, end_idx }, 152 { priority = vim.hl.priorities.user } 153 ) 154 end 155 contents[#contents + 1] = '---' 156 end 157 158 -- Remove last linebreak ('---') 159 contents[#contents] = nil 160 161 local _, winid = lsp.util.open_floating_preview(contents, format, config) 162 163 api.nvim_create_autocmd('WinClosed', { 164 pattern = tostring(winid), 165 once = true, 166 callback = function() 167 api.nvim_buf_clear_namespace(bufnr, hover_ns, 0, -1) 168 return true 169 end, 170 }) 171 end) 172 end 173 174 local function request_with_opts(name, params, opts) 175 local req_handler --- @type function? 176 if opts then 177 req_handler = function(err, result, ctx, config) 178 local client = assert(lsp.get_client_by_id(ctx.client_id)) 179 local handler = client.handlers[name] or lsp.handlers[name] 180 handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts)) 181 end 182 end 183 lsp.buf_request(0, name, params, req_handler) 184 end 185 186 ---@param method vim.lsp.protocol.Method.ClientToServer.Request 187 ---@param opts? vim.lsp.LocationOpts 188 local function get_locations(method, opts) 189 opts = opts or {} 190 local bufnr = api.nvim_get_current_buf() 191 local win = api.nvim_get_current_win() 192 193 local clients = lsp.get_clients({ method = method, bufnr = bufnr }) 194 if not next(clients) then 195 vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) 196 return 197 end 198 199 local from = vim.fn.getpos('.') 200 from[1] = bufnr 201 local tagname = vim.fn.expand('<cword>') 202 203 lsp.buf_request_all(bufnr, method, function(client) 204 return util.make_position_params(win, client.offset_encoding) 205 end, function(results) 206 ---@type vim.quickfix.entry[] 207 local all_items = {} 208 209 for client_id, res in pairs(results) do 210 local client = assert(lsp.get_client_by_id(client_id)) 211 local locations = {} 212 if res then 213 locations = vim.islist(res.result) and res.result or { res.result } 214 end 215 local items = util.locations_to_items(locations, client.offset_encoding) 216 vim.list_extend(all_items, items) 217 end 218 219 if vim.tbl_isempty(all_items) then 220 vim.notify('No locations found', vim.log.levels.INFO) 221 return 222 end 223 224 local title = 'LSP locations' 225 if opts.on_list then 226 assert(vim.is_callable(opts.on_list), 'on_list is not a function') 227 opts.on_list({ 228 title = title, 229 items = all_items, 230 context = { bufnr = bufnr, method = method }, 231 }) 232 return 233 end 234 235 if #all_items == 1 then 236 local item = all_items[1] 237 local b = item.bufnr or vim.fn.bufadd(item.filename) 238 239 -- Save position in jumplist 240 vim.cmd("normal! m'") 241 -- Push a new item into tagstack 242 local tagstack = { { tagname = tagname, from = from } } 243 vim.fn.settagstack(vim.fn.win_getid(win), { items = tagstack }, 't') 244 245 vim.bo[b].buflisted = true 246 local w = win 247 if opts.reuse_win and api.nvim_win_get_buf(w) ~= b then 248 w = vim.fn.bufwinid(b) 249 w = w >= 0 and w or vim.fn.win_findbuf(b)[1] or win 250 if w ~= win then 251 api.nvim_set_current_win(w) 252 end 253 end 254 api.nvim_win_set_buf(w, b) 255 api.nvim_win_set_cursor(w, { item.lnum, item.col - 1 }) 256 vim._with({ win = w }, function() 257 -- Open folds under the cursor 258 vim.cmd('normal! zv') 259 end) 260 return 261 end 262 if opts.loclist then 263 vim.fn.setloclist(0, {}, ' ', { title = title, items = all_items }) 264 vim.cmd.lopen() 265 else 266 vim.fn.setqflist({}, ' ', { title = title, items = all_items }) 267 vim.cmd('botright copen') 268 end 269 end) 270 end 271 272 --- @class vim.lsp.ListOpts 273 --- 274 --- list-handler replacing the default handler. 275 --- Called for any non-empty result. 276 --- This table can be used with |setqflist()| or |setloclist()|. E.g.: 277 --- ```lua 278 --- local function on_list(options) 279 --- vim.fn.setqflist({}, ' ', options) 280 --- vim.cmd.cfirst() 281 --- end 282 --- 283 --- vim.lsp.buf.definition({ on_list = on_list }) 284 --- vim.lsp.buf.references(nil, { on_list = on_list }) 285 --- ``` 286 --- @field on_list? fun(t: vim.lsp.LocationOpts.OnList) 287 --- 288 --- Whether to use the |location-list| or the |quickfix| list in the default handler. 289 --- ```lua 290 --- vim.lsp.buf.definition({ loclist = true }) 291 --- vim.lsp.buf.references(nil, { loclist = false }) 292 --- ``` 293 --- @field loclist? boolean 294 295 --- @class vim.lsp.LocationOpts.OnList 296 --- @field items vim.quickfix.entry[] See |setqflist-what| 297 --- @field title? string Title for the list. 298 --- @field context? { bufnr: integer, method: string } Subset of `ctx` from |lsp-handler|. 299 300 --- @class vim.lsp.LocationOpts: vim.lsp.ListOpts 301 --- 302 --- Jump to existing window if buffer is already open. 303 --- @field reuse_win? boolean 304 305 --- Jumps to the declaration of the symbol under the cursor. 306 --- @note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. 307 --- @param opts? vim.lsp.LocationOpts 308 function M.declaration(opts) 309 validate('opts', opts, 'table', true) 310 get_locations('textDocument/declaration', opts) 311 end 312 313 --- Jumps to the definition of the symbol under the cursor. 314 --- @param opts? vim.lsp.LocationOpts 315 function M.definition(opts) 316 validate('opts', opts, 'table', true) 317 get_locations('textDocument/definition', opts) 318 end 319 320 --- Jumps to the definition of the type of the symbol under the cursor. 321 --- @param opts? vim.lsp.LocationOpts 322 function M.type_definition(opts) 323 validate('opts', opts, 'table', true) 324 get_locations('textDocument/typeDefinition', opts) 325 end 326 327 --- Lists all the implementations for the symbol under the cursor in the 328 --- quickfix window. 329 --- @param opts? vim.lsp.LocationOpts 330 function M.implementation(opts) 331 validate('opts', opts, 'table', true) 332 get_locations('textDocument/implementation', opts) 333 end 334 335 --- @param results table<integer,{err: lsp.ResponseError?, result: lsp.SignatureHelp?}> 336 local function process_signature_help_results(results) 337 local signatures = {} --- @type [vim.lsp.Client,lsp.SignatureInformation][] 338 local active_signature = 1 339 340 -- Pre-process results 341 for client_id, r in pairs(results) do 342 local err = r.err 343 local client = assert(lsp.get_client_by_id(client_id)) 344 if err then 345 vim.notify( 346 client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, 347 vim.log.levels.ERROR 348 ) 349 api.nvim_command('redraw') 350 else 351 local result = r.result 352 if result and result.signatures then 353 for i, sig in ipairs(result.signatures) do 354 sig.activeParameter = sig.activeParameter or result.activeParameter 355 local idx = #signatures + 1 356 if (result.activeSignature or 0) + 1 == i then 357 active_signature = idx 358 end 359 signatures[idx] = { client, sig } 360 end 361 end 362 end 363 end 364 365 return signatures, active_signature 366 end 367 368 local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help') 369 370 --- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts 371 --- @field silent? boolean 372 373 --- Displays signature information about the symbol under the cursor in a 374 --- floating window. Allows cycling through signature overloads with `<C-s>`, 375 --- which can be remapped via `<Plug>(nvim.lsp.ctrl-s)` 376 --- 377 --- Example: 378 --- 379 --- ```lua 380 --- vim.keymap.set('n', '<C-b>', '<Plug>(nvim.lsp.ctrl-s)') 381 --- ``` 382 --- 383 --- @param config? vim.lsp.buf.signature_help.Opts 384 function M.signature_help(config) 385 validate('config', config, 'table', true) 386 387 local method = 'textDocument/signatureHelp' 388 389 config = config and vim.deepcopy(config) or {} 390 config.focus_id = method 391 local user_title = config.title 392 393 lsp.buf_request_all(0, method, client_positional_params(), function(results, ctx) 394 if api.nvim_get_current_buf() ~= ctx.bufnr then 395 -- Ignore result since buffer changed. This happens for slow language servers. 396 return 397 end 398 399 local signatures, active_signature = process_signature_help_results(results) 400 401 if not next(signatures) then 402 if config.silent ~= true then 403 vim.notify('No signature help available', vim.log.levels.INFO) 404 end 405 return 406 end 407 408 local ft = vim.bo[ctx.bufnr].filetype 409 local total = #signatures 410 local can_cycle = total > 1 and config.focusable ~= false 411 local idx = active_signature - 1 412 413 --- @param update_win? integer 414 local function show_signature(update_win) 415 idx = (idx % total) + 1 416 local client, result = signatures[idx][1], signatures[idx][2] 417 --- @type string[]? 418 local triggers = 419 vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') 420 local lines, hl = 421 util.convert_signature_help_to_markdown_lines({ signatures = { result } }, ft, triggers) 422 if not lines then 423 return 424 end 425 426 -- Show title only if there are multiple clients or multiple signatures. 427 if total > 1 then 428 local sfx = total > 1 429 and string.format(' (%d/%d)%s', idx, total, can_cycle and ' (<C-s> to cycle)' or '') 430 or '' 431 config.title = user_title or string.format('Signature Help: %s%s', client.name, sfx) 432 -- If no border is set, render title inside the window. 433 if not (config.border or vim.o.winborder ~= '') then 434 table.insert(lines, 1, '# ' .. config.title) 435 if hl then 436 hl[1] = hl[1] + 1 437 hl[3] = hl[3] + 1 438 end 439 end 440 end 441 442 config._update_win = update_win 443 444 local buf, win = util.open_floating_preview(lines, 'markdown', config) 445 446 if hl then 447 api.nvim_buf_clear_namespace(buf, sig_help_ns, 0, -1) 448 vim.hl.range( 449 buf, 450 sig_help_ns, 451 'LspSignatureActiveParameter', 452 { hl[1], hl[2] }, 453 { hl[3], hl[4] } 454 ) 455 end 456 return buf, win 457 end 458 459 local fbuf, fwin = show_signature() 460 461 if can_cycle then 462 vim.keymap.set('n', '<Plug>(nvim.lsp.ctrl-s)', function() 463 show_signature(fwin) 464 end, { 465 buffer = fbuf, 466 desc = 'Cycle next signature', 467 }) 468 if vim.fn.hasmapto('<Plug>(nvim.lsp.ctrl-s)', 'n') == 0 then 469 vim.keymap.set('n', '<C-s>', '<Plug>(nvim.lsp.ctrl-s)', { 470 buffer = fbuf, 471 desc = 'Cycle next signature', 472 }) 473 end 474 end 475 end) 476 end 477 478 --- @deprecated 479 --- Retrieves the completion items at the current cursor position. Can only be 480 --- called in Insert mode. 481 --- 482 ---@param context table (context support not yet implemented) Additional information 483 --- about the context in which a completion was triggered (how it was triggered, 484 --- and by which trigger character, if applicable) 485 --- 486 ---@see vim.lsp.protocol.CompletionTriggerKind 487 function M.completion(context) 488 validate('context', context, 'table', true) 489 vim.deprecate('vim.lsp.buf.completion', 'vim.lsp.completion.trigger', '0.12') 490 return lsp.buf_request( 491 0, 492 'textDocument/completion', 493 client_positional_params({ 494 context = context, 495 }) 496 ) 497 end 498 499 ---@param bufnr integer 500 ---@param mode "v"|"V" 501 ---@return table {start={row,col}, end={row,col}} using (1, 0) indexing 502 local function range_from_selection(bufnr, mode) 503 -- TODO: Use `vim.fn.getregionpos()` instead. 504 505 -- [bufnum, lnum, col, off]; both row and column 1-indexed 506 local start = vim.fn.getpos('v') 507 local end_ = vim.fn.getpos('.') 508 local start_row = start[2] 509 local start_col = start[3] 510 local end_row = end_[2] 511 local end_col = end_[3] 512 513 -- A user can start visual selection at the end and move backwards 514 -- Normalize the range to start < end 515 if start_row == end_row and end_col < start_col then 516 end_col, start_col = start_col, end_col --- @type integer, integer 517 elseif end_row < start_row then 518 start_row, end_row = end_row, start_row --- @type integer, integer 519 start_col, end_col = end_col, start_col --- @type integer, integer 520 end 521 if mode == 'V' then 522 start_col = 1 523 local lines = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true) 524 end_col = #lines[1] 525 end 526 return { 527 ['start'] = { start_row, start_col - 1 }, 528 ['end'] = { end_row, end_col - 1 }, 529 } 530 end 531 532 --- @class vim.lsp.buf.format.Opts 533 --- @inlinedoc 534 --- 535 --- Can be used to specify FormattingOptions. Some unspecified options will be 536 --- automatically derived from the current Nvim options. 537 --- See https://microsoft.github.io/language-server-protocol/specification/#formattingOptions 538 --- @field formatting_options? lsp.FormattingOptions 539 --- 540 --- Time in milliseconds to block for formatting requests. No effect if async=true. 541 --- (default: `1000`) 542 --- @field timeout_ms? integer 543 --- 544 --- Restrict formatting to the clients attached to the given buffer. 545 --- (default: current buffer) 546 --- @field bufnr? integer 547 --- 548 --- Predicate used to filter clients. Receives a client as argument and must 549 --- return a boolean. Clients matching the predicate are included. Example: 550 --- ```lua 551 --- -- Never request typescript-language-server for formatting 552 --- vim.lsp.buf.format { 553 --- filter = function(client) return client.name ~= "ts_ls" end 554 --- } 555 --- ``` 556 --- @field filter? fun(client: vim.lsp.Client): boolean? 557 --- 558 --- If true the method won't block. 559 --- Editing the buffer while formatting asynchronous can lead to unexpected 560 --- changes. 561 --- (Default: false) 562 --- @field async? boolean 563 --- 564 --- Restrict formatting to the client with ID (client.id) matching this field. 565 --- @field id? integer 566 --- 567 --- Restrict formatting to the client with name (client.name) matching this field. 568 --- @field name? string 569 --- 570 --- Range to format. 571 --- Table must contain `start` and `end` keys with {row,col} tuples using 572 --- (1,0) indexing. 573 --- Can also be a list of tables that contain `start` and `end` keys as described above, 574 --- in which case `textDocument/rangesFormatting` support is required. 575 --- (Default: current selection in visual mode, `nil` in other modes, 576 --- formatting the full buffer) 577 --- @field range? {start:[integer,integer],end:[integer, integer]}|{start:[integer,integer],end:[integer,integer]}[] 578 579 --- Formats a buffer using the attached (and optionally filtered) language 580 --- server clients. 581 --- 582 --- @param opts? vim.lsp.buf.format.Opts 583 function M.format(opts) 584 validate('opts', opts, 'table', true) 585 586 opts = opts or {} 587 local bufnr = vim._resolve_bufnr(opts.bufnr) 588 local mode = api.nvim_get_mode().mode 589 local range = opts.range 590 -- Try to use visual selection if no range is given 591 if not range and mode == 'v' or mode == 'V' then 592 range = range_from_selection(bufnr, mode) 593 end 594 595 local passed_multiple_ranges = (range and #range ~= 0 and type(range[1]) == 'table') 596 local method ---@type vim.lsp.protocol.Method.ClientToServer.Request 597 if passed_multiple_ranges then 598 method = 'textDocument/rangesFormatting' 599 elseif range then 600 method = 'textDocument/rangeFormatting' 601 else 602 method = 'textDocument/formatting' 603 end 604 605 local clients = lsp.get_clients({ 606 id = opts.id, 607 bufnr = bufnr, 608 name = opts.name, 609 method = method, 610 }) 611 if opts.filter then 612 clients = vim.tbl_filter(opts.filter, clients) 613 end 614 615 if #clients == 0 then 616 vim.notify('[LSP] Format request failed, no matching language servers.') 617 end 618 619 --- @param client vim.lsp.Client 620 --- @param params lsp.DocumentFormattingParams 621 --- @return lsp.DocumentFormattingParams|lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams 622 local function set_range(client, params) 623 --- @param r {start:[integer,integer],end:[integer, integer]} 624 local function to_lsp_range(r) 625 return util.make_given_range_params(r.start, r['end'], bufnr, client.offset_encoding).range 626 end 627 628 local ret = params --[[@as lsp.DocumentFormattingParams|lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams]] 629 if passed_multiple_ranges then 630 --- @cast range {start:[integer,integer],end:[integer, integer]}[] 631 ret = params --[[@as lsp.DocumentRangesFormattingParams]] 632 ret.ranges = vim.tbl_map(to_lsp_range, range) 633 elseif range then 634 --- @cast range {start:[integer,integer],end:[integer, integer]} 635 ret = params --[[@as lsp.DocumentRangeFormattingParams]] 636 ret.range = to_lsp_range(range) 637 end 638 return ret 639 end 640 641 if opts.async then 642 --- @param idx? integer 643 --- @param client? vim.lsp.Client 644 local function do_format(idx, client) 645 if not idx or not client then 646 return 647 end 648 local params = set_range(client, util.make_formatting_params(opts.formatting_options)) 649 client:request(method, params, function(...) 650 local handler = client.handlers[method] or lsp.handlers[method] 651 handler(...) 652 do_format(next(clients, idx)) 653 end, bufnr) 654 end 655 do_format(next(clients)) 656 else 657 local timeout_ms = opts.timeout_ms or 1000 658 for _, client in pairs(clients) do 659 local params = set_range(client, util.make_formatting_params(opts.formatting_options)) 660 local result, err = client:request_sync(method, params, timeout_ms, bufnr) 661 if result and result.result then 662 util.apply_text_edits(result.result, bufnr, client.offset_encoding) 663 elseif err then 664 vim.notify(string.format('[LSP][%s] %s', client.name, err), vim.log.levels.WARN) 665 end 666 end 667 end 668 end 669 670 --- @class vim.lsp.buf.rename.Opts 671 --- @inlinedoc 672 --- 673 --- Predicate used to filter clients. Receives a client as argument and 674 --- must return a boolean. Clients matching the predicate are included. 675 --- @field filter? fun(client: vim.lsp.Client): boolean? 676 --- 677 --- Restrict clients used for rename to ones where client.name matches 678 --- this field. 679 --- @field name? string 680 --- 681 --- (default: current buffer) 682 --- @field bufnr? integer 683 684 --- Renames all references to the symbol under the cursor. 685 --- 686 ---@param new_name string|nil If not provided, the user will be prompted for a new 687 --- name using |vim.ui.input()|. 688 ---@param opts? vim.lsp.buf.rename.Opts Additional options: 689 function M.rename(new_name, opts) 690 validate('new_name', new_name, 'string', true) 691 validate('opts', opts, 'table', true) 692 693 opts = opts or {} 694 local bufnr = vim._resolve_bufnr(opts.bufnr) 695 local clients = lsp.get_clients({ 696 bufnr = bufnr, 697 name = opts.name, 698 -- Clients must at least support rename, prepareRename is optional 699 method = 'textDocument/rename', 700 }) 701 if opts.filter then 702 clients = vim.tbl_filter(opts.filter, clients) 703 end 704 705 if #clients == 0 then 706 vim.notify('[LSP] Rename, no matching language servers with rename capability.') 707 end 708 709 local win = api.nvim_get_current_win() 710 711 -- Compute early to account for cursor movements after going async 712 local cword = vim.fn.expand('<cword>') 713 714 --- @param range lsp.Range 715 --- @param position_encoding 'utf-8'|'utf-16'|'utf-32' 716 local function get_text_at_range(range, position_encoding) 717 return api.nvim_buf_get_text( 718 bufnr, 719 range.start.line, 720 util._get_line_byte_from_position(bufnr, range.start, position_encoding), 721 range['end'].line, 722 util._get_line_byte_from_position(bufnr, range['end'], position_encoding), 723 {} 724 )[1] 725 end 726 727 --- @param idx? integer 728 --- @param client? vim.lsp.Client 729 local function try_use_client(idx, client) 730 if not idx or not client then 731 return 732 end 733 734 --- @param name string 735 local function rename(name) 736 local params = util.make_position_params(win, client.offset_encoding) --[[@as lsp.RenameParams]] 737 params.newName = name 738 local handler = client.handlers['textDocument/rename'] or lsp.handlers['textDocument/rename'] 739 client:request('textDocument/rename', params, function(...) 740 handler(...) 741 try_use_client(next(clients, idx)) 742 end, bufnr) 743 end 744 745 if client:supports_method('textDocument/prepareRename') then 746 local params = util.make_position_params(win, client.offset_encoding) 747 ---@param result? lsp.Range|{ range: lsp.Range, placeholder: string } 748 client:request('textDocument/prepareRename', params, function(err, result) 749 if err or result == nil then 750 if next(clients, idx) then 751 try_use_client(next(clients, idx)) 752 else 753 local msg = err and ('Error on prepareRename: ' .. (err.message or '')) 754 or 'Nothing to rename' 755 vim.notify(msg, vim.log.levels.INFO) 756 end 757 return 758 end 759 760 if new_name then 761 rename(new_name) 762 return 763 end 764 765 local range ---@type lsp.Range? 766 if result.start then 767 ---@cast result lsp.Range 768 range = result 769 elseif result.range then 770 ---@cast result { range: lsp.Range, placeholder: string } 771 range = result.range 772 end 773 if range then 774 local start = range.start 775 local end_ = range['end'] 776 local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) 777 local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) 778 779 vim.hl.range( 780 bufnr, 781 rename_ns, 782 'LspReferenceTarget', 783 { start.line, start_idx }, 784 { end_.line, end_idx }, 785 { priority = vim.hl.priorities.user } 786 ) 787 end 788 789 local prompt_opts = { 790 prompt = 'New Name: ', 791 } 792 if result.placeholder then 793 prompt_opts.default = result.placeholder 794 elseif result.start then 795 prompt_opts.default = get_text_at_range(result, client.offset_encoding) 796 elseif result.range then 797 prompt_opts.default = get_text_at_range(result.range, client.offset_encoding) 798 else 799 prompt_opts.default = cword 800 end 801 vim.ui.input(prompt_opts, function(input) 802 if input and #input ~= 0 then 803 rename(input) 804 end 805 api.nvim_buf_clear_namespace(bufnr, rename_ns, 0, -1) 806 end) 807 end, bufnr) 808 else 809 assert( 810 client:supports_method('textDocument/rename'), 811 'Client must support textDocument/rename' 812 ) 813 if new_name then 814 rename(new_name) 815 return 816 end 817 818 local prompt_opts = { 819 prompt = 'New Name: ', 820 default = cword, 821 } 822 vim.ui.input(prompt_opts, function(input) 823 if not input or #input == 0 then 824 return 825 end 826 rename(input) 827 end) 828 end 829 end 830 831 try_use_client(next(clients)) 832 end 833 834 --- Lists all the references to the symbol under the cursor in the quickfix window. 835 --- 836 ---@param context lsp.ReferenceContext? Context for the request 837 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references 838 ---@param opts? vim.lsp.ListOpts 839 function M.references(context, opts) 840 validate('context', context, 'table', true) 841 validate('opts', opts, 'table', true) 842 843 local bufnr = api.nvim_get_current_buf() 844 local win = api.nvim_get_current_win() 845 opts = opts or {} 846 847 lsp.buf_request_all(bufnr, 'textDocument/references', function(client) 848 local params = util.make_position_params(win, client.offset_encoding) 849 ---@diagnostic disable-next-line: inject-field 850 params.context = context or { includeDeclaration = true } 851 return params 852 end, function(results) 853 local all_items = {} 854 local title = 'References' 855 856 for client_id, res in pairs(results) do 857 local client = assert(lsp.get_client_by_id(client_id)) 858 local items = util.locations_to_items(res.result or {}, client.offset_encoding) 859 vim.list_extend(all_items, items) 860 end 861 862 if not next(all_items) then 863 vim.notify('No references found') 864 else 865 local list = { 866 title = title, 867 items = all_items, 868 context = { 869 method = 'textDocument/references', 870 bufnr = bufnr, 871 }, 872 } 873 if opts.on_list then 874 assert(vim.is_callable(opts.on_list), 'on_list is not a function') 875 opts.on_list(list) 876 elseif opts.loclist then 877 vim.fn.setloclist(0, {}, ' ', list) 878 vim.cmd.lopen() 879 else 880 vim.fn.setqflist({}, ' ', list) 881 vim.cmd('botright copen') 882 end 883 end 884 end) 885 end 886 887 --- Lists all symbols in the current buffer in the |location-list|. 888 --- @param opts? vim.lsp.ListOpts 889 function M.document_symbol(opts) 890 validate('opts', opts, 'table', true) 891 opts = vim.tbl_deep_extend('keep', opts or {}, { loclist = true }) 892 local params = { textDocument = util.make_text_document_params() } 893 request_with_opts('textDocument/documentSymbol', params, opts) 894 end 895 896 --- @param client_id integer 897 --- @param method vim.lsp.protocol.Method.ClientToServer.Request 898 --- @param params table 899 --- @param handler? lsp.Handler 900 --- @param bufnr? integer 901 local function request_with_id(client_id, method, params, handler, bufnr) 902 local client = lsp.get_client_by_id(client_id) 903 if not client then 904 vim.notify( 905 string.format('Client with id=%d disappeared during hierarchy request', client_id), 906 vim.log.levels.WARN 907 ) 908 return 909 end 910 client:request(method, params, handler, bufnr) 911 end 912 913 --- @param item lsp.TypeHierarchyItem|lsp.CallHierarchyItem 914 local function format_hierarchy_item(item) 915 if not item.detail or #item.detail == 0 then 916 return item.name 917 end 918 return string.format('%s %s', item.name, item.detail) 919 end 920 921 --- @alias vim.lsp.buf.HierarchyMethod 922 --- | 'typeHierarchy/subtypes' 923 --- | 'typeHierarchy/supertypes' 924 --- | 'callHierarchy/incomingCalls' 925 --- | 'callHierarchy/outgoingCalls' 926 927 --- @type table<vim.lsp.buf.HierarchyMethod, 'type' | 'call'> 928 local hierarchy_methods = { 929 ['typeHierarchy/subtypes'] = 'type', 930 ['typeHierarchy/supertypes'] = 'type', 931 ['callHierarchy/incomingCalls'] = 'call', 932 ['callHierarchy/outgoingCalls'] = 'call', 933 } 934 935 --- @param method vim.lsp.buf.HierarchyMethod 936 local function hierarchy(method) 937 local kind = hierarchy_methods[method] 938 939 local prepare_method = kind == 'type' and 'textDocument/prepareTypeHierarchy' 940 or 'textDocument/prepareCallHierarchy' 941 942 local bufnr = api.nvim_get_current_buf() 943 local clients = lsp.get_clients({ bufnr = bufnr, method = prepare_method }) 944 if not next(clients) then 945 vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) 946 return 947 end 948 949 local win = api.nvim_get_current_win() 950 951 lsp.buf_request_all(bufnr, prepare_method, function(client) 952 return util.make_position_params(win, client.offset_encoding) 953 end, function(req_results) 954 local results = {} --- @type [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][] 955 for client_id, res in pairs(req_results) do 956 if res.err then 957 vim.notify(res.err.message, vim.log.levels.WARN) 958 elseif res.result then 959 local result = res.result --- @type lsp.TypeHierarchyItem[]|lsp.CallHierarchyItem[] 960 for _, item in ipairs(result) do 961 results[#results + 1] = { client_id, item } 962 end 963 end 964 end 965 966 if #results == 0 then 967 vim.notify('No item resolved', vim.log.levels.WARN) 968 elseif #results == 1 then 969 local client_id, item = results[1][1], results[1][2] 970 request_with_id(client_id, method, { item = item }, nil, bufnr) 971 else 972 vim.ui.select(results, { 973 prompt = string.format('Select a %s hierarchy item:', kind), 974 kind = kind .. 'hierarchy', 975 format_item = function(x) 976 return format_hierarchy_item(x[2]) 977 end, 978 }, function(x) 979 if x then 980 local client_id, item = x[1], x[2] 981 request_with_id(client_id, method, { item = item }, nil, bufnr) 982 end 983 end) 984 end 985 end) 986 end 987 988 --- Lists all the call sites of the symbol under the cursor in the 989 --- |quickfix| window. If the symbol can resolve to multiple 990 --- items, the user can pick one in the |inputlist()|. 991 function M.incoming_calls() 992 hierarchy('callHierarchy/incomingCalls') 993 end 994 995 --- Lists all the items that are called by the symbol under the 996 --- cursor in the |quickfix| window. If the symbol can resolve to 997 --- multiple items, the user can pick one in the |inputlist()|. 998 function M.outgoing_calls() 999 hierarchy('callHierarchy/outgoingCalls') 1000 end 1001 1002 --- Lists all the subtypes or supertypes of the symbol under the 1003 --- cursor in the |quickfix| window. If the symbol can resolve to 1004 --- multiple items, the user can pick one using |vim.ui.select()|. 1005 ---@param kind "subtypes"|"supertypes" 1006 function M.typehierarchy(kind) 1007 validate('kind', kind, function(v) 1008 return v == 'subtypes' or v == 'supertypes' 1009 end) 1010 1011 local method = kind == 'subtypes' and 'typeHierarchy/subtypes' or 'typeHierarchy/supertypes' 1012 hierarchy(method) 1013 end 1014 1015 --- List workspace folders. 1016 --- 1017 function M.list_workspace_folders() 1018 local workspace_folders = {} 1019 for _, client in pairs(lsp.get_clients({ bufnr = 0 })) do 1020 for _, folder in pairs(client.workspace_folders or {}) do 1021 table.insert(workspace_folders, folder.name) 1022 end 1023 end 1024 return workspace_folders 1025 end 1026 1027 --- Add the folder at path to the workspace folders. If {path} is 1028 --- not provided, the user will be prompted for a path using |input()|. 1029 --- @param workspace_folder? string 1030 function M.add_workspace_folder(workspace_folder) 1031 validate('workspace_folder', workspace_folder, 'string', true) 1032 1033 workspace_folder = workspace_folder 1034 or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h'), 'dir') 1035 api.nvim_command('redraw') 1036 if not (workspace_folder and #workspace_folder > 0) then 1037 return 1038 end 1039 if vim.fn.isdirectory(workspace_folder) == 0 then 1040 vim.notify(workspace_folder .. ' is not a valid directory') 1041 return 1042 end 1043 local bufnr = api.nvim_get_current_buf() 1044 for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do 1045 client:_add_workspace_folder(workspace_folder) 1046 end 1047 end 1048 1049 --- Remove the folder at path from the workspace folders. If 1050 --- {path} is not provided, the user will be prompted for 1051 --- a path using |input()|. 1052 --- @param workspace_folder? string 1053 function M.remove_workspace_folder(workspace_folder) 1054 validate('workspace_folder', workspace_folder, 'string', true) 1055 1056 workspace_folder = workspace_folder 1057 or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h')) 1058 api.nvim_command('redraw') 1059 if not workspace_folder or #workspace_folder == 0 then 1060 return 1061 end 1062 local bufnr = api.nvim_get_current_buf() 1063 for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do 1064 client:_remove_workspace_folder(workspace_folder) 1065 end 1066 vim.notify(workspace_folder .. 'is not currently part of the workspace') 1067 end 1068 1069 --- Lists all symbols in the current workspace in the quickfix window. 1070 --- 1071 --- The list is filtered against {query}; if the argument is omitted from the 1072 --- call, the user is prompted to enter a string on the command line. An empty 1073 --- string means no filtering is done. 1074 --- 1075 --- @param query string? optional 1076 --- @param opts? vim.lsp.ListOpts 1077 function M.workspace_symbol(query, opts) 1078 validate('query', query, 'string', true) 1079 validate('opts', opts, 'table', true) 1080 1081 query = query or npcall(vim.fn.input, 'Query: ') 1082 if query == nil then 1083 return 1084 end 1085 local params = { query = query } 1086 request_with_opts('workspace/symbol', params, opts) 1087 end 1088 1089 --- @class vim.lsp.WorkspaceDiagnosticsOpts 1090 --- @inlinedoc 1091 --- 1092 --- Only request diagnostics from the indicated client. If nil, the request is sent to all clients. 1093 --- @field client_id? integer 1094 1095 --- Request workspace-wide diagnostics. 1096 --- @param opts? vim.lsp.WorkspaceDiagnosticsOpts 1097 --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics 1098 function M.workspace_diagnostics(opts) 1099 validate('opts', opts, 'table', true) 1100 1101 lsp.diagnostic._workspace_diagnostics(opts or {}) 1102 end 1103 1104 --- Send request to the server to resolve document highlights for the current 1105 --- text document position. This request can be triggered by a key mapping or 1106 --- by events such as `CursorHold`, e.g.: 1107 --- 1108 --- ```vim 1109 --- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight() 1110 --- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight() 1111 --- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references() 1112 --- ``` 1113 --- 1114 --- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups 1115 --- to be defined or you won't be able to see the actual highlights. 1116 --- |hl-LspReferenceText| 1117 --- |hl-LspReferenceRead| 1118 --- |hl-LspReferenceWrite| 1119 function M.document_highlight() 1120 lsp.buf_request(0, 'textDocument/documentHighlight', client_positional_params()) 1121 end 1122 1123 --- Removes document highlights from current buffer. 1124 function M.clear_references() 1125 util.buf_clear_references() 1126 end 1127 1128 ---@nodoc 1129 ---@class vim.lsp.CodeActionResultEntry 1130 ---@field err? lsp.ResponseError 1131 ---@field result? (lsp.Command|lsp.CodeAction)[] 1132 ---@field context lsp.HandlerContext 1133 1134 --- @class vim.lsp.buf.code_action.Opts 1135 --- @inlinedoc 1136 --- 1137 --- Corresponds to `CodeActionContext` of the LSP specification: 1138 --- - {diagnostics}? (`table`) LSP `Diagnostic[]`. Inferred from the current 1139 --- position if not provided. 1140 --- - {only}? (`table`) List of LSP `CodeActionKind`s used to filter the code actions. 1141 --- Most language servers support values like `refactor` 1142 --- or `quickfix`. 1143 --- - {triggerKind}? (`integer`) The reason why code actions were requested. 1144 --- @field context? lsp.CodeActionContext 1145 --- 1146 --- Predicate taking a code action or command and the provider's ID. 1147 --- If it returns false, the action is filtered out. 1148 --- @field filter? fun(x: lsp.CodeAction|lsp.Command, client_id: integer):boolean 1149 --- 1150 --- When set to `true`, and there is just one remaining action 1151 --- (after filtering), the action is applied without user query. 1152 --- @field apply? boolean 1153 --- 1154 --- Range for which code actions should be requested. 1155 --- If in visual mode this defaults to the active selection. 1156 --- Table must contain `start` and `end` keys with {row,col} tuples 1157 --- using mark-like indexing. See |api-indexing| 1158 --- @field range? {start: integer[], end: integer[]} 1159 1160 --- This is not public because the main extension point is 1161 --- vim.ui.select which can be overridden independently. 1162 --- 1163 --- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects 1164 --- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results 1165 --- from multiple clients to have 1 single UI prompt for the user, yet we still 1166 --- need to be able to link a `CodeAction|Command` to the right client for 1167 --- `codeAction/resolve` 1168 ---@param results table<integer, vim.lsp.CodeActionResultEntry> 1169 ---@param opts? vim.lsp.buf.code_action.Opts 1170 local function on_code_action_results(results, opts) 1171 ---@param a lsp.Command|lsp.CodeAction 1172 ---@param client_id integer 1173 local function action_filter(a, client_id) 1174 -- filter by specified action kind 1175 if opts and opts.context then 1176 if opts.context.only then 1177 if not a.kind then 1178 return false 1179 end 1180 local found = false 1181 for _, o in ipairs(opts.context.only) do 1182 -- action kinds are hierarchical with . as a separator: when requesting only 'type-annotate' 1183 -- this filter allows both 'type-annotate' and 'type-annotate.foo', for example 1184 if a.kind == o or vim.startswith(a.kind, o .. '.') then 1185 found = true 1186 break 1187 end 1188 end 1189 if not found then 1190 return false 1191 end 1192 end 1193 -- Only show disabled code actions when the trigger kind is "Invoked". 1194 if a.disabled and opts.context.triggerKind ~= lsp.protocol.CodeActionTriggerKind.Invoked then 1195 return false 1196 end 1197 end 1198 -- filter by user function 1199 if opts and opts.filter and not opts.filter(a, client_id) then 1200 return false 1201 end 1202 -- no filter removed this action 1203 return true 1204 end 1205 1206 ---@type {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext}[] 1207 local actions = {} 1208 for _, result in pairs(results) do 1209 for _, action in pairs(result.result or {}) do 1210 if action_filter(action, result.context.client_id) then 1211 table.insert(actions, { action = action, ctx = result.context }) 1212 end 1213 end 1214 end 1215 if #actions == 0 then 1216 vim.notify('No code actions available', vim.log.levels.INFO) 1217 return 1218 end 1219 1220 ---@param action lsp.Command|lsp.CodeAction 1221 ---@param client vim.lsp.Client 1222 ---@param ctx lsp.HandlerContext 1223 local function apply_action(action, client, ctx) 1224 if action.edit then 1225 util.apply_workspace_edit(action.edit, client.offset_encoding) 1226 end 1227 local a_cmd = action.command 1228 if a_cmd then 1229 local command = type(a_cmd) == 'table' and a_cmd or action 1230 --- @cast command lsp.Command 1231 client:exec_cmd(command, ctx) 1232 end 1233 end 1234 1235 ---@param choice {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext} 1236 local function on_user_choice(choice) 1237 if not choice then 1238 return 1239 end 1240 1241 -- textDocument/codeAction can return either Command[] or CodeAction[] 1242 -- 1243 -- CodeAction 1244 -- ... 1245 -- edit?: WorkspaceEdit -- <- must be applied before command 1246 -- command?: Command 1247 -- 1248 -- Command: 1249 -- title: string 1250 -- command: string 1251 -- arguments?: any[] 1252 1253 local client = assert(lsp.get_client_by_id(choice.ctx.client_id)) 1254 local action = choice.action 1255 local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number') 1256 1257 -- Only code actions are resolved, so if we have a command, just apply it. 1258 if type(action.title) == 'string' and type(action.command) == 'string' then 1259 apply_action(action, client, choice.ctx) 1260 return 1261 end 1262 1263 if action.disabled then 1264 vim.notify(action.disabled.reason, vim.log.levels.ERROR) 1265 return 1266 end 1267 1268 if not (action.edit and action.command) and client:supports_method('codeAction/resolve') then 1269 client:request('codeAction/resolve', action, function(err, resolved_action) 1270 if err then 1271 -- If resolve fails, try to apply the edit/command from the original code action. 1272 if action.edit or action.command then 1273 apply_action(action, client, choice.ctx) 1274 else 1275 vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR) 1276 end 1277 else 1278 apply_action(resolved_action, client, choice.ctx) 1279 end 1280 end, bufnr) 1281 else 1282 apply_action(action, client, choice.ctx) 1283 end 1284 end 1285 1286 -- If options.apply is given, and there are just one remaining code action, 1287 -- apply it directly without querying the user. 1288 if opts and opts.apply and #actions == 1 then 1289 on_user_choice(actions[1]) 1290 return 1291 end 1292 1293 ---@param item {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext} 1294 local function format_item(item) 1295 local clients = lsp.get_clients({ bufnr = item.ctx.bufnr }) 1296 local title = item.action.title:gsub('\r\n', '\\r\\n'):gsub('\n', '\\n') 1297 1298 if item.action.disabled then 1299 title = title .. ' (disabled)' 1300 end 1301 1302 if #clients == 1 then 1303 return title 1304 end 1305 1306 local source = assert(lsp.get_client_by_id(item.ctx.client_id)).name 1307 return ('%s [%s]'):format(title, source) 1308 end 1309 1310 local select_opts = { 1311 prompt = 'Code actions:', 1312 kind = 'codeaction', 1313 format_item = format_item, 1314 } 1315 vim.ui.select(actions, select_opts, on_user_choice) 1316 end 1317 1318 --- Selects a code action (LSP: "textDocument/codeAction" request) available at cursor position. 1319 --- 1320 ---@param opts? vim.lsp.buf.code_action.Opts 1321 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction 1322 ---@see vim.lsp.protocol.CodeActionTriggerKind 1323 function M.code_action(opts) 1324 validate('options', opts, 'table', true) 1325 opts = opts or {} 1326 -- Detect old API call code_action(context) which should now be 1327 -- code_action({ context = context} ) 1328 --- @diagnostic disable-next-line:undefined-field 1329 if opts.diagnostics or opts.only then 1330 opts = { options = opts } 1331 end 1332 local context = opts.context and vim.deepcopy(opts.context) or {} 1333 if not context.triggerKind then 1334 context.triggerKind = lsp.protocol.CodeActionTriggerKind.Invoked 1335 end 1336 local mode = api.nvim_get_mode().mode 1337 local bufnr = api.nvim_get_current_buf() 1338 local win = api.nvim_get_current_win() 1339 local clients = lsp.get_clients({ bufnr = bufnr, method = 'textDocument/codeAction' }) 1340 if not next(clients) then 1341 vim.notify(lsp._unsupported_method('textDocument/codeAction'), vim.log.levels.WARN) 1342 return 1343 end 1344 1345 lsp.buf_request_all(bufnr, 'textDocument/codeAction', function(client) 1346 ---@type lsp.CodeActionParams 1347 local params 1348 1349 if opts.range then 1350 assert(type(opts.range) == 'table', 'code_action range must be a table') 1351 local start = assert(opts.range.start, 'range must have a `start` property') 1352 local end_ = assert(opts.range['end'], 'range must have a `end` property') 1353 params = util.make_given_range_params(start, end_, bufnr, client.offset_encoding) 1354 elseif mode == 'v' or mode == 'V' then 1355 local range = range_from_selection(bufnr, mode) 1356 params = 1357 util.make_given_range_params(range.start, range['end'], bufnr, client.offset_encoding) 1358 else 1359 params = util.make_range_params(win, client.offset_encoding) 1360 end 1361 1362 --- @cast params lsp.CodeActionParams 1363 1364 if context.diagnostics then 1365 params.context = context 1366 else 1367 local ns_push = lsp.diagnostic.get_namespace(client.id, false) 1368 local ns_pull = lsp.diagnostic.get_namespace(client.id, true) 1369 local diagnostics = {} 1370 local lnum = api.nvim_win_get_cursor(0)[1] - 1 1371 vim.list_extend(diagnostics, vim.diagnostic.get(bufnr, { namespace = ns_pull, lnum = lnum })) 1372 vim.list_extend(diagnostics, vim.diagnostic.get(bufnr, { namespace = ns_push, lnum = lnum })) 1373 params.context = vim.tbl_extend('force', context, { 1374 ---@diagnostic disable-next-line: no-unknown 1375 diagnostics = vim.tbl_map(function(d) 1376 return d.user_data.lsp 1377 end, diagnostics), 1378 }) 1379 end 1380 1381 return params 1382 end, function(results) 1383 on_code_action_results(results, opts) 1384 end) 1385 end 1386 1387 --- @deprecated 1388 --- Executes an LSP server command. 1389 --- @param command_params lsp.ExecuteCommandParams 1390 --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand 1391 function M.execute_command(command_params) 1392 validate('command', command_params.command, 'string') 1393 validate('arguments', command_params.arguments, 'table', true) 1394 vim.deprecate('execute_command', 'client:exec_cmd', '0.12') 1395 command_params = { 1396 command = command_params.command, 1397 arguments = command_params.arguments, 1398 workDoneToken = command_params.workDoneToken, 1399 } 1400 lsp.buf_request(0, 'workspace/executeCommand', command_params) 1401 end 1402 1403 ---@type { index: integer, ranges: lsp.Range[] }? 1404 local selection_ranges = nil 1405 1406 ---@param range lsp.Range 1407 local function select_range(range) 1408 local start_line = range.start.line + 1 1409 local end_line = range['end'].line + 1 1410 1411 local start_col = range.start.character 1412 local end_col = range['end'].character 1413 1414 -- If the selection ends at column 0, adjust the position to the end of the previous line. 1415 if end_col == 0 then 1416 end_line = end_line - 1 1417 local end_line_text = api.nvim_buf_get_lines(0, end_line - 1, end_line, true)[1] 1418 end_col = #end_line_text 1419 end 1420 1421 vim.fn.setpos("'<", { 0, start_line, start_col + 1, 0 }) 1422 vim.fn.setpos("'>", { 0, end_line, end_col, 0 }) 1423 vim.cmd.normal({ 'gv', bang = true }) 1424 end 1425 1426 ---@param range lsp.Range 1427 local function is_empty(range) 1428 return range.start.line == range['end'].line and range.start.character == range['end'].character 1429 end 1430 1431 --- Perform an incremental selection at the cursor position based on ranges given by the LSP. The 1432 --- `direction` parameter specifies the number of times to expand the selection. Negative values 1433 --- will shrink the selection. 1434 --- 1435 --- @param direction integer 1436 --- @param timeout_ms integer? (default: `1000`) Maximum time (milliseconds) to wait for a result. 1437 function M.selection_range(direction, timeout_ms) 1438 validate('direction', direction, 'number') 1439 validate('timeout_ms', timeout_ms, 'number', true) 1440 1441 if selection_ranges then 1442 local new_index = selection_ranges.index + direction 1443 selection_ranges.index = math.min(#selection_ranges.ranges, math.max(1, new_index)) 1444 1445 select_range(selection_ranges.ranges[selection_ranges.index]) 1446 return 1447 end 1448 1449 local method = 'textDocument/selectionRange' 1450 local client = lsp.get_clients({ method = method, bufnr = 0 })[1] 1451 if not client then 1452 vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) 1453 return 1454 end 1455 1456 local position_params = util.make_position_params(0, client.offset_encoding) 1457 1458 ---@type lsp.SelectionRangeParams 1459 local params = { 1460 textDocument = position_params.textDocument, 1461 positions = { position_params.position }, 1462 } 1463 1464 timeout_ms = timeout_ms or 1000 1465 local result, err = lsp.buf_request_sync(0, method, params, timeout_ms) 1466 if err then 1467 lsp.log.error('selectionRange request failed: ' .. err) 1468 return 1469 end 1470 if not result or not result[client.id] or not result[client.id].result then 1471 return 1472 end 1473 if result[client.id].error then 1474 lsp.log.error(result[client.id].error.code, result[client.id].error.message) 1475 end 1476 1477 -- We only requested one range, thus we get the first and only response here. 1478 local response = assert(result[client.id].result[1]) ---@type lsp.SelectionRange 1479 local ranges = {} ---@type lsp.Range[] 1480 local lines = api.nvim_buf_get_lines(0, 0, -1, false) 1481 1482 -- Populate the list of ranges from the given request. 1483 while response do 1484 local range = response.range 1485 if not is_empty(range) then 1486 local start_line = range.start.line 1487 local end_line = range['end'].line 1488 range.start.character = vim.str_byteindex( 1489 lines[start_line + 1] or '', 1490 client.offset_encoding, 1491 range.start.character, 1492 false 1493 ) 1494 range['end'].character = vim.str_byteindex( 1495 lines[end_line + 1] or '', 1496 client.offset_encoding, 1497 range['end'].character, 1498 false 1499 ) 1500 ranges[#ranges + 1] = range 1501 end 1502 response = response.parent 1503 end 1504 1505 -- Clear selection ranges when leaving visual mode. 1506 api.nvim_create_autocmd('ModeChanged', { 1507 once = true, 1508 pattern = 'v*:*', 1509 callback = function() 1510 selection_ranges = nil 1511 end, 1512 }) 1513 1514 if #ranges > 0 then 1515 local index = math.min(#ranges, math.max(1, direction)) 1516 selection_ranges = { index = index, ranges = ranges } 1517 select_range(ranges[index]) 1518 end 1519 end 1520 1521 return M