mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +00:00 
			
		
		
		
	* First implementation of workspace folders * Add completion for current directory * Add tracking of workspace folders * Add workspace folder listing * Add checks on adding/removing workspaces * Add appropriate initialization options * Add documentation * Make workspaceFolders available wherever client is
		
			
				
	
	
		
			1371 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			1371 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
local protocol = require 'vim.lsp.protocol'
 | 
						|
local vim = vim
 | 
						|
local validate = vim.validate
 | 
						|
local api = vim.api
 | 
						|
local list_extend = vim.list_extend
 | 
						|
local highlight = require 'vim.highlight'
 | 
						|
 | 
						|
local npcall = vim.F.npcall
 | 
						|
local split = vim.split
 | 
						|
 | 
						|
local _warned = {}
 | 
						|
local warn_once = function(message)
 | 
						|
  if not _warned[message] then
 | 
						|
    vim.api.nvim_err_writeln(message)
 | 
						|
    _warned[message] = true
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
local M = {}
 | 
						|
 | 
						|
-- TODO(remove-callbacks)
 | 
						|
M.diagnostics_by_buf = setmetatable({}, {
 | 
						|
  __index = function(_, bufnr)
 | 
						|
    warn_once("diagnostics_by_buf is deprecated. Use 'vim.lsp.diagnostic.get'")
 | 
						|
    return vim.lsp.diagnostic.get(bufnr)
 | 
						|
  end
 | 
						|
})
 | 
						|
 | 
						|
--@private
 | 
						|
local function split_lines(value)
 | 
						|
  return split(value, '\n', true)
 | 
						|
end
 | 
						|
 | 
						|
--- Replaces text in a range with new text.
 | 
						|
---
 | 
						|
--- CAUTION: Changes in-place!
 | 
						|
---
 | 
						|
--@param lines (table) Original list of strings
 | 
						|
--@param A (table) Start position; a 2-tuple of {line, col} numbers
 | 
						|
--@param B (table) End position; a 2-tuple of {line, col} numbers
 | 
						|
--@param new_lines A list of strings to replace the original
 | 
						|
--@returns (table) The modified {lines} object
 | 
						|
function M.set_lines(lines, A, B, new_lines)
 | 
						|
  -- 0-indexing to 1-indexing
 | 
						|
  local i_0 = A[1] + 1
 | 
						|
  -- If it extends past the end, truncate it to the end. This is because the
 | 
						|
  -- way the LSP describes the range including the last newline is by
 | 
						|
  -- specifying a line number after what we would call the last line.
 | 
						|
  local i_n = math.min(B[1] + 1, #lines)
 | 
						|
  if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
 | 
						|
    error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
 | 
						|
  end
 | 
						|
  local prefix = ""
 | 
						|
  local suffix = lines[i_n]:sub(B[2]+1)
 | 
						|
  if A[2] > 0 then
 | 
						|
    prefix = lines[i_0]:sub(1, A[2])
 | 
						|
  end
 | 
						|
  local n = i_n - i_0 + 1
 | 
						|
  if n ~= #new_lines then
 | 
						|
    for _ = 1, n - #new_lines do table.remove(lines, i_0) end
 | 
						|
    for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end
 | 
						|
  end
 | 
						|
  for i = 1, #new_lines do
 | 
						|
    lines[i - 1 + i_0] = new_lines[i]
 | 
						|
  end
 | 
						|
  if #suffix > 0 then
 | 
						|
    local i = i_0 + #new_lines - 1
 | 
						|
    lines[i] = lines[i]..suffix
 | 
						|
  end
 | 
						|
  if #prefix > 0 then
 | 
						|
    lines[i_0] = prefix..lines[i_0]
 | 
						|
  end
 | 
						|
  return lines
 | 
						|
end
 | 
						|
 | 
						|
--@private
 | 
						|
local function sort_by_key(fn)
 | 
						|
  return function(a,b)
 | 
						|
    local ka, kb = fn(a), fn(b)
 | 
						|
    assert(#ka == #kb)
 | 
						|
    for i = 1, #ka do
 | 
						|
      if ka[i] ~= kb[i] then
 | 
						|
        return ka[i] < kb[i]
 | 
						|
      end
 | 
						|
    end
 | 
						|
    -- every value must have been equal here, which means it's not less than.
 | 
						|
    return false
 | 
						|
  end
 | 
						|
end
 | 
						|
--@private
 | 
						|
local edit_sort_key = sort_by_key(function(e)
 | 
						|
  return {e.A[1], e.A[2], e.i}
 | 
						|
end)
 | 
						|
 | 
						|
--@private
 | 
						|
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
 | 
						|
--- Returns a zero-indexed column, since set_lines() does the conversion to
 | 
						|
--- 1-indexed
 | 
						|
local function get_line_byte_from_position(bufnr, position)
 | 
						|
  -- LSP's line and characters are 0-indexed
 | 
						|
  -- Vim's line and columns are 1-indexed
 | 
						|
  local col = position.character
 | 
						|
  -- When on the first character, we can ignore the difference between byte and
 | 
						|
  -- character
 | 
						|
  if col > 0 then
 | 
						|
    if not api.nvim_buf_is_loaded(bufnr) then
 | 
						|
      vim.fn.bufload(bufnr)
 | 
						|
    end
 | 
						|
 | 
						|
    local line = position.line
 | 
						|
    local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
 | 
						|
    if #lines > 0 then
 | 
						|
      local ok, result = pcall(vim.str_byteindex, lines[1], col)
 | 
						|
 | 
						|
      if ok then
 | 
						|
        return result
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return col
 | 
						|
end
 | 
						|
 | 
						|
--- Applies a list of text edits to a buffer.
 | 
						|
--@param text_edits (table) list of `TextEdit` objects
 | 
						|
--@param buf_nr (number) Buffer id
 | 
						|
function M.apply_text_edits(text_edits, bufnr)
 | 
						|
  if not next(text_edits) then return end
 | 
						|
  if not api.nvim_buf_is_loaded(bufnr) then
 | 
						|
    vim.fn.bufload(bufnr)
 | 
						|
  end
 | 
						|
  api.nvim_buf_set_option(bufnr, 'buflisted', true)
 | 
						|
  local start_line, finish_line = math.huge, -1
 | 
						|
  local cleaned = {}
 | 
						|
  for i, e in ipairs(text_edits) do
 | 
						|
    -- adjust start and end column for UTF-16 encoding of non-ASCII characters
 | 
						|
    local start_row = e.range.start.line
 | 
						|
    local start_col = get_line_byte_from_position(bufnr, e.range.start)
 | 
						|
    local end_row = e.range["end"].line
 | 
						|
    local end_col = get_line_byte_from_position(bufnr, e.range['end'])
 | 
						|
    start_line = math.min(e.range.start.line, start_line)
 | 
						|
    finish_line = math.max(e.range["end"].line, finish_line)
 | 
						|
    -- TODO(ashkan) sanity check ranges for overlap.
 | 
						|
    table.insert(cleaned, {
 | 
						|
      i = i;
 | 
						|
      A = {start_row; start_col};
 | 
						|
      B = {end_row; end_col};
 | 
						|
      lines = vim.split(e.newText, '\n', true);
 | 
						|
    })
 | 
						|
  end
 | 
						|
 | 
						|
  -- Reverse sort the orders so we can apply them without interfering with
 | 
						|
  -- eachother. Also add i as a sort key to mimic a stable sort.
 | 
						|
  table.sort(cleaned, edit_sort_key)
 | 
						|
  local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false)
 | 
						|
  local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol')
 | 
						|
  local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1
 | 
						|
  if set_eol and #lines[#lines] ~= 0 then
 | 
						|
    table.insert(lines, '')
 | 
						|
  end
 | 
						|
 | 
						|
  for i = #cleaned, 1, -1 do
 | 
						|
    local e = cleaned[i]
 | 
						|
    local A = {e.A[1] - start_line, e.A[2]}
 | 
						|
    local B = {e.B[1] - start_line, e.B[2]}
 | 
						|
    lines = M.set_lines(lines, A, B, e.lines)
 | 
						|
  end
 | 
						|
  if set_eol and #lines[#lines] == 0 then
 | 
						|
    table.remove(lines)
 | 
						|
  end
 | 
						|
  api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines)
 | 
						|
end
 | 
						|
 | 
						|
-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
 | 
						|
-- local valid_unix_path_characters = "[^/]"
 | 
						|
-- https://github.com/davidm/lua-glob-pattern
 | 
						|
-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
 | 
						|
-- function M.glob_to_regex(glob)
 | 
						|
-- end
 | 
						|
 | 
						|
--- Can be used to extract the completion items from a
 | 
						|
--- `textDocument/completion` request, which may return one of
 | 
						|
--- `CompletionItem[]`, `CompletionList` or null.
 | 
						|
--@param result (table) The result of a `textDocument/completion` request
 | 
						|
--@returns (table) List of completion items
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
 | 
						|
function M.extract_completion_items(result)
 | 
						|
  if type(result) == 'table' and result.items then
 | 
						|
    -- result is a `CompletionList`
 | 
						|
    return result.items
 | 
						|
  elseif result ~= nil then
 | 
						|
    -- result is `CompletionItem[]`
 | 
						|
    return result
 | 
						|
  else
 | 
						|
    -- result is `null`
 | 
						|
    return {}
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
--- Applies a `TextDocumentEdit`, which is a list of changes to a single
 | 
						|
-- document.
 | 
						|
---
 | 
						|
--@param text_document_edit (table) a `TextDocumentEdit` object
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
 | 
						|
function M.apply_text_document_edit(text_document_edit)
 | 
						|
  local text_document = text_document_edit.textDocument
 | 
						|
  local bufnr = vim.uri_to_bufnr(text_document.uri)
 | 
						|
 | 
						|
  -- `VersionedTextDocumentIdentifier`s version may be null
 | 
						|
  --  https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
 | 
						|
  if text_document.version
 | 
						|
      and M.buf_versions[bufnr]
 | 
						|
      and M.buf_versions[bufnr] > text_document.version then
 | 
						|
    print("Buffer ", text_document.uri, " newer than edits.")
 | 
						|
    return
 | 
						|
  end
 | 
						|
 | 
						|
  M.apply_text_edits(text_document_edit.edits, bufnr)
 | 
						|
end
 | 
						|
 | 
						|
--@private
 | 
						|
--- Recursively parses snippets in a completion entry.
 | 
						|
---
 | 
						|
--@param input (string) Snippet text to parse for snippets
 | 
						|
--@param inner (bool) Whether this function is being called recursively
 | 
						|
--@returns 2-tuple of strings: The first is the parsed result, the second is the
 | 
						|
---unparsed rest of the input
 | 
						|
local function parse_snippet_rec(input, inner)
 | 
						|
  local res = ""
 | 
						|
 | 
						|
  local close, closeend = nil, nil
 | 
						|
  if inner then
 | 
						|
    close, closeend = input:find("}", 1, true)
 | 
						|
    while close ~= nil and input:sub(close-1,close-1) == "\\" do
 | 
						|
      close, closeend = input:find("}", closeend+1, true)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  local didx = input:find('$',  1, true)
 | 
						|
  if didx == nil and close == nil then
 | 
						|
    return input, ""
 | 
						|
  elseif close ~=nil and (didx == nil or close < didx) then
 | 
						|
    -- No inner placeholders
 | 
						|
    return input:sub(0, close-1), input:sub(closeend+1)
 | 
						|
  end
 | 
						|
 | 
						|
  res = res .. input:sub(0, didx-1)
 | 
						|
  input = input:sub(didx+1)
 | 
						|
 | 
						|
  local tabstop, tabstopend = input:find('^%d+')
 | 
						|
  local placeholder, placeholderend = input:find('^{%d+:')
 | 
						|
  local choice, choiceend = input:find('^{%d+|')
 | 
						|
 | 
						|
  if tabstop then
 | 
						|
    input = input:sub(tabstopend+1)
 | 
						|
  elseif choice then
 | 
						|
    input = input:sub(choiceend+1)
 | 
						|
    close, closeend = input:find("|}", 1, true)
 | 
						|
 | 
						|
    res = res .. input:sub(0, close-1)
 | 
						|
    input = input:sub(closeend+1)
 | 
						|
  elseif placeholder then
 | 
						|
    -- TODO: add support for variables
 | 
						|
    input = input:sub(placeholderend+1)
 | 
						|
 | 
						|
    -- placeholders and variables are recursive
 | 
						|
    while input ~= "" do
 | 
						|
      local r, tail = parse_snippet_rec(input, true)
 | 
						|
      r = r:gsub("\\}", "}")
 | 
						|
 | 
						|
      res = res .. r
 | 
						|
      input = tail
 | 
						|
    end
 | 
						|
  else
 | 
						|
    res = res .. "$"
 | 
						|
  end
 | 
						|
 | 
						|
  return res, input
 | 
						|
end
 | 
						|
 | 
						|
--- Parses snippets in a completion entry.
 | 
						|
---
 | 
						|
--@param input (string) unparsed snippet
 | 
						|
--@returns (string) parsed snippet
 | 
						|
function M.parse_snippet(input)
 | 
						|
  local res, _ = parse_snippet_rec(input, false)
 | 
						|
 | 
						|
  return res
 | 
						|
end
 | 
						|
 | 
						|
--@private
 | 
						|
--- Sorts by CompletionItem.sortText.
 | 
						|
---
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
 | 
						|
local function sort_completion_items(items)
 | 
						|
  table.sort(items, function(a, b)
 | 
						|
    return (a.sortText or a.label) < (b.sortText or b.label)
 | 
						|
  end)
 | 
						|
end
 | 
						|
 | 
						|
--@private
 | 
						|
--- Returns text that should be inserted when selecting completion item. The
 | 
						|
--- precedence is as follows: textEdit.newText > insertText > label
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
 | 
						|
local function get_completion_word(item)
 | 
						|
  if item.textEdit ~= nil and item.textEdit.newText ~= nil then
 | 
						|
    if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
 | 
						|
      return item.textEdit.newText
 | 
						|
    else
 | 
						|
      return M.parse_snippet(item.textEdit.newText)
 | 
						|
    end
 | 
						|
  elseif item.insertText ~= nil then
 | 
						|
    if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
 | 
						|
      return item.insertText
 | 
						|
    else
 | 
						|
      return M.parse_snippet(item.insertText)
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return item.label
 | 
						|
end
 | 
						|
 | 
						|
--@private
 | 
						|
--- Some language servers return complementary candidates whose prefixes do not
 | 
						|
--- match are also returned. So we exclude completion candidates whose prefix
 | 
						|
--- does not match.
 | 
						|
local function remove_unmatch_completion_items(items, prefix)
 | 
						|
  return vim.tbl_filter(function(item)
 | 
						|
    local word = get_completion_word(item)
 | 
						|
    return vim.startswith(word, prefix)
 | 
						|
  end, items)
 | 
						|
end
 | 
						|
 | 
						|
--- Acording to LSP spec, if the client set `completionItemKind.valueSet`,
 | 
						|
--- the client must handle it properly even if it receives a value outside the
 | 
						|
--- specification.
 | 
						|
---
 | 
						|
--@param completion_item_kind (`vim.lsp.protocol.completionItemKind`)
 | 
						|
--@returns (`vim.lsp.protocol.completionItemKind`)
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
 | 
						|
function M._get_completion_item_kind_name(completion_item_kind)
 | 
						|
  return protocol.CompletionItemKind[completion_item_kind] or "Unknown"
 | 
						|
end
 | 
						|
 | 
						|
--- Turns the result of a `textDocument/completion` request into vim-compatible
 | 
						|
--- |complete-items|.
 | 
						|
---
 | 
						|
--@param result The result of a `textDocument/completion` call, e.g. from
 | 
						|
---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
 | 
						|
--- `CompletionList` or `null`
 | 
						|
--@param prefix (string) the prefix to filter the completion items
 | 
						|
--@returns { matches = complete-items table, incomplete = bool }
 | 
						|
--@see |complete-items|
 | 
						|
function M.text_document_completion_list_to_complete_items(result, prefix)
 | 
						|
  local items = M.extract_completion_items(result)
 | 
						|
  if vim.tbl_isempty(items) then
 | 
						|
    return {}
 | 
						|
  end
 | 
						|
 | 
						|
  items = remove_unmatch_completion_items(items, prefix)
 | 
						|
  sort_completion_items(items)
 | 
						|
 | 
						|
  local matches = {}
 | 
						|
 | 
						|
  for _, completion_item in ipairs(items) do
 | 
						|
    local info = ' '
 | 
						|
    local documentation = completion_item.documentation
 | 
						|
    if documentation then
 | 
						|
      if type(documentation) == 'string' and documentation ~= '' then
 | 
						|
        info = documentation
 | 
						|
      elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
 | 
						|
        info = documentation.value
 | 
						|
      -- else
 | 
						|
        -- TODO(ashkan) Validation handling here?
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    local word = get_completion_word(completion_item)
 | 
						|
    table.insert(matches, {
 | 
						|
      word = word,
 | 
						|
      abbr = completion_item.label,
 | 
						|
      kind = M._get_completion_item_kind_name(completion_item.kind),
 | 
						|
      menu = completion_item.detail or '',
 | 
						|
      info = info,
 | 
						|
      icase = 1,
 | 
						|
      dup = 1,
 | 
						|
      empty = 1,
 | 
						|
      user_data = {
 | 
						|
        nvim = {
 | 
						|
          lsp = {
 | 
						|
            completion_item = completion_item
 | 
						|
          }
 | 
						|
        }
 | 
						|
      },
 | 
						|
    })
 | 
						|
  end
 | 
						|
 | 
						|
  return matches
 | 
						|
end
 | 
						|
 | 
						|
--- Applies a `WorkspaceEdit`.
 | 
						|
---
 | 
						|
--@param workspace_edit (table) `WorkspaceEdit`
 | 
						|
-- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
 | 
						|
function M.apply_workspace_edit(workspace_edit)
 | 
						|
  if workspace_edit.documentChanges then
 | 
						|
    for _, change in ipairs(workspace_edit.documentChanges) do
 | 
						|
      if change.kind then
 | 
						|
        -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
 | 
						|
        error(string.format("Unsupported change: %q", vim.inspect(change)))
 | 
						|
      else
 | 
						|
        M.apply_text_document_edit(change)
 | 
						|
      end
 | 
						|
    end
 | 
						|
    return
 | 
						|
  end
 | 
						|
 | 
						|
  local all_changes = workspace_edit.changes
 | 
						|
  if not (all_changes and not vim.tbl_isempty(all_changes)) then
 | 
						|
    return
 | 
						|
  end
 | 
						|
 | 
						|
  for uri, changes in pairs(all_changes) do
 | 
						|
    local bufnr = vim.uri_to_bufnr(uri)
 | 
						|
    M.apply_text_edits(changes, bufnr)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
--- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into
 | 
						|
--- a list of lines containing valid markdown. Useful to populate the hover
 | 
						|
--- window for `textDocument/hover`, for parsing the result of
 | 
						|
--- `textDocument/signatureHelp`, and potentially others.
 | 
						|
---
 | 
						|
--@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
 | 
						|
--@param contents (table, optional, default `{}`) List of strings to extend with converted lines
 | 
						|
--@returns {contents}, extended with lines of converted markdown.
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
 | 
						|
function M.convert_input_to_markdown_lines(input, contents)
 | 
						|
  contents = contents or {}
 | 
						|
  -- MarkedString variation 1
 | 
						|
  if type(input) == 'string' then
 | 
						|
    list_extend(contents, split_lines(input))
 | 
						|
  else
 | 
						|
    assert(type(input) == 'table', "Expected a table for Hover.contents")
 | 
						|
    -- MarkupContent
 | 
						|
    if input.kind then
 | 
						|
      -- The kind can be either plaintext or markdown. However, either way we
 | 
						|
      -- will just be rendering markdown, so we handle them both the same way.
 | 
						|
      -- TODO these can have escaped/sanitized html codes in markdown. We
 | 
						|
      -- should make sure we handle this correctly.
 | 
						|
 | 
						|
      -- Some servers send input.value as empty, so let's ignore this :(
 | 
						|
      -- assert(type(input.value) == 'string')
 | 
						|
      list_extend(contents, split_lines(input.value or ''))
 | 
						|
    -- MarkupString variation 2
 | 
						|
    elseif input.language then
 | 
						|
      -- Some servers send input.value as empty, so let's ignore this :(
 | 
						|
      -- assert(type(input.value) == 'string')
 | 
						|
      table.insert(contents, "```"..input.language)
 | 
						|
      list_extend(contents, split_lines(input.value or ''))
 | 
						|
      table.insert(contents, "```")
 | 
						|
    -- By deduction, this must be MarkedString[]
 | 
						|
    else
 | 
						|
      -- Use our existing logic to handle MarkedString
 | 
						|
      for _, marked_string in ipairs(input) do
 | 
						|
        M.convert_input_to_markdown_lines(marked_string, contents)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  if (contents[1] == '' or contents[1] == nil) and #contents == 1 then
 | 
						|
    return {}
 | 
						|
  end
 | 
						|
  return contents
 | 
						|
end
 | 
						|
 | 
						|
--- Converts `textDocument/SignatureHelp` response to markdown lines.
 | 
						|
---
 | 
						|
--@param signature_help Response of `textDocument/SignatureHelp`
 | 
						|
--@returns list of lines of converted markdown.
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
 | 
						|
function M.convert_signature_help_to_markdown_lines(signature_help)
 | 
						|
  if not signature_help.signatures then
 | 
						|
    return
 | 
						|
  end
 | 
						|
  --The active signature. If omitted or the value lies outside the range of
 | 
						|
  --`signatures` the value defaults to zero or is ignored if `signatures.length
 | 
						|
  --=== 0`. Whenever possible implementors should make an active decision about
 | 
						|
  --the active signature and shouldn't rely on a default value.
 | 
						|
  local contents = {}
 | 
						|
  local active_signature = signature_help.activeSignature or 0
 | 
						|
  -- If the activeSignature is not inside the valid range, then clip it.
 | 
						|
  if active_signature >= #signature_help.signatures then
 | 
						|
    active_signature = 0
 | 
						|
  end
 | 
						|
  local signature = signature_help.signatures[active_signature + 1]
 | 
						|
  if not signature then
 | 
						|
    return
 | 
						|
  end
 | 
						|
  vim.list_extend(contents, vim.split(signature.label, '\n', true))
 | 
						|
  if signature.documentation then
 | 
						|
    M.convert_input_to_markdown_lines(signature.documentation, contents)
 | 
						|
  end
 | 
						|
  if signature.parameters and #signature.parameters > 0 then
 | 
						|
    local active_parameter = signature_help.activeParameter or 0
 | 
						|
    -- If the activeParameter is not inside the valid range, then clip it.
 | 
						|
    if active_parameter >= #signature.parameters then
 | 
						|
      active_parameter = 0
 | 
						|
    end
 | 
						|
    local parameter = signature.parameters[active_parameter + 1]
 | 
						|
    if parameter then
 | 
						|
      --[=[
 | 
						|
      --Represents a parameter of a callable-signature. A parameter can
 | 
						|
      --have a label and a doc-comment.
 | 
						|
      interface ParameterInformation {
 | 
						|
        --The label of this parameter information.
 | 
						|
        --
 | 
						|
        --Either a string or an inclusive start and exclusive end offsets within its containing
 | 
						|
        --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
 | 
						|
        --string representation as `Position` and `Range` does.
 | 
						|
        --
 | 
						|
        --*Note*: a label of type string should be a substring of its containing signature label.
 | 
						|
        --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
 | 
						|
        label: string | [number, number];
 | 
						|
        --The human-readable doc-comment of this parameter. Will be shown
 | 
						|
        --in the UI but can be omitted.
 | 
						|
        documentation?: string | MarkupContent;
 | 
						|
      }
 | 
						|
      --]=]
 | 
						|
      -- TODO highlight parameter
 | 
						|
      if parameter.documentation then
 | 
						|
        M.convert_input_to_markdown_lines(parameter.documentation, contents)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return contents
 | 
						|
end
 | 
						|
 | 
						|
--- Creates a table with sensible default options for a floating window. The
 | 
						|
--- table can be passed to |nvim_open_win()|.
 | 
						|
---
 | 
						|
--@param width (number) window width (in character cells)
 | 
						|
--@param height (number) window height (in character cells)
 | 
						|
--@param opts (table, optional)
 | 
						|
--@returns (table) Options
 | 
						|
function M.make_floating_popup_options(width, height, opts)
 | 
						|
  validate {
 | 
						|
    opts = { opts, 't', true };
 | 
						|
  }
 | 
						|
  opts = opts or {}
 | 
						|
  validate {
 | 
						|
    ["opts.offset_x"] = { opts.offset_x, 'n', true };
 | 
						|
    ["opts.offset_y"] = { opts.offset_y, 'n', true };
 | 
						|
  }
 | 
						|
 | 
						|
  local anchor = ''
 | 
						|
  local row, col
 | 
						|
 | 
						|
  local lines_above = vim.fn.winline() - 1
 | 
						|
  local lines_below = vim.fn.winheight(0) - lines_above
 | 
						|
 | 
						|
  if lines_above < lines_below then
 | 
						|
    anchor = anchor..'N'
 | 
						|
    height = math.min(lines_below, height)
 | 
						|
    row = 1
 | 
						|
  else
 | 
						|
    anchor = anchor..'S'
 | 
						|
    height = math.min(lines_above, height)
 | 
						|
    row = 0
 | 
						|
  end
 | 
						|
 | 
						|
  if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
 | 
						|
    anchor = anchor..'W'
 | 
						|
    col = 0
 | 
						|
  else
 | 
						|
    anchor = anchor..'E'
 | 
						|
    col = 1
 | 
						|
  end
 | 
						|
 | 
						|
  return {
 | 
						|
    anchor = anchor,
 | 
						|
    col = col + (opts.offset_x or 0),
 | 
						|
    height = height,
 | 
						|
    relative = 'cursor',
 | 
						|
    row = row + (opts.offset_y or 0),
 | 
						|
    style = 'minimal',
 | 
						|
    width = width,
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
--- Jumps to a location.
 | 
						|
---
 | 
						|
--@param location (`Location`|`LocationLink`)
 | 
						|
--@returns `true` if the jump succeeded
 | 
						|
function M.jump_to_location(location)
 | 
						|
  -- location may be Location or LocationLink
 | 
						|
  local uri = location.uri or location.targetUri
 | 
						|
  if uri == nil then return end
 | 
						|
  local bufnr = vim.uri_to_bufnr(uri)
 | 
						|
  -- Save position in jumplist
 | 
						|
  vim.cmd "normal! m'"
 | 
						|
 | 
						|
  -- Push a new item into tagstack
 | 
						|
  local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0}
 | 
						|
  local items = {{tagname=vim.fn.expand('<cword>'), from=from}}
 | 
						|
  vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't')
 | 
						|
 | 
						|
  --- Jump to new location (adjusting for UTF-16 encoding of characters)
 | 
						|
  api.nvim_set_current_buf(bufnr)
 | 
						|
  api.nvim_buf_set_option(0, 'buflisted', true)
 | 
						|
  local range = location.range or location.targetSelectionRange
 | 
						|
  local row = range.start.line
 | 
						|
  local col = get_line_byte_from_position(0, range.start)
 | 
						|
  api.nvim_win_set_cursor(0, {row + 1, col})
 | 
						|
  return true
 | 
						|
end
 | 
						|
 | 
						|
--- Previews a location in a floating window
 | 
						|
---
 | 
						|
--- behavior depends on type of location:
 | 
						|
---   - for Location, range is shown (e.g., function definition)
 | 
						|
---   - for LocationLink, targetRange is shown (e.g., body of function definition)
 | 
						|
---
 | 
						|
--@param location a single `Location` or `LocationLink`
 | 
						|
--@returns (bufnr,winnr) buffer and window number of floating window or nil
 | 
						|
function M.preview_location(location)
 | 
						|
  -- location may be LocationLink or Location (more useful for the former)
 | 
						|
  local uri = location.targetUri or location.uri
 | 
						|
  if uri == nil then return end
 | 
						|
  local bufnr = vim.uri_to_bufnr(uri)
 | 
						|
  if not api.nvim_buf_is_loaded(bufnr) then
 | 
						|
    vim.fn.bufload(bufnr)
 | 
						|
  end
 | 
						|
  local range = location.targetRange or location.range
 | 
						|
  local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
 | 
						|
  local filetype = api.nvim_buf_get_option(bufnr, 'filetype')
 | 
						|
  return M.open_floating_preview(contents, filetype)
 | 
						|
end
 | 
						|
 | 
						|
--@private
 | 
						|
local function find_window_by_var(name, value)
 | 
						|
  for _, win in ipairs(api.nvim_list_wins()) do
 | 
						|
    if npcall(api.nvim_win_get_var, win, name) == value then
 | 
						|
      return win
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
--- Enters/leaves the focusable window associated with the current buffer via the
 | 
						|
--window - variable `unique_name`. If no such window exists, run the function
 | 
						|
--{fn}.
 | 
						|
---
 | 
						|
--@param unique_name (string) Window variable
 | 
						|
--@param fn (function) should return create a new window and return a tuple of
 | 
						|
---({focusable_buffer_id}, {window_id}). if {focusable_buffer_id} is a valid
 | 
						|
---buffer id, the newly created window will be the new focus associated with
 | 
						|
---the current buffer via the tag `unique_name`.
 | 
						|
--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise
 | 
						|
function M.focusable_float(unique_name, fn)
 | 
						|
  -- Go back to previous window if we are in a focusable one
 | 
						|
  if npcall(api.nvim_win_get_var, 0, unique_name) then
 | 
						|
    return api.nvim_command("wincmd p")
 | 
						|
  end
 | 
						|
  local bufnr = api.nvim_get_current_buf()
 | 
						|
  do
 | 
						|
    local win = find_window_by_var(unique_name, bufnr)
 | 
						|
    if win and api.nvim_win_is_valid(win) and not vim.fn.pumvisible() then
 | 
						|
      api.nvim_set_current_win(win)
 | 
						|
      api.nvim_command("stopinsert")
 | 
						|
      return
 | 
						|
    end
 | 
						|
  end
 | 
						|
  local pbufnr, pwinnr = fn()
 | 
						|
  if pbufnr then
 | 
						|
    api.nvim_win_set_var(pwinnr, unique_name, bufnr)
 | 
						|
    return pbufnr, pwinnr
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
--- Focuses/unfocuses the floating preview window associated with the current
 | 
						|
--- buffer via the window variable `unique_name`. If no such preview window
 | 
						|
--- exists, makes a new one.
 | 
						|
---
 | 
						|
--@param unique_name (string) Window variable
 | 
						|
--@param fn (function) The return values of this function will be passed
 | 
						|
---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new
 | 
						|
---floating window should be created
 | 
						|
function M.focusable_preview(unique_name, fn)
 | 
						|
  return M.focusable_float(unique_name, function()
 | 
						|
    return M.open_floating_preview(fn())
 | 
						|
  end)
 | 
						|
end
 | 
						|
 | 
						|
--- Trims empty lines from input and pad left and right with spaces
 | 
						|
---
 | 
						|
---@param contents table of lines to trim and pad
 | 
						|
---@param opts dictionary with optional fields
 | 
						|
---             - pad_left   number of columns to pad contents at left (default 1)
 | 
						|
---             - pad_right  number of columns to pad contents at right (default 1)
 | 
						|
---             - pad_top    number of lines to pad contents at top (default 0)
 | 
						|
---             - pad_bottom number of lines to pad contents at bottom (default 0)
 | 
						|
---@return contents table of trimmed and padded lines
 | 
						|
function M._trim_and_pad(contents, opts)
 | 
						|
  validate {
 | 
						|
    contents = { contents, 't' };
 | 
						|
    opts = { opts, 't', true };
 | 
						|
  }
 | 
						|
  opts = opts or {}
 | 
						|
  local left_padding = (" "):rep(opts.pad_left or 1)
 | 
						|
  local right_padding = (" "):rep(opts.pad_right or 1)
 | 
						|
  contents = M.trim_empty_lines(contents)
 | 
						|
  for i, line in ipairs(contents) do
 | 
						|
    contents[i] = string.format('%s%s%s', left_padding, line:gsub("\r", ""), right_padding)
 | 
						|
  end
 | 
						|
  if opts.pad_top then
 | 
						|
    for _ = 1, opts.pad_top do
 | 
						|
      table.insert(contents, 1, "")
 | 
						|
    end
 | 
						|
  end
 | 
						|
  if opts.pad_bottom then
 | 
						|
    for _ = 1, opts.pad_bottom do
 | 
						|
      table.insert(contents, "")
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return contents
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
 | 
						|
-- TODO: refactor to separate stripping/converting and make use of open_floating_preview
 | 
						|
--
 | 
						|
--- Converts markdown into syntax highlighted regions by stripping the code
 | 
						|
--- blocks and converting them into highlighted code.
 | 
						|
--- This will by default insert a blank line separator after those code block
 | 
						|
--- regions to improve readability.
 | 
						|
--- The result is shown in a floating preview.
 | 
						|
---
 | 
						|
---@param contents table of lines to show in window
 | 
						|
---@param opts dictionary with optional fields
 | 
						|
---  - height    of floating window
 | 
						|
---  - width     of floating window
 | 
						|
---  - wrap_at   character to wrap at for computing height
 | 
						|
---  - max_width  maximal width of floating window
 | 
						|
---  - max_height maximal height of floating window
 | 
						|
---  - pad_left   number of columns to pad contents at left
 | 
						|
---  - pad_right  number of columns to pad contents at right
 | 
						|
---  - pad_top    number of lines to pad contents at top
 | 
						|
---  - pad_bottom number of lines to pad contents at bottom
 | 
						|
---  - separator insert separator after code block
 | 
						|
---@returns width,height size of float
 | 
						|
function M.fancy_floating_markdown(contents, opts)
 | 
						|
  validate {
 | 
						|
    contents = { contents, 't' };
 | 
						|
    opts = { opts, 't', true };
 | 
						|
  }
 | 
						|
  opts = opts or {}
 | 
						|
 | 
						|
  local stripped = {}
 | 
						|
  local highlights = {}
 | 
						|
  do
 | 
						|
    local i = 1
 | 
						|
    while i <= #contents do
 | 
						|
      local line = contents[i]
 | 
						|
      -- TODO(ashkan): use a more strict regex for filetype?
 | 
						|
      local ft = line:match("^```([a-zA-Z0-9_]*)$")
 | 
						|
      -- local ft = line:match("^```(.*)$")
 | 
						|
      -- TODO(ashkan): validate the filetype here.
 | 
						|
      if ft then
 | 
						|
        local start = #stripped
 | 
						|
        i = i + 1
 | 
						|
        while i <= #contents do
 | 
						|
          line = contents[i]
 | 
						|
          if line == "```" then
 | 
						|
            i = i + 1
 | 
						|
            break
 | 
						|
          end
 | 
						|
          table.insert(stripped, line)
 | 
						|
          i = i + 1
 | 
						|
        end
 | 
						|
        table.insert(highlights, {
 | 
						|
          ft = ft;
 | 
						|
          start = start + 1;
 | 
						|
          finish = #stripped + 1 - 1;
 | 
						|
        })
 | 
						|
      else
 | 
						|
        table.insert(stripped, line)
 | 
						|
        i = i + 1
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  -- Clean up and add padding
 | 
						|
  stripped = M._trim_and_pad(stripped, opts)
 | 
						|
 | 
						|
  -- Compute size of float needed to show (wrapped) lines
 | 
						|
  opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
 | 
						|
  local width, height = M._make_floating_popup_size(stripped, opts)
 | 
						|
 | 
						|
  -- Insert blank line separator after code block
 | 
						|
  local insert_separator = opts.separator
 | 
						|
  if insert_separator == nil then insert_separator = true end
 | 
						|
  if insert_separator then
 | 
						|
    for i, h in ipairs(highlights) do
 | 
						|
      h.start = h.start + i - 1
 | 
						|
      h.finish = h.finish + i - 1
 | 
						|
      if h.finish + 1 <= #stripped then
 | 
						|
        table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
 | 
						|
        height = height + 1
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  -- Make the floating window.
 | 
						|
  local bufnr = api.nvim_create_buf(false, true)
 | 
						|
  local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
 | 
						|
  vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
 | 
						|
  api.nvim_buf_set_option(bufnr, 'modifiable', false)
 | 
						|
 | 
						|
  -- Switch to the floating window to apply the syntax highlighting.
 | 
						|
  -- This is because the syntax command doesn't accept a target.
 | 
						|
  local cwin = vim.api.nvim_get_current_win()
 | 
						|
  vim.api.nvim_set_current_win(winnr)
 | 
						|
 | 
						|
  vim.cmd("ownsyntax markdown")
 | 
						|
  local idx = 1
 | 
						|
  --@private
 | 
						|
  local function apply_syntax_to_region(ft, start, finish)
 | 
						|
    if ft == '' then return end
 | 
						|
    local name = ft..idx
 | 
						|
    idx = idx + 1
 | 
						|
    local lang = "@"..ft:upper()
 | 
						|
    -- TODO(ashkan): better validation before this.
 | 
						|
    if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
 | 
						|
      return
 | 
						|
    end
 | 
						|
    vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s", name, start, finish + 1, lang))
 | 
						|
  end
 | 
						|
  -- Previous highlight region.
 | 
						|
  -- TODO(ashkan): this wasn't working for some reason, but I would like to
 | 
						|
  -- make sure that regions between code blocks are definitely markdown.
 | 
						|
  -- local ph = {start = 0; finish = 1;}
 | 
						|
  for _, h in ipairs(highlights) do
 | 
						|
    -- apply_syntax_to_region('markdown', ph.finish, h.start)
 | 
						|
    apply_syntax_to_region(h.ft, h.start, h.finish)
 | 
						|
    -- ph = h
 | 
						|
  end
 | 
						|
 | 
						|
  vim.api.nvim_set_current_win(cwin)
 | 
						|
  return bufnr, winnr
 | 
						|
end
 | 
						|
 | 
						|
--- Creates autocommands to close a preview window when events happen.
 | 
						|
---
 | 
						|
--@param events (table) list of events
 | 
						|
--@param winnr (number) window id of preview window
 | 
						|
--@see |autocmd-events|
 | 
						|
function M.close_preview_autocmd(events, winnr)
 | 
						|
  api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
 | 
						|
end
 | 
						|
 | 
						|
--@internal
 | 
						|
--- Computes size of float needed to show contents (with optional wrapping)
 | 
						|
---
 | 
						|
--@param contents table of lines to show in window
 | 
						|
--@param opts dictionary with optional fields
 | 
						|
--             - height  of floating window
 | 
						|
--             - width   of floating window
 | 
						|
--             - wrap_at character to wrap at for computing height
 | 
						|
--             - max_width  maximal width of floating window
 | 
						|
--             - max_height maximal height of floating window
 | 
						|
--@returns width,height size of float
 | 
						|
function M._make_floating_popup_size(contents, opts)
 | 
						|
  validate {
 | 
						|
    contents = { contents, 't' };
 | 
						|
    opts = { opts, 't', true };
 | 
						|
  }
 | 
						|
  opts = opts or {}
 | 
						|
 | 
						|
  local width = opts.width
 | 
						|
  local height = opts.height
 | 
						|
  local wrap_at = opts.wrap_at
 | 
						|
  local max_width = opts.max_width
 | 
						|
  local max_height = opts.max_height
 | 
						|
  local line_widths = {}
 | 
						|
 | 
						|
  if not width then
 | 
						|
    width = 0
 | 
						|
    for i, line in ipairs(contents) do
 | 
						|
      -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
 | 
						|
      line_widths[i] = vim.fn.strdisplaywidth(line)
 | 
						|
      width = math.max(line_widths[i], width)
 | 
						|
    end
 | 
						|
  end
 | 
						|
  if max_width then
 | 
						|
    width = math.min(width, max_width)
 | 
						|
    wrap_at = math.min(wrap_at or max_width, max_width)
 | 
						|
  end
 | 
						|
 | 
						|
  if not height then
 | 
						|
    height = #contents
 | 
						|
    if wrap_at and width >= wrap_at then
 | 
						|
      height = 0
 | 
						|
      if vim.tbl_isempty(line_widths) then
 | 
						|
        for _, line in ipairs(contents) do
 | 
						|
          local line_width = vim.fn.strdisplaywidth(line)
 | 
						|
          height = height + math.ceil(line_width/wrap_at)
 | 
						|
        end
 | 
						|
      else
 | 
						|
        for i = 1, #contents do
 | 
						|
          height = height + math.max(1, math.ceil(line_widths[i]/wrap_at))
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  if max_height then
 | 
						|
    height = math.min(height, max_height)
 | 
						|
  end
 | 
						|
 | 
						|
  return width, height
 | 
						|
end
 | 
						|
 | 
						|
--- Shows contents in a floating window.
 | 
						|
---
 | 
						|
--@param contents table of lines to show in window
 | 
						|
--@param filetype string of filetype to set for opened buffer
 | 
						|
--@param opts dictionary with optional fields
 | 
						|
--             - height    of floating window
 | 
						|
--             - width     of floating window
 | 
						|
--             - wrap_at   character to wrap at for computing height
 | 
						|
--             - max_width  maximal width of floating window
 | 
						|
--             - max_height maximal height of floating window
 | 
						|
--             - pad_left   number of columns to pad contents at left
 | 
						|
--             - pad_right  number of columns to pad contents at right
 | 
						|
--             - pad_top    number of lines to pad contents at top
 | 
						|
--             - pad_bottom number of lines to pad contents at bottom
 | 
						|
--@returns bufnr,winnr buffer and window number of the newly created floating
 | 
						|
---preview window
 | 
						|
function M.open_floating_preview(contents, filetype, opts)
 | 
						|
  validate {
 | 
						|
    contents = { contents, 't' };
 | 
						|
    filetype = { filetype, 's', true };
 | 
						|
    opts = { opts, 't', true };
 | 
						|
  }
 | 
						|
  opts = opts or {}
 | 
						|
 | 
						|
  -- Clean up input: trim empty lines from the end, pad
 | 
						|
  contents = M._trim_and_pad(contents, opts)
 | 
						|
 | 
						|
  -- Compute size of float needed to show (wrapped) lines
 | 
						|
  opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
 | 
						|
  local width, height = M._make_floating_popup_size(contents, opts)
 | 
						|
 | 
						|
  local floating_bufnr = api.nvim_create_buf(false, true)
 | 
						|
  if filetype then
 | 
						|
    api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype)
 | 
						|
  end
 | 
						|
  local float_option = M.make_floating_popup_options(width, height, opts)
 | 
						|
  local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
 | 
						|
  if filetype == 'markdown' then
 | 
						|
    api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
 | 
						|
  end
 | 
						|
  api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
 | 
						|
  api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
 | 
						|
  M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr)
 | 
						|
  return floating_bufnr, floating_winnr
 | 
						|
end
 | 
						|
 | 
						|
-- TODO(remove-callbacks)
 | 
						|
do
 | 
						|
  --@deprecated
 | 
						|
  function M.get_severity_highlight_name(severity)
 | 
						|
    warn_once("vim.lsp.util.get_severity_highlight_name is deprecated.")
 | 
						|
    return vim.lsp.diagnostic._get_severity_highlight_name(severity)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.buf_clear_diagnostics(bufnr, client_id)
 | 
						|
    warn_once("buf_clear_diagnostics is deprecated. Use vim.lsp.diagnostic.clear")
 | 
						|
    return vim.lsp.diagnostic.clear(bufnr, client_id)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.get_line_diagnostics()
 | 
						|
    warn_once("get_line_diagnostics is deprecated. Use vim.lsp.diagnostic.get_line_diagnostics")
 | 
						|
 | 
						|
    local bufnr = api.nvim_get_current_buf()
 | 
						|
    local line_nr = api.nvim_win_get_cursor(0)[1] - 1
 | 
						|
 | 
						|
    return vim.lsp.diagnostic.get_line_diagnostics(bufnr, line_nr)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.show_line_diagnostics()
 | 
						|
    warn_once("show_line_diagnostics is deprecated. Use vim.lsp.diagnostic.show_line_diagnostics")
 | 
						|
 | 
						|
    local bufnr = api.nvim_get_current_buf()
 | 
						|
    local line_nr = api.nvim_win_get_cursor(0)[1] - 1
 | 
						|
 | 
						|
    return vim.lsp.diagnostic.show_line_diagnostics(bufnr, line_nr)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.buf_diagnostics_save_positions(bufnr, diagnostics, client_id)
 | 
						|
    warn_once("buf_diagnostics_save_positions is deprecated. Use vim.lsp.diagnostic.save")
 | 
						|
    return vim.lsp.diagnostic.save(diagnostics, bufnr, client_id)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.buf_diagnostics_get_positions(bufnr, client_id)
 | 
						|
    warn_once("buf_diagnostics_get_positions is deprecated. Use vim.lsp.diagnostic.get")
 | 
						|
    return vim.lsp.diagnostic.get(bufnr, client_id)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.buf_diagnostics_underline(bufnr, diagnostics, client_id)
 | 
						|
    warn_once("buf_diagnostics_underline is deprecated. Use 'vim.lsp.diagnostic.set_underline'")
 | 
						|
    return vim.lsp.diagnostic.set_underline(diagnostics, bufnr, client_id)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.buf_diagnostics_virtual_text(bufnr, diagnostics, client_id)
 | 
						|
    warn_once("buf_diagnostics_virtual_text is deprecated. Use 'vim.lsp.diagnostic.set_virtual_text'")
 | 
						|
    return vim.lsp.diagnostic.set_virtual_text(diagnostics, bufnr, client_id)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.buf_diagnostics_signs(bufnr, diagnostics, client_id)
 | 
						|
    warn_once("buf_diagnostics_signs is deprecated. Use 'vim.lsp.diagnostics.set_signs'")
 | 
						|
    return vim.lsp.diagnostic.set_signs(diagnostics, bufnr, client_id)
 | 
						|
  end
 | 
						|
 | 
						|
  --@deprecated
 | 
						|
  function M.buf_diagnostics_count(kind, client_id)
 | 
						|
    warn_once("buf_diagnostics_count is deprecated. Use 'vim.lsp.diagnostic.get_count'")
 | 
						|
    return vim.lsp.diagnostic.get_count(vim.api.nvim_get_current_buf(), client_id, kind)
 | 
						|
  end
 | 
						|
 | 
						|
end
 | 
						|
 | 
						|
do --[[ References ]]
 | 
						|
  local reference_ns = api.nvim_create_namespace("vim_lsp_references")
 | 
						|
 | 
						|
  --- Removes document highlights from a buffer.
 | 
						|
  ---
 | 
						|
  --@param bufnr buffer id
 | 
						|
  function M.buf_clear_references(bufnr)
 | 
						|
    validate { bufnr = {bufnr, 'n', true} }
 | 
						|
    api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
 | 
						|
  end
 | 
						|
 | 
						|
  --- Shows a list of document highlights for a certain buffer.
 | 
						|
  ---
 | 
						|
  --@param bufnr buffer id
 | 
						|
  --@param references List of `DocumentHighlight` objects to highlight
 | 
						|
  function M.buf_highlight_references(bufnr, references)
 | 
						|
    validate { bufnr = {bufnr, 'n', true} }
 | 
						|
    for _, reference in ipairs(references) do
 | 
						|
      local start_pos = {reference["range"]["start"]["line"], reference["range"]["start"]["character"]}
 | 
						|
      local end_pos = {reference["range"]["end"]["line"], reference["range"]["end"]["character"]}
 | 
						|
      local document_highlight_kind = {
 | 
						|
        [protocol.DocumentHighlightKind.Text] = "LspReferenceText";
 | 
						|
        [protocol.DocumentHighlightKind.Read] = "LspReferenceRead";
 | 
						|
        [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite";
 | 
						|
      }
 | 
						|
      local kind = reference["kind"] or protocol.DocumentHighlightKind.Text
 | 
						|
      highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
local position_sort = sort_by_key(function(v)
 | 
						|
  return {v.start.line, v.start.character}
 | 
						|
end)
 | 
						|
 | 
						|
--- Returns the items with the byte position calculated correctly and in sorted
 | 
						|
--- order, for display in quickfix and location lists.
 | 
						|
---
 | 
						|
--@param locations (table) list of `Location`s or `LocationLink`s
 | 
						|
--@returns (table) list of items
 | 
						|
function M.locations_to_items(locations)
 | 
						|
  local items = {}
 | 
						|
  local grouped = setmetatable({}, {
 | 
						|
    __index = function(t, k)
 | 
						|
      local v = {}
 | 
						|
      rawset(t, k, v)
 | 
						|
      return v
 | 
						|
    end;
 | 
						|
  })
 | 
						|
  for _, d in ipairs(locations) do
 | 
						|
    -- locations may be Location or LocationLink
 | 
						|
    local uri = d.uri or d.targetUri
 | 
						|
    local range = d.range or d.targetSelectionRange
 | 
						|
    table.insert(grouped[uri], {start = range.start})
 | 
						|
  end
 | 
						|
 | 
						|
 | 
						|
  local keys = vim.tbl_keys(grouped)
 | 
						|
  table.sort(keys)
 | 
						|
  -- TODO(ashkan) I wish we could do this lazily.
 | 
						|
  for _, uri in ipairs(keys) do
 | 
						|
    local rows = grouped[uri]
 | 
						|
    table.sort(rows, position_sort)
 | 
						|
    local bufnr = vim.uri_to_bufnr(uri)
 | 
						|
    vim.fn.bufload(bufnr)
 | 
						|
    local filename = vim.uri_to_fname(uri)
 | 
						|
    for _, temp in ipairs(rows) do
 | 
						|
      local pos = temp.start
 | 
						|
      local row = pos.line
 | 
						|
      local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
 | 
						|
      local col = M.character_offset(bufnr, row, pos.character)
 | 
						|
      table.insert(items, {
 | 
						|
        filename = filename,
 | 
						|
        lnum = row + 1,
 | 
						|
        col = col + 1;
 | 
						|
        text = line;
 | 
						|
      })
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return items
 | 
						|
end
 | 
						|
 | 
						|
--- Fills current window's location list with given list of items.
 | 
						|
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
 | 
						|
---
 | 
						|
--@param items (table) list of items
 | 
						|
function M.set_loclist(items)
 | 
						|
  vim.fn.setloclist(0, {}, ' ', {
 | 
						|
    title = 'Language Server';
 | 
						|
    items = items;
 | 
						|
  })
 | 
						|
end
 | 
						|
 | 
						|
--- Fills quickfix list with given list of items.
 | 
						|
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
 | 
						|
---
 | 
						|
--@param items (table) list of items
 | 
						|
function M.set_qflist(items)
 | 
						|
  vim.fn.setqflist({}, ' ', {
 | 
						|
    title = 'Language Server';
 | 
						|
    items = items;
 | 
						|
  })
 | 
						|
end
 | 
						|
 | 
						|
-- Acording to LSP spec, if the client set "symbolKind.valueSet",
 | 
						|
-- the client must handle it properly even if it receives a value outside the specification.
 | 
						|
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
 | 
						|
function M._get_symbol_kind_name(symbol_kind)
 | 
						|
  return protocol.SymbolKind[symbol_kind] or "Unknown"
 | 
						|
end
 | 
						|
 | 
						|
--- Converts symbols to quickfix list items.
 | 
						|
---
 | 
						|
--@param symbols DocumentSymbol[] or SymbolInformation[]
 | 
						|
function M.symbols_to_items(symbols, bufnr)
 | 
						|
  --@private
 | 
						|
  local function _symbols_to_items(_symbols, _items, _bufnr)
 | 
						|
    for _, symbol in ipairs(_symbols) do
 | 
						|
      if symbol.location then -- SymbolInformation type
 | 
						|
        local range = symbol.location.range
 | 
						|
        local kind = M._get_symbol_kind_name(symbol.kind)
 | 
						|
        table.insert(_items, {
 | 
						|
          filename = vim.uri_to_fname(symbol.location.uri),
 | 
						|
          lnum = range.start.line + 1,
 | 
						|
          col = range.start.character + 1,
 | 
						|
          kind = kind,
 | 
						|
          text = '['..kind..'] '..symbol.name,
 | 
						|
        })
 | 
						|
      elseif symbol.range then -- DocumentSymbole type
 | 
						|
        local kind = M._get_symbol_kind_name(symbol.kind)
 | 
						|
        table.insert(_items, {
 | 
						|
          -- bufnr = _bufnr,
 | 
						|
          filename = vim.api.nvim_buf_get_name(_bufnr),
 | 
						|
          lnum = symbol.range.start.line + 1,
 | 
						|
          col = symbol.range.start.character + 1,
 | 
						|
          kind = kind,
 | 
						|
          text = '['..kind..'] '..symbol.name
 | 
						|
        })
 | 
						|
        if symbol.children then
 | 
						|
          for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do
 | 
						|
            vim.list_extend(_items, v)
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
    return _items
 | 
						|
  end
 | 
						|
  return _symbols_to_items(symbols, {}, bufnr)
 | 
						|
end
 | 
						|
 | 
						|
--- Removes empty lines from the beginning and end.
 | 
						|
--@param lines (table) list of lines to trim
 | 
						|
--@returns (table) trimmed list of lines
 | 
						|
function M.trim_empty_lines(lines)
 | 
						|
  local start = 1
 | 
						|
  for i = 1, #lines do
 | 
						|
    if #lines[i] > 0 then
 | 
						|
      start = i
 | 
						|
      break
 | 
						|
    end
 | 
						|
  end
 | 
						|
  local finish = 1
 | 
						|
  for i = #lines, 1, -1 do
 | 
						|
    if #lines[i] > 0 then
 | 
						|
      finish = i
 | 
						|
      break
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return vim.list_extend({}, lines, start, finish)
 | 
						|
end
 | 
						|
 | 
						|
--- Accepts markdown lines and tries to reduce them to a filetype if they
 | 
						|
--- comprise just a single code block.
 | 
						|
---
 | 
						|
--- CAUTION: Modifies the input in-place!
 | 
						|
---
 | 
						|
--@param lines (table) list of lines
 | 
						|
--@returns (string) filetype or 'markdown' if it was unchanged.
 | 
						|
function M.try_trim_markdown_code_blocks(lines)
 | 
						|
  local language_id = lines[1]:match("^```(.*)")
 | 
						|
  if language_id then
 | 
						|
    local has_inner_code_fence = false
 | 
						|
    for i = 2, (#lines - 1) do
 | 
						|
      local line = lines[i]
 | 
						|
      if line:sub(1,3) == '```' then
 | 
						|
        has_inner_code_fence = true
 | 
						|
        break
 | 
						|
      end
 | 
						|
    end
 | 
						|
    -- No inner code fences + starting with code fence = hooray.
 | 
						|
    if not has_inner_code_fence then
 | 
						|
      table.remove(lines, 1)
 | 
						|
      table.remove(lines)
 | 
						|
      return language_id
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return 'markdown'
 | 
						|
end
 | 
						|
 | 
						|
local str_utfindex = vim.str_utfindex
 | 
						|
--@private
 | 
						|
local function make_position_param()
 | 
						|
  local row, col = unpack(api.nvim_win_get_cursor(0))
 | 
						|
  row = row - 1
 | 
						|
  local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
 | 
						|
  if not line then
 | 
						|
    return { line = 0; character = 0; }
 | 
						|
  end
 | 
						|
  col = str_utfindex(line, col)
 | 
						|
  return { line = row; character = col; }
 | 
						|
end
 | 
						|
 | 
						|
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
 | 
						|
---
 | 
						|
--@returns `TextDocumentPositionParams` object
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
 | 
						|
function M.make_position_params()
 | 
						|
  return {
 | 
						|
    textDocument = M.make_text_document_params();
 | 
						|
    position = make_position_param()
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
--- Using the current position in the current buffer, creates an object that
 | 
						|
--- can be used as a building block for several LSP requests, such as
 | 
						|
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
 | 
						|
--- `textDocument/rangeFormatting`.
 | 
						|
---
 | 
						|
--@returns { textDocument = { uri = `current_file_uri` }, range = { start =
 | 
						|
---`current_position`, end = `current_position` } }
 | 
						|
function M.make_range_params()
 | 
						|
  local position = make_position_param()
 | 
						|
  return {
 | 
						|
    textDocument = M.make_text_document_params(),
 | 
						|
    range = { start = position; ["end"] = position; }
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
--- Using the given range in the current buffer, creates an object that
 | 
						|
--- is similar to |vim.lsp.util.make_range_params()|.
 | 
						|
---
 | 
						|
--@param start_pos ({number, number}, optional) mark-indexed position.
 | 
						|
---Defaults to the start of the last visual selection.
 | 
						|
--@param end_pos ({number, number}, optional) mark-indexed position.
 | 
						|
---Defaults to the end of the last visual selection.
 | 
						|
--@returns { textDocument = { uri = `current_file_uri` }, range = { start =
 | 
						|
---`start_position`, end = `end_position` } }
 | 
						|
function M.make_given_range_params(start_pos, end_pos)
 | 
						|
  validate {
 | 
						|
    start_pos = {start_pos, 't', true};
 | 
						|
    end_pos = {end_pos, 't', true};
 | 
						|
  }
 | 
						|
  local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<'))
 | 
						|
  local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>'))
 | 
						|
  -- convert to 0-index
 | 
						|
  A[1] = A[1] - 1
 | 
						|
  B[1] = B[1] - 1
 | 
						|
  -- account for encoding.
 | 
						|
  if A[2] > 0 then
 | 
						|
    A = {A[1], M.character_offset(0, A[1], A[2])}
 | 
						|
  end
 | 
						|
  if B[2] > 0 then
 | 
						|
    B = {B[1], M.character_offset(0, B[1], B[2])}
 | 
						|
  end
 | 
						|
  return {
 | 
						|
    textDocument = M.make_text_document_params(),
 | 
						|
    range = {
 | 
						|
      start = {line = A[1], character = A[2]},
 | 
						|
      ['end'] = {line = B[1], character = B[2]}
 | 
						|
    }
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
--- Creates a `TextDocumentIdentifier` object for the current buffer.
 | 
						|
---
 | 
						|
--@returns `TextDocumentIdentifier`
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
 | 
						|
function M.make_text_document_params()
 | 
						|
  return { uri = vim.uri_from_bufnr(0) }
 | 
						|
end
 | 
						|
 | 
						|
function M.make_workspace_params(added, removed)
 | 
						|
  return { event = { added = added; removed = removed; } }
 | 
						|
end
 | 
						|
--- Returns visual width of tabstop.
 | 
						|
---
 | 
						|
--@see |softtabstop|
 | 
						|
--@param bufnr (optional, number): Buffer handle, defaults to current
 | 
						|
--@returns (number) tabstop visual width
 | 
						|
function M.get_effective_tabstop(bufnr)
 | 
						|
  validate { bufnr = {bufnr, 'n', true} }
 | 
						|
  local bo = bufnr and vim.bo[bufnr] or vim.bo
 | 
						|
  local sts = bo.softtabstop
 | 
						|
  return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
 | 
						|
end
 | 
						|
 | 
						|
--- Creates a `FormattingOptions` object for the current buffer and cursor position.
 | 
						|
---
 | 
						|
--@param options Table with valid `FormattingOptions` entries
 | 
						|
--@returns `FormattingOptions object
 | 
						|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
 | 
						|
function M.make_formatting_params(options)
 | 
						|
  validate { options = {options, 't', true} }
 | 
						|
  options = vim.tbl_extend('keep', options or {}, {
 | 
						|
    tabSize = M.get_effective_tabstop();
 | 
						|
    insertSpaces = vim.bo.expandtab;
 | 
						|
  })
 | 
						|
  return {
 | 
						|
    textDocument = { uri = vim.uri_from_bufnr(0) };
 | 
						|
    options = options;
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
 | 
						|
---
 | 
						|
--@param buf buffer id (0 for current)
 | 
						|
--@param row 0-indexed line
 | 
						|
--@param col 0-indexed byte offset in line
 | 
						|
--@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
 | 
						|
function M.character_offset(buf, row, col)
 | 
						|
  local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
 | 
						|
  -- If the col is past the EOL, use the line length.
 | 
						|
  if col > #line then
 | 
						|
    return str_utfindex(line)
 | 
						|
  end
 | 
						|
  return str_utfindex(line, col)
 | 
						|
end
 | 
						|
 | 
						|
M._get_line_byte_from_position = get_line_byte_from_position
 | 
						|
M._warn_once = warn_once
 | 
						|
 | 
						|
M.buf_versions = {}
 | 
						|
 | 
						|
return M
 | 
						|
-- vim:sw=2 ts=2 et
 |