1local protocol = require 'vim.lsp.protocol' 2local snippet = require 'vim.lsp._snippet' 3local vim = vim 4local validate = vim.validate 5local api = vim.api 6local list_extend = vim.list_extend 7local highlight = require 'vim.highlight' 8local uv = vim.loop 9 10local npcall = vim.F.npcall 11local split = vim.split 12 13local _warned = {} 14local warn_once = function(message) 15 if not _warned[message] then 16 vim.api.nvim_err_writeln(message) 17 _warned[message] = true 18 end 19end 20 21local M = {} 22 23local default_border = { 24 {"", "NormalFloat"}, 25 {"", "NormalFloat"}, 26 {"", "NormalFloat"}, 27 {" ", "NormalFloat"}, 28 {"", "NormalFloat"}, 29 {"", "NormalFloat"}, 30 {"", "NormalFloat"}, 31 {" ", "NormalFloat"}, 32} 33 34---@private 35--- Check the border given by opts or the default border for the additional 36--- size it adds to a float. 37---@param opts (table, optional) options for the floating window 38--- - border (string or table) the border 39---@returns (table) size of border in the form of { height = height, width = width } 40local function get_border_size(opts) 41 local border = opts and opts.border or default_border 42 local height = 0 43 local width = 0 44 45 if type(border) == 'string' then 46 local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, rounded = {2, 2}, solid = {2, 2}, shadow = {1, 1}} 47 if border_size[border] == nil then 48 error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) 49 end 50 height, width = unpack(border_size[border]) 51 else 52 if 8 % #border ~= 0 then 53 error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) 54 end 55 ---@private 56 local function border_width(id) 57 id = (id - 1) % #border + 1 58 if type(border[id]) == "table" then 59 -- border specified as a table of <character, highlight group> 60 return vim.fn.strdisplaywidth(border[id][1]) 61 elseif type(border[id]) == "string" then 62 -- border specified as a list of border characters 63 return vim.fn.strdisplaywidth(border[id]) 64 end 65 error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) 66 end 67 ---@private 68 local function border_height(id) 69 id = (id - 1) % #border + 1 70 if type(border[id]) == "table" then 71 -- border specified as a table of <character, highlight group> 72 return #border[id][1] > 0 and 1 or 0 73 elseif type(border[id]) == "string" then 74 -- border specified as a list of border characters 75 return #border[id] > 0 and 1 or 0 76 end 77 error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) 78 end 79 height = height + border_height(2) -- top 80 height = height + border_height(6) -- bottom 81 width = width + border_width(4) -- right 82 width = width + border_width(8) -- left 83 end 84 85 return { height = height, width = width } 86end 87 88---@private 89local function split_lines(value) 90 return split(value, '\n', true) 91end 92 93--- Convert byte index to `encoding` index. 94--- Convenience wrapper around vim.str_utfindex 95---@param line string line to be indexed 96---@param index number byte index (utf-8), or `nil` for length 97---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 98---@return number `encoding` index of `index` in `line` 99function M._str_utfindex_enc(line, index, encoding) 100 if not encoding then encoding = 'utf-16' end 101 if encoding == 'utf-8' then 102 if index then return index else return #line end 103 elseif encoding == 'utf-16' then 104 local _, col16 = vim.str_utfindex(line, index) 105 return col16 106 elseif encoding == 'utf-32' then 107 local col32, _ = vim.str_utfindex(line, index) 108 return col32 109 else 110 error("Invalid encoding: " .. vim.inspect(encoding)) 111 end 112end 113 114--- Convert UTF index to `encoding` index. 115--- Convenience wrapper around vim.str_byteindex 116---Alternative to vim.str_byteindex that takes an encoding. 117---@param line string line to be indexed 118---@param index number UTF index 119---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 120---@return number byte (utf-8) index of `encoding` index `index` in `line` 121function M._str_byteindex_enc(line, index, encoding) 122 if not encoding then encoding = 'utf-16' end 123 if encoding == 'utf-8' then 124 if index then return index else return #line end 125 elseif encoding == 'utf-16' then 126 return vim.str_byteindex(line, index, true) 127 elseif encoding == 'utf-32' then 128 return vim.str_byteindex(line, index) 129 else 130 error("Invalid encoding: " .. vim.inspect(encoding)) 131 end 132end 133 134local _str_utfindex_enc = M._str_utfindex_enc 135local _str_byteindex_enc = M._str_byteindex_enc 136--- Replaces text in a range with new text. 137--- 138--- CAUTION: Changes in-place! 139--- 140---@param lines (table) Original list of strings 141---@param A (table) Start position; a 2-tuple of {line, col} numbers 142---@param B (table) End position; a 2-tuple of {line, col} numbers 143---@param new_lines A list of strings to replace the original 144---@returns (table) The modified {lines} object 145function M.set_lines(lines, A, B, new_lines) 146 -- 0-indexing to 1-indexing 147 local i_0 = A[1] + 1 148 -- If it extends past the end, truncate it to the end. This is because the 149 -- way the LSP describes the range including the last newline is by 150 -- specifying a line number after what we would call the last line. 151 local i_n = math.min(B[1] + 1, #lines) 152 if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then 153 error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines}) 154 end 155 local prefix = "" 156 local suffix = lines[i_n]:sub(B[2]+1) 157 if A[2] > 0 then 158 prefix = lines[i_0]:sub(1, A[2]) 159 end 160 local n = i_n - i_0 + 1 161 if n ~= #new_lines then 162 for _ = 1, n - #new_lines do table.remove(lines, i_0) end 163 for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end 164 end 165 for i = 1, #new_lines do 166 lines[i - 1 + i_0] = new_lines[i] 167 end 168 if #suffix > 0 then 169 local i = i_0 + #new_lines - 1 170 lines[i] = lines[i]..suffix 171 end 172 if #prefix > 0 then 173 lines[i_0] = prefix..lines[i_0] 174 end 175 return lines 176end 177 178---@private 179local function sort_by_key(fn) 180 return function(a,b) 181 local ka, kb = fn(a), fn(b) 182 assert(#ka == #kb) 183 for i = 1, #ka do 184 if ka[i] ~= kb[i] then 185 return ka[i] < kb[i] 186 end 187 end 188 -- every value must have been equal here, which means it's not less than. 189 return false 190 end 191end 192 193---@private 194--- Gets the zero-indexed lines from the given buffer. 195--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. 196--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. 197--- 198---@param bufnr number bufnr to get the lines from 199---@param rows number[] zero-indexed line numbers 200---@return table<number string> a table mapping rows to lines 201local function get_lines(bufnr, rows) 202 rows = type(rows) == "table" and rows or { rows } 203 204 ---@private 205 local function buf_lines() 206 local lines = {} 207 for _, row in pairs(rows) do 208 lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1] 209 end 210 return lines 211 end 212 213 local uri = vim.uri_from_bufnr(bufnr) 214 215 -- load the buffer if this is not a file uri 216 -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds. 217 if uri:sub(1, 4) ~= "file" then 218 vim.fn.bufload(bufnr) 219 return buf_lines() 220 end 221 222 -- use loaded buffers if available 223 if vim.fn.bufloaded(bufnr) == 1 then 224 return buf_lines() 225 end 226 227 local filename = api.nvim_buf_get_name(bufnr) 228 229 -- get the data from the file 230 local fd = uv.fs_open(filename, "r", 438) 231 if not fd then return "" end 232 local stat = uv.fs_fstat(fd) 233 local data = uv.fs_read(fd, stat.size, 0) 234 uv.fs_close(fd) 235 236 local lines = {} -- rows we need to retrieve 237 local need = 0 -- keep track of how many unique rows we need 238 for _, row in pairs(rows) do 239 if not lines[row] then 240 need = need + 1 241 end 242 lines[row] = true 243 end 244 245 local found = 0 246 local lnum = 0 247 248 for line in string.gmatch(data, "([^\n]*)\n?") do 249 if lines[lnum] == true then 250 lines[lnum] = line 251 found = found + 1 252 if found == need then break 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 264end 265 266 267---@private 268--- Gets the zero-indexed line from the given buffer. 269--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. 270--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. 271--- 272---@param bufnr number 273---@param row number zero-indexed line number 274---@return string the line at row in filename 275local function get_line(bufnr, row) 276 return get_lines(bufnr, { row })[row] 277end 278 279 280---@private 281--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position 282--- Returns a zero-indexed column, since set_lines() does the conversion to 283---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 284--- 1-indexed 285local function get_line_byte_from_position(bufnr, position, offset_encoding) 286 -- LSP's line and characters are 0-indexed 287 -- Vim's line and columns are 1-indexed 288 local col = position.character 289 -- When on the first character, we can ignore the difference between byte and 290 -- character 291 if col > 0 then 292 local line = get_line(bufnr, position.line) 293 local ok, result 294 ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding) 295 if ok then 296 return result 297 end 298 return math.min(#line, col) 299 end 300 return col 301end 302 303--- Process and return progress reports from lsp server 304---@private 305function M.get_progress_messages() 306 307 local new_messages = {} 308 local msg_remove = {} 309 local progress_remove = {} 310 311 for _, client in ipairs(vim.lsp.get_active_clients()) do 312 local messages = client.messages 313 local data = messages 314 for token, ctx in pairs(data.progress) do 315 316 local new_report = { 317 name = data.name, 318 title = ctx.title or "empty title", 319 message = ctx.message, 320 percentage = ctx.percentage, 321 done = ctx.done, 322 progress = true, 323 } 324 table.insert(new_messages, new_report) 325 326 if ctx.done then 327 table.insert(progress_remove, {client = client, token = token}) 328 end 329 end 330 331 for i, msg in ipairs(data.messages) do 332 if msg.show_once then 333 msg.shown = msg.shown + 1 334 if msg.shown > 1 then 335 table.insert(msg_remove, {client = client, idx = i}) 336 end 337 end 338 339 table.insert(new_messages, {name = data.name, content = msg.content}) 340 end 341 342 if next(data.status) ~= nil then 343 table.insert(new_messages, { 344 name = data.name, 345 content = data.status.content, 346 uri = data.status.uri, 347 status = true 348 }) 349 end 350 for _, item in ipairs(msg_remove) do 351 table.remove(client.messages, item.idx) 352 end 353 354 end 355 356 for _, item in ipairs(progress_remove) do 357 item.client.messages.progress[item.token] = nil 358 end 359 360 return new_messages 361end 362 363--- Applies a list of text edits to a buffer. 364---@param text_edits table list of `TextEdit` objects 365---@param bufnr number Buffer id 366---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to encoding of first client of `bufnr` 367---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit 368function M.apply_text_edits(text_edits, bufnr, offset_encoding) 369 validate { 370 text_edits = { text_edits, 't', false }; 371 bufnr = { bufnr, 'number', false }; 372 offset_encoding = { offset_encoding, 'string', true }; 373 } 374 offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) 375 if not next(text_edits) then return end 376 if not api.nvim_buf_is_loaded(bufnr) then 377 vim.fn.bufload(bufnr) 378 end 379 api.nvim_buf_set_option(bufnr, 'buflisted', true) 380 381 -- Fix reversed range and indexing each text_edits 382 local index = 0 383 text_edits = vim.tbl_map(function(text_edit) 384 index = index + 1 385 text_edit._index = index 386 387 if text_edit.range.start.line > text_edit.range['end'].line or text_edit.range.start.line == text_edit.range['end'].line and text_edit.range.start.character > text_edit.range['end'].character then 388 local start = text_edit.range.start 389 text_edit.range.start = text_edit.range['end'] 390 text_edit.range['end'] = start 391 end 392 return text_edit 393 end, text_edits) 394 395 -- Sort text_edits 396 table.sort(text_edits, function(a, b) 397 if a.range.start.line ~= b.range.start.line then 398 return a.range.start.line > b.range.start.line 399 end 400 if a.range.start.character ~= b.range.start.character then 401 return a.range.start.character > b.range.start.character 402 end 403 if a._index ~= b._index then 404 return a._index > b._index 405 end 406 end) 407 408 -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here. 409 local has_eol_text_edit = false 410 local max = vim.api.nvim_buf_line_count(bufnr) 411 local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding) 412 text_edits = vim.tbl_map(function(text_edit) 413 if max <= text_edit.range.start.line then 414 text_edit.range.start.line = max - 1 415 text_edit.range.start.character = len 416 text_edit.newText = '\n' .. text_edit.newText 417 has_eol_text_edit = true 418 end 419 if max <= text_edit.range['end'].line then 420 text_edit.range['end'].line = max - 1 421 text_edit.range['end'].character = len 422 has_eol_text_edit = true 423 end 424 return text_edit 425 end, text_edits) 426 427 -- Some LSP servers are depending on the VSCode behavior. 428 -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it. 429 local is_current_buf = vim.api.nvim_get_current_buf() == bufnr 430 local cursor = (function() 431 if not is_current_buf then 432 return { 433 row = -1, 434 col = -1, 435 } 436 end 437 local cursor = vim.api.nvim_win_get_cursor(0) 438 return { 439 row = cursor[1] - 1, 440 col = cursor[2], 441 } 442 end)() 443 444 -- Apply text edits. 445 local is_cursor_fixed = false 446 for _, text_edit in ipairs(text_edits) do 447 local e = { 448 start_row = text_edit.range.start.line, 449 start_col = get_line_byte_from_position(bufnr, text_edit.range.start), 450 end_row = text_edit.range['end'].line, 451 end_col = get_line_byte_from_position(bufnr, text_edit.range['end']), 452 text = vim.split(text_edit.newText, '\n', true), 453 } 454 vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text) 455 456 local row_count = (e.end_row - e.start_row) + 1 457 if e.end_row < cursor.row then 458 cursor.row = cursor.row + (#e.text - row_count) 459 is_cursor_fixed = true 460 elseif e.end_row == cursor.row and e.end_col <= cursor.col then 461 cursor.row = cursor.row + (#e.text - row_count) 462 cursor.col = #e.text[#e.text] + (cursor.col - e.end_col) 463 if #e.text == 1 then 464 cursor.col = cursor.col + e.start_col 465 end 466 is_cursor_fixed = true 467 end 468 end 469 470 if is_cursor_fixed then 471 local is_valid_cursor = true 472 is_valid_cursor = is_valid_cursor and cursor.row < vim.api.nvim_buf_line_count(bufnr) 473 is_valid_cursor = is_valid_cursor and cursor.col <= #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or '') 474 if is_valid_cursor then 475 vim.api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col }) 476 end 477 end 478 479 -- Remove final line if needed 480 local fix_eol = has_eol_text_edit 481 fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol') 482 fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == '' 483 if fix_eol then 484 vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {}) 485 end 486end 487 488-- local valid_windows_path_characters = "[^<>:\"/\\|?*]" 489-- local valid_unix_path_characters = "[^/]" 490-- https://github.com/davidm/lua-glob-pattern 491-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names 492-- function M.glob_to_regex(glob) 493-- end 494 495--- Can be used to extract the completion items from a 496--- `textDocument/completion` request, which may return one of 497--- `CompletionItem[]`, `CompletionList` or null. 498---@param result (table) The result of a `textDocument/completion` request 499---@returns (table) List of completion items 500---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion 501function M.extract_completion_items(result) 502 if type(result) == 'table' and result.items then 503 -- result is a `CompletionList` 504 return result.items 505 elseif result ~= nil then 506 -- result is `CompletionItem[]` 507 return result 508 else 509 -- result is `null` 510 return {} 511 end 512end 513 514--- Applies a `TextDocumentEdit`, which is a list of changes to a single 515--- document. 516--- 517---@param text_document_edit table: a `TextDocumentEdit` object 518---@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list) 519---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit 520function M.apply_text_document_edit(text_document_edit, index) 521 local text_document = text_document_edit.textDocument 522 local bufnr = vim.uri_to_bufnr(text_document.uri) 523 524 -- For lists of text document edits, 525 -- do not check the version after the first edit. 526 local should_check_version = true 527 if index and index > 1 then 528 should_check_version = false 529 end 530 531 -- `VersionedTextDocumentIdentifier`s version may be null 532 -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier 533 if should_check_version and (text_document.version 534 and text_document.version > 0 535 and M.buf_versions[bufnr] 536 and M.buf_versions[bufnr] > text_document.version) then 537 print("Buffer ", text_document.uri, " newer than edits.") 538 return 539 end 540 541 M.apply_text_edits(text_document_edit.edits, bufnr) 542end 543 544--- Parses snippets in a completion entry. 545--- 546---@param input string unparsed snippet 547---@returns string parsed snippet 548function M.parse_snippet(input) 549 local ok, parsed = pcall(function() 550 return tostring(snippet.parse(input)) 551 end) 552 if not ok then 553 return input 554 end 555 return parsed 556end 557 558---@private 559--- Sorts by CompletionItem.sortText. 560--- 561--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion 562local function sort_completion_items(items) 563 table.sort(items, function(a, b) 564 return (a.sortText or a.label) < (b.sortText or b.label) 565 end) 566end 567 568---@private 569--- Returns text that should be inserted when selecting completion item. The 570--- precedence is as follows: textEdit.newText > insertText > label 571--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion 572local function get_completion_word(item) 573 if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then 574 local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat] 575 if insert_text_format == "PlainText" or insert_text_format == nil then 576 return item.textEdit.newText 577 else 578 return M.parse_snippet(item.textEdit.newText) 579 end 580 elseif item.insertText ~= nil and item.insertText ~= "" then 581 local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat] 582 if insert_text_format == "PlainText" or insert_text_format == nil then 583 return item.insertText 584 else 585 return M.parse_snippet(item.insertText) 586 end 587 end 588 return item.label 589end 590 591---@private 592--- Some language servers return complementary candidates whose prefixes do not 593--- match are also returned. So we exclude completion candidates whose prefix 594--- does not match. 595local function remove_unmatch_completion_items(items, prefix) 596 return vim.tbl_filter(function(item) 597 local word = get_completion_word(item) 598 return vim.startswith(word, prefix) 599 end, items) 600end 601 602--- According to LSP spec, if the client set `completionItemKind.valueSet`, 603--- the client must handle it properly even if it receives a value outside the 604--- specification. 605--- 606---@param completion_item_kind (`vim.lsp.protocol.completionItemKind`) 607---@returns (`vim.lsp.protocol.completionItemKind`) 608---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion 609function M._get_completion_item_kind_name(completion_item_kind) 610 return protocol.CompletionItemKind[completion_item_kind] or "Unknown" 611end 612 613--- Turns the result of a `textDocument/completion` request into vim-compatible 614--- |complete-items|. 615--- 616---@param result The result of a `textDocument/completion` call, e.g. from 617---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`, 618--- `CompletionList` or `null` 619---@param prefix (string) the prefix to filter the completion items 620---@returns { matches = complete-items table, incomplete = bool } 621---@see |complete-items| 622function M.text_document_completion_list_to_complete_items(result, prefix) 623 local items = M.extract_completion_items(result) 624 if vim.tbl_isempty(items) then 625 return {} 626 end 627 628 items = remove_unmatch_completion_items(items, prefix) 629 sort_completion_items(items) 630 631 local matches = {} 632 633 for _, completion_item in ipairs(items) do 634 local info = ' ' 635 local documentation = completion_item.documentation 636 if documentation then 637 if type(documentation) == 'string' and documentation ~= '' then 638 info = documentation 639 elseif type(documentation) == 'table' and type(documentation.value) == 'string' then 640 info = documentation.value 641 -- else 642 -- TODO(ashkan) Validation handling here? 643 end 644 end 645 646 local word = get_completion_word(completion_item) 647 table.insert(matches, { 648 word = word, 649 abbr = completion_item.label, 650 kind = M._get_completion_item_kind_name(completion_item.kind), 651 menu = completion_item.detail or '', 652 info = info, 653 icase = 1, 654 dup = 1, 655 empty = 1, 656 user_data = { 657 nvim = { 658 lsp = { 659 completion_item = completion_item 660 } 661 } 662 }, 663 }) 664 end 665 666 return matches 667end 668 669 670--- Rename old_fname to new_fname 671--- 672---@param opts (table) 673-- overwrite? bool 674-- ignoreIfExists? bool 675function M.rename(old_fname, new_fname, opts) 676 opts = opts or {} 677 local target_exists = vim.loop.fs_stat(new_fname) ~= nil 678 if target_exists and not opts.overwrite or opts.ignoreIfExists then 679 vim.notify('Rename target already exists. Skipping rename.') 680 return 681 end 682 local oldbuf = vim.fn.bufadd(old_fname) 683 vim.fn.bufload(oldbuf) 684 685 -- The there may be pending changes in the buffer 686 api.nvim_buf_call(oldbuf, function() 687 vim.cmd('w!') 688 end) 689 690 local ok, err = os.rename(old_fname, new_fname) 691 assert(ok, err) 692 693 local newbuf = vim.fn.bufadd(new_fname) 694 for _, win in pairs(api.nvim_list_wins()) do 695 if api.nvim_win_get_buf(win) == oldbuf then 696 api.nvim_win_set_buf(win, newbuf) 697 end 698 end 699 api.nvim_buf_delete(oldbuf, { force = true }) 700end 701 702---@private 703local function create_file(change) 704 local opts = change.options or {} 705 -- from spec: Overwrite wins over `ignoreIfExists` 706 local fname = vim.uri_to_fname(change.uri) 707 if not opts.ignoreIfExists or opts.overwrite then 708 local file = io.open(fname, 'w') 709 file:close() 710 end 711 vim.fn.bufadd(fname) 712end 713 714---@private 715local function delete_file(change) 716 local opts = change.options or {} 717 local fname = vim.uri_to_fname(change.uri) 718 local stat = vim.loop.fs_stat(fname) 719 if opts.ignoreIfNotExists and not stat then 720 return 721 end 722 assert(stat, "Cannot delete not existing file or folder " .. fname) 723 local flags 724 if stat and stat.type == 'directory' then 725 flags = opts.recursive and 'rf' or 'd' 726 else 727 flags = '' 728 end 729 local bufnr = vim.fn.bufadd(fname) 730 local result = tonumber(vim.fn.delete(fname, flags)) 731 assert(result == 0, 'Could not delete file: ' .. fname .. ', stat: ' .. vim.inspect(stat)) 732 api.nvim_buf_delete(bufnr, { force = true }) 733end 734 735 736--- Applies a `WorkspaceEdit`. 737--- 738---@param workspace_edit (table) `WorkspaceEdit` 739--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit 740function M.apply_workspace_edit(workspace_edit) 741 if workspace_edit.documentChanges then 742 for idx, change in ipairs(workspace_edit.documentChanges) do 743 if change.kind == "rename" then 744 M.rename( 745 vim.uri_to_fname(change.oldUri), 746 vim.uri_to_fname(change.newUri), 747 change.options 748 ) 749 elseif change.kind == 'create' then 750 create_file(change) 751 elseif change.kind == 'delete' then 752 delete_file(change) 753 elseif change.kind then 754 error(string.format("Unsupported change: %q", vim.inspect(change))) 755 else 756 M.apply_text_document_edit(change, idx) 757 end 758 end 759 return 760 end 761 762 local all_changes = workspace_edit.changes 763 if not (all_changes and not vim.tbl_isempty(all_changes)) then 764 return 765 end 766 767 for uri, changes in pairs(all_changes) do 768 local bufnr = vim.uri_to_bufnr(uri) 769 M.apply_text_edits(changes, bufnr) 770 end 771end 772 773--- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into 774--- a list of lines containing valid markdown. Useful to populate the hover 775--- window for `textDocument/hover`, for parsing the result of 776--- `textDocument/signatureHelp`, and potentially others. 777--- 778---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`) 779---@param contents (table, optional, default `{}`) List of strings to extend with converted lines 780---@returns {contents}, extended with lines of converted markdown. 781---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover 782function M.convert_input_to_markdown_lines(input, contents) 783 contents = contents or {} 784 -- MarkedString variation 1 785 if type(input) == 'string' then 786 list_extend(contents, split_lines(input)) 787 else 788 assert(type(input) == 'table', "Expected a table for Hover.contents") 789 -- MarkupContent 790 if input.kind then 791 -- The kind can be either plaintext or markdown. 792 -- If it's plaintext, then wrap it in a <text></text> block 793 794 -- Some servers send input.value as empty, so let's ignore this :( 795 local value = input.value or '' 796 797 if input.kind == "plaintext" then 798 -- wrap this in a <text></text> block so that stylize_markdown 799 -- can properly process it as plaintext 800 value = string.format("<text>\n%s\n</text>", value) 801 end 802 803 -- assert(type(value) == 'string') 804 list_extend(contents, split_lines(value)) 805 -- MarkupString variation 2 806 elseif input.language then 807 -- Some servers send input.value as empty, so let's ignore this :( 808 -- assert(type(input.value) == 'string') 809 table.insert(contents, "```"..input.language) 810 list_extend(contents, split_lines(input.value or '')) 811 table.insert(contents, "```") 812 -- By deduction, this must be MarkedString[] 813 else 814 -- Use our existing logic to handle MarkedString 815 for _, marked_string in ipairs(input) do 816 M.convert_input_to_markdown_lines(marked_string, contents) 817 end 818 end 819 end 820 if (contents[1] == '' or contents[1] == nil) and #contents == 1 then 821 return {} 822 end 823 return contents 824end 825 826--- Converts `textDocument/SignatureHelp` response to markdown lines. 827--- 828---@param signature_help Response of `textDocument/SignatureHelp` 829---@param ft optional filetype that will be use as the `lang` for the label markdown code block 830---@param triggers optional list of trigger characters from the lsp server. used to better determine parameter offsets 831---@returns list of lines of converted markdown. 832---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp 833function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers) 834 if not signature_help.signatures then 835 return 836 end 837 --The active signature. If omitted or the value lies outside the range of 838 --`signatures` the value defaults to zero or is ignored if `signatures.length 839 --=== 0`. Whenever possible implementors should make an active decision about 840 --the active signature and shouldn't rely on a default value. 841 local contents = {} 842 local active_hl 843 local active_signature = signature_help.activeSignature or 0 844 -- If the activeSignature is not inside the valid range, then clip it. 845 if active_signature >= #signature_help.signatures then 846 active_signature = 0 847 end 848 local signature = signature_help.signatures[active_signature + 1] 849 if not signature then 850 return 851 end 852 local label = signature.label 853 if ft then 854 -- wrap inside a code block so stylize_markdown can render it properly 855 label = ("```%s\n%s\n```"):format(ft, label) 856 end 857 vim.list_extend(contents, vim.split(label, '\n', true)) 858 if signature.documentation then 859 M.convert_input_to_markdown_lines(signature.documentation, contents) 860 end 861 if signature.parameters and #signature.parameters > 0 then 862 local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) 863 if active_parameter < 0 864 then active_parameter = 0 865 end 866 867 -- If the activeParameter is > #parameters, then set it to the last 868 -- NOTE: this is not fully according to the spec, but a client-side interpretation 869 if active_parameter >= #signature.parameters then 870 active_parameter = #signature.parameters - 1 871 end 872 873 local parameter = signature.parameters[active_parameter + 1] 874 if parameter then 875 --[=[ 876 --Represents a parameter of a callable-signature. A parameter can 877 --have a label and a doc-comment. 878 interface ParameterInformation { 879 --The label of this parameter information. 880 -- 881 --Either a string or an inclusive start and exclusive end offsets within its containing 882 --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 883 --string representation as `Position` and `Range` does. 884 -- 885 --*Note*: a label of type string should be a substring of its containing signature label. 886 --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. 887 label: string | [number, number]; 888 --The human-readable doc-comment of this parameter. Will be shown 889 --in the UI but can be omitted. 890 documentation?: string | MarkupContent; 891 } 892 --]=] 893 if parameter.label then 894 if type(parameter.label) == "table" then 895 active_hl = parameter.label 896 else 897 local offset = 1 898 -- try to set the initial offset to the first found trigger character 899 for _, t in ipairs(triggers or {}) do 900 local trigger_offset = signature.label:find(t, 1, true) 901 if trigger_offset and (offset == 1 or trigger_offset < offset) then 902 offset = trigger_offset 903 end 904 end 905 for p, param in pairs(signature.parameters) do 906 offset = signature.label:find(param.label, offset, true) 907 if not offset then break end 908 if p == active_parameter + 1 then 909 active_hl = {offset - 1, offset + #parameter.label - 1} 910 break 911 end 912 offset = offset + #param.label + 1 913 end 914 end 915 end 916 if parameter.documentation then 917 M.convert_input_to_markdown_lines(parameter.documentation, contents) 918 end 919 end 920 end 921 return contents, active_hl 922end 923 924--- Creates a table with sensible default options for a floating window. The 925--- table can be passed to |nvim_open_win()|. 926--- 927---@param width (number) window width (in character cells) 928---@param height (number) window height (in character cells) 929---@param opts (table, optional) 930--- - offset_x (number) offset to add to `col` 931--- - offset_y (number) offset to add to `row` 932--- - border (string or table) override `border` 933--- - focusable (string or table) override `focusable` 934--- - zindex (string or table) override `zindex`, defaults to 50 935---@returns (table) Options 936function M.make_floating_popup_options(width, height, opts) 937 validate { 938 opts = { opts, 't', true }; 939 } 940 opts = opts or {} 941 validate { 942 ["opts.offset_x"] = { opts.offset_x, 'n', true }; 943 ["opts.offset_y"] = { opts.offset_y, 'n', true }; 944 } 945 946 local anchor = '' 947 local row, col 948 949 local lines_above = vim.fn.winline() - 1 950 local lines_below = vim.fn.winheight(0) - lines_above 951 952 if lines_above < lines_below then 953 anchor = anchor..'N' 954 height = math.min(lines_below, height) 955 row = 1 956 else 957 anchor = anchor..'S' 958 height = math.min(lines_above, height) 959 row = 0 960 end 961 962 if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then 963 anchor = anchor..'W' 964 col = 0 965 else 966 anchor = anchor..'E' 967 col = 1 968 end 969 970 return { 971 anchor = anchor, 972 col = col + (opts.offset_x or 0), 973 height = height, 974 focusable = opts.focusable, 975 relative = 'cursor', 976 row = row + (opts.offset_y or 0), 977 style = 'minimal', 978 width = width, 979 border = opts.border or default_border, 980 zindex = opts.zindex or 50, 981 } 982end 983 984--- Jumps to a location. 985--- 986---@param location (`Location`|`LocationLink`) 987---@returns `true` if the jump succeeded 988function M.jump_to_location(location) 989 -- location may be Location or LocationLink 990 local uri = location.uri or location.targetUri 991 if uri == nil then return end 992 local bufnr = vim.uri_to_bufnr(uri) 993 -- Save position in jumplist 994 vim.cmd "normal! m'" 995 996 -- Push a new item into tagstack 997 local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0} 998 local items = {{tagname=vim.fn.expand('<cword>'), from=from}} 999 vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't') 1000 1001 --- Jump to new location (adjusting for UTF-16 encoding of characters) 1002 api.nvim_set_current_buf(bufnr) 1003 api.nvim_buf_set_option(0, 'buflisted', true) 1004 local range = location.range or location.targetSelectionRange 1005 local row = range.start.line 1006 local col = get_line_byte_from_position(0, range.start) 1007 api.nvim_win_set_cursor(0, {row + 1, col}) 1008 -- Open folds under the cursor 1009 vim.cmd("normal! zv") 1010 return true 1011end 1012 1013--- Previews a location in a floating window 1014--- 1015--- behavior depends on type of location: 1016--- - for Location, range is shown (e.g., function definition) 1017--- - for LocationLink, targetRange is shown (e.g., body of function definition) 1018--- 1019---@param location a single `Location` or `LocationLink` 1020---@returns (bufnr,winnr) buffer and window number of floating window or nil 1021function M.preview_location(location, opts) 1022 -- location may be LocationLink or Location (more useful for the former) 1023 local uri = location.targetUri or location.uri 1024 if uri == nil then return end 1025 local bufnr = vim.uri_to_bufnr(uri) 1026 if not api.nvim_buf_is_loaded(bufnr) then 1027 vim.fn.bufload(bufnr) 1028 end 1029 local range = location.targetRange or location.range 1030 local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false) 1031 local syntax = api.nvim_buf_get_option(bufnr, 'syntax') 1032 if syntax == "" then 1033 -- When no syntax is set, we use filetype as fallback. This might not result 1034 -- in a valid syntax definition. See also ft detection in stylize_markdown. 1035 -- An empty syntax is more common now with TreeSitter, since TS disables syntax. 1036 syntax = api.nvim_buf_get_option(bufnr, 'filetype') 1037 end 1038 opts = opts or {} 1039 opts.focus_id = "location" 1040 return M.open_floating_preview(contents, syntax, opts) 1041end 1042 1043---@private 1044local function find_window_by_var(name, value) 1045 for _, win in ipairs(api.nvim_list_wins()) do 1046 if npcall(api.nvim_win_get_var, win, name) == value then 1047 return win 1048 end 1049 end 1050end 1051 1052--- Trims empty lines from input and pad top and bottom with empty lines 1053--- 1054---@param contents table of lines to trim and pad 1055---@param opts dictionary with optional fields 1056--- - pad_top number of lines to pad contents at top (default 0) 1057--- - pad_bottom number of lines to pad contents at bottom (default 0) 1058---@return contents table of trimmed and padded lines 1059function M._trim(contents, opts) 1060 validate { 1061 contents = { contents, 't' }; 1062 opts = { opts, 't', true }; 1063 } 1064 opts = opts or {} 1065 contents = M.trim_empty_lines(contents) 1066 if opts.pad_top then 1067 for _ = 1, opts.pad_top do 1068 table.insert(contents, 1, "") 1069 end 1070 end 1071 if opts.pad_bottom then 1072 for _ = 1, opts.pad_bottom do 1073 table.insert(contents, "") 1074 end 1075 end 1076 return contents 1077end 1078 1079--- Generates a table mapping markdown code block lang to vim syntax, 1080--- based on g:markdown_fenced_languages 1081---@return a table of lang -> syntax mappings 1082---@private 1083local function get_markdown_fences() 1084 local fences = {} 1085 for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do 1086 local lang, syntax = fence:match("^(.*)=(.*)$") 1087 if lang then 1088 fences[lang] = syntax 1089 end 1090 end 1091 return fences 1092end 1093 1094--- Converts markdown into syntax highlighted regions by stripping the code 1095--- blocks and converting them into highlighted code. 1096--- This will by default insert a blank line separator after those code block 1097--- regions to improve readability. 1098--- 1099--- This method configures the given buffer and returns the lines to set. 1100--- 1101--- If you want to open a popup with fancy markdown, use `open_floating_preview` instead 1102--- 1103---@param contents table of lines to show in window 1104---@param opts dictionary with optional fields 1105--- - height of floating window 1106--- - width of floating window 1107--- - wrap_at character to wrap at for computing height 1108--- - max_width maximal width of floating window 1109--- - max_height maximal height of floating window 1110--- - pad_top number of lines to pad contents at top 1111--- - pad_bottom number of lines to pad contents at bottom 1112--- - separator insert separator after code block 1113---@returns width,height size of float 1114function M.stylize_markdown(bufnr, contents, opts) 1115 validate { 1116 contents = { contents, 't' }; 1117 opts = { opts, 't', true }; 1118 } 1119 opts = opts or {} 1120 1121 -- table of fence types to {ft, begin, end} 1122 -- when ft is nil, we get the ft from the regex match 1123 local matchers = { 1124 block = {nil, "```+([a-zA-Z0-9_]*)", "```+"}, 1125 pre = {"", "<pre>", "</pre>"}, 1126 code = {"", "<code>", "</code>"}, 1127 text = {"plaintex", "<text>", "</text>"}, 1128 } 1129 1130 local match_begin = function(line) 1131 for type, pattern in pairs(matchers) do 1132 local ret = line:match(string.format("^%%s*%s%%s*$", pattern[2])) 1133 if ret then 1134 return { 1135 type = type, 1136 ft = pattern[1] or ret 1137 } 1138 end 1139 end 1140 end 1141 1142 local match_end = function(line, match) 1143 local pattern = matchers[match.type] 1144 return line:match(string.format("^%%s*%s%%s*$", pattern[3])) 1145 end 1146 1147 -- Clean up 1148 contents = M._trim(contents, opts) 1149 1150 -- Insert blank line separator after code block? 1151 local add_sep = opts.separator == nil and true or opts.separator 1152 local stripped = {} 1153 local highlights = {} 1154 -- keep track of lnums that contain markdown 1155 local markdown_lines = {} 1156 do 1157 local i = 1 1158 while i <= #contents do 1159 local line = contents[i] 1160 local match = match_begin(line) 1161 if match then 1162 local start = #stripped 1163 i = i + 1 1164 while i <= #contents do 1165 line = contents[i] 1166 if match_end(line, match) then 1167 i = i + 1 1168 break 1169 end 1170 table.insert(stripped, line) 1171 i = i + 1 1172 end 1173 table.insert(highlights, { 1174 ft = match.ft; 1175 start = start + 1; 1176 finish = #stripped; 1177 }) 1178 -- add a separator, but not on the last line 1179 if add_sep and i < #contents then 1180 table.insert(stripped, "---") 1181 markdown_lines[#stripped] = true 1182 end 1183 else 1184 -- strip any empty lines or separators prior to this separator in actual markdown 1185 if line:match("^---+$") then 1186 while markdown_lines[#stripped] and (stripped[#stripped]:match("^%s*$") or stripped[#stripped]:match("^---+$")) do 1187 markdown_lines[#stripped] = false 1188 table.remove(stripped, #stripped) 1189 end 1190 end 1191 -- add the line if its not an empty line following a separator 1192 if not (line:match("^%s*$") and markdown_lines[#stripped] and stripped[#stripped]:match("^---+$")) then 1193 table.insert(stripped, line) 1194 markdown_lines[#stripped] = true 1195 end 1196 i = i + 1 1197 end 1198 end 1199 end 1200 1201 -- Compute size of float needed to show (wrapped) lines 1202 opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) 1203 local width = M._make_floating_popup_size(stripped, opts) 1204 1205 local sep_line = string.rep("─", math.min(width, opts.wrap_at or width)) 1206 1207 for l in pairs(markdown_lines) do 1208 if stripped[l]:match("^---+$") then 1209 stripped[l] = sep_line 1210 end 1211 end 1212 1213 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) 1214 1215 local idx = 1 1216 ---@private 1217 -- keep track of syntaxes we already included. 1218 -- no need to include the same syntax more than once 1219 local langs = {} 1220 local fences = get_markdown_fences() 1221 local function apply_syntax_to_region(ft, start, finish) 1222 if ft == "" then 1223 vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1)) 1224 return 1225 end 1226 ft = fences[ft] or ft 1227 local name = ft..idx 1228 idx = idx + 1 1229 local lang = "@"..ft:upper() 1230 if not langs[lang] then 1231 -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set 1232 pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax") 1233 -- TODO(ashkan): better validation before this. 1234 if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then 1235 return 1236 end 1237 langs[lang] = true 1238 end 1239 vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", name, start, finish + 1, lang)) 1240 end 1241 1242 -- needs to run in the buffer for the regions to work 1243 api.nvim_buf_call(bufnr, function() 1244 -- we need to apply lsp_markdown regions speperately, since otherwise 1245 -- markdown regions can "bleed" through the other syntax regions 1246 -- and mess up the formatting 1247 local last = 1 1248 for _, h in ipairs(highlights) do 1249 if last < h.start then 1250 apply_syntax_to_region("lsp_markdown", last, h.start - 1) 1251 end 1252 apply_syntax_to_region(h.ft, h.start, h.finish) 1253 last = h.finish + 1 1254 end 1255 if last <= #stripped then 1256 apply_syntax_to_region("lsp_markdown", last, #stripped) 1257 end 1258 end) 1259 1260 return stripped 1261end 1262 1263---@private 1264--- Creates autocommands to close a preview window when events happen. 1265--- 1266---@param events table list of events 1267---@param winnr number window id of preview window 1268---@param bufnrs table list of buffers where the preview window will remain visible 1269---@see |autocmd-events| 1270local function close_preview_autocmd(events, winnr, bufnrs) 1271 local augroup = 'preview_window_'..winnr 1272 1273 -- close the preview window when entered a buffer that is not 1274 -- the floating window buffer or the buffer that spawned it 1275 vim.cmd(string.format([[ 1276 augroup %s 1277 autocmd! 1278 autocmd BufEnter * lua vim.lsp.util._close_preview_window(%d, {%s}) 1279 augroup end 1280 ]], augroup, winnr, table.concat(bufnrs, ','))) 1281 1282 if #events > 0 then 1283 vim.cmd(string.format([[ 1284 augroup %s 1285 autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d) 1286 augroup end 1287 ]], augroup, table.concat(events, ','), winnr)) 1288 end 1289end 1290 1291---@private 1292--- Closes the preview window 1293--- 1294---@param winnr number window id of preview window 1295---@param bufnrs table|nil optional list of ignored buffers 1296function M._close_preview_window(winnr, bufnrs) 1297 vim.schedule(function() 1298 -- exit if we are in one of ignored buffers 1299 if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then 1300 return 1301 end 1302 1303 local augroup = 'preview_window_'..winnr 1304 vim.cmd(string.format([[ 1305 augroup %s 1306 autocmd! 1307 augroup end 1308 augroup! %s 1309 ]], augroup, augroup)) 1310 pcall(vim.api.nvim_win_close, winnr, true) 1311 end) 1312end 1313 1314---@internal 1315--- Computes size of float needed to show contents (with optional wrapping) 1316--- 1317---@param contents table of lines to show in window 1318---@param opts dictionary with optional fields 1319--- - height of floating window 1320--- - width of floating window 1321--- - wrap_at character to wrap at for computing height 1322--- - max_width maximal width of floating window 1323--- - max_height maximal height of floating window 1324---@returns width,height size of float 1325function M._make_floating_popup_size(contents, opts) 1326 validate { 1327 contents = { contents, 't' }; 1328 opts = { opts, 't', true }; 1329 } 1330 opts = opts or {} 1331 1332 local width = opts.width 1333 local height = opts.height 1334 local wrap_at = opts.wrap_at 1335 local max_width = opts.max_width 1336 local max_height = opts.max_height 1337 local line_widths = {} 1338 1339 if not width then 1340 width = 0 1341 for i, line in ipairs(contents) do 1342 -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. 1343 line_widths[i] = vim.fn.strdisplaywidth(line) 1344 width = math.max(line_widths[i], width) 1345 end 1346 end 1347 1348 local border_width = get_border_size(opts).width 1349 local screen_width = api.nvim_win_get_width(0) 1350 width = math.min(width, screen_width) 1351 1352 -- make sure borders are always inside the screen 1353 if width + border_width > screen_width then 1354 width = width - (width + border_width - screen_width) 1355 end 1356 1357 if wrap_at and wrap_at > width then 1358 wrap_at = width 1359 end 1360 1361 if max_width then 1362 width = math.min(width, max_width) 1363 wrap_at = math.min(wrap_at or max_width, max_width) 1364 end 1365 1366 if not height then 1367 height = #contents 1368 if wrap_at and width >= wrap_at then 1369 height = 0 1370 if vim.tbl_isempty(line_widths) then 1371 for _, line in ipairs(contents) do 1372 local line_width = vim.fn.strdisplaywidth(line) 1373 height = height + math.ceil(line_width/wrap_at) 1374 end 1375 else 1376 for i = 1, #contents do 1377 height = height + math.max(1, math.ceil(line_widths[i]/wrap_at)) 1378 end 1379 end 1380 end 1381 end 1382 if max_height then 1383 height = math.min(height, max_height) 1384 end 1385 1386 return width, height 1387end 1388 1389--- Shows contents in a floating window. 1390--- 1391---@param contents table of lines to show in window 1392---@param syntax string of syntax to set for opened buffer 1393---@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|) 1394--- - height: (number) height of floating window 1395--- - width: (number) width of floating window 1396--- - wrap: (boolean, default true) wrap long lines 1397--- - wrap_at: (string) character to wrap at for computing height when wrap is enabled 1398--- - max_width: (number) maximal width of floating window 1399--- - max_height: (number) maximal height of floating window 1400--- - pad_top: (number) number of lines to pad contents at top 1401--- - pad_bottom: (number) number of lines to pad contents at bottom 1402--- - focus_id: (string) if a popup with this id is opened, then focus it 1403--- - close_events: (table) list of events that closes the floating window 1404--- - focusable: (boolean, default true) Make float focusable 1405--- - focus: (boolean, default true) If `true`, and if {focusable} 1406--- is also `true`, focus an existing floating window with the same 1407--- {focus_id} 1408---@returns bufnr,winnr buffer and window number of the newly created floating 1409---preview window 1410function M.open_floating_preview(contents, syntax, opts) 1411 validate { 1412 contents = { contents, 't' }; 1413 syntax = { syntax, 's', true }; 1414 opts = { opts, 't', true }; 1415 } 1416 opts = opts or {} 1417 opts.wrap = opts.wrap ~= false -- wrapping by default 1418 opts.stylize_markdown = opts.stylize_markdown ~= false 1419 opts.focus = opts.focus ~= false 1420 opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"} 1421 1422 local bufnr = api.nvim_get_current_buf() 1423 1424 -- check if this popup is focusable and we need to focus 1425 if opts.focus_id and opts.focusable ~= false and opts.focus then 1426 -- Go back to previous window if we are in a focusable one 1427 local current_winnr = api.nvim_get_current_win() 1428 if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then 1429 api.nvim_command("wincmd p") 1430 return bufnr, current_winnr 1431 end 1432 do 1433 local win = find_window_by_var(opts.focus_id, bufnr) 1434 if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then 1435 -- focus and return the existing buf, win 1436 api.nvim_set_current_win(win) 1437 api.nvim_command("stopinsert") 1438 return api.nvim_win_get_buf(win), win 1439 end 1440 end 1441 end 1442 1443 -- check if another floating preview already exists for this buffer 1444 -- and close it if needed 1445 local existing_float = npcall(api.nvim_buf_get_var, bufnr, "lsp_floating_preview") 1446 if existing_float and api.nvim_win_is_valid(existing_float) then 1447 api.nvim_win_close(existing_float, true) 1448 end 1449 1450 local floating_bufnr = api.nvim_create_buf(false, true) 1451 local do_stylize = syntax == "markdown" and opts.stylize_markdown 1452 1453 1454 -- Clean up input: trim empty lines from the end, pad 1455 contents = M._trim(contents, opts) 1456 1457 if do_stylize then 1458 -- applies the syntax and sets the lines to the buffer 1459 contents = M.stylize_markdown(floating_bufnr, contents, opts) 1460 else 1461 if syntax then 1462 api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax) 1463 end 1464 api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) 1465 end 1466 1467 -- Compute size of float needed to show (wrapped) lines 1468 if opts.wrap then 1469 opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0) 1470 else 1471 opts.wrap_at = nil 1472 end 1473 local width, height = M._make_floating_popup_size(contents, opts) 1474 1475 local float_option = M.make_floating_popup_options(width, height, opts) 1476 local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) 1477 if do_stylize then 1478 api.nvim_win_set_option(floating_winnr, 'conceallevel', 2) 1479 api.nvim_win_set_option(floating_winnr, 'concealcursor', 'n') 1480 end 1481 -- disable folding 1482 api.nvim_win_set_option(floating_winnr, 'foldenable', false) 1483 -- soft wrapping 1484 api.nvim_win_set_option(floating_winnr, 'wrap', opts.wrap) 1485 1486 api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) 1487 api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe') 1488 api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true}) 1489 close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr}) 1490 1491 -- save focus_id 1492 if opts.focus_id then 1493 api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) 1494 end 1495 api.nvim_buf_set_var(bufnr, "lsp_floating_preview", floating_winnr) 1496 1497 return floating_bufnr, floating_winnr 1498end 1499 1500do --[[ References ]] 1501 local reference_ns = api.nvim_create_namespace("vim_lsp_references") 1502 1503 --- Removes document highlights from a buffer. 1504 --- 1505 ---@param bufnr number Buffer id 1506 function M.buf_clear_references(bufnr) 1507 validate { bufnr = {bufnr, 'n', true} } 1508 api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1) 1509 end 1510 1511 --- Shows a list of document highlights for a certain buffer. 1512 --- 1513 ---@param bufnr number Buffer id 1514 ---@param references table List of `DocumentHighlight` objects to highlight 1515 ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to `offset_encoding` of first client of `bufnr` 1516 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight 1517 function M.buf_highlight_references(bufnr, references, offset_encoding) 1518 validate { bufnr = {bufnr, 'n', true} } 1519 offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) 1520 for _, reference in ipairs(references) do 1521 local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"] 1522 local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"] 1523 1524 local start_idx = get_line_byte_from_position(bufnr, { line = start_line, character = start_char }, offset_encoding) 1525 local end_idx = get_line_byte_from_position(bufnr, { line = start_line, character = end_char }, offset_encoding) 1526 1527 local document_highlight_kind = { 1528 [protocol.DocumentHighlightKind.Text] = "LspReferenceText"; 1529 [protocol.DocumentHighlightKind.Read] = "LspReferenceRead"; 1530 [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite"; 1531 } 1532 local kind = reference["kind"] or protocol.DocumentHighlightKind.Text 1533 highlight.range(bufnr, 1534 reference_ns, 1535 document_highlight_kind[kind], 1536 { start_line, start_idx }, 1537 { end_line, end_idx }) 1538 end 1539 end 1540end 1541 1542local position_sort = sort_by_key(function(v) 1543 return {v.start.line, v.start.character} 1544end) 1545 1546--- Returns the items with the byte position calculated correctly and in sorted 1547--- order, for display in quickfix and location lists. 1548--- 1549--- The result can be passed to the {list} argument of |setqflist()| or 1550--- |setloclist()|. 1551--- 1552---@param locations (table) list of `Location`s or `LocationLink`s 1553---@returns (table) list of items 1554function M.locations_to_items(locations) 1555 local items = {} 1556 local grouped = setmetatable({}, { 1557 __index = function(t, k) 1558 local v = {} 1559 rawset(t, k, v) 1560 return v 1561 end; 1562 }) 1563 for _, d in ipairs(locations) do 1564 -- locations may be Location or LocationLink 1565 local uri = d.uri or d.targetUri 1566 local range = d.range or d.targetSelectionRange 1567 table.insert(grouped[uri], {start = range.start}) 1568 end 1569 1570 1571 local keys = vim.tbl_keys(grouped) 1572 table.sort(keys) 1573 -- TODO(ashkan) I wish we could do this lazily. 1574 for _, uri in ipairs(keys) do 1575 local rows = grouped[uri] 1576 table.sort(rows, position_sort) 1577 local filename = vim.uri_to_fname(uri) 1578 1579 -- list of row numbers 1580 local uri_rows = {} 1581 for _, temp in ipairs(rows) do 1582 local pos = temp.start 1583 local row = pos.line 1584 table.insert(uri_rows, row) 1585 end 1586 1587 -- get all the lines for this uri 1588 local lines = get_lines(vim.uri_to_bufnr(uri), uri_rows) 1589 1590 for _, temp in ipairs(rows) do 1591 local pos = temp.start 1592 local row = pos.line 1593 local line = lines[row] or "" 1594 local col = pos.character 1595 table.insert(items, { 1596 filename = filename, 1597 lnum = row + 1, 1598 col = col + 1; 1599 text = line; 1600 }) 1601 end 1602 end 1603 return items 1604end 1605 1606--- Fills target window's location list with given list of items. 1607--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. 1608--- Defaults to current window. 1609--- 1610---@deprecated Use |setloclist()| 1611--- 1612---@param items (table) list of items 1613function M.set_loclist(items, win_id) 1614 vim.api.nvim_echo({{'vim.lsp.util.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) 1615 vim.fn.setloclist(win_id or 0, {}, ' ', { 1616 title = 'Language Server'; 1617 items = items; 1618 }) 1619end 1620 1621--- Fills quickfix list with given list of items. 1622--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. 1623--- 1624---@deprecated Use |setqflist()| 1625--- 1626---@param items (table) list of items 1627function M.set_qflist(items) 1628 vim.api.nvim_echo({{'vim.lsp.util.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) 1629 vim.fn.setqflist({}, ' ', { 1630 title = 'Language Server'; 1631 items = items; 1632 }) 1633end 1634 1635-- According to LSP spec, if the client set "symbolKind.valueSet", 1636-- the client must handle it properly even if it receives a value outside the specification. 1637-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol 1638function M._get_symbol_kind_name(symbol_kind) 1639 return protocol.SymbolKind[symbol_kind] or "Unknown" 1640end 1641 1642--- Converts symbols to quickfix list items. 1643--- 1644---@param symbols DocumentSymbol[] or SymbolInformation[] 1645function M.symbols_to_items(symbols, bufnr) 1646 ---@private 1647 local function _symbols_to_items(_symbols, _items, _bufnr) 1648 for _, symbol in ipairs(_symbols) do 1649 if symbol.location then -- SymbolInformation type 1650 local range = symbol.location.range 1651 local kind = M._get_symbol_kind_name(symbol.kind) 1652 table.insert(_items, { 1653 filename = vim.uri_to_fname(symbol.location.uri), 1654 lnum = range.start.line + 1, 1655 col = range.start.character + 1, 1656 kind = kind, 1657 text = '['..kind..'] '..symbol.name, 1658 }) 1659 elseif symbol.selectionRange then -- DocumentSymbole type 1660 local kind = M._get_symbol_kind_name(symbol.kind) 1661 table.insert(_items, { 1662 -- bufnr = _bufnr, 1663 filename = vim.api.nvim_buf_get_name(_bufnr), 1664 lnum = symbol.selectionRange.start.line + 1, 1665 col = symbol.selectionRange.start.character + 1, 1666 kind = kind, 1667 text = '['..kind..'] '..symbol.name 1668 }) 1669 if symbol.children then 1670 for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do 1671 for _, s in ipairs(v) do 1672 table.insert(_items, s) 1673 end 1674 end 1675 end 1676 end 1677 end 1678 return _items 1679 end 1680 return _symbols_to_items(symbols, {}, bufnr) 1681end 1682 1683--- Removes empty lines from the beginning and end. 1684---@param lines (table) list of lines to trim 1685---@returns (table) trimmed list of lines 1686function M.trim_empty_lines(lines) 1687 local start = 1 1688 for i = 1, #lines do 1689 if lines[i] ~= nil and #lines[i] > 0 then 1690 start = i 1691 break 1692 end 1693 end 1694 local finish = 1 1695 for i = #lines, 1, -1 do 1696 if lines[i] ~= nil and #lines[i] > 0 then 1697 finish = i 1698 break 1699 end 1700 end 1701 return vim.list_extend({}, lines, start, finish) 1702end 1703 1704--- Accepts markdown lines and tries to reduce them to a filetype if they 1705--- comprise just a single code block. 1706--- 1707--- CAUTION: Modifies the input in-place! 1708--- 1709---@param lines (table) list of lines 1710---@returns (string) filetype or 'markdown' if it was unchanged. 1711function M.try_trim_markdown_code_blocks(lines) 1712 local language_id = lines[1]:match("^```(.*)") 1713 if language_id then 1714 local has_inner_code_fence = false 1715 for i = 2, (#lines - 1) do 1716 local line = lines[i] 1717 if line:sub(1,3) == '```' then 1718 has_inner_code_fence = true 1719 break 1720 end 1721 end 1722 -- No inner code fences + starting with code fence = hooray. 1723 if not has_inner_code_fence then 1724 table.remove(lines, 1) 1725 table.remove(lines) 1726 return language_id 1727 end 1728 end 1729 return 'markdown' 1730end 1731 1732---@private 1733---@param window (optional, number): window handle or 0 for current, defaults to current 1734---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` 1735local function make_position_param(window, offset_encoding) 1736 window = window or 0 1737 local buf = vim.api.nvim_win_get_buf(window) 1738 local row, col = unpack(api.nvim_win_get_cursor(window)) 1739 offset_encoding = offset_encoding or M._get_offset_encoding(buf) 1740 row = row - 1 1741 local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] 1742 if not line then 1743 return { line = 0; character = 0; } 1744 end 1745 1746 col = _str_utfindex_enc(line, col, offset_encoding) 1747 1748 return { line = row; character = col; } 1749end 1750 1751--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. 1752--- 1753---@param window (optional, number): window handle or 0 for current, defaults to current 1754---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` 1755---@returns `TextDocumentPositionParams` object 1756---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams 1757function M.make_position_params(window, offset_encoding) 1758 window = window or 0 1759 local buf = vim.api.nvim_win_get_buf(window) 1760 offset_encoding = offset_encoding or M._get_offset_encoding(buf) 1761 return { 1762 textDocument = M.make_text_document_params(buf); 1763 position = make_position_param(window, offset_encoding) 1764 } 1765end 1766 1767--- Utility function for getting the encoding of the first LSP client on the given buffer. 1768---@param bufnr (number) buffer handle or 0 for current, defaults to current 1769---@returns (string) encoding first client if there is one, nil otherwise 1770function M._get_offset_encoding(bufnr) 1771 validate { 1772 bufnr = {bufnr, 'n', true}; 1773 } 1774 1775 local offset_encoding 1776 1777 for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do 1778 local this_offset_encoding = client.offset_encoding or "utf-16" 1779 if not offset_encoding then 1780 offset_encoding = this_offset_encoding 1781 elseif offset_encoding ~= this_offset_encoding then 1782 vim.notify("warning: multiple different client offset_encodings detected for buffer, this is not supported yet", vim.log.levels.WARN) 1783 end 1784 end 1785 1786 return offset_encoding 1787end 1788 1789--- Using the current position in the current buffer, creates an object that 1790--- can be used as a building block for several LSP requests, such as 1791--- `textDocument/codeAction`, `textDocument/colorPresentation`, 1792--- `textDocument/rangeFormatting`. 1793--- 1794---@param window (optional, number): window handle or 0 for current, defaults to current 1795---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` 1796---@returns { textDocument = { uri = `current_file_uri` }, range = { start = 1797---`current_position`, end = `current_position` } } 1798function M.make_range_params(window, offset_encoding) 1799 local buf = vim.api.nvim_win_get_buf(window) 1800 offset_encoding = offset_encoding or M._get_offset_encoding(buf) 1801 local position = make_position_param(window, offset_encoding) 1802 return { 1803 textDocument = M.make_text_document_params(buf), 1804 range = { start = position; ["end"] = position; } 1805 } 1806end 1807 1808--- Using the given range in the current buffer, creates an object that 1809--- is similar to |vim.lsp.util.make_range_params()|. 1810--- 1811---@param start_pos ({number, number}, optional) mark-indexed position. 1812---Defaults to the start of the last visual selection. 1813---@param end_pos ({number, number}, optional) mark-indexed position. 1814---Defaults to the end of the last visual selection. 1815---@param bufnr (optional, number): buffer handle or 0 for current, defaults to current 1816---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `bufnr` 1817---@returns { textDocument = { uri = `current_file_uri` }, range = { start = 1818---`start_position`, end = `end_position` } } 1819function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) 1820 validate { 1821 start_pos = {start_pos, 't', true}; 1822 end_pos = {end_pos, 't', true}; 1823 offset_encoding = {offset_encoding, 's', true}; 1824 } 1825 bufnr = bufnr or 0 1826 offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) 1827 local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) 1828 local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) 1829 -- convert to 0-index 1830 A[1] = A[1] - 1 1831 B[1] = B[1] - 1 1832 -- account for offset_encoding. 1833 if A[2] > 0 then 1834 A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)} 1835 end 1836 if B[2] > 0 then 1837 B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)} 1838 end 1839 -- we need to offset the end character position otherwise we loose the last 1840 -- character of the selection, as LSP end position is exclusive 1841 -- see https://microsoft.github.io/language-server-protocol/specification#range 1842 if vim.o.selection ~= 'exclusive' then 1843 B[2] = B[2] + 1 1844 end 1845 return { 1846 textDocument = M.make_text_document_params(bufnr), 1847 range = { 1848 start = {line = A[1], character = A[2]}, 1849 ['end'] = {line = B[1], character = B[2]} 1850 } 1851 } 1852end 1853 1854--- Creates a `TextDocumentIdentifier` object for the current buffer. 1855--- 1856---@param bufnr (optional, number): Buffer handle, defaults to current 1857---@returns `TextDocumentIdentifier` 1858---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier 1859function M.make_text_document_params(bufnr) 1860 return { uri = vim.uri_from_bufnr(bufnr or 0) } 1861end 1862 1863--- Create the workspace params 1864---@param added 1865---@param removed 1866function M.make_workspace_params(added, removed) 1867 return { event = { added = added; removed = removed; } } 1868end 1869--- Returns visual width of tabstop. 1870--- 1871---@see |softtabstop| 1872---@param bufnr (optional, number): Buffer handle, defaults to current 1873---@returns (number) tabstop visual width 1874function M.get_effective_tabstop(bufnr) 1875 validate { bufnr = {bufnr, 'n', true} } 1876 local bo = bufnr and vim.bo[bufnr] or vim.bo 1877 local sts = bo.softtabstop 1878 return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop 1879end 1880 1881--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position. 1882--- 1883---@param options Table with valid `FormattingOptions` entries 1884---@returns `DocumentFormattingParams` object 1885---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting 1886function M.make_formatting_params(options) 1887 validate { options = {options, 't', true} } 1888 options = vim.tbl_extend('keep', options or {}, { 1889 tabSize = M.get_effective_tabstop(); 1890 insertSpaces = vim.bo.expandtab; 1891 }) 1892 return { 1893 textDocument = { uri = vim.uri_from_bufnr(0) }; 1894 options = options; 1895 } 1896end 1897 1898--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer. 1899--- 1900---@param buf buffer id (0 for current) 1901---@param row 0-indexed line 1902---@param col 0-indexed byte offset in line 1903---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf` 1904---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf} 1905function M.character_offset(buf, row, col, offset_encoding) 1906 local line = get_line(buf, row) 1907 offset_encoding = offset_encoding or M._get_offset_encoding(buf) 1908 -- If the col is past the EOL, use the line length. 1909 if col > #line then 1910 return _str_utfindex_enc(line, nil, offset_encoding) 1911 end 1912 return _str_utfindex_enc(line, col, offset_encoding) 1913end 1914 1915--- Helper function to return nested values in language server settings 1916--- 1917---@param settings a table of language server settings 1918---@param section a string indicating the field of the settings table 1919---@returns (table or string) The value of settings accessed via section 1920function M.lookup_section(settings, section) 1921 for part in vim.gsplit(section, '.', true) do 1922 settings = settings[part] 1923 if not settings then 1924 return 1925 end 1926 end 1927 return settings 1928end 1929 1930M._get_line_byte_from_position = get_line_byte_from_position 1931M._warn_once = warn_once 1932 1933M.buf_versions = {} 1934 1935return M 1936-- vim:sw=2 ts=2 et 1937