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