mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			277 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| local M = {}
 | |
| local api = vim.api
 | |
| local lsp = vim.lsp
 | |
| local protocol = lsp.protocol
 | |
| local ms = protocol.Methods
 | |
| 
 | |
| --- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
 | |
| 
 | |
| -- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
 | |
| -- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331).
 | |
| --- @class lsp.ItemDefaults
 | |
| --- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil
 | |
| --- @field insertTextFormat lsp.InsertTextFormat?
 | |
| --- @field insertTextMode lsp.InsertTextMode?
 | |
| --- @field data any
 | |
| 
 | |
| ---@param input string unparsed snippet
 | |
| ---@return string parsed snippet
 | |
| local function parse_snippet(input)
 | |
|   local ok, parsed = pcall(function()
 | |
|     return vim.lsp._snippet_grammar.parse(input)
 | |
|   end)
 | |
|   return ok and tostring(parsed) or input
 | |
| end
 | |
| 
 | |
| --- 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
 | |
| ---
 | |
| ---@param item lsp.CompletionItem
 | |
| ---@return string
 | |
| local function get_completion_word(item)
 | |
|   if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
 | |
|     if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
 | |
|       return item.textEdit.newText
 | |
|     else
 | |
|       return parse_snippet(item.textEdit.newText)
 | |
|     end
 | |
|   elseif item.insertText ~= nil and item.insertText ~= '' then
 | |
|     if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
 | |
|       return item.insertText
 | |
|     else
 | |
|       return parse_snippet(item.insertText)
 | |
|     end
 | |
|   end
 | |
|   return item.label
 | |
| end
 | |
| 
 | |
| --- Applies the given defaults to the completion item, modifying it in place.
 | |
| ---
 | |
| --- @param item lsp.CompletionItem
 | |
| --- @param defaults lsp.ItemDefaults?
 | |
| local function apply_defaults(item, defaults)
 | |
|   if not defaults then
 | |
|     return
 | |
|   end
 | |
| 
 | |
|   item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
 | |
|   item.insertTextMode = item.insertTextMode or defaults.insertTextMode
 | |
|   item.data = item.data or defaults.data
 | |
|   if defaults.editRange then
 | |
|     local textEdit = item.textEdit or {}
 | |
|     item.textEdit = textEdit
 | |
|     textEdit.newText = textEdit.newText or item.textEditText or item.insertText
 | |
|     if defaults.editRange.start then
 | |
|       textEdit.range = textEdit.range or defaults.editRange
 | |
|     elseif defaults.editRange.insert then
 | |
|       textEdit.insert = defaults.editRange.insert
 | |
|       textEdit.replace = defaults.editRange.replace
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| ---@param result vim.lsp.CompletionResult
 | |
| ---@return lsp.CompletionItem[]
 | |
| local function get_items(result)
 | |
|   if result.items then
 | |
|     for _, item in ipairs(result.items) do
 | |
|       ---@diagnostic disable-next-line: param-type-mismatch
 | |
|       apply_defaults(item, result.itemDefaults)
 | |
|     end
 | |
|     return result.items
 | |
|   else
 | |
|     return result
 | |
|   end
 | |
| end
 | |
| 
 | |
| --- Turns the result of a `textDocument/completion` request into vim-compatible
 | |
| --- |complete-items|.
 | |
| ---
 | |
| ---@param result vim.lsp.CompletionResult Result of `textDocument/completion`
 | |
| ---@param prefix string prefix to filter the completion items
 | |
| ---@return table[]
 | |
| ---@see complete-items
 | |
| function M._lsp_to_complete_items(result, prefix)
 | |
|   local items = get_items(result)
 | |
|   if vim.tbl_isempty(items) then
 | |
|     return {}
 | |
|   end
 | |
| 
 | |
|   local function matches_prefix(item)
 | |
|     return vim.startswith(get_completion_word(item), prefix)
 | |
|   end
 | |
| 
 | |
|   items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]]
 | |
|   table.sort(items, function(a, b)
 | |
|     return (a.sortText or a.label) < (b.sortText or b.label)
 | |
|   end)
 | |
| 
 | |
|   local matches = {}
 | |
|   for _, item in ipairs(items) do
 | |
|     local info = ''
 | |
|     local documentation = 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
 | |
|         vim.notify(
 | |
|           ('invalid documentation value %s'):format(vim.inspect(documentation)),
 | |
|           vim.log.levels.WARN
 | |
|         )
 | |
|       end
 | |
|     end
 | |
|     local word = get_completion_word(item)
 | |
|     table.insert(matches, {
 | |
|       word = word,
 | |
|       abbr = item.label,
 | |
|       kind = protocol.CompletionItemKind[item.kind] or 'Unknown',
 | |
|       menu = item.detail or '',
 | |
|       info = #info > 0 and info or nil,
 | |
|       icase = 1,
 | |
|       dup = 1,
 | |
|       empty = 1,
 | |
|       user_data = {
 | |
|         nvim = {
 | |
|           lsp = {
 | |
|             completion_item = item,
 | |
|           },
 | |
|         },
 | |
|       },
 | |
|     })
 | |
|   end
 | |
|   return matches
 | |
| end
 | |
| 
 | |
| ---@param lnum integer 0-indexed
 | |
| ---@param items lsp.CompletionItem[]
 | |
| local function adjust_start_col(lnum, line, items, encoding)
 | |
|   local min_start_char = nil
 | |
|   for _, item in pairs(items) do
 | |
|     if item.textEdit and item.textEdit.range.start.line == lnum then
 | |
|       if min_start_char and min_start_char ~= item.textEdit.range.start.character then
 | |
|         return nil
 | |
|       end
 | |
|       min_start_char = item.textEdit.range.start.character
 | |
|     end
 | |
|   end
 | |
|   if min_start_char then
 | |
|     return vim.lsp.util._str_byteindex_enc(line, min_start_char, encoding)
 | |
|   else
 | |
|     return nil
 | |
|   end
 | |
| end
 | |
| 
 | |
| ---@private
 | |
| ---@param line string line content
 | |
| ---@param lnum integer 0-indexed line number
 | |
| ---@param client_start_boundary integer 0-indexed word boundary
 | |
| ---@param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character
 | |
| ---@param result vim.lsp.CompletionResult
 | |
| ---@param encoding string
 | |
| ---@return table[] matches
 | |
| ---@return integer? server_start_boundary
 | |
| function M._convert_results(
 | |
|   line,
 | |
|   lnum,
 | |
|   cursor_col,
 | |
|   client_start_boundary,
 | |
|   server_start_boundary,
 | |
|   result,
 | |
|   encoding
 | |
| )
 | |
|   -- Completion response items may be relative to a position different than `client_start_boundary`.
 | |
|   -- Concrete example, with lua-language-server:
 | |
|   --
 | |
|   -- require('plenary.asy|
 | |
|   --         ▲       ▲   ▲
 | |
|   --         │       │   └── cursor_pos:                     20
 | |
|   --         │       └────── client_start_boundary:          17
 | |
|   --         └────────────── textEdit.range.start.character: 9
 | |
|   --                                 .newText = 'plenary.async'
 | |
|   --                  ^^^
 | |
|   --                  prefix (We'd remove everything not starting with `asy`,
 | |
|   --                  so we'd eliminate the `plenary.async` result
 | |
|   --
 | |
|   -- `adjust_start_col` is used to prefer the language server boundary.
 | |
|   --
 | |
|   local candidates = get_items(result)
 | |
|   local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
 | |
|   if server_start_boundary == nil then
 | |
|     server_start_boundary = curstartbyte
 | |
|   elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
 | |
|     server_start_boundary = client_start_boundary
 | |
|   end
 | |
|   local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
 | |
|   local matches = M._lsp_to_complete_items(result, prefix)
 | |
|   return matches, server_start_boundary
 | |
| end
 | |
| 
 | |
| ---@param findstart integer 0 or 1, decides behavior
 | |
| ---@param base integer findstart=0, text to match against
 | |
| ---@return integer|table Decided by {findstart}:
 | |
| --- - findstart=0: column where the completion starts, or -2 or -3
 | |
| --- - findstart=1: list of matches (actually just calls |complete()|)
 | |
| function M.omnifunc(findstart, base)
 | |
|   assert(base) -- silence luals
 | |
|   local bufnr = api.nvim_get_current_buf()
 | |
|   local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
 | |
|   local remaining = #clients
 | |
|   if remaining == 0 then
 | |
|     return findstart == 1 and -1 or {}
 | |
|   end
 | |
| 
 | |
|   local win = api.nvim_get_current_win()
 | |
|   local cursor = api.nvim_win_get_cursor(win)
 | |
|   local lnum = cursor[1] - 1
 | |
|   local cursor_col = cursor[2]
 | |
|   local line = api.nvim_get_current_line()
 | |
|   local line_to_cursor = line:sub(1, cursor_col)
 | |
|   local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]]
 | |
|   local server_start_boundary = nil
 | |
|   local items = {}
 | |
| 
 | |
|   local function on_done()
 | |
|     local mode = api.nvim_get_mode()['mode']
 | |
|     if mode == 'i' or mode == 'ic' then
 | |
|       vim.fn.complete((server_start_boundary or client_start_boundary) + 1, items)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   local util = vim.lsp.util
 | |
|   for _, client in ipairs(clients) do
 | |
|     local params = util.make_position_params(win, client.offset_encoding)
 | |
|     client.request(ms.textDocument_completion, params, function(err, result)
 | |
|       if err then
 | |
|         vim.lsp.log.warn(err.message)
 | |
|       end
 | |
|       if result and vim.fn.mode() == 'i' then
 | |
|         local matches
 | |
|         matches, server_start_boundary = M._convert_results(
 | |
|           line,
 | |
|           lnum,
 | |
|           cursor_col,
 | |
|           client_start_boundary,
 | |
|           server_start_boundary,
 | |
|           result,
 | |
|           client.offset_encoding
 | |
|         )
 | |
|         vim.list_extend(items, matches)
 | |
|       end
 | |
|       remaining = remaining - 1
 | |
|       if remaining == 0 then
 | |
|         vim.schedule(on_done)
 | |
|       end
 | |
|     end, bufnr)
 | |
|   end
 | |
| 
 | |
|   -- Return -2 to signal that we should continue completion so that we can
 | |
|   -- async complete.
 | |
|   return -2
 | |
| end
 | |
| 
 | |
| return M
 | 
