util.lua (80067B)
1 local protocol = require('vim.lsp.protocol') 2 local validate = vim.validate 3 local api = vim.api 4 local list_extend = vim.list_extend 5 local uv = vim.uv 6 7 local M = {} 8 9 --- @param border string|(string|[string,string])[] 10 local function border_error(border) 11 error( 12 string.format( 13 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', 14 vim.inspect(border) 15 ), 16 2 17 ) 18 end 19 20 local border_size = { 21 none = { 0, 0 }, 22 single = { 2, 2 }, 23 double = { 2, 2 }, 24 rounded = { 2, 2 }, 25 solid = { 2, 2 }, 26 shadow = { 1, 1 }, 27 bold = { 2, 2 }, 28 } 29 30 --- Check the border given by opts or the default border for the additional 31 --- size it adds to a float. 32 --- @param opts? {border:string|(string|[string,string])[]} 33 --- @return integer height 34 --- @return integer width 35 local function get_border_size(opts) 36 local border = opts and opts.border or vim.o.winborder 37 38 if border == '' then 39 border = 'none' 40 end 41 42 -- Convert winborder string option with custom characters into a table 43 if type(border) == 'string' and border:find(',') then 44 border = vim.split(border, ',') 45 end 46 47 if type(border) == 'string' then 48 if not border_size[border] then 49 border_error(border) 50 end 51 local r = border_size[border] 52 return r[1], r[2] 53 end 54 55 if 8 % #border ~= 0 then 56 border_error(border) 57 end 58 59 --- @param id integer 60 --- @return string 61 local function elem(id) 62 id = (id - 1) % #border + 1 63 local e = border[id] 64 if type(e) == 'table' then 65 -- border specified as a table of <character, highlight group> 66 return e[1] 67 elseif type(e) == 'string' then 68 -- border specified as a list of border characters 69 return e 70 end 71 --- @diagnostic disable-next-line:missing-return 72 border_error(border) 73 end 74 75 --- @param e string 76 --- @return integer 77 local function border_height(e) 78 return #e > 0 and 1 or 0 79 end 80 81 local top, bottom = elem(2), elem(6) 82 local height = border_height(top) + border_height(bottom) 83 84 local right, left = elem(4), elem(8) 85 local width = vim.fn.strdisplaywidth(right) + vim.fn.strdisplaywidth(left) 86 87 return height, width 88 end 89 90 --- Splits string at newlines, optionally removing unwanted blank lines. 91 --- 92 --- @param s string Multiline string 93 --- @param no_blank boolean? Drop blank lines for each @param/@return (except one empty line 94 --- separating each). Workaround for https://github.com/LuaLS/lua-language-server/issues/2333 95 local function split_lines(s, no_blank) 96 s = string.gsub(s, '\r\n?', '\n') 97 local lines = {} 98 local in_desc = true -- Main description block, before seeing any @foo. 99 for line in vim.gsplit(s, '\n', { plain = true, trimempty = true }) do 100 local start_annotation = not not line:find('^ ?%@.?[pr]') 101 in_desc = (not start_annotation) and in_desc or false 102 if start_annotation and no_blank and not (lines[#lines] or ''):find('^%s*$') then 103 table.insert(lines, '') -- Separate each @foo with a blank line. 104 end 105 if in_desc or not no_blank or not line:find('^%s*$') then 106 table.insert(lines, line) 107 end 108 end 109 return lines 110 end 111 112 local function create_window_without_focus() 113 local prev = api.nvim_get_current_win() 114 vim.cmd.new() 115 local new = api.nvim_get_current_win() 116 api.nvim_set_current_win(prev) 117 return new 118 end 119 120 --- Replaces text in a range with new text. 121 --- 122 --- CAUTION: Changes in-place! 123 --- 124 ---@deprecated 125 ---@param lines string[] Original list of strings 126 ---@param A [integer, integer] Start position; a 2-tuple of {line,col} numbers 127 ---@param B [integer, integer] End position; a 2-tuple {line,col} numbers 128 ---@param new_lines string[] list of strings to replace the original 129 ---@return string[] The modified {lines} object 130 function M.set_lines(lines, A, B, new_lines) 131 vim.deprecate('vim.lsp.util.set_lines()', nil, '0.12') 132 -- 0-indexing to 1-indexing 133 local i_0 = A[1] + 1 134 -- If it extends past the end, truncate it to the end. This is because the 135 -- way the LSP describes the range including the last newline is by 136 -- specifying a line number after what we would call the last line. 137 local i_n = math.min(B[1] + 1, #lines) 138 if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then 139 error('Invalid range: ' .. vim.inspect({ A = A, B = B, #lines, new_lines })) 140 end 141 local prefix = '' 142 local suffix = assert(lines[i_n]):sub(B[2] + 1) 143 if A[2] > 0 then 144 prefix = assert(lines[i_0]):sub(1, A[2]) 145 end 146 local n = i_n - i_0 + 1 147 if n ~= #new_lines then 148 for _ = 1, n - #new_lines do 149 table.remove(lines, i_0) 150 end 151 for _ = 1, #new_lines - n do 152 table.insert(lines, i_0, '') 153 end 154 end 155 for i = 1, #new_lines do 156 lines[i - 1 + i_0] = new_lines[i] 157 end 158 if #suffix > 0 then 159 local i = i_0 + #new_lines - 1 160 lines[i] = lines[i] .. suffix 161 end 162 if #prefix > 0 then 163 lines[i_0] = prefix .. lines[i_0] 164 end 165 return lines 166 end 167 168 --- @param fn fun(x:any):any[] 169 --- @return function 170 local function sort_by_key(fn) 171 return function(a, b) 172 local ka, kb = fn(a), fn(b) 173 assert(#ka == #kb) 174 for i = 1, #ka do 175 if ka[i] ~= kb[i] then 176 return ka[i] < kb[i] 177 end 178 end 179 -- every value must have been equal here, which means it's not less than. 180 return false 181 end 182 end 183 184 --- Gets the zero-indexed lines from the given buffer. 185 --- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. 186 --- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. 187 --- 188 ---@param bufnr integer bufnr to get the lines from 189 ---@param rows integer[] zero-indexed line numbers 190 ---@return table<integer, string> # a table mapping rows to lines 191 local function get_lines(bufnr, rows) 192 --- @type integer[] 193 rows = type(rows) == 'table' and rows or { rows } 194 195 -- This is needed for bufload and bufloaded 196 bufnr = vim._resolve_bufnr(bufnr) 197 198 local function buf_lines() 199 local lines = {} --- @type table<integer,string> 200 for _, row in ipairs(rows) do 201 lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1] 202 end 203 return lines 204 end 205 206 -- use loaded buffers if available 207 if vim.fn.bufloaded(bufnr) == 1 then 208 return buf_lines() 209 end 210 211 local uri = vim.uri_from_bufnr(bufnr) 212 213 -- load the buffer if this is not a file uri 214 -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds. 215 if uri:sub(1, 4) ~= 'file' then 216 vim.fn.bufload(bufnr) 217 return buf_lines() 218 end 219 220 local filename = api.nvim_buf_get_name(bufnr) 221 if vim.fn.isdirectory(filename) ~= 0 then 222 return {} 223 end 224 225 -- get the data from the file 226 local fd = uv.fs_open(filename, 'r', 438) 227 if not fd then 228 return {} 229 end 230 local stat = assert(uv.fs_fstat(fd)) 231 local data = assert(uv.fs_read(fd, stat.size, 0)) 232 uv.fs_close(fd) 233 234 local lines = {} --- @type table<integer,true|string> rows we need to retrieve 235 local need = 0 -- keep track of how many unique rows we need 236 for _, row in pairs(rows) do 237 if not lines[row] then 238 need = need + 1 239 end 240 lines[row] = true 241 end 242 243 local found = 0 244 local lnum = 0 245 246 for line in string.gmatch(data, '([^\n]*)\n?') do 247 if lines[lnum] == true then 248 lines[lnum] = line 249 found = found + 1 250 if found == need then 251 break 252 end 253 end 254 lnum = lnum + 1 255 end 256 257 -- change any lines we didn't find to the empty string 258 for i, line in pairs(lines) do 259 if line == true then 260 lines[i] = '' 261 end 262 end 263 return lines --[[@as table<integer,string>]] 264 end 265 266 --- Gets the zero-indexed line from the given buffer. 267 --- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. 268 --- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. 269 --- 270 ---@param bufnr integer 271 ---@param row integer zero-indexed line number 272 ---@return string the line at row in filename 273 local function get_line(bufnr, row) 274 return get_lines(bufnr, { row })[row] 275 end 276 277 --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position 278 ---@param position lsp.Position 279 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 280 ---@return integer 281 local function get_line_byte_from_position(bufnr, position, position_encoding) 282 -- LSP's line and characters are 0-indexed 283 -- Vim's line and columns are 1-indexed 284 local col = position.character 285 -- When on the first character, we can ignore the difference between byte and 286 -- character 287 if col > 0 then 288 local line = get_line(bufnr, position.line) or '' 289 return vim.str_byteindex(line, position_encoding, col, false) 290 end 291 return col 292 end 293 294 --- Applies a list of text edits to a buffer. 295 ---@param text_edits (lsp.TextEdit|lsp.AnnotatedTextEdit)[] 296 ---@param bufnr integer Buffer id 297 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 298 ---@param change_annotations? table<string, lsp.ChangeAnnotation> 299 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit 300 function M.apply_text_edits(text_edits, bufnr, position_encoding, change_annotations) 301 validate('text_edits', text_edits, 'table', false) 302 validate('bufnr', bufnr, 'number', false) 303 validate('position_encoding', position_encoding, 'string', false) 304 validate('change_annotations', change_annotations, 'table', true) 305 306 if not next(text_edits) then 307 return 308 end 309 310 assert(bufnr ~= 0, 'Explicit buffer number is required') 311 312 if not api.nvim_buf_is_loaded(bufnr) then 313 vim.fn.bufload(bufnr) 314 end 315 vim.bo[bufnr].buflisted = true 316 317 local marks = {} --- @type table<string,[integer,integer]> 318 local has_eol_text_edit = false 319 320 local function apply_text_edits() 321 -- Fix reversed range and indexing each text_edits 322 for index, text_edit in ipairs(text_edits) do 323 --- @cast text_edit lsp.TextEdit|{_index: integer} 324 text_edit._index = index 325 326 if 327 text_edit.range.start.line > text_edit.range['end'].line 328 or text_edit.range.start.line == text_edit.range['end'].line 329 and text_edit.range.start.character > text_edit.range['end'].character 330 then 331 local start = text_edit.range.start 332 text_edit.range.start = text_edit.range['end'] 333 text_edit.range['end'] = start 334 end 335 end 336 337 --- @cast text_edits (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer})[] 338 339 -- Sort text_edits 340 ---@param a (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer}) 341 ---@param b (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer}) 342 ---@return boolean 343 table.sort(text_edits, function(a, b) 344 if a.range.start.line ~= b.range.start.line then 345 return a.range.start.line > b.range.start.line 346 end 347 if a.range.start.character ~= b.range.start.character then 348 return a.range.start.character > b.range.start.character 349 end 350 return a._index > b._index 351 end) 352 353 -- save and restore local marks since they get deleted by nvim_buf_set_lines 354 for _, m in pairs(vim.fn.getmarklist(bufnr)) do 355 if m.mark:match("^'[a-z]$") then 356 marks[m.mark:sub(2, 2)] = { m.pos[2], m.pos[3] - 1 } -- api-indexed 357 end 358 end 359 360 for _, text_edit in ipairs(text_edits) do 361 -- Normalize line ending 362 text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n') 363 364 -- Convert from LSP style ranges to Neovim style ranges. 365 local start_row = text_edit.range.start.line 366 local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, position_encoding) 367 local end_row = text_edit.range['end'].line 368 local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], position_encoding) 369 local text = vim.split(text_edit.newText, '\n', { plain = true }) 370 371 local max = api.nvim_buf_line_count(bufnr) 372 -- If the whole edit is after the lines in the buffer we can simply add the new text to the end 373 -- of the buffer. 374 if max <= start_row then 375 api.nvim_buf_set_lines(bufnr, max, max, false, text) 376 else 377 local last_line_len = #(get_line(bufnr, math.min(end_row, max - 1)) or '') 378 -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't 379 -- accept it so we should fix it here. 380 if max <= end_row then 381 end_row = max - 1 382 end_col = last_line_len 383 has_eol_text_edit = true 384 else 385 -- If the replacement is over the end of a line (i.e. end_col is equal to the line length and the 386 -- replacement text ends with a newline We can likely assume that the replacement is assumed 387 -- to be meant to replace the newline with another newline and we need to make sure this 388 -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r' 389 -- in the file some servers (clangd on windows) will include that character in the line 390 -- while nvim_buf_set_text doesn't count it as part of the line. 391 if 392 end_col >= last_line_len 393 and text_edit.range['end'].character > end_col 394 and #text_edit.newText > 0 395 and string.sub(text_edit.newText, -1) == '\n' 396 then 397 table.remove(text, #text) 398 end 399 end 400 -- Make sure we don't go out of bounds for end_col 401 end_col = math.min(last_line_len, end_col) 402 403 api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, text) 404 end 405 end 406 end 407 408 --- Track how many times each change annotation is applied to build up the final description. 409 ---@type table<string, integer> 410 local change_count = {} 411 412 -- If there are any annotated text edits, we need to confirm them before applying the edits. 413 local confirmations = {} ---@type table<string, integer> 414 for _, text_edit in ipairs(text_edits) do 415 if text_edit.annotationId then 416 assert( 417 change_annotations ~= nil, 418 'change_annotations must be provided for annotated text edits' 419 ) 420 421 local annotation = assert( 422 change_annotations[text_edit.annotationId], 423 string.format('No change annotation found for ID: %s', text_edit.annotationId) 424 ) 425 426 if annotation.needsConfirmation then 427 confirmations[text_edit.annotationId] = (confirmations[text_edit.annotationId] or 0) + 1 428 end 429 430 change_count[text_edit.annotationId] = (change_count[text_edit.annotationId] or 0) + 1 431 end 432 end 433 434 if next(confirmations) then 435 local message = { 'Apply all changes?' } 436 for id, count in pairs(confirmations) do 437 local annotation = assert(change_annotations)[id] 438 message[#message + 1] = annotation.label 439 .. (annotation.description and (string.format(': %s', annotation.description)) or '') 440 .. (count > 1 and string.format(' (%d)', count) or '') 441 end 442 443 local response = vim.fn.confirm(table.concat(message, '\n'), '&Yes\n&No', 1, 'Question') 444 if response == 1 then 445 -- Proceed with applying text edits. 446 apply_text_edits() 447 else 448 -- Don't apply any text edits. 449 return 450 end 451 else 452 -- No confirmations needed, apply text edits directly. 453 apply_text_edits() 454 end 455 456 if change_annotations ~= nil and next(change_count) then 457 local change_message = { 'Applied changes:' } 458 for id, count in pairs(change_count) do 459 local annotation = change_annotations[id] 460 change_message[#change_message + 1] = annotation.label 461 .. (annotation.description and (': ' .. annotation.description) or '') 462 .. (count > 1 and string.format(' (%d)', count) or '') 463 end 464 vim.notify(table.concat(change_message, '\n'), vim.log.levels.INFO) 465 end 466 467 local max = api.nvim_buf_line_count(bufnr) 468 469 -- no need to restore marks that still exist 470 for _, m in pairs(vim.fn.getmarklist(bufnr)) do 471 marks[m.mark:sub(2, 2)] = nil 472 end 473 -- restore marks 474 for mark, pos in pairs(marks) do 475 if pos then 476 -- make sure we don't go out of bounds 477 pos[1] = math.min(pos[1], max) 478 pos[2] = math.min(pos[2], #(get_line(bufnr, pos[1] - 1) or '')) 479 api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {}) 480 end 481 end 482 483 -- Remove final line if needed 484 local fix_eol = has_eol_text_edit 485 fix_eol = fix_eol and (vim.bo[bufnr].eol or (vim.bo[bufnr].fixeol and not vim.bo[bufnr].binary)) 486 fix_eol = fix_eol and get_line(bufnr, max - 1) == '' 487 if fix_eol then 488 api.nvim_buf_set_lines(bufnr, -2, -1, false, {}) 489 end 490 end 491 492 --- Applies a `TextDocumentEdit`, which is a list of changes to a single 493 --- document. 494 --- 495 ---@param text_document_edit lsp.TextDocumentEdit 496 ---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list) 497 ---@param position_encoding? 'utf-8'|'utf-16'|'utf-32' 498 ---@param change_annotations? table<string, lsp.ChangeAnnotation> 499 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit 500 function M.apply_text_document_edit( 501 text_document_edit, 502 index, 503 position_encoding, 504 change_annotations 505 ) 506 local text_document = text_document_edit.textDocument 507 local bufnr = vim.uri_to_bufnr(text_document.uri) 508 if position_encoding == nil then 509 vim.notify_once( 510 'apply_text_document_edit must be called with valid position encoding', 511 vim.log.levels.WARN 512 ) 513 return 514 end 515 516 -- `VersionedTextDocumentIdentifier`s version may be null 517 -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier 518 if 519 -- For lists of text document edits, 520 -- do not check the version after the first edit. 521 not (index and index > 1) 522 and ( 523 text_document.version ~= vim.NIL 524 and text_document.version > 0 525 and M.buf_versions[bufnr] > text_document.version 526 ) 527 then 528 print('Buffer ', text_document.uri, ' newer than edits.') 529 return 530 end 531 532 M.apply_text_edits(text_document_edit.edits, bufnr, position_encoding, change_annotations) 533 end 534 535 local function path_components(path) 536 return vim.split(path, '/', { plain = true }) 537 end 538 539 --- @param path string[] 540 --- @param prefix string[] 541 --- @return boolean 542 local function path_under_prefix(path, prefix) 543 for i, c in ipairs(prefix) do 544 if c ~= path[i] then 545 return false 546 end 547 end 548 return true 549 end 550 551 --- Get list of loaded writable buffers whose filename matches the given path 552 --- prefix (normalized full path). 553 ---@param prefix string 554 ---@return integer[] 555 local function get_writable_bufs(prefix) 556 local prefix_parts = path_components(prefix) 557 local buffers = {} --- @type integer[] 558 for _, buf in ipairs(api.nvim_list_bufs()) do 559 -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them. 560 if 561 api.nvim_buf_is_loaded(buf) 562 and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[buf].buftype) 563 then 564 local bname = api.nvim_buf_get_name(buf) 565 local path = path_components(vim.fs.normalize(bname, { expand_env = false })) 566 if path_under_prefix(path, prefix_parts) then 567 buffers[#buffers + 1] = buf 568 end 569 end 570 end 571 return buffers 572 end 573 574 local function escape_gsub_repl(s) 575 return (s:gsub('%%', '%%%%')) 576 end 577 578 --- @class vim.lsp.util.rename.Opts 579 --- @inlinedoc 580 --- @field overwrite? boolean 581 --- @field ignoreIfExists? boolean 582 583 --- Rename old_fname to new_fname 584 --- 585 --- Existing buffers are renamed as well, while maintaining their bufnr. 586 --- 587 --- It deletes existing buffers that conflict with the renamed file name only when 588 --- * `opts` requests overwriting; or 589 --- * the conflicting buffers are not loaded, so that deleting them does not result in data loss. 590 --- 591 --- @param old_fname string 592 --- @param new_fname string 593 --- @param opts? vim.lsp.util.rename.Opts Options: 594 function M.rename(old_fname, new_fname, opts) 595 opts = opts or {} 596 local skip = not opts.overwrite or opts.ignoreIfExists 597 598 local old_fname_full = uv.fs_realpath(vim.fs.normalize(old_fname, { expand_env = false })) 599 if not old_fname_full then 600 vim.notify('Invalid path: ' .. old_fname, vim.log.levels.ERROR) 601 return 602 end 603 604 local target_exists = uv.fs_stat(new_fname) ~= nil 605 if target_exists and skip then 606 vim.notify(new_fname .. ' already exists. Skipping rename.', vim.log.levels.ERROR) 607 return 608 end 609 610 local buf_rename = {} ---@type table<integer, {from: string, to: string}> 611 local old_fname_pat = '^' .. vim.pesc(old_fname_full) 612 for _, b in ipairs(get_writable_bufs(old_fname_full)) do 613 -- Renaming a buffer may conflict with another buffer that happens to have the same name. In 614 -- most cases, this would have been already detected by the file conflict check above, but the 615 -- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile" 616 -- or "nowrite", or the buffer can be a normal buffer but has not been written to the file yet. 617 -- Renaming should fail in such cases to avoid losing the contents of the conflicting buffer. 618 local old_bname = api.nvim_buf_get_name(b) 619 local new_bname = old_bname:gsub(old_fname_pat, escape_gsub_repl(new_fname)) 620 if vim.fn.bufexists(new_bname) == 1 then 621 local existing_buf = vim.fn.bufnr(new_bname) 622 if api.nvim_buf_is_loaded(existing_buf) and skip then 623 vim.notify( 624 new_bname .. ' already exists in the buffer list. Skipping rename.', 625 vim.log.levels.ERROR 626 ) 627 return 628 end 629 -- no need to preserve if such a buffer is empty 630 api.nvim_buf_delete(existing_buf, {}) 631 end 632 633 buf_rename[b] = { from = old_bname, to = new_bname } 634 end 635 636 local newdir = vim.fs.dirname(new_fname) 637 vim.fn.mkdir(newdir, 'p') 638 639 local ok, err = os.rename(old_fname_full, new_fname) 640 assert(ok, err) 641 642 local old_undofile = vim.fn.undofile(old_fname_full) 643 if uv.fs_stat(old_undofile) ~= nil then 644 local new_undofile = vim.fn.undofile(new_fname) 645 vim.fn.mkdir(vim.fs.dirname(new_undofile), 'p') 646 os.rename(old_undofile, new_undofile) 647 end 648 649 for b, rename in pairs(buf_rename) do 650 -- Rename with :saveas. This does two things: 651 -- * Unset BF_WRITE_MASK, so that users don't get E13 when they do :write. 652 -- * Send didClose and didOpen via textDocument/didSave handler. 653 vim._with({ buf = b }, function() 654 vim.cmd('keepalt saveas! ' .. vim.fn.fnameescape(rename.to)) 655 end) 656 -- Delete the new buffer with the old name created by :saveas. nvim_buf_delete and 657 -- :bwipeout are futile because the buffer will be added again somewhere else. 658 vim.cmd('bdelete! ' .. vim.fn.bufnr(rename.from)) 659 end 660 end 661 662 --- @param change lsp.CreateFile 663 local function create_file(change) 664 local opts = change.options or {} 665 -- from spec: Overwrite wins over `ignoreIfExists` 666 local fname = vim.uri_to_fname(change.uri) 667 if not opts.ignoreIfExists or opts.overwrite then 668 vim.fn.mkdir(vim.fs.dirname(fname), 'p') 669 local file = io.open(fname, 'w') 670 if file then 671 file:close() 672 end 673 end 674 vim.fn.bufadd(fname) 675 end 676 677 --- @param change lsp.DeleteFile 678 local function delete_file(change) 679 local opts = change.options or {} 680 local fname = vim.uri_to_fname(change.uri) 681 local bufnr = vim.fn.bufadd(fname) 682 vim.fs.rm(fname, { 683 force = opts.ignoreIfNotExists, 684 recursive = opts.recursive, 685 }) 686 api.nvim_buf_delete(bufnr, { force = true }) 687 end 688 689 --- Applies a `WorkspaceEdit`. 690 --- 691 ---@param workspace_edit lsp.WorkspaceEdit 692 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' (required) 693 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit 694 function M.apply_workspace_edit(workspace_edit, position_encoding) 695 if position_encoding == nil then 696 vim.notify_once( 697 'apply_workspace_edit must be called with valid position encoding', 698 vim.log.levels.WARN 699 ) 700 return 701 end 702 if workspace_edit.documentChanges then 703 for idx, change in ipairs(workspace_edit.documentChanges) do 704 if change.kind == 'rename' then 705 local options = change.options --[[@as vim.lsp.util.rename.Opts]] 706 M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), options) 707 elseif change.kind == 'create' then 708 create_file(change) 709 elseif change.kind == 'delete' then 710 delete_file(change) 711 elseif change.kind then --- @diagnostic disable-line:undefined-field 712 error(string.format('Unsupported change: %q', vim.inspect(change))) 713 else 714 M.apply_text_document_edit(change, idx, position_encoding, workspace_edit.changeAnnotations) 715 end 716 end 717 return 718 end 719 720 local all_changes = workspace_edit.changes 721 if not (all_changes and not vim.tbl_isempty(all_changes)) then 722 return 723 end 724 725 for uri, changes in pairs(all_changes) do 726 local bufnr = vim.uri_to_bufnr(uri) 727 M.apply_text_edits(changes, bufnr, position_encoding, workspace_edit.changeAnnotations) 728 end 729 end 730 731 --- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into 732 --- a list of lines containing valid markdown. Useful to populate the hover 733 --- window for `textDocument/hover`, for parsing the result of 734 --- `textDocument/signatureHelp`, and potentially others. 735 --- 736 --- Note that if the input is of type `MarkupContent` and its kind is `plaintext`, 737 --- then the corresponding value is returned without further modifications. 738 --- 739 ---@param input lsp.MarkedString|lsp.MarkedString[]|lsp.MarkupContent 740 ---@param contents string[]? List of strings to extend with converted lines. Defaults to {}. 741 ---@return string[] extended with lines of converted markdown. 742 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover 743 function M.convert_input_to_markdown_lines(input, contents) 744 contents = contents or {} 745 -- MarkedString variation 1 746 if type(input) == 'string' then 747 list_extend(contents, split_lines(input, true)) 748 else 749 assert(type(input) == 'table', 'Expected a table for LSP input') 750 -- MarkupContent 751 if input.kind then 752 local value = input.value or '' 753 list_extend(contents, split_lines(value, true)) 754 -- MarkupString variation 2 755 elseif input.language then 756 table.insert(contents, '```' .. input.language) 757 list_extend(contents, split_lines(input.value or '')) 758 table.insert(contents, '```') 759 -- By deduction, this must be MarkedString[] 760 else 761 -- Use our existing logic to handle MarkedString 762 for _, marked_string in ipairs(input) do 763 M.convert_input_to_markdown_lines(marked_string, contents) 764 end 765 end 766 end 767 if (contents[1] == '' or contents[1] == nil) and #contents == 1 then 768 return {} 769 end 770 return contents 771 end 772 773 --- Returns the line/column-based position in `contents` at the given offset. 774 --- 775 ---@param offset integer 776 ---@param contents string[] 777 ---@return { [1]: integer, [2]: integer }? 778 local function get_pos_from_offset(offset, contents) 779 local i = 0 780 for l, line in ipairs(contents) do 781 if offset >= i and offset < i + #line then 782 return { l - 1, offset - i + 1 } 783 else 784 i = i + #line + 1 785 end 786 end 787 end 788 789 --- Converts `textDocument/signatureHelp` response to markdown lines. 790 --- 791 ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp` 792 ---@param ft string? filetype that will be use as the `lang` for the label markdown code block 793 ---@param triggers string[]? list of trigger characters from the lsp server. used to better determine parameter offsets 794 ---@return string[]? # lines of converted markdown. 795 ---@return Range4? # highlight range for the active parameter 796 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp 797 function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers) 798 --The active signature. If omitted or the value lies outside the range of 799 --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`. 800 --Whenever possible implementors should make an active decision about 801 --the active signature and shouldn't rely on a default value. 802 local contents = {} --- @type string[] 803 local active_offset ---@type [integer, integer]? 804 local active_signature = signature_help.activeSignature or 0 805 -- If the activeSignature is not inside the valid range, then clip it. 806 -- In 3.15 of the protocol, activeSignature was allowed to be negative 807 if active_signature >= #signature_help.signatures or active_signature < 0 then 808 active_signature = 0 809 end 810 local signature = vim.deepcopy(signature_help.signatures[active_signature + 1]) 811 local label = signature.label 812 if ft then 813 -- wrap inside a code block for proper rendering 814 label = ('```%s\n%s\n```'):format(ft, label) 815 end 816 list_extend(contents, vim.split(label, '\n', { plain = true, trimempty = true })) 817 local doc = signature.documentation 818 if doc then 819 -- if LSP returns plain string, we treat it as plaintext. This avoids 820 -- special characters like underscore or similar from being interpreted 821 -- as markdown font modifiers 822 if type(doc) == 'string' then 823 signature.documentation = { kind = 'plaintext', value = doc } 824 end 825 -- Add delimiter if there is documentation to display 826 if signature.documentation.value ~= '' then 827 contents[#contents + 1] = '---' 828 end 829 M.convert_input_to_markdown_lines(signature.documentation, contents) 830 end 831 if signature.parameters and #signature.parameters > 0 then 832 local active_parameter = signature.activeParameter or signature_help.activeParameter 833 834 -- NOTE: We intentionally violate the LSP spec, which states that if `activeParameter` 835 -- is not provided or is out-of-bounds, it should default to 0. 836 -- Instead, we default to `nil`, as most clients do. In practice, 'no active parameter' 837 -- is better default than 'first parameter' and aligns better with user expectations. 838 -- Related discussion: https://github.com/microsoft/language-server-protocol/issues/1271 839 if 840 not active_parameter 841 or active_parameter == vim.NIL 842 or active_parameter < 0 843 or active_parameter >= #signature.parameters 844 then 845 return contents, nil 846 end 847 848 local parameter = signature.parameters[active_parameter + 1] 849 local parameter_label = parameter.label 850 if type(parameter_label) == 'table' then 851 active_offset = parameter_label 852 else 853 local offset = 1 ---@type integer? 854 -- try to set the initial offset to the first found trigger character 855 for _, t in ipairs(triggers or {}) do 856 local trigger_offset = signature.label:find(t, 1, true) 857 if trigger_offset and (offset == 1 or trigger_offset < offset) then 858 offset = trigger_offset 859 end 860 end 861 for p, param in pairs(signature.parameters) do 862 local plabel = param.label 863 assert(type(plabel) == 'string', 'Expected label to be a string') 864 offset = signature.label:find(plabel, offset, true) 865 if not offset then 866 break 867 end 868 if p == active_parameter + 1 then 869 active_offset = { offset - 1, offset + #parameter_label - 1 } 870 break 871 end 872 offset = offset + #plabel + 1 873 end 874 end 875 if parameter.documentation then 876 M.convert_input_to_markdown_lines(parameter.documentation, contents) 877 end 878 end 879 880 local active_hl = nil 881 if active_offset then 882 -- Account for the start of the markdown block. 883 if ft then 884 active_offset[1] = active_offset[1] + #contents[1] 885 active_offset[2] = active_offset[2] + #contents[1] 886 end 887 888 local a_start = get_pos_from_offset(active_offset[1], contents) 889 local a_end = get_pos_from_offset(active_offset[2], contents) 890 if a_start and a_end then 891 active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] } 892 end 893 end 894 895 return contents, active_hl 896 end 897 898 --- Creates a table with sensible default options for a floating window. The 899 --- table can be passed to |nvim_open_win()|. 900 --- 901 ---@param width integer window width (in character cells) 902 ---@param height integer window height (in character cells) 903 ---@param opts? vim.lsp.util.open_floating_preview.Opts 904 ---@return vim.api.keyset.win_config 905 function M.make_floating_popup_options(width, height, opts) 906 validate('opts', opts, 'table', true) 907 opts = opts or {} 908 validate('opts.offset_x', opts.offset_x, 'number', true) 909 validate('opts.offset_y', opts.offset_y, 'number', true) 910 911 local anchor = '' 912 913 local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1 914 or vim.fn.winline() - 1 915 local lines_below = vim.fn.winheight(0) - lines_above 916 917 local anchor_bias = opts.anchor_bias or 'auto' 918 919 local anchor_below --- @type boolean? 920 921 if anchor_bias == 'below' then 922 anchor_below = (lines_below > lines_above) or (height <= lines_below) 923 elseif anchor_bias == 'above' then 924 local anchor_above = (lines_above > lines_below) or (height <= lines_above) 925 anchor_below = not anchor_above 926 else 927 anchor_below = lines_below > lines_above 928 end 929 930 local border_height = get_border_size(opts) 931 local row, col --- @type integer?, integer? 932 if anchor_below then 933 anchor = anchor .. 'N' 934 height = math.max(math.min(lines_below - border_height, height), 0) 935 row = 1 936 else 937 anchor = anchor .. 'S' 938 height = math.max(math.min(lines_above - border_height, height), 0) 939 row = 0 940 end 941 942 local wincol = opts.relative == 'mouse' and vim.fn.getmousepos().column or vim.fn.wincol() 943 944 if wincol + width + (opts.offset_x or 0) <= vim.o.columns then 945 anchor = anchor .. 'W' 946 col = 0 947 else 948 anchor = anchor .. 'E' 949 col = 1 950 end 951 952 local title = ((opts.border or vim.o.winborder ~= '') and opts.title) and opts.title or nil 953 local title_pos --- @type 'left'|'center'|'right'? 954 955 if title then 956 title_pos = opts.title_pos or 'center' 957 end 958 959 return { 960 anchor = anchor, 961 row = row + (opts.offset_y or 0), 962 col = col + (opts.offset_x or 0), 963 height = height, 964 focusable = opts.focusable, 965 relative = (opts.relative == 'mouse' or opts.relative == 'editor') and opts.relative 966 or 'cursor', 967 style = 'minimal', 968 width = width, 969 border = opts.border, 970 zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1, 971 title = title, 972 title_pos = title_pos, 973 } 974 end 975 976 --- @class vim.lsp.util.show_document.Opts 977 --- @inlinedoc 978 --- 979 --- Jump to existing window if buffer is already open. 980 --- @field reuse_win? boolean 981 --- 982 --- Whether to focus/jump to location if possible. 983 --- (defaults: true) 984 --- @field focus? boolean 985 986 --- Shows document and optionally jumps to the location. 987 --- 988 ---@param location lsp.Location|lsp.LocationLink 989 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'? 990 ---@param opts? vim.lsp.util.show_document.Opts 991 ---@return boolean `true` if succeeded 992 function M.show_document(location, position_encoding, opts) 993 -- location may be Location or LocationLink 994 local uri = location.uri or location.targetUri 995 if uri == nil then 996 return false 997 end 998 if position_encoding == nil then 999 vim.notify_once( 1000 'show_document must be called with valid position encoding', 1001 vim.log.levels.WARN 1002 ) 1003 return false 1004 end 1005 local bufnr = vim.uri_to_bufnr(uri) 1006 1007 opts = opts or {} 1008 local focus = vim.F.if_nil(opts.focus, true) 1009 if focus then 1010 -- Save position in jumplist 1011 vim.cmd("normal! m'") 1012 1013 -- Push a new item into tagstack 1014 local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 } 1015 local items = { { tagname = vim.fn.expand('<cword>'), from = from } } 1016 vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't') 1017 end 1018 1019 local win = opts.reuse_win and vim.fn.win_findbuf(bufnr)[1] 1020 or focus and api.nvim_get_current_win() 1021 or create_window_without_focus() 1022 1023 vim.bo[bufnr].buflisted = true 1024 api.nvim_win_set_buf(win, bufnr) 1025 if focus then 1026 api.nvim_set_current_win(win) 1027 end 1028 1029 -- location may be Location or LocationLink 1030 local range = location.range or location.targetSelectionRange 1031 if range then 1032 -- Jump to new location (adjusting for encoding of characters) 1033 local row = range.start.line 1034 local col = get_line_byte_from_position(bufnr, range.start, position_encoding) 1035 api.nvim_win_set_cursor(win, { row + 1, col }) 1036 vim._with({ win = win }, function() 1037 -- Open folds under the cursor 1038 vim.cmd('normal! zv') 1039 end) 1040 end 1041 1042 return true 1043 end 1044 1045 --- Jumps to a location. 1046 --- 1047 ---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead 1048 ---@param location lsp.Location|lsp.LocationLink 1049 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'? 1050 ---@param reuse_win boolean? Jump to existing window if buffer is already open. 1051 ---@return boolean `true` if the jump succeeded 1052 function M.jump_to_location(location, position_encoding, reuse_win) 1053 vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12') 1054 return M.show_document(location, position_encoding, { reuse_win = reuse_win, focus = true }) 1055 end 1056 1057 --- Previews a location in a floating window 1058 --- 1059 --- behavior depends on type of location: 1060 --- - for Location, range is shown (e.g., function definition) 1061 --- - for LocationLink, targetRange is shown (e.g., body of function definition) 1062 --- 1063 ---@param location lsp.Location|lsp.LocationLink 1064 ---@param opts? vim.lsp.util.open_floating_preview.Opts 1065 ---@return integer? buffer id of float window 1066 ---@return integer? window id of float window 1067 function M.preview_location(location, opts) 1068 -- location may be LocationLink or Location (more useful for the former) 1069 local uri = location.targetUri or location.uri 1070 if uri == nil then 1071 return 1072 end 1073 local bufnr = vim.uri_to_bufnr(uri) 1074 if not api.nvim_buf_is_loaded(bufnr) then 1075 vim.fn.bufload(bufnr) 1076 end 1077 local range = location.targetRange or location.range 1078 local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range['end'].line + 1, false) 1079 local syntax = vim.bo[bufnr].syntax 1080 if syntax == '' then 1081 -- When no syntax is set, we use filetype as fallback. This might not result 1082 -- in a valid syntax definition. 1083 -- An empty syntax is more common now with TreeSitter, since TS disables syntax. 1084 syntax = vim.bo[bufnr].filetype 1085 end 1086 opts = opts or {} 1087 opts.focus_id = 'location' 1088 return M.open_floating_preview(contents, syntax, opts) 1089 end 1090 1091 local function find_window_by_var(name, value) 1092 for _, win in ipairs(api.nvim_list_wins()) do 1093 if vim.w[win][name] == value then 1094 return win 1095 end 1096 end 1097 end 1098 1099 ---Returns true if the line is empty or only contains whitespace. 1100 ---@param line string 1101 ---@return boolean 1102 local function is_blank_line(line) 1103 return line and line:match('^%s*$') 1104 end 1105 1106 ---Returns true if the line corresponds to a Markdown thematic break. 1107 ---@see https://github.github.com/gfm/#thematic-break 1108 ---@param line string 1109 ---@return boolean 1110 local function is_separator_line(line) 1111 local i = 1 1112 -- 1. Skip up to 3 leading spaces 1113 local leading_spaces = 3 1114 while i <= #line and line:byte(i) == string.byte(' ') and leading_spaces > 0 do 1115 i = i + 1 1116 leading_spaces = leading_spaces - 1 1117 end 1118 -- 2. Determine the delimiter character 1119 local delimiter = line:byte(i) -- nil if i > #line 1120 if 1121 delimiter ~= string.byte('-') 1122 and delimiter ~= string.byte('_') 1123 and delimiter ~= string.byte('*') 1124 then 1125 return false 1126 end 1127 local ndelimiters = 1 1128 i = i + 1 1129 -- 3. Iterate until found non-whitespace or other than expected delimiter 1130 while i <= #line do 1131 local char = line:byte(i) 1132 if char == delimiter then 1133 ndelimiters = ndelimiters + 1 1134 elseif not (char == string.byte(' ') or char == string.byte('\t')) then 1135 return false 1136 end 1137 i = i + 1 1138 end 1139 return ndelimiters >= 3 1140 end 1141 1142 ---Replaces separator lines by the given divider and removing surrounding blank lines. 1143 ---@param contents string[] 1144 ---@param divider string 1145 ---@return string[] 1146 local function replace_separators(contents, divider) 1147 local trimmed = {} 1148 local l = 1 1149 while l <= #contents do 1150 local line = contents[l] 1151 if is_separator_line(line) then 1152 if l > 1 and is_blank_line(contents[l - 1]) then 1153 table.remove(trimmed) 1154 end 1155 table.insert(trimmed, divider) 1156 if is_blank_line(contents[l + 1]) then 1157 l = l + 1 1158 end 1159 else 1160 table.insert(trimmed, line) 1161 end 1162 l = l + 1 1163 end 1164 1165 return trimmed 1166 end 1167 1168 ---Collapses successive blank lines in the input table into a single one. 1169 ---@param contents string[] 1170 ---@return string[] 1171 local function collapse_blank_lines(contents) 1172 local collapsed = {} 1173 local l = 1 1174 while l <= #contents do 1175 local line = contents[l] 1176 if is_blank_line(line) then 1177 while is_blank_line(contents[l + 1]) do 1178 l = l + 1 1179 end 1180 end 1181 table.insert(collapsed, line) 1182 l = l + 1 1183 end 1184 return collapsed 1185 end 1186 1187 local function get_markdown_fences() 1188 local fences = {} --- @type table<string,string> 1189 for _, fence in 1190 pairs(vim.g.markdown_fenced_languages or {} --[[@as string[] ]]) 1191 do 1192 local lang, syntax = fence:match('^(.*)=(.*)$') 1193 if lang then 1194 fences[lang] = syntax 1195 end 1196 end 1197 return fences 1198 end 1199 1200 --- @deprecated 1201 --- Converts markdown into syntax highlighted regions by stripping the code 1202 --- blocks and converting them into highlighted code. 1203 --- This will by default insert a blank line separator after those code block 1204 --- regions to improve readability. 1205 --- 1206 --- This method configures the given buffer and returns the lines to set. 1207 --- 1208 --- If you want to open a popup with fancy markdown, use `open_floating_preview` instead 1209 --- 1210 ---@param bufnr integer 1211 ---@param contents string[] of lines to show in window 1212 ---@param opts? table with optional fields 1213 --- - height of floating window 1214 --- - width of floating window 1215 --- - wrap_at character to wrap at for computing height 1216 --- - max_width maximal width of floating window 1217 --- - max_height maximal height of floating window 1218 --- - separator insert separator after code block 1219 ---@return table stripped content 1220 function M.stylize_markdown(bufnr, contents, opts) 1221 vim.deprecate('vim.lsp.util.stylize_markdown', nil, '0.14') 1222 validate('contents', contents, 'table') 1223 validate('opts', opts, 'table', true) 1224 opts = opts or {} 1225 1226 -- table of fence types to {ft, begin, end} 1227 -- when ft is nil, we get the ft from the regex match 1228 local matchers = { 1229 block = { nil, '```+%s*([a-zA-Z0-9_]*)', '```+' }, 1230 pre = { nil, '<pre>([a-z0-9]*)', '</pre>' }, 1231 code = { '', '<code>', '</code>' }, 1232 text = { 'text', '<text>', '</text>' }, 1233 } 1234 1235 --- @param line string 1236 --- @return {type:string,ft:string}? 1237 local function match_begin(line) 1238 for type, pattern in pairs(matchers) do 1239 --- @type string? 1240 local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2])) 1241 if ret then 1242 return { 1243 type = type, 1244 ft = pattern[1] or ret, 1245 } 1246 end 1247 end 1248 end 1249 1250 --- @param line string 1251 --- @param match {type:string,ft:string} 1252 --- @return string 1253 local function match_end(line, match) 1254 local pattern = matchers[match.type] 1255 return line:match(string.format('^%%s*%s%%s*$', pattern[3])) 1256 end 1257 1258 -- Clean up 1259 contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) 1260 1261 local stripped = {} --- @type string[] 1262 local highlights = {} --- @type {ft:string,start:integer,finish:integer}[] 1263 1264 local i = 1 1265 while i <= #contents do 1266 local line = contents[i] 1267 local match = match_begin(line) 1268 if match then 1269 local start = #stripped 1270 i = i + 1 1271 while i <= #contents do 1272 line = contents[i] 1273 if match_end(line, match) then 1274 i = i + 1 1275 break 1276 end 1277 stripped[#stripped + 1] = line 1278 i = i + 1 1279 end 1280 table.insert(highlights, { 1281 ft = match.ft, 1282 start = start + 1, 1283 finish = #stripped, 1284 }) 1285 -- add a separator, but not on the last line 1286 if opts.separator and i < #contents then 1287 stripped[#stripped + 1] = '---' 1288 end 1289 else 1290 -- strip any empty lines or separators prior to this separator in actual markdown 1291 if line:match('^---+$') then 1292 while 1293 stripped[#stripped] 1294 and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$')) 1295 do 1296 stripped[#stripped] = nil 1297 end 1298 end 1299 -- add the line if its not an empty line following a separator 1300 if 1301 not (line:match('^%s*$') and stripped[#stripped] and stripped[#stripped]:match('^---+$')) 1302 then 1303 stripped[#stripped + 1] = line 1304 end 1305 i = i + 1 1306 end 1307 end 1308 1309 -- Handle some common html escape sequences 1310 --- @type string[] 1311 stripped = vim.tbl_map( 1312 --- @param line string 1313 function(line) 1314 local escapes = { 1315 ['>'] = '>', 1316 ['<'] = '<', 1317 ['"'] = '"', 1318 ['''] = "'", 1319 [' '] = ' ', 1320 [' '] = ' ', 1321 ['&'] = '&', 1322 } 1323 return (line:gsub('&[^ ;]+;', escapes)) 1324 end, 1325 stripped 1326 ) 1327 1328 -- Compute size of float needed to show (wrapped) lines 1329 opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0)) 1330 local width = M._make_floating_popup_size(stripped, opts) 1331 1332 local sep_line = string.rep('─', math.min(width, opts.wrap_at or width)) 1333 1334 for l in ipairs(stripped) do 1335 if stripped[l]:match('^---+$') then 1336 stripped[l] = sep_line 1337 end 1338 end 1339 1340 api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) 1341 1342 local idx = 1 1343 -- keep track of syntaxes we already included. 1344 -- no need to include the same syntax more than once 1345 local langs = {} --- @type table<string,boolean> 1346 local fences = get_markdown_fences() 1347 local function apply_syntax_to_region(ft, start, finish) 1348 if ft == '' then 1349 vim.cmd( 1350 string.format( 1351 'syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend', 1352 start, 1353 finish + 1 1354 ) 1355 ) 1356 return 1357 end 1358 ft = fences[ft] or ft 1359 local name = ft .. idx 1360 idx = idx + 1 1361 local lang = '@' .. ft:upper() 1362 if not langs[lang] then 1363 -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set 1364 pcall(api.nvim_buf_del_var, bufnr, 'current_syntax') 1365 if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then 1366 return 1367 end 1368 --- @diagnostic disable-next-line:param-type-mismatch 1369 pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) 1370 langs[lang] = true 1371 end 1372 vim.cmd( 1373 string.format( 1374 'syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend', 1375 name, 1376 start, 1377 finish + 1, 1378 lang 1379 ) 1380 ) 1381 end 1382 1383 -- needs to run in the buffer for the regions to work 1384 vim._with({ buf = bufnr }, function() 1385 -- we need to apply lsp_markdown regions speperately, since otherwise 1386 -- markdown regions can "bleed" through the other syntax regions 1387 -- and mess up the formatting 1388 local last = 1 1389 for _, h in ipairs(highlights) do 1390 if last < h.start then 1391 apply_syntax_to_region('lsp_markdown', last, h.start - 1) 1392 end 1393 apply_syntax_to_region(h.ft, h.start, h.finish) 1394 last = h.finish + 1 1395 end 1396 if last <= #stripped then 1397 apply_syntax_to_region('lsp_markdown', last, #stripped) 1398 end 1399 end) 1400 1401 return stripped 1402 end 1403 1404 --- @class (private) vim.lsp.util._normalize_markdown.Opts 1405 --- @field width integer Thematic breaks are expanded to this size. Defaults to 80. 1406 1407 --- Normalizes Markdown input to a canonical form. 1408 --- 1409 --- The returned Markdown adheres to the GitHub Flavored Markdown (GFM) 1410 --- specification, as required by the LSP. 1411 --- 1412 --- The following transformations are made: 1413 --- 1414 --- 1. Carriage returns ('\r') and empty lines at the beginning and end are removed 1415 --- 2. Successive empty lines are collapsed into a single empty line 1416 --- 3. Thematic breaks are expanded to the given width 1417 --- 1418 ---@param contents string[] 1419 ---@param opts? vim.lsp.util._normalize_markdown.Opts 1420 ---@return string[] table of lines containing normalized Markdown 1421 ---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#markupContent 1422 ---@see https://github.github.com/gfm 1423 function M._normalize_markdown(contents, opts) 1424 validate('contents', contents, 'table') 1425 validate('opts', opts, 'table', true) 1426 opts = opts or {} 1427 1428 -- 1. Carriage returns are removed 1429 contents = vim.split(table.concat(contents, '\n'):gsub('\r', ''), '\n', { trimempty = true }) 1430 1431 -- 2. Successive empty lines are collapsed into a single empty line 1432 contents = collapse_blank_lines(contents) 1433 1434 -- 3. Thematic breaks are expanded to the given width 1435 local divider = string.rep('─', opts.width or 80) 1436 contents = replace_separators(contents, divider) 1437 1438 return contents 1439 end 1440 1441 --- Closes the preview window 1442 --- 1443 ---@param winnr integer window id of preview window 1444 ---@param bufnrs table? optional list of ignored buffers 1445 local function close_preview_window(winnr, bufnrs) 1446 vim.schedule(function() 1447 -- exit if we are in one of ignored buffers 1448 if bufnrs and vim.list_contains(bufnrs, api.nvim_get_current_buf()) then 1449 return 1450 end 1451 1452 local augroup = 'nvim.preview_window_' .. winnr 1453 pcall(api.nvim_del_augroup_by_name, augroup) 1454 pcall(api.nvim_win_close, winnr, true) 1455 end) 1456 end 1457 1458 --- Creates autocommands to close a preview window when events happen. 1459 --- 1460 ---@param events table list of events 1461 ---@param winnr integer window id of preview window 1462 ---@param floating_bufnr integer floating preview buffer 1463 ---@param bufnr integer buffer that opened the floating preview buffer 1464 ---@see autocmd-events 1465 local function close_preview_autocmd(events, winnr, floating_bufnr, bufnr) 1466 local augroup = api.nvim_create_augroup('nvim.preview_window_' .. winnr, { 1467 clear = true, 1468 }) 1469 1470 -- close the preview window when entered a buffer that is not 1471 -- the floating window buffer or the buffer that spawned it 1472 api.nvim_create_autocmd('BufLeave', { 1473 group = augroup, 1474 buffer = bufnr, 1475 callback = function() 1476 vim.schedule(function() 1477 -- When jumping to the quickfix window from the preview window, 1478 -- do not close the preview window. 1479 if api.nvim_get_option_value('filetype', { buf = 0 }) ~= 'qf' then 1480 close_preview_window(winnr, { floating_bufnr, bufnr }) 1481 end 1482 end) 1483 end, 1484 }) 1485 1486 if #events > 0 then 1487 api.nvim_create_autocmd(events, { 1488 group = augroup, 1489 buffer = bufnr, 1490 callback = function() 1491 close_preview_window(winnr) 1492 end, 1493 }) 1494 end 1495 end 1496 1497 --- Computes size of float needed to show contents (with optional wrapping) 1498 --- 1499 ---@param contents string[] of lines to show in window 1500 ---@param opts? vim.lsp.util.open_floating_preview.Opts 1501 ---@return integer width size of float 1502 ---@return integer height size of float 1503 function M._make_floating_popup_size(contents, opts) 1504 validate('contents', contents, 'table') 1505 validate('opts', opts, 'table', true) 1506 opts = opts or {} 1507 1508 local width = opts.width 1509 local height = opts.height 1510 local wrap_at = opts.wrap_at 1511 local max_width = opts.max_width 1512 local max_height = opts.max_height 1513 local line_widths = {} --- @type table<integer,integer> 1514 1515 if not width then 1516 width = 0 1517 for i, line in ipairs(contents) do 1518 -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. 1519 line_widths[i] = vim.fn.strdisplaywidth(line:gsub('%z', '\n')) 1520 width = math.max(line_widths[i], width) 1521 end 1522 end 1523 1524 local _, border_width = get_border_size(opts) 1525 local screen_width = api.nvim_win_get_width(0) 1526 width = math.min(width, screen_width) 1527 1528 -- make sure borders are always inside the screen 1529 width = math.min(width, screen_width - border_width) 1530 1531 -- Make sure that the width is large enough to fit the title. 1532 local title_length = 0 1533 local chunks = type(opts.title) == 'string' and { { opts.title } } or opts.title or {} 1534 for _, chunk in 1535 ipairs(chunks --[=[@as [string, string][]]=]) 1536 do 1537 title_length = title_length + vim.fn.strdisplaywidth(chunk[1]) 1538 end 1539 1540 width = math.max(width, title_length) 1541 1542 if wrap_at then 1543 wrap_at = math.min(wrap_at, width) 1544 end 1545 1546 if max_width then 1547 width = math.min(width, max_width) 1548 wrap_at = math.min(wrap_at or max_width, max_width) 1549 end 1550 1551 if not height then 1552 height = #contents 1553 if wrap_at and width >= wrap_at then 1554 height = 0 1555 if vim.tbl_isempty(line_widths) then 1556 for _, line in ipairs(contents) do 1557 local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n')) 1558 height = height + math.max(1, math.ceil(line_width / wrap_at)) 1559 end 1560 else 1561 for i = 1, #contents do 1562 height = height + math.max(1, math.ceil(line_widths[i] / wrap_at)) 1563 end 1564 end 1565 end 1566 end 1567 if max_height then 1568 height = math.min(height, max_height) 1569 end 1570 1571 return width, height 1572 end 1573 1574 --- @class vim.lsp.util.open_floating_preview.Opts 1575 --- 1576 --- Height of floating window 1577 --- @field height? integer 1578 --- 1579 --- Width of floating window 1580 --- @field width? integer 1581 --- 1582 --- Wrap long lines 1583 --- (default: `true`) 1584 --- @field wrap? boolean 1585 --- 1586 --- Character to wrap at for computing height when wrap is enabled 1587 --- @field wrap_at? integer 1588 --- 1589 --- Maximal width of floating window 1590 --- @field max_width? integer 1591 --- 1592 --- Maximal height of floating window 1593 --- @field max_height? integer 1594 --- 1595 --- If a popup with this id is opened, then focus it 1596 --- @field focus_id? string 1597 --- 1598 --- List of events that closes the floating window 1599 --- @field close_events? table 1600 --- 1601 --- Make float focusable. 1602 --- (default: `true`) 1603 --- @field focusable? boolean 1604 --- 1605 --- If `true`, and if {focusable} is also `true`, focus an existing floating 1606 --- window with the same {focus_id} 1607 --- (default: `true`) 1608 --- @field focus? boolean 1609 --- 1610 --- offset to add to `col` 1611 --- @field offset_x? integer 1612 --- 1613 --- offset to add to `row` 1614 --- @field offset_y? integer 1615 --- @field border? string|(string|[string,string])[] override `border` 1616 --- @field zindex? integer override `zindex`, defaults to 50 1617 --- @field title? string|[string,string][] 1618 --- @field title_pos? 'left'|'center'|'right' 1619 --- 1620 --- (default: `'cursor'`) 1621 --- @field relative? 'mouse'|'cursor'|'editor' 1622 --- 1623 --- Adjusts placement relative to cursor. 1624 --- - "auto": place window based on which side of the cursor has more lines 1625 --- - "above": place the window above the cursor unless there are not enough lines 1626 --- to display the full window height. 1627 --- - "below": place the window below the cursor unless there are not enough lines 1628 --- to display the full window height. 1629 --- (default: `'auto'`) 1630 --- @field anchor_bias? 'auto'|'above'|'below' 1631 --- 1632 --- @field _update_win? integer 1633 1634 --- Shows contents in a floating window. 1635 --- 1636 ---@param contents table of lines to show in window 1637 ---@param syntax string of syntax to set for opened buffer 1638 ---@param opts? vim.lsp.util.open_floating_preview.Opts with optional fields 1639 --- (additional keys are filtered with |vim.lsp.util.make_floating_popup_options()| 1640 --- before they are passed on to |nvim_open_win()|) 1641 ---@return integer bufnr of newly created float window 1642 ---@return integer winid of newly created float window preview window 1643 function M.open_floating_preview(contents, syntax, opts) 1644 validate('contents', contents, 'table') 1645 validate('syntax', syntax, 'string', true) 1646 validate('opts', opts, 'table', true) 1647 opts = opts or {} 1648 opts.wrap = opts.wrap ~= false -- wrapping by default 1649 opts.focus = opts.focus ~= false 1650 opts.close_events = opts.close_events or { 'CursorMoved', 'CursorMovedI', 'InsertCharPre' } 1651 1652 local bufnr = api.nvim_get_current_buf() 1653 1654 local floating_winnr = opts._update_win 1655 1656 -- Create/get the buffer 1657 local floating_bufnr --- @type integer 1658 if floating_winnr then 1659 floating_bufnr = api.nvim_win_get_buf(floating_winnr) 1660 else 1661 -- check if this popup is focusable and we need to focus 1662 if opts.focus_id and opts.focusable ~= false and opts.focus then 1663 -- Go back to previous window if we are in a focusable one 1664 local current_winnr = api.nvim_get_current_win() 1665 if vim.w[current_winnr][opts.focus_id] then 1666 api.nvim_command('wincmd p') 1667 return bufnr, current_winnr 1668 end 1669 do 1670 local win = find_window_by_var(opts.focus_id, bufnr) 1671 if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then 1672 -- focus and return the existing buf, win 1673 api.nvim_set_current_win(win) 1674 api.nvim_command('stopinsert') 1675 return api.nvim_win_get_buf(win), win 1676 end 1677 end 1678 end 1679 1680 -- check if another floating preview already exists for this buffer 1681 -- and close it if needed 1682 local existing_float = vim.b[bufnr].lsp_floating_preview 1683 if existing_float and api.nvim_win_is_valid(existing_float) then 1684 api.nvim_win_close(existing_float, true) 1685 end 1686 floating_bufnr = api.nvim_create_buf(false, true) 1687 end 1688 1689 -- Set up the contents, using treesitter for markdown 1690 local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil 1691 1692 if do_stylize then 1693 local width = M._make_floating_popup_size(contents, opts) 1694 contents = M._normalize_markdown(contents, { width = width }) 1695 else 1696 -- Clean up input: trim empty lines 1697 contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) 1698 1699 if syntax then 1700 vim.bo[floating_bufnr].syntax = syntax 1701 end 1702 end 1703 1704 vim.bo[floating_bufnr].modifiable = true 1705 api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) 1706 1707 if floating_winnr then 1708 api.nvim_win_set_config(floating_winnr, { 1709 border = opts.border, 1710 title = opts.title, 1711 }) 1712 else 1713 -- Compute size of float needed to show (wrapped) lines 1714 if opts.wrap then 1715 opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0) 1716 else 1717 opts.wrap_at = nil 1718 end 1719 1720 -- TODO(lewis6991): These function assume the current window to determine options, 1721 -- therefore it won't work for opts._update_win and the current window if the floating 1722 -- window 1723 local width, height = M._make_floating_popup_size(contents, opts) 1724 local float_option = M.make_floating_popup_options(width, height, opts) 1725 1726 floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) 1727 1728 api.nvim_buf_set_keymap( 1729 floating_bufnr, 1730 'n', 1731 'q', 1732 '<cmd>bdelete<cr>', 1733 { silent = true, noremap = true, nowait = true } 1734 ) 1735 close_preview_autocmd(opts.close_events, floating_winnr, floating_bufnr, bufnr) 1736 1737 -- save focus_id 1738 if opts.focus_id then 1739 api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) 1740 end 1741 api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) 1742 api.nvim_win_set_var(floating_winnr, 'lsp_floating_bufnr', bufnr) 1743 end 1744 1745 api.nvim_create_autocmd('WinClosed', { 1746 group = api.nvim_create_augroup('nvim.closing_floating_preview', { clear = true }), 1747 callback = function(args) 1748 local winid = tonumber(args.match) 1749 local ok, preview_bufnr = pcall(api.nvim_win_get_var, winid, 'lsp_floating_bufnr') 1750 if 1751 ok 1752 and api.nvim_buf_is_valid(preview_bufnr) 1753 and winid == vim.b[preview_bufnr].lsp_floating_preview 1754 then 1755 vim.b[bufnr].lsp_floating_preview = nil 1756 return true 1757 end 1758 end, 1759 }) 1760 1761 vim.wo[floating_winnr].foldenable = false -- Disable folding. 1762 vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping. 1763 vim.wo[floating_winnr].linebreak = true -- Break lines a bit nicer 1764 vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation. 1765 vim.wo[floating_winnr].smoothscroll = true -- Scroll by screen-line instead of buffer-line. 1766 1767 vim.bo[floating_bufnr].modifiable = false 1768 vim.bo[floating_bufnr].bufhidden = 'wipe' 1769 1770 if do_stylize then 1771 vim.wo[floating_winnr].conceallevel = 2 1772 vim.wo[floating_winnr].concealcursor = '' 1773 vim.bo[floating_bufnr].filetype = 'markdown' 1774 vim.treesitter.start(floating_bufnr) 1775 if not opts.height then 1776 -- Reduce window height if TS highlighter conceals code block backticks. 1777 local win_height = api.nvim_win_get_height(floating_winnr) 1778 local text_height = api.nvim_win_text_height(floating_winnr, { max_height = win_height }).all 1779 if text_height < win_height then 1780 api.nvim_win_set_height(floating_winnr, text_height) 1781 end 1782 end 1783 end 1784 1785 return floating_bufnr, floating_winnr 1786 end 1787 1788 do --[[ References ]] 1789 local reference_ns = api.nvim_create_namespace('nvim.lsp.references') 1790 1791 --- Removes document highlights from a buffer. 1792 --- 1793 ---@param bufnr integer? Buffer id 1794 function M.buf_clear_references(bufnr) 1795 api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) 1796 end 1797 1798 --- Shows a list of document highlights for a certain buffer. 1799 --- 1800 ---@param bufnr integer Buffer id 1801 ---@param references lsp.DocumentHighlight[] objects to highlight 1802 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 1803 ---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent 1804 function M.buf_highlight_references(bufnr, references, position_encoding) 1805 validate('bufnr', bufnr, 'number', true) 1806 validate('position_encoding', position_encoding, 'string', false) 1807 for _, reference in ipairs(references) do 1808 local range = reference.range 1809 local start_line = range.start.line 1810 local end_line = range['end'].line 1811 1812 local start_idx = get_line_byte_from_position(bufnr, range.start, position_encoding) 1813 local end_idx = get_line_byte_from_position(bufnr, range['end'], position_encoding) 1814 1815 local document_highlight_kind = { 1816 [protocol.DocumentHighlightKind.Text] = 'LspReferenceText', 1817 [protocol.DocumentHighlightKind.Read] = 'LspReferenceRead', 1818 [protocol.DocumentHighlightKind.Write] = 'LspReferenceWrite', 1819 } 1820 local kind = reference['kind'] or protocol.DocumentHighlightKind.Text 1821 vim.hl.range( 1822 bufnr, 1823 reference_ns, 1824 document_highlight_kind[kind], 1825 { start_line, start_idx }, 1826 { end_line, end_idx }, 1827 { priority = vim.hl.priorities.user } 1828 ) 1829 end 1830 end 1831 end 1832 1833 local position_sort = sort_by_key(function(v) 1834 return { v.start.line, v.start.character } 1835 end) 1836 1837 --- Returns the items with the byte position calculated correctly and in sorted 1838 --- order, for display in quickfix and location lists. 1839 --- 1840 --- The `user_data` field of each resulting item will contain the original 1841 --- `Location` or `LocationLink` it was computed from. 1842 --- 1843 --- The result can be passed to the {list} argument of |setqflist()| or 1844 --- |setloclist()|. 1845 --- 1846 ---@param locations lsp.Location[]|lsp.LocationLink[] 1847 ---@param position_encoding? 'utf-8'|'utf-16'|'utf-32' 1848 --- default to first client of buffer 1849 ---@return vim.quickfix.entry[] # See |setqflist()| for the format 1850 function M.locations_to_items(locations, position_encoding) 1851 if position_encoding == nil then 1852 vim.notify_once( 1853 'locations_to_items must be called with valid position encoding', 1854 vim.log.levels.WARN 1855 ) 1856 position_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding 1857 end 1858 1859 local items = {} --- @type vim.quickfix.entry[] 1860 1861 ---@type table<string, {start: lsp.Position, end: lsp.Position, location: lsp.Location|lsp.LocationLink}[]> 1862 local grouped = {} 1863 for _, d in ipairs(locations) do 1864 -- locations may be Location or LocationLink 1865 local uri = d.uri or d.targetUri 1866 local range = d.range or d.targetSelectionRange 1867 grouped[uri] = grouped[uri] or {} 1868 table.insert(grouped[uri], { start = range.start, ['end'] = range['end'], location = d }) 1869 end 1870 1871 for uri, rows in vim.spairs(grouped) do 1872 table.sort(rows, position_sort) 1873 local filename = vim.uri_to_fname(uri) 1874 1875 local line_numbers = {} 1876 for _, temp in ipairs(rows) do 1877 table.insert(line_numbers, temp.start.line) 1878 if temp.start.line ~= temp['end'].line then 1879 table.insert(line_numbers, temp['end'].line) 1880 end 1881 end 1882 1883 -- get all the lines for this uri 1884 local lines = get_lines(vim.uri_to_bufnr(uri), line_numbers) 1885 1886 for _, temp in ipairs(rows) do 1887 local pos = temp.start 1888 local end_pos = temp['end'] 1889 local row = pos.line 1890 local end_row = end_pos.line 1891 local line = lines[row] or '' 1892 local end_line = lines[end_row] or '' 1893 local col = vim.str_byteindex(line, position_encoding, pos.character, false) 1894 local end_col = vim.str_byteindex(end_line, position_encoding, end_pos.character, false) 1895 1896 items[#items + 1] = { 1897 filename = filename, 1898 lnum = row + 1, 1899 end_lnum = end_row + 1, 1900 col = col + 1, 1901 end_col = end_col + 1, 1902 text = line, 1903 user_data = temp.location, 1904 } 1905 end 1906 end 1907 return items 1908 end 1909 1910 --- Converts symbols to quickfix list items. 1911 --- 1912 ---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[]|lsp.WorkspaceSymbol[] list of symbols 1913 ---@param bufnr? integer buffer handle or 0 for current, defaults to current 1914 ---@param position_encoding? 'utf-8'|'utf-16'|'utf-32' 1915 --- default to first client of buffer 1916 ---@return vim.quickfix.entry[] # See |setqflist()| for the format 1917 function M.symbols_to_items(symbols, bufnr, position_encoding) 1918 bufnr = vim._resolve_bufnr(bufnr) 1919 if position_encoding == nil then 1920 vim.notify_once( 1921 'symbols_to_items must be called with valid position encoding', 1922 vim.log.levels.WARN 1923 ) 1924 position_encoding = assert(vim.lsp.get_clients({ bufnr = bufnr })[1]).offset_encoding 1925 end 1926 1927 local items = {} --- @type vim.quickfix.entry[] 1928 for _, symbol in ipairs(symbols) do 1929 --- @type string?, lsp.Range? 1930 local filename, range 1931 1932 if symbol.location then 1933 --- @cast symbol lsp.SymbolInformation 1934 filename = vim.uri_to_fname(symbol.location.uri) 1935 range = symbol.location.range 1936 elseif symbol.selectionRange then 1937 --- @cast symbol lsp.DocumentSymbol 1938 filename = api.nvim_buf_get_name(bufnr) 1939 range = symbol.selectionRange 1940 end 1941 1942 if filename and range then 1943 local kind = protocol.SymbolKind[symbol.kind] or 'Unknown' 1944 1945 local lnum = range['start'].line + 1 1946 local col = get_line_byte_from_position(bufnr, range['start'], position_encoding) + 1 1947 local end_lnum = range['end'].line + 1 1948 local end_col = get_line_byte_from_position(bufnr, range['end'], position_encoding) + 1 1949 1950 local is_deprecated = symbol.deprecated 1951 or (symbol.tags and vim.tbl_contains(symbol.tags, protocol.SymbolTag.Deprecated)) 1952 local text = string.format( 1953 '[%s] %s%s%s', 1954 kind, 1955 symbol.name, 1956 symbol.containerName and ' in ' .. symbol.containerName or '', 1957 is_deprecated and ' (deprecated)' or '' 1958 ) 1959 1960 items[#items + 1] = { 1961 filename = filename, 1962 lnum = lnum, 1963 col = col, 1964 end_lnum = end_lnum, 1965 end_col = end_col, 1966 kind = kind, 1967 text = text, 1968 } 1969 end 1970 1971 if symbol.children then 1972 list_extend(items, M.symbols_to_items(symbol.children, bufnr, position_encoding)) 1973 end 1974 end 1975 1976 return items 1977 end 1978 1979 --- Removes empty lines from the beginning and end. 1980 ---@deprecated use `vim.split()` with `trimempty` instead 1981 ---@param lines table list of lines to trim 1982 ---@return table trimmed list of lines 1983 function M.trim_empty_lines(lines) 1984 vim.deprecate('vim.lsp.util.trim_empty_lines()', 'vim.split() with `trimempty`', '0.12') 1985 local start = 1 1986 for i = 1, #lines do 1987 if lines[i] ~= nil and #lines[i] > 0 then 1988 start = i 1989 break 1990 end 1991 end 1992 local finish = 1 1993 for i = #lines, 1, -1 do 1994 if lines[i] ~= nil and #lines[i] > 0 then 1995 finish = i 1996 break 1997 end 1998 end 1999 return vim.list_slice(lines, start, finish) 2000 end 2001 2002 --- Accepts markdown lines and tries to reduce them to a filetype if they 2003 --- comprise just a single code block. 2004 --- 2005 --- CAUTION: Modifies the input in-place! 2006 --- 2007 ---@deprecated 2008 ---@param lines string[] list of lines 2009 ---@return string filetype or "markdown" if it was unchanged. 2010 function M.try_trim_markdown_code_blocks(lines) 2011 vim.deprecate('vim.lsp.util.try_trim_markdown_code_blocks()', nil, '0.12') 2012 local language_id = assert(lines[1]):match('^```(.*)') 2013 if language_id then 2014 local has_inner_code_fence = false 2015 for i = 2, (#lines - 1) do 2016 local line = lines[i] --[[@as string]] 2017 if line:sub(1, 3) == '```' then 2018 has_inner_code_fence = true 2019 break 2020 end 2021 end 2022 -- No inner code fences + starting with code fence = hooray. 2023 if not has_inner_code_fence then 2024 table.remove(lines, 1) 2025 table.remove(lines) 2026 return language_id 2027 end 2028 end 2029 return 'markdown' 2030 end 2031 2032 ---@param window integer?: |window-ID| or 0 for current, defaults to current 2033 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 2034 local function make_position_param(window, position_encoding) 2035 window = window or 0 2036 local buf = api.nvim_win_get_buf(window) 2037 local row, col = unpack(api.nvim_win_get_cursor(window)) 2038 row = row - 1 2039 local line = api.nvim_buf_get_lines(buf, row, row + 1, true)[1] 2040 if not line then 2041 return { line = 0, character = 0 } 2042 end 2043 2044 col = vim.str_utfindex(line, position_encoding, col, false) 2045 2046 return { line = row, character = col } 2047 end 2048 2049 --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. 2050 --- 2051 ---@param window integer?: |window-ID| or 0 for current, defaults to current 2052 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 2053 ---@return lsp.TextDocumentPositionParams 2054 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams 2055 function M.make_position_params(window, position_encoding) 2056 window = window or 0 2057 local buf = api.nvim_win_get_buf(window) 2058 if position_encoding == nil then 2059 vim.notify_once( 2060 'position_encoding param is required in vim.lsp.util.make_position_params. Defaulting to position encoding of the first client.', 2061 vim.log.levels.WARN 2062 ) 2063 --- @diagnostic disable-next-line: deprecated 2064 position_encoding = M._get_offset_encoding(buf) 2065 end 2066 return { 2067 textDocument = M.make_text_document_params(buf), 2068 position = make_position_param(window, position_encoding), 2069 } 2070 end 2071 2072 --- Utility function for getting the encoding of the first LSP client on the given buffer. 2073 ---@deprecated 2074 ---@param bufnr integer buffer handle or 0 for current, defaults to current 2075 ---@return 'utf-8'|'utf-16'|'utf-32' encoding first client if there is one, nil otherwise 2076 function M._get_offset_encoding(bufnr) 2077 validate('bufnr', bufnr, 'number', true) 2078 2079 local offset_encoding --- @type 'utf-8'|'utf-16'|'utf-32'? 2080 2081 for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do 2082 if client.offset_encoding == nil then 2083 vim.notify_once( 2084 string.format( 2085 'Client (id: %s) offset_encoding is nil. Do not unset offset_encoding.', 2086 client.id 2087 ), 2088 vim.log.levels.ERROR 2089 ) 2090 end 2091 local this_offset_encoding = client.offset_encoding 2092 if not offset_encoding then 2093 offset_encoding = this_offset_encoding 2094 elseif offset_encoding ~= this_offset_encoding then 2095 vim.notify_once( 2096 'warning: multiple different client offset_encodings detected for buffer, vim.lsp.util._get_offset_encoding() uses the offset_encoding from the first client', 2097 vim.log.levels.WARN 2098 ) 2099 end 2100 end 2101 --- @cast offset_encoding -? hack - not safe 2102 2103 return offset_encoding 2104 end 2105 2106 --- Using the current position in the current buffer, creates an object that 2107 --- can be used as a building block for several LSP requests, such as 2108 --- `textDocument/codeAction`, `textDocument/colorPresentation`, 2109 --- `textDocument/rangeFormatting`. 2110 --- 2111 ---@param window integer?: |window-ID| or 0 for current, defaults to current 2112 ---@param position_encoding "utf-8"|"utf-16"|"utf-32" 2113 ---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range } 2114 function M.make_range_params(window, position_encoding) 2115 local buf = api.nvim_win_get_buf(window or 0) 2116 if position_encoding == nil then 2117 vim.notify_once( 2118 'position_encoding param is required in vim.lsp.util.make_range_params. Defaulting to position encoding of the first client.', 2119 vim.log.levels.WARN 2120 ) 2121 --- @diagnostic disable-next-line: deprecated 2122 position_encoding = M._get_offset_encoding(buf) 2123 end 2124 local position = make_position_param(window, position_encoding) 2125 return { 2126 textDocument = M.make_text_document_params(buf), 2127 range = { start = position, ['end'] = position }, 2128 } 2129 end 2130 2131 --- Using the given range in the current buffer, creates an object that 2132 --- is similar to |vim.lsp.util.make_range_params()|. 2133 --- 2134 ---@param start_pos [integer,integer]? {row,col} mark-indexed position. 2135 --- Defaults to the start of the last visual selection. 2136 ---@param end_pos [integer,integer]? {row,col} mark-indexed position. 2137 --- Defaults to the end of the last visual selection. 2138 ---@param bufnr integer? buffer handle or 0 for current, defaults to current 2139 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 2140 ---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range } 2141 function M.make_given_range_params(start_pos, end_pos, bufnr, position_encoding) 2142 validate('start_pos', start_pos, 'table', true) 2143 validate('end_pos', end_pos, 'table', true) 2144 validate('position_encoding', position_encoding, 'string', true) 2145 bufnr = vim._resolve_bufnr(bufnr) 2146 if position_encoding == nil then 2147 vim.notify_once( 2148 'position_encoding param is required in vim.lsp.util.make_given_range_params. Defaulting to position encoding of the first client.', 2149 vim.log.levels.WARN 2150 ) 2151 --- @diagnostic disable-next-line: deprecated 2152 position_encoding = M._get_offset_encoding(bufnr) 2153 end 2154 --- @type [integer, integer] 2155 local A = { unpack(start_pos or api.nvim_buf_get_mark(bufnr, '<')) } 2156 --- @type [integer, integer] 2157 local B = { unpack(end_pos or api.nvim_buf_get_mark(bufnr, '>')) } 2158 -- convert to 0-index 2159 A[1] = A[1] - 1 2160 B[1] = B[1] - 1 2161 -- account for position_encoding. 2162 if A[2] > 0 then 2163 A[2] = M.character_offset(bufnr, A[1], A[2], position_encoding) 2164 end 2165 if B[2] > 0 then 2166 B[2] = M.character_offset(bufnr, B[1], B[2], position_encoding) 2167 end 2168 -- we need to offset the end character position otherwise we loose the last 2169 -- character of the selection, as LSP end position is exclusive 2170 -- see https://microsoft.github.io/language-server-protocol/specification#range 2171 if vim.o.selection ~= 'exclusive' then 2172 B[2] = B[2] + 1 2173 end 2174 return { 2175 textDocument = M.make_text_document_params(bufnr), 2176 range = { 2177 start = { line = A[1], character = A[2] }, 2178 ['end'] = { line = B[1], character = B[2] }, 2179 }, 2180 } 2181 end 2182 2183 --- Creates a `TextDocumentIdentifier` object for the current buffer. 2184 --- 2185 ---@param bufnr integer?: Buffer handle, defaults to current 2186 ---@return lsp.TextDocumentIdentifier 2187 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier 2188 function M.make_text_document_params(bufnr) 2189 return { uri = vim.uri_from_bufnr(bufnr or 0) } 2190 end 2191 2192 --- Create the workspace params 2193 ---@param added lsp.WorkspaceFolder[] 2194 ---@param removed lsp.WorkspaceFolder[] 2195 ---@return lsp.DidChangeWorkspaceFoldersParams 2196 function M.make_workspace_params(added, removed) 2197 return { event = { added = added, removed = removed } } 2198 end 2199 2200 --- Returns indentation size. 2201 --- 2202 ---@see 'shiftwidth' 2203 ---@param bufnr integer?: Buffer handle, defaults to current 2204 ---@return integer indentation size 2205 function M.get_effective_tabstop(bufnr) 2206 validate('bufnr', bufnr, 'number', true) 2207 local bo = bufnr and vim.bo[bufnr] or vim.bo 2208 local sw = bo.shiftwidth 2209 return (sw == 0 and bo.tabstop) or sw 2210 end 2211 2212 --- Creates a `DocumentFormattingParams` object for the current buffer and cursor position. 2213 --- 2214 ---@param options lsp.FormattingOptions? with valid `FormattingOptions` entries 2215 ---@return lsp.DocumentFormattingParams object 2216 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting 2217 function M.make_formatting_params(options) 2218 validate('options', options, 'table', true) 2219 options = vim.tbl_extend('keep', options or {}, { 2220 tabSize = M.get_effective_tabstop(), 2221 insertSpaces = vim.bo.expandtab, 2222 }) 2223 return { 2224 textDocument = { uri = vim.uri_from_bufnr(0) }, 2225 options = options, 2226 } 2227 end 2228 2229 --- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer. 2230 --- 2231 ---@param buf integer buffer number (0 for current) 2232 ---@param row integer 0-indexed line 2233 ---@param col integer 0-indexed byte offset in line 2234 ---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32' 2235 --- defaults to `offset_encoding` of first client of `buf` 2236 ---@return integer `offset_encoding` index of the character in line {row} column {col} in buffer {buf} 2237 function M.character_offset(buf, row, col, offset_encoding) 2238 local line = get_line(buf, row) 2239 if offset_encoding == nil then 2240 vim.notify_once( 2241 'character_offset must be called with valid offset encoding', 2242 vim.log.levels.WARN 2243 ) 2244 offset_encoding = assert(vim.lsp.get_clients({ bufnr = buf })[1]).offset_encoding 2245 end 2246 return vim.str_utfindex(line, offset_encoding, col, false) 2247 end 2248 2249 --- Helper function to return nested values in language server settings 2250 --- 2251 ---@param settings table language server settings 2252 ---@param section string indicating the field of the settings table 2253 ---@return table|string|vim.NIL The value of settings accessed via section. `vim.NIL` if not found. 2254 ---@deprecated 2255 function M.lookup_section(settings, section) 2256 vim.deprecate('vim.lsp.util.lookup_section()', 'vim.tbl_get() with `vim.split`', '0.12') 2257 for part in vim.gsplit(section, '.', { plain = true }) do 2258 --- @diagnostic disable-next-line:no-unknown 2259 settings = settings[part] 2260 if settings == nil then 2261 return vim.NIL 2262 end 2263 end 2264 return settings 2265 end 2266 2267 --- Converts line range (0-based, end-inclusive) to lsp range, 2268 --- handles absence of a trailing newline 2269 --- 2270 ---@param bufnr integer 2271 ---@param start_line integer 2272 ---@param end_line integer 2273 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 2274 ---@return lsp.Range 2275 function M._make_line_range_params(bufnr, start_line, end_line, position_encoding) 2276 local last_line = api.nvim_buf_line_count(bufnr) - 1 2277 2278 ---@type lsp.Position 2279 local end_pos 2280 2281 if end_line == last_line and not vim.bo[bufnr].endofline then 2282 end_pos = { 2283 line = end_line, 2284 character = M.character_offset( 2285 bufnr, 2286 end_line, 2287 #get_line(bufnr, end_line), 2288 position_encoding 2289 ), 2290 } 2291 else 2292 end_pos = { line = end_line + 1, character = 0 } 2293 end 2294 2295 return { 2296 start = { line = start_line, character = 0 }, 2297 ['end'] = end_pos, 2298 } 2299 end 2300 2301 ---@class (private) vim.lsp.util._cancel_requests.Filter 2302 ---@field bufnr? integer 2303 ---@field clients? vim.lsp.Client[] 2304 ---@field method? vim.lsp.protocol.Method.ClientToServer.Request 2305 ---@field type? string 2306 2307 --- Cancel all {filter}ed requests. 2308 --- 2309 ---@param filter? vim.lsp.util._cancel_requests.Filter 2310 function M._cancel_requests(filter) 2311 filter = filter or {} 2312 local bufnr = filter.bufnr and vim._resolve_bufnr(filter.bufnr) or nil 2313 local clients = filter.clients 2314 local method = filter.method 2315 local type = filter.type 2316 2317 for _, client in 2318 ipairs(clients or vim.lsp.get_clients({ 2319 bufnr = bufnr, 2320 method = method, 2321 })) 2322 do 2323 for id, request in pairs(client.requests) do 2324 if 2325 (bufnr == nil or bufnr == request.bufnr) 2326 and (method == nil or method == request.method) 2327 and (type == nil or type == request.type) 2328 then 2329 client:cancel_request(id) 2330 end 2331 end 2332 end 2333 end 2334 2335 M._get_line_byte_from_position = get_line_byte_from_position 2336 2337 ---@nodoc 2338 ---@type table<integer,integer> 2339 M.buf_versions = setmetatable({}, { 2340 __index = function(t, bufnr) 2341 return rawget(t, bufnr) or 0 2342 end, 2343 }) 2344 2345 return M