From 69a0712a9c2bae0969f7a84e5ec321ce6120982b Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:23:50 -0800 Subject: [PATCH 01/31] Reduce code blocks in markdown previews. If the preview is just a code block, then use the language as the filetype instead of using markdown. This reduces the size of the preview. --- runtime/lua/vim/lsp/builtin_callbacks.lua | 54 ++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua index cc739ce3ad..717fda395e 100644 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ b/runtime/lua/vim/lsp/builtin_callbacks.lua @@ -81,6 +81,56 @@ builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) -- util.buf_loclist(bufnr, result.diagnostics) end +local function focusable_popup() + local popup_win + return function(winnr) + if popup_win and nvim.win_is_valid(popup_win) then + if nvim.get_current_win() == popup_win then + nvim.ex.wincmd "p" + else + nvim.set_current_win(popup_win) + end + return + end + popup_win = winnr + end +end + +local hover_popup = focusable_popup() + +local function trim_empty_lines(lines) + local result = {} + for _, line in ipairs(lines) do + if #line > 0 then + table.insert(result, line) + end + end + return result +end + +-- Accepts markdown lines and tries to reduce it to a filetype if it is +-- just a single code block. +local function try_trim_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 lines, language_id + end + end + return lines, 'markdown' +end + -- textDocument/hover -- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover -- @params MarkedString | MarkedString[] | MarkupContent @@ -91,10 +141,12 @@ builtin_callbacks['textDocument/hover'] = function(_, _, result) if result.contents ~= nil then local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = trim_empty_lines(markdown_lines) if vim.tbl_isempty(markdown_lines) then markdown_lines = { 'No information available' } end - util.open_floating_preview(markdown_lines, 'markdown') + local _, winnr = util.open_floating_preview(try_trim_code_blocks(markdown_lines)) + hover_popup(winnr) end end From 3ae9b3781e0b66f5874971665b38314d61668627 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:34:10 -0800 Subject: [PATCH 02/31] Bugfix for floating_preview Don't modify your inputs. --- runtime/lua/vim/lsp/util.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index f96e0f01a8..707e2d0d3b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -262,6 +262,9 @@ function M.open_floating_preview(contents, filetype, opts) opts = { opts, 't', true }; } + -- Don't modify our inputs. + contents = vim.deepcopy(contents) + -- Trim empty lines from the end. for i = #contents, 1, -1 do if #contents[i] == 0 then From 0904ffe387f825404d8467f9787678c2251728dd Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:36:50 -0800 Subject: [PATCH 03/31] Bugfixes. - Return after an error in RPC. - Use an empty vim table for serialization. --- runtime/lua/vim/lsp.lua | 2 +- runtime/lua/vim/lsp/rpc.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9dbe03dace..4724ff0281 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -400,7 +400,7 @@ function lsp.start_client(config) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) assert(result, "server sent empty result") - rpc.notify('initialized', {}) + rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary}) client.initialized = true uninitialized_clients[client_id] = nil client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index e0ec8863d6..a558f66a42 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -340,6 +340,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para local decoded, err = json_decode(body) if not decoded then on_error(client_errors.INVALID_SERVER_JSON, err) + return end local _ = log.debug() and log.debug("decoded", decoded) From 568b4540884bb852b733dfcdd6a2da0d4fe2b42e Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:39:54 -0800 Subject: [PATCH 04/31] Add vim.uri_to_bufnr --- runtime/lua/vim/lsp/builtin_callbacks.lua | 8 ++------ runtime/lua/vim/uri.lua | 7 ++++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua index 717fda395e..a5e1162d02 100644 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ b/runtime/lua/vim/lsp/builtin_callbacks.lua @@ -62,14 +62,10 @@ builtin_callbacks['textDocument/rename'] = function(_, _, result) util.workspace_apply_workspace_edit(result) end -local function uri_to_bufnr(uri) - return vim.fn.bufadd((vim.uri_to_fname(uri))) -end - builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) if not result then return end local uri = result.uri - local bufnr = uri_to_bufnr(uri) + local bufnr = vim.uri_to_bufnr(uri) if not bufnr then api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) return @@ -154,7 +150,7 @@ builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result) if result == nil or vim.tbl_isempty(result) then return end -- TODO(ashkan) what to do with multiple locations? result = result[1] - local bufnr = uri_to_bufnr(result.uri) + local bufnr = vim.uri_to_bufnr(result.uri) assert(bufnr) local start = result.range.start local finish = result.range["end"] diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 0a6e0fcb97..1065f84f4c 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -77,13 +77,18 @@ local function uri_to_fname(uri) else uri = uri:gsub('^file://', '') end - return uri_decode(uri) end +-- Return or create a buffer for a uri. +local function uri_to_bufnr(uri) + return vim.fn.bufadd((uri_to_fname(uri))) +end + return { uri_from_fname = uri_from_fname, uri_from_bufnr = uri_from_bufnr, uri_to_fname = uri_to_fname, + uri_to_bufnr = uri_to_bufnr, } -- vim:sw=2 ts=2 et From ff65cc990afba288df6d39d6f95901cf0786c668 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:41:45 -0800 Subject: [PATCH 05/31] Bugfix. Don't use nvim.lua that doesn't exist :) --- runtime/lua/vim/lsp/builtin_callbacks.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua index a5e1162d02..2229547f20 100644 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ b/runtime/lua/vim/lsp/builtin_callbacks.lua @@ -80,11 +80,11 @@ end local function focusable_popup() local popup_win return function(winnr) - if popup_win and nvim.win_is_valid(popup_win) then - if nvim.get_current_win() == popup_win then - nvim.ex.wincmd "p" + if popup_win and api.nvim_win_is_valid(popup_win) then + if api.nvim_get_current_win() == popup_win then + api.nvim_command("wincmd p") else - nvim.set_current_win(popup_win) + api.nvim_set_current_win(popup_win) end return end From c83380cf80561d45c067db5e70c7275eda7671af Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 14:21:57 -0800 Subject: [PATCH 06/31] Add lsp.buf and hover implementation. --- runtime/lua/vim/lsp/buf.lua | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 runtime/lua/vim/lsp/buf.lua diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua new file mode 100644 index 0000000000..6398bcca74 --- /dev/null +++ b/runtime/lua/vim/lsp/buf.lua @@ -0,0 +1,85 @@ +local validate = vim.validate +local api = vim.api +local util = require 'vim.lsp.util' +local protocol = require 'vim.lsp.protocol' + +local M = {} + +local function resolve_bufnr(bufnr) + validate { bufnr = { bufnr, 'n', true } } + if bufnr == nil or bufnr == 0 then + return api.nvim_get_current_buf() + end + return bufnr +end + +local function ok_or_nil(status, ...) + if not status then return end + return ... +end +local function npcall(fn, ...) + return ok_or_nil(pcall(fn, ...)) +end + +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 + +local hover_window_tag = 'lsp_hover' +function M.hover(bufnr) + if npcall(api.nvim_win_get_var, 0, hover_window_tag) then + api.nvim_command("wincmd p") + return + end + + bufnr = resolve_bufnr(bufnr) + do + local win = find_window_by_var(hover_window_tag, bufnr) + if win then + api.nvim_set_current_win(win) + return + end + end + local params = protocol.make_text_document_position_params() + vim.lsp.buf_request(bufnr, 'textDocument/hover', params, function(_, _, result, _) + if result == nil or vim.tbl_isempty(result) then + return + end + + if result.contents ~= nil then + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + markdown_lines = { 'No information available' } + end + local filetype = util.try_trim_markdown_code_blocks(markdown_lines) + local _, winnr = util.open_floating_preview(markdown_lines, filetype) + api.nvim_win_set_var(winnr, hover_window_tag, bufnr) + end + end) +end + +function M.signature_help() +end + +function M.declaration() +end + +function M.type_definition() +end + +function M.implementation() +end + +-- TODO(ashkan) ? +function M.completion() +end + +function M.range_formatting() +end + +return M From 2d580756ca707945c01703d404e10f4bf412a72c Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 15:35:18 -0800 Subject: [PATCH 07/31] Add everything to lsp.buf and get rid of autoload. --- runtime/autoload/lsp.vim | 45 ---------- runtime/lua/vim/lsp/buf.lua | 165 +++++++++++++++++++++++++++++------ runtime/lua/vim/lsp/util.lua | 88 ++++++++++++++----- 3 files changed, 206 insertions(+), 92 deletions(-) delete mode 100644 runtime/autoload/lsp.vim diff --git a/runtime/autoload/lsp.vim b/runtime/autoload/lsp.vim deleted file mode 100644 index 4c8f8b396a..0000000000 --- a/runtime/autoload/lsp.vim +++ /dev/null @@ -1,45 +0,0 @@ -function! lsp#add_filetype_config(config) abort - call luaeval('vim.lsp.add_filetype_config(_A)', a:config) -endfunction - -function! lsp#set_log_level(level) abort - call luaeval('vim.lsp.set_log_level(_A)', a:level) -endfunction - -function! lsp#get_log_path() abort - return luaeval('vim.lsp.get_log_path()') -endfunction - -function! lsp#omnifunc(findstart, base) abort - return luaeval("vim.lsp.omnifunc(_A[1], _A[2])", [a:findstart, a:base]) -endfunction - -function! lsp#text_document_hover() abort - lua vim.lsp.buf_request(nil, 'textDocument/hover', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_declaration() abort - lua vim.lsp.buf_request(nil, 'textDocument/declaration', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_definition() abort - lua vim.lsp.buf_request(nil, 'textDocument/definition', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_signature_help() abort - lua vim.lsp.buf_request(nil, 'textDocument/signatureHelp', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_type_definition() abort - lua vim.lsp.buf_request(nil, 'textDocument/typeDefinition', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_implementation() abort - lua vim.lsp.buf_request(nil, 'textDocument/implementation', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 6398bcca74..5f54196dcc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,7 +1,9 @@ local validate = vim.validate local api = vim.api +local vfn = vim.fn local util = require 'vim.lsp.util' local protocol = require 'vim.lsp.protocol' +local log = require 'vim.lsp.log' local M = {} @@ -29,54 +31,165 @@ local function find_window_by_var(name, value) end end -local hover_window_tag = 'lsp_hover' -function M.hover(bufnr) - if npcall(api.nvim_win_get_var, 0, hover_window_tag) then - api.nvim_command("wincmd p") - return +local function request(method, params, callback) + -- TODO(ashkan) enable this. + -- callback = vim.lsp.default_callback[method] or callback + validate { + method = {method, 's'}; + callback = {callback, 'f'}; + } + return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) + if err then error(tostring(err)) end + return callback(err, method, result, client_id) + end) +end + +local function focusable_preview(method, params, fn) + if npcall(api.nvim_win_get_var, 0, method) then + return api.nvim_command("wincmd p") end - bufnr = resolve_bufnr(bufnr) + local bufnr = api.nvim_get_current_buf() do - local win = find_window_by_var(hover_window_tag, bufnr) + local win = find_window_by_var(method, bufnr) if win then - api.nvim_set_current_win(win) - return + return api.nvim_set_current_win(win) end end - local params = protocol.make_text_document_position_params() - vim.lsp.buf_request(bufnr, 'textDocument/hover', params, function(_, _, result, _) - if result == nil or vim.tbl_isempty(result) then - return - end - - if result.contents ~= nil then - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = util.trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No information available' } - end - local filetype = util.try_trim_markdown_code_blocks(markdown_lines) - local _, winnr = util.open_floating_preview(markdown_lines, filetype) - api.nvim_win_set_var(winnr, hover_window_tag, bufnr) + return request(method, params, function(_, _, result, _) + -- TODO(ashkan) could show error in preview... + local lines, filetype, opts = fn(result) + if lines then + local _, winnr = util.open_floating_preview(lines, filetype, opts) + api.nvim_win_set_var(winnr, method, bufnr) end end) end -function M.signature_help() +function M.hover() + local params = protocol.make_text_document_position_params() + focusable_preview('textDocument/hover', params, function(result) + if not (result and result.contents) then return end + + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + return { 'No information available' } + end + return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) + end) +end + +function M.peek_definition() + local params = protocol.make_text_document_position_params() + request('textDocument/peekDefinition', params, function(_, _, result, _) + if not (result and result[1]) then return end + local loc = result[1] + local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) + local start = loc.range.start + local finish = loc.range["end"] + util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) + local headbuf = util.open_floating_preview({"Peek:"}, nil, { + offset_y = -(finish.line - start.line); + width = finish.character - start.character + 2; + }) + -- TODO(ashkan) change highlight group? + api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) + end) +end + + +local function update_tagstack() + local bufnr = api.nvim_get_current_buf() + local line = vfn.line('.') + local col = vfn.col('.') + local tagname = vfn.expand('') + local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } + local winid = vfn.win_getid() + local tagstack = vfn.gettagstack(winid) + local action + if tagstack.length == tagstack.curidx then + action = 'r' + tagstack.items[tagstack.curidx] = item + elseif tagstack.length > tagstack.curidx then + action = 'r' + if tagstack.curidx > 1 then + tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) + else + tagstack.items = { item } + end + else + action = 'a' + tagstack.items = { item } + end + tagstack.curidx = tagstack.curidx + 1 + vfn.settagstack(winid, tagstack, action) +end +local function handle_location(result) + -- We can sometimes get a list of locations, so set the first value as the + -- only value we want to handle + -- TODO(ashkan) was this correct^? We could use location lists. + if result[1] ~= nil then + result = result[1] + end + if result.uri == nil then + api.nvim_err_writeln('[LSP] Could not find a valid location') + return + end + local result_file = vim.uri_to_fname(result.uri) + local bufnr = vfn.bufadd(result_file) + update_tagstack() + api.nvim_set_current_buf(bufnr) + local start = result.range.start + api.nvim_win_set_cursor(0, {start.line + 1, start.character}) + return true +end +local function location_callback(_, method, result) + if result == nil or vim.tbl_isempty(result) then + local _ = log.info() and log.info(method, 'No location found') + return nil + end + return handle_location(result) end function M.declaration() + local params = protocol.make_text_document_position_params() + request('textDocument/declaration', params, location_callback) +end + +function M.definition() + local params = protocol.make_text_document_position_params() + request('textDocument/definition', params, location_callback) end function M.type_definition() + local params = protocol.make_text_document_position_params() + request('textDocument/typeDefinition', params, location_callback) end function M.implementation() + local params = protocol.make_text_document_position_params() + request('textDocument/implementation', params, location_callback) +end + +function M.signature_help() + local params = protocol.make_text_document_position_params() + request('textDocument/signatureHelp', params, location_callback) end -- TODO(ashkan) ? -function M.completion() +function M.completion(context) + local params = protocol.make_text_document_position_params() + params.context = context + return request('textDocument/completion', params, function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) + + local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) + vim.fn.complete(col, matches) + end) end function M.range_formatting() diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 707e2d0d3b..13c83fefd6 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -261,31 +261,26 @@ function M.open_floating_preview(contents, filetype, opts) filetype = { filetype, 's', true }; opts = { opts, 't', true }; } - - -- Don't modify our inputs. - contents = vim.deepcopy(contents) + opts = opts or {} -- Trim empty lines from the end. - for i = #contents, 1, -1 do - if #contents[i] == 0 then - table.remove(contents) - else - break - end - end + contents = M.trim_empty_lines(contents) - local width = 0 - local height = #contents - for i, line in ipairs(contents) do - -- Clean up the input and add left pad. - line = " "..line:gsub("\r", "") - -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. - local line_width = vim.fn.strdisplaywidth(line) - width = math.max(line_width, width) - contents[i] = line + local width = opts.width + local height = opts.height or #contents + if not width then + width = 0 + for i, line in ipairs(contents) do + -- Clean up the input and add left pad. + line = " "..line:gsub("\r", "") + -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. + local line_width = vim.fn.strdisplaywidth(line) + width = math.max(line_width, width) + contents[i] = line + end + -- Add right padding of 1 each. + width = width + 1 end - -- Add right padding of 1 each. - width = width + 1 local floating_bufnr = api.nvim_create_buf(false, true) if filetype then @@ -556,5 +551,56 @@ function M.buf_loclist(bufnr, locations) vim.fn.setloclist(targetwin, items, ' ', 'Language Server') end +-- Remove empty lines from the beginning and end. +function M.trim_empty_lines(lines) + local result = {} + 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 + -- TODO(ashkan) use tbl_slice. + for i = start, finish do + table.insert(result, lines[i]) + end + return result +end + +-- Accepts markdown lines and tries to reduce it to a filetype if it is +-- just a single code block. +-- Note: This modifies the input. +-- +-- Returns: 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 + + return M -- vim:sw=2 ts=2 et From a4b7004f489030d9ee7e3bbfc156ab540744279b Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:03:32 -0800 Subject: [PATCH 08/31] Move everything to buf & default_callbacks - Rename builtin_callbacks to default_callbacks and slightly change its semantics: - No longer contains the default implementations. Instead, any default_callbacks will be used in preference for our .buf methods. - Add this to the docs. --- runtime/doc/lsp.txt | 37 +-- runtime/lua/vim/lsp.lua | 137 +-------- runtime/lua/vim/lsp/buf.lua | 86 +++++- runtime/lua/vim/lsp/builtin_callbacks.lua | 344 ---------------------- runtime/lua/vim/lsp/default_callbacks.lua | 57 ++++ runtime/lua/vim/lsp/protocol.lua | 1 - 6 files changed, 167 insertions(+), 495 deletions(-) delete mode 100644 runtime/lua/vim/lsp/builtin_callbacks.lua create mode 100644 runtime/lua/vim/lsp/default_callbacks.lua diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 26850b3683..8bafe0245f 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -346,31 +346,34 @@ vim.lsp.rpc_response_error({code}, [{message}], [{data}]) the server. ================================================================================ - *vim.lsp.builtin_callbacks* + *vim.lsp.default_callbacks* -The |vim.lsp.builtin_callbacks| table contains the default |lsp-callbacks| +The |vim.lsp.default_callbacks| table contains the default |lsp-callbacks| that are used when creating a new client. The keys are the LSP method names. The following requests and notifications have built-in callbacks defined to handle the response in an idiomatic way. - textDocument/completion - textDocument/declaration - textDocument/definition - textDocument/hover - textDocument/implementation - textDocument/rename - textDocument/signatureHelp - textDocument/typeDefinition + textDocument/publishDiagnostics window/logMessage window/showMessage -You can check these via `vim.tbl_keys(vim.lsp.builtin_callbacks)`. +You can check these via `vim.tbl_keys(vim.lsp.default_callbacks)`. -These will be automatically used and can be overridden by users (either by -modifying the |vim.lsp.builtin_callbacks| object or on a per-client basis -by passing in a table via the {callbacks} parameter on |vim.lsp.start_client| -or |vim.lsp.add_filetype_config|. +These will be used preferrentially in `vim.lsp.buf` methods when handling +requests. They will also be used when responding to server requests and +notifications. + +Use cases: +- Users can modify this to customize to their preferences. +- UI plugins can modify this by assigning to + `vim.lsp.default_callbacks[method]` so as to provide more specialized + handling, allowing you to leverage the UI capabilities available. UIs should + try to be conscientious of any existing changes the user may have set + already by checking for existing values. + +Any callbacks passed directly to `request` methods on a server client will +have the highest precedence, followed by the `default_callbacks`. More information about callbacks can be found in |lsp-callbacks|. @@ -379,8 +382,8 @@ More information about callbacks can be found in |lsp-callbacks|. Callbacks are functions which are called in a variety of situations by the client. Their signature is `function(err, method, params, client_id)` They can -be set by the {callbacks} parameter for |vim.lsp.start_client| and -|vim.lsp.add_filetype_config| or via the |vim.lsp.builtin_callbacks|. +be set by the {callbacks} parameter for |vim.lsp.start_client| or via the +|vim.lsp.default_callbacks|. This will be called for: - notifications from the server, where `err` will always be `nil` diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 4724ff0281..44909a85ee 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,4 +1,4 @@ -local builtin_callbacks = require 'vim.lsp.builtin_callbacks' +local default_callbacks = require 'vim.lsp.default_callbacks' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' @@ -12,7 +12,8 @@ local validate = vim.validate local lsp = { protocol = protocol; - builtin_callbacks = builtin_callbacks; + default_callbacks = default_callbacks; + buf = require'vim.lsp.buf'; util = util; -- Allow raw RPC access. rpc = lsp_rpc; @@ -283,7 +284,7 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks) + local callbacks = tbl_extend("keep", config.callbacks or {}, default_callbacks) -- Copy metatable if it has one. if config.callbacks and config.callbacks.__metatable then setmetatable(callbacks, getmetatable(config.callbacks)) @@ -869,134 +870,8 @@ function lsp.omnifunc(findstart, base) end end ---- ---- FileType based configuration utility ---- - -local all_filetype_configs = {} - --- Lookup a filetype config client by its name. -function lsp.get_filetype_client_by_name(name) - local config = all_filetype_configs[name] - if config.client_id then - return active_clients[config.client_id] - end -end - -local function start_filetype_config(config) - config.client_id = lsp.start_client(config) - nvim_command(string.format( - "autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)", - table.concat(config.filetypes, ','), - config.client_id)) - return config.client_id -end - --- Easy configuration option for common LSP use-cases. --- This will lazy initialize the client when the filetypes specified are --- encountered and attach to those buffers. --- --- The configuration options are the same as |vim.lsp.start_client()|, but --- with a few additions and distinctions: --- --- Additional parameters: --- - filetype: {string} or {list} of filetypes to attach to. --- - name: A unique string among all other servers configured with --- |vim.lsp.add_filetype_config|. --- --- Differences: --- - root_dir: will default to |getcwd()| --- -function lsp.add_filetype_config(config) - -- Additional defaults. - -- Keep a copy of the user's input for debugging reasons. - local user_config = config - config = tbl_extend("force", {}, user_config) - config.root_dir = config.root_dir or uv.cwd() - -- Validate config. - validate_client_config(config) - validate { - name = { config.name, 's' }; - } - assert(config.filetype, "config must have 'filetype' key") - - local filetypes - if type(config.filetype) == 'string' then - filetypes = { config.filetype } - elseif type(config.filetype) == 'table' then - filetypes = config.filetype - assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table") - else - error("config.filetype must be a string or a list of strings") - end - - if all_filetype_configs[config.name] then - -- If the client exists, then it is likely that they are doing some kind of - -- reload flow, so let's not throw an error here. - if all_filetype_configs[config.name].client_id then - -- TODO log here? It might be unnecessarily annoying. - return - end - error(string.format('A configuration with the name %q already exists. They must be unique', config.name)) - end - - all_filetype_configs[config.name] = tbl_extend("keep", config, { - client_id = nil; - filetypes = filetypes; - user_config = user_config; - }) - - nvim_command(string.format( - "autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)", - table.concat(filetypes, ','), - config.name)) -end - --- Create a copy of an existing configuration, and override config with values --- from new_config. --- This is useful if you wish you create multiple LSPs with different root_dirs --- or other use cases. --- --- You can specify a new unique name, but if you do not, a unique name will be --- created like `name-dup_count`. --- --- existing_name: the name of the existing config to copy. --- new_config: the new configuration options. @see |vim.lsp.start_client()|. --- @returns string the new name. -function lsp.copy_filetype_config(existing_name, new_config) - local config = all_filetype_configs[existing_name] - or error(string.format("Configuration with name %q doesn't exist", existing_name)) - config = tbl_extend("force", config, new_config or {}) - config.client_id = nil - config.original_config_name = existing_name - - -- If the user didn't rename it, we will. - if config.name == existing_name then - -- Create a new, unique name. - local duplicate_count = 0 - for _, conf in pairs(all_filetype_configs) do - if conf.original_config_name == existing_name then - duplicate_count = duplicate_count + 1 - end - end - config.name = string.format("%s-%d", existing_name, duplicate_count + 1) - end - print("New config name:", config.name) - lsp.add_filetype_config(config) - return config.name -end - --- Autocmd handler to actually start the client when an applicable filetype is --- encountered. -function lsp._start_filetype_config_client(name) - local config = all_filetype_configs[name] - -- If it exists and is running, don't make it again. - if config.client_id and active_clients[config.client_id] then - -- TODO log here? - return - end - lsp.buf_attach_client(0, start_filetype_config(config)) - return config.client_id +function lsp.client_is_stopped(client_id) + return active_clients[client_id] == nil end --- diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 5f54196dcc..e8a38aa6ef 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -33,7 +33,7 @@ end local function request(method, params, callback) -- TODO(ashkan) enable this. - -- callback = vim.lsp.default_callback[method] or callback + -- callback = vim.lsp.default_callbacks[method] or callback validate { method = {method, 's'}; callback = {callback, 'f'}; @@ -172,9 +172,78 @@ function M.implementation() request('textDocument/implementation', params, location_callback) end +--- Convert SignatureHelp response to preview contents. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +local function signature_help_to_preview_contents(input) + if not input.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 = input.activeSignature or 0 + -- If the activeSignature is not inside the valid range, then clip it. + if active_signature >= #input.signatures then + active_signature = 0 + end + local signature = input.signatures[active_signature + 1] + if not signature then + return + end + vim.list_extend(contents, split_lines(signature.label)) + if signature.documentation then + util.convert_input_to_markdown_lines(signature.documentation, contents) + end + if input.parameters then + local active_parameter = input.activeParameter or 0 + -- If the activeParameter is not inside the valid range, then clip it. + if active_parameter >= #input.parameters then + active_parameter = 0 + end + local parameter = signature.parameters and signature.parameters[active_parameter] + 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 + util.convert_input_to_markdown_lines(parameter.documentation, contents) + end + end + end + return contents +end + function M.signature_help() local params = protocol.make_text_document_position_params() - request('textDocument/signatureHelp', params, location_callback) + focusable_preview('textDocument/signatureHelp', params, function(result) + if not (result and result.signatures and result.signatures[1]) then return end + + -- TODO show empty popup when signatures is empty? + local lines = signature_help_to_preview_contents(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + return { 'No signature available' } + end + return lines, util.try_trim_markdown_code_blocks(lines) + end) end -- TODO(ashkan) ? @@ -195,4 +264,17 @@ end function M.range_formatting() end +function M.rename(new_name) + -- TODO(ashkan) use prepareRename + -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. + local params = protocol.make_text_document_position_params() + new_name = new_name or npcall(vfn.input, "New Name: ") + if not (new_name and #new_name > 0) then return end + params.newName = new_name + request('textDocument/rename', params, function(_, _, result) + if not result then return end + util.workspace_apply_workspace_edit(result) + end) +end + return M diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua deleted file mode 100644 index 2229547f20..0000000000 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ /dev/null @@ -1,344 +0,0 @@ ---- Implements the following default callbacks: --- --- vim.api.nvim_buf_set_lines(0, 0, 0, false, vim.tbl_keys(vim.lsp.builtin_callbacks)) --- - --- textDocument/completion --- textDocument/declaration --- textDocument/definition --- textDocument/hover --- textDocument/implementation --- textDocument/publishDiagnostics --- textDocument/rename --- textDocument/signatureHelp --- textDocument/typeDefinition --- TODO codeLens/resolve --- TODO completionItem/resolve --- TODO documentLink/resolve --- TODO textDocument/codeAction --- TODO textDocument/codeLens --- TODO textDocument/documentHighlight --- TODO textDocument/documentLink --- TODO textDocument/documentSymbol --- TODO textDocument/formatting --- TODO textDocument/onTypeFormatting --- TODO textDocument/rangeFormatting --- TODO textDocument/references --- window/logMessage --- window/showMessage - -local log = require 'vim.lsp.log' -local protocol = require 'vim.lsp.protocol' -local util = require 'vim.lsp.util' -local api = vim.api - -local function split_lines(value) - return vim.split(value, '\n', true) -end - -local builtin_callbacks = {} - --- textDocument/completion --- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion -builtin_callbacks['textDocument/completion'] = function(_, _, result) - if not result or vim.tbl_isempty(result) then - return - end - local pos = api.nvim_win_get_cursor(0) - local row, col = pos[1], pos[2] - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) - - local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) - local match_result = vim.fn.matchstrpos(line_to_cursor, '\\k\\+$') - local match_start, match_finish = match_result[2], match_result[3] - - vim.fn.complete(col + 1 - (match_finish - match_start), matches) -end - --- textDocument/rename -builtin_callbacks['textDocument/rename'] = function(_, _, result) - if not result then return end - util.workspace_apply_workspace_edit(result) -end - -builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) - if not result then return end - local uri = result.uri - local bufnr = vim.uri_to_bufnr(uri) - if not bufnr then - api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) - return - end - util.buf_clear_diagnostics(bufnr) - util.buf_diagnostics_save_positions(bufnr, result.diagnostics) - util.buf_diagnostics_underline(bufnr, result.diagnostics) - util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) - -- util.buf_loclist(bufnr, result.diagnostics) -end - -local function focusable_popup() - local popup_win - return function(winnr) - if popup_win and api.nvim_win_is_valid(popup_win) then - if api.nvim_get_current_win() == popup_win then - api.nvim_command("wincmd p") - else - api.nvim_set_current_win(popup_win) - end - return - end - popup_win = winnr - end -end - -local hover_popup = focusable_popup() - -local function trim_empty_lines(lines) - local result = {} - for _, line in ipairs(lines) do - if #line > 0 then - table.insert(result, line) - end - end - return result -end - --- Accepts markdown lines and tries to reduce it to a filetype if it is --- just a single code block. -local function try_trim_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 lines, language_id - end - end - return lines, 'markdown' -end - --- textDocument/hover --- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover --- @params MarkedString | MarkedString[] | MarkupContent -builtin_callbacks['textDocument/hover'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then - return - end - - if result.contents ~= nil then - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No information available' } - end - local _, winnr = util.open_floating_preview(try_trim_code_blocks(markdown_lines)) - hover_popup(winnr) - end -end - -builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then return end - -- TODO(ashkan) what to do with multiple locations? - result = result[1] - local bufnr = vim.uri_to_bufnr(result.uri) - assert(bufnr) - local start = result.range.start - local finish = result.range["end"] - util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) - util.open_floating_preview({"*Peek:*", string.rep(" ", finish.character - start.character + 1) }, 'markdown', { offset_y = -(finish.line - start.line) }) -end - ---- Convert SignatureHelp response to preview contents. --- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp -local function signature_help_to_preview_contents(input) - if not input.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 = input.activeSignature or 0 - -- If the activeSignature is not inside the valid range, then clip it. - if active_signature >= #input.signatures then - active_signature = 0 - end - local signature = input.signatures[active_signature + 1] - if not signature then - return - end - vim.list_extend(contents, split_lines(signature.label)) - if signature.documentation then - util.convert_input_to_markdown_lines(signature.documentation, contents) - end - if input.parameters then - local active_parameter = input.activeParameter or 0 - -- If the activeParameter is not inside the valid range, then clip it. - if active_parameter >= #input.parameters then - active_parameter = 0 - end - local parameter = signature.parameters and signature.parameters[active_parameter] - 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 - util.convert_input_to_markdown_lines(parameter.documentation, contents) - end - end - end - return contents -end - --- textDocument/signatureHelp --- https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp -builtin_callbacks['textDocument/signatureHelp'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then - return - end - - -- TODO show empty popup when signatures is empty? - if #result.signatures > 0 then - local markdown_lines = signature_help_to_preview_contents(result) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No signature available' } - end - util.open_floating_preview(markdown_lines, 'markdown') - end -end - -local function update_tagstack() - local bufnr = api.nvim_get_current_buf() - local line = vim.fn.line('.') - local col = vim.fn.col('.') - local tagname = vim.fn.expand('') - local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } - local winid = vim.fn.win_getid() - local tagstack = vim.fn.gettagstack(winid) - - local action - - if tagstack.length == tagstack.curidx then - action = 'r' - tagstack.items[tagstack.curidx] = item - elseif tagstack.length > tagstack.curidx then - action = 'r' - if tagstack.curidx > 1 then - tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) - else - tagstack.items = { item } - end - else - action = 'a' - tagstack.items = { item } - end - - tagstack.curidx = tagstack.curidx + 1 - vim.fn.settagstack(winid, tagstack, action) -end - -local function handle_location(result) - -- We can sometimes get a list of locations, so set the first value as the - -- only value we want to handle - -- TODO(ashkan) was this correct^? We could use location lists. - if result[1] ~= nil then - result = result[1] - end - if result.uri == nil then - api.nvim_err_writeln('[LSP] Could not find a valid location') - return - end - local result_file = vim.uri_to_fname(result.uri) - local bufnr = vim.fn.bufadd(result_file) - update_tagstack() - api.nvim_set_current_buf(bufnr) - local start = result.range.start - api.nvim_win_set_cursor(0, {start.line + 1, start.character}) -end - -local function location_callback(_, method, result) - if result == nil or vim.tbl_isempty(result) then - local _ = log.info() and log.info(method, 'No location found') - return nil - end - handle_location(result) - return true -end - -local location_callbacks = { - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration - 'textDocument/declaration'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_definition - 'textDocument/definition'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation - 'textDocument/implementation'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition - 'textDocument/typeDefinition'; -} - -for _, location_method in ipairs(location_callbacks) do - builtin_callbacks[location_method] = location_callback -end - -local function log_message(_, _, result, client_id) - local message_type = result.type - local message = result.message - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) - if not client then - api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) - end - if message_type == protocol.MessageType.Error then - -- Might want to not use err_writeln, - -- but displaying a message with red highlights or something - api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) - else - local message_type_name = protocol.MessageType[message_type] - api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) - end - return result -end - -builtin_callbacks['window/showMessage'] = log_message -builtin_callbacks['window/logMessage'] = log_message - --- Add boilerplate error validation and logging for all of these. -for k, fn in pairs(builtin_callbacks) do - builtin_callbacks[k] = function(err, method, params, client_id) - local _ = log.debug() and log.debug('builtin_callback', method, { params = params, client_id = client_id, err = err }) - if err then - error(tostring(err)) - end - return fn(err, method, params, client_id) - end -end - -return builtin_callbacks --- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua new file mode 100644 index 0000000000..63e62075b4 --- /dev/null +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -0,0 +1,57 @@ +local log = require 'vim.lsp.log' +local protocol = require 'vim.lsp.protocol' +local util = require 'vim.lsp.util' +local api = vim.api + +local M = {} + +M['textDocument/publishDiagnostics'] = function(_, _, result) + if not result then return end + local uri = result.uri + local bufnr = vim.uri_to_bufnr(uri) + if not bufnr then + api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) + return + end + util.buf_clear_diagnostics(bufnr) + util.buf_diagnostics_save_positions(bufnr, result.diagnostics) + util.buf_diagnostics_underline(bufnr, result.diagnostics) + util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) + -- util.buf_loclist(bufnr, result.diagnostics) +end + +local function log_message(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) + end + if message_type == protocol.MessageType.Error then + -- Might want to not use err_writeln, + -- but displaying a message with red highlights or something + api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) + else + local message_type_name = protocol.MessageType[message_type] + api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) + end + return result +end + +M['window/showMessage'] = log_message +M['window/logMessage'] = log_message + +-- Add boilerplate error validation and logging for all of these. +for k, fn in pairs(M) do + M[k] = function(err, method, params, client_id) + local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) + if err then + error(tostring(err)) + end + return fn(err, method, params, client_id) + end +end + +return M +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1413a88ce2..1f51e7bef7 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -10,7 +10,6 @@ end --[=[ -- Useful for interfacing with: --- https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md -- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md function transform_schema_comments() nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]] From 03eb88848c2bea6c0c1da7acc97754d6f47b5118 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:09:03 -0800 Subject: [PATCH 09/31] Change callback resolution to be dynamic. This allows default_callbacks to be specified after client creation to be considered. Also it simplifies the code. --- runtime/doc/lsp.txt | 4 ++-- runtime/lua/vim/lsp.lua | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 8bafe0245f..f4819dc6f8 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -178,8 +178,8 @@ vim.lsp.start_client({config}) `callbacks` A {table} of whose keys are language server method names and the values are `function(err, method, params, client_id)` See |lsp-callbacks| for - more. This will be combined with |lsp-builtin-callbacks| to provide - defaults. + more. This will be combined with |lsp-default-callbacks| to resolve + the callbacks for a client as a fallback. `init_options` A {table} of values to pass in the initialization request as diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 44909a85ee..a284dd1ee7 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -284,19 +284,19 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = tbl_extend("keep", config.callbacks or {}, default_callbacks) - -- Copy metatable if it has one. - if config.callbacks and config.callbacks.__metatable then - setmetatable(callbacks, getmetatable(config.callbacks)) - end + local callbacks = config.callbacks or {} local name = config.name or tostring(client_id) local log_prefix = string.format("LSP[%s]", name) local handlers = {} + local function resolve_callback(method) + return callbacks[method] or default_callbacks[method] + end + function handlers.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then -- Method name is provided here for convenience. callback(nil, method, params, client_id) @@ -305,7 +305,7 @@ function lsp.start_client(config) function handlers.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then local _ = log.debug() and log.debug("server_request: found callback for", method) return callback(nil, method, params, client_id) @@ -316,7 +316,8 @@ function lsp.start_client(config) function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) - nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + print(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) +-- nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then @@ -440,7 +441,7 @@ function lsp.start_client(config) --- Checks capabilities before rpc.request-ing. function client.request(method, params, callback) if not callback then - callback = client.callbacks[method] + callback = resolve_callback(method) or error(string.format("request callback is empty and no default was found for client %s", client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) From c40f8600d2418dfdfeacbba3efe11ae7c6c70ad3 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:16:13 -0800 Subject: [PATCH 10/31] Change error writer to not be annoying. --- runtime/lua/vim/lsp.lua | 12 ++++++++---- runtime/lua/vim/lsp/buf.lua | 7 ++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index a284dd1ee7..1b9d788c43 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -27,6 +27,11 @@ local lsp = { -- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send. -- TODO improve handling of scratch buffers with LSP attached. +local function err_message(...) + nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + nvim_command("redraw") +end + local function resolve_bufnr(bufnr) validate { bufnr = { bufnr, 'n', true } } if bufnr == nil or bufnr == 0 then @@ -316,13 +321,12 @@ function lsp.start_client(config) function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) - print(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) --- nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr }) - nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr)) + err_message(log_prefix, ' user on_error failed: ', tostring(usererr)) end end end @@ -434,7 +438,7 @@ function lsp.start_client(config) local function unsupported_method(method) local msg = "server doesn't support "..method local _ = log.warn() and log.warn(msg) - nvim_err_writeln(msg) + err_message(msg) return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) end diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index e8a38aa6ef..ff045cbbfc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -23,6 +23,11 @@ local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + 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 @@ -133,7 +138,7 @@ local function handle_location(result) result = result[1] end if result.uri == nil then - api.nvim_err_writeln('[LSP] Could not find a valid location') + err_message('[LSP] Could not find a valid location') return end local result_file = vim.uri_to_fname(result.uri) From 1e16b3cf281bec73ccbd155dd11b2db048d1219a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:16:36 -0800 Subject: [PATCH 11/31] Spaces not tabs. --- runtime/lua/vim/lsp/buf.lua | 201 ++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 100 deletions(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ff045cbbfc..8b21370800 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -16,11 +16,11 @@ local function resolve_bufnr(bufnr) end local function ok_or_nil(status, ...) - if not status then return end - return ... + if not status then return end + return ... end local function npcall(fn, ...) - return ok_or_nil(pcall(fn, ...)) + return ok_or_nil(pcall(fn, ...)) end local function err_message(...) @@ -29,78 +29,78 @@ local function err_message(...) end 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 + 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 local function request(method, params, callback) - -- TODO(ashkan) enable this. - -- callback = vim.lsp.default_callbacks[method] or callback - validate { - method = {method, 's'}; - callback = {callback, 'f'}; - } - return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) - if err then error(tostring(err)) end - return callback(err, method, result, client_id) - end) + -- TODO(ashkan) enable this. + -- callback = vim.lsp.default_callbacks[method] or callback + validate { + method = {method, 's'}; + callback = {callback, 'f'}; + } + return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) + if err then error(tostring(err)) end + return callback(err, method, result, client_id) + end) end local function focusable_preview(method, params, fn) - if npcall(api.nvim_win_get_var, 0, method) then - return api.nvim_command("wincmd p") - end + if npcall(api.nvim_win_get_var, 0, method) then + return api.nvim_command("wincmd p") + end - local bufnr = api.nvim_get_current_buf() - do - local win = find_window_by_var(method, bufnr) - if win then - return api.nvim_set_current_win(win) - end - end - return request(method, params, function(_, _, result, _) - -- TODO(ashkan) could show error in preview... - local lines, filetype, opts = fn(result) - if lines then - local _, winnr = util.open_floating_preview(lines, filetype, opts) - api.nvim_win_set_var(winnr, method, bufnr) - end - end) + local bufnr = api.nvim_get_current_buf() + do + local win = find_window_by_var(method, bufnr) + if win then + return api.nvim_set_current_win(win) + end + end + return request(method, params, function(_, _, result, _) + -- TODO(ashkan) could show error in preview... + local lines, filetype, opts = fn(result) + if lines then + local _, winnr = util.open_floating_preview(lines, filetype, opts) + api.nvim_win_set_var(winnr, method, bufnr) + end + end) end function M.hover() - local params = protocol.make_text_document_position_params() - focusable_preview('textDocument/hover', params, function(result) - if not (result and result.contents) then return end + local params = protocol.make_text_document_position_params() + focusable_preview('textDocument/hover', params, function(result) + if not (result and result.contents) then return end - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = util.trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - return { 'No information available' } - end - return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) - end) + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + return { 'No information available' } + end + return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) + end) end function M.peek_definition() - local params = protocol.make_text_document_position_params() - request('textDocument/peekDefinition', params, function(_, _, result, _) - if not (result and result[1]) then return end - local loc = result[1] - local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) - local start = loc.range.start - local finish = loc.range["end"] - util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) - local headbuf = util.open_floating_preview({"Peek:"}, nil, { - offset_y = -(finish.line - start.line); - width = finish.character - start.character + 2; - }) - -- TODO(ashkan) change highlight group? - api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) - end) + local params = protocol.make_text_document_position_params() + request('textDocument/peekDefinition', params, function(_, _, result, _) + if not (result and result[1]) then return end + local loc = result[1] + local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) + local start = loc.range.start + local finish = loc.range["end"] + util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) + local headbuf = util.open_floating_preview({"Peek:"}, nil, { + offset_y = -(finish.line - start.line); + width = finish.character - start.character + 2; + }) + -- TODO(ashkan) change highlight group? + api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) + end) end @@ -158,23 +158,23 @@ local function location_callback(_, method, result) end function M.declaration() - local params = protocol.make_text_document_position_params() - request('textDocument/declaration', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/declaration', params, location_callback) end function M.definition() - local params = protocol.make_text_document_position_params() - request('textDocument/definition', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/definition', params, location_callback) end function M.type_definition() - local params = protocol.make_text_document_position_params() - request('textDocument/typeDefinition', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/typeDefinition', params, location_callback) end function M.implementation() - local params = protocol.make_text_document_position_params() - request('textDocument/implementation', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/implementation', params, location_callback) end --- Convert SignatureHelp response to preview contents. @@ -237,49 +237,50 @@ local function signature_help_to_preview_contents(input) end function M.signature_help() - local params = protocol.make_text_document_position_params() - focusable_preview('textDocument/signatureHelp', params, function(result) - if not (result and result.signatures and result.signatures[1]) then return end + local params = protocol.make_text_document_position_params() + focusable_preview('textDocument/signatureHelp', params, function(result) + if not (result and result.signatures and result.signatures[1]) then return end - -- TODO show empty popup when signatures is empty? - local lines = signature_help_to_preview_contents(result) - lines = util.trim_empty_lines(lines) - if vim.tbl_isempty(lines) then - return { 'No signature available' } - end - return lines, util.try_trim_markdown_code_blocks(lines) - end) + -- TODO show empty popup when signatures is empty? + local lines = signature_help_to_preview_contents(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + return { 'No signature available' } + end + return lines, util.try_trim_markdown_code_blocks(lines) + end) end -- TODO(ashkan) ? function M.completion(context) - local params = protocol.make_text_document_position_params() - params.context = context - return request('textDocument/completion', params, function(_, _, result) - if vim.tbl_isempty(result or {}) then return end - local row, col = unpack(api.nvim_win_get_cursor(0)) - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) + local params = protocol.make_text_document_position_params() + params.context = context + return request('textDocument/completion', params, function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) - local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) - vim.fn.complete(col, matches) - end) + local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) + vim.fn.complete(col, matches) + end) end function M.range_formatting() end function M.rename(new_name) - -- TODO(ashkan) use prepareRename - -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. - local params = protocol.make_text_document_position_params() - new_name = new_name or npcall(vfn.input, "New Name: ") - if not (new_name and #new_name > 0) then return end - params.newName = new_name - request('textDocument/rename', params, function(_, _, result) - if not result then return end - util.workspace_apply_workspace_edit(result) - end) + -- TODO(ashkan) use prepareRename + -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. + local params = protocol.make_text_document_position_params() + new_name = new_name or npcall(vfn.input, "New Name: ") + if not (new_name and #new_name > 0) then return end + params.newName = new_name + request('textDocument/rename', params, function(_, _, result) + if not result then return end + util.workspace_apply_workspace_edit(result) + end) end return M +-- vim:sw=2 ts=2 et From 93beae4f31d42dc70c874020011220444d7f979c Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:35:11 -0800 Subject: [PATCH 12/31] Fix rename support. --- runtime/lua/vim/lsp/buf.lua | 1 + runtime/lua/vim/lsp/util.lua | 29 +++++++++++------------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 8b21370800..01174a1e48 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -44,6 +44,7 @@ local function request(method, params, callback) callback = {callback, 'f'}; } return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) + local _ = log.debug() and log.debug("vim.lsp.buf", method, client_id, err, result) if err then error(tostring(err)) end return callback(err, method, result, client_id) end) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 13c83fefd6..ab2c02ffcd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -52,16 +52,12 @@ function M.text_document_apply_text_edit(text_edit, bufnr) api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines) return end - api.nvim_err_writeln('apply_text_edit currently only supports character ranges starting at 0') - error('apply_text_edit currently only supports character ranges starting at 0') - return - -- TODO test and finish this support for character ranges. --- local lines = api.nvim_buf_get_lines(0, start.line, finish.line + 1, false) --- local suffix = lines[#lines]:sub(finish.character+2) --- local prefix = lines[1]:sub(start.character+2) --- new_lines[#new_lines] = new_lines[#new_lines]..suffix --- new_lines[1] = prefix..new_lines[1] --- api.nvim_buf_set_lines(0, start.line, finish.line, false, new_lines) + local lines = api.nvim_buf_get_lines(bufnr, start.line, finish.line + 1, false) + local suffix = lines[#lines]:sub(finish.character+1) + local prefix = lines[1]:sub(1, start.character) + new_lines[#new_lines] = new_lines[#new_lines]..suffix + new_lines[1] = prefix..new_lines[1] + api.nvim_buf_set_lines(bufnr, start.line, finish.line + 1, false, new_lines) end -- textDocument/completion response returns one of CompletionItem[], CompletionList or null. @@ -158,18 +154,15 @@ function M.workspace_apply_workspace_edit(workspace_edit) return end - if workspace_edit.changes == nil or #workspace_edit.changes == 0 then + 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(workspace_edit.changes) do - local fname = vim.uri_to_fname(uri) - -- TODO improve this approach. Try to edit open buffers without switching. - -- Not sure how to handle files which aren't open. This is deprecated - -- anyway, so I guess it could be left as is. - api.nvim_command('edit '..fname) + for uri, changes in pairs(all_changes) do + local bufnr = vim.uri_to_bufnr(uri) for _, change in ipairs(changes) do - M.text_document_apply_text_edit(change) + M.text_document_apply_text_edit(change, bufnr) end end end From 4c7ef3754a7679a7f095fa9e739a665ead4c71b4 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:37:23 -0800 Subject: [PATCH 13/31] Satisfy lualint. --- runtime/lua/vim/lsp.lua | 2 +- runtime/lua/vim/lsp/buf.lua | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1b9d788c43..b2dc1315dd 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -903,7 +903,7 @@ end -- Print some debug information about all LSP related things. -- The output of this function should not be relied upon and may change. function lsp.print_debug_info() - print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs })) + print(vim.inspect({ clients = active_clients })) end -- Log level dictionary with reverse lookup as well. diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 01174a1e48..1c2e60f1ca 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -7,14 +7,6 @@ local log = require 'vim.lsp.log' local M = {} -local function resolve_bufnr(bufnr) - validate { bufnr = { bufnr, 'n', true } } - if bufnr == nil or bufnr == 0 then - return api.nvim_get_current_buf() - end - return bufnr -end - local function ok_or_nil(status, ...) if not status then return end return ... @@ -198,7 +190,7 @@ local function signature_help_to_preview_contents(input) if not signature then return end - vim.list_extend(contents, split_lines(signature.label)) + vim.list_extend(contents, vim.split(signature.label, '\n', true)) if signature.documentation then util.convert_input_to_markdown_lines(signature.documentation, contents) end From 9b7f95671559a9189569dcb93778e54eaa87f255 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:39:45 -0800 Subject: [PATCH 14/31] Use err_message in default_callbacks --- runtime/lua/vim/lsp/default_callbacks.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index 63e62075b4..67c33a1667 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -20,18 +20,23 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) -- util.buf_loclist(bufnr, result.diagnostics) end +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + local function log_message(_, _, result, client_id) local message_type = result.type local message = result.message local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format("id=%d", client_id) if not client then - api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) + err_message("LSP[", client_name, "] client has shut down after sending the message") end if message_type == protocol.MessageType.Error then -- Might want to not use err_writeln, -- but displaying a message with red highlights or something - api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) + err_message("LSP[", client_name, "] ", message) else local message_type_name = protocol.MessageType[message_type] api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) From b984f613c1e8dadbe59bf0d7093a6ed12af61b37 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 17:09:21 -0800 Subject: [PATCH 15/31] Extend list_extend to take start/finish. --- runtime/lua/vim/lsp/default_callbacks.lua | 14 +++++----- runtime/lua/vim/lsp/util.lua | 7 +---- runtime/lua/vim/shared.lua | 31 ++++++++++++++--------- test/functional/lua/vim_spec.lua | 6 ++++- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index 67c33a1667..5e8552307b 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -5,12 +5,17 @@ local api = vim.api local M = {} +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + M['textDocument/publishDiagnostics'] = function(_, _, result) if not result then return end local uri = result.uri local bufnr = vim.uri_to_bufnr(uri) if not bufnr then - api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) + err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) return end util.buf_clear_diagnostics(bufnr) @@ -20,11 +25,6 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) -- util.buf_loclist(bufnr, result.diagnostics) end -local function err_message(...) - api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) - api.nvim_command("redraw") -end - local function log_message(_, _, result, client_id) local message_type = result.type local message = result.message @@ -34,8 +34,6 @@ local function log_message(_, _, result, client_id) err_message("LSP[", client_name, "] client has shut down after sending the message") end if message_type == protocol.MessageType.Error then - -- Might want to not use err_writeln, - -- but displaying a message with red highlights or something err_message("LSP[", client_name, "] ", message) else local message_type_name = protocol.MessageType[message_type] diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ab2c02ffcd..3a2142a478 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -546,7 +546,6 @@ end -- Remove empty lines from the beginning and end. function M.trim_empty_lines(lines) - local result = {} local start = 1 for i = 1, #lines do if #lines[i] > 0 then @@ -561,11 +560,7 @@ function M.trim_empty_lines(lines) break end end - -- TODO(ashkan) use tbl_slice. - for i = start, finish do - table.insert(result, lines[i]) - end - return result + return vim.list_extend({}, lines, start, finish) end -- Accepts markdown lines and tries to reduce it to a filetype if it is diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index ff89acc524..25287ed1aa 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -226,18 +226,25 @@ function vim.tbl_add_reverse_lookup(o) return o end ---- Extends a list-like table with the values of another list-like table. ---- ---NOTE: This *mutates* dst! ---@see |extend()| ---- ---@param dst The list which will be modified and appended to. ---@param src The list from which values will be inserted. -function vim.list_extend(dst, src) - assert(type(dst) == 'table', "dst must be a table") - assert(type(src) == 'table', "src must be a table") - for _, v in ipairs(src) do - table.insert(dst, v) +-- Extends a list-like table with the values of another list-like table. +-- +-- NOTE: This *mutates* dst! +-- @see |extend()| +-- +-- @param dst list which will be modified and appended to. +-- @param src list from which values will be inserted. +-- @param start Start index on src. defaults to 1 +-- @param finish Final index on src. defaults to #src +-- @returns dst +function vim.list_extend(dst, src, start, finish) + vim.validate { + dst = {dst, 't'}; + src = {src, 't'}; + start = {start, 'n', true}; + finish = {finish, 'n', true}; + } + for i = start or 1, finish or #src do + table.insert(dst, src[i]) end return dst end diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 028f2dcd52..c8a4c1364f 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -353,10 +353,14 @@ describe('lua stdlib', function() it('vim.list_extend', function() eq({1,2,3}, exec_lua [[ return vim.list_extend({1}, {2,3}) ]]) - eq('Error executing lua: .../shared.lua: src must be a table', + eq('Error executing lua: .../shared.lua: src: expected table, got nil', pcall_err(exec_lua, [[ return vim.list_extend({1}, nil) ]])) eq({1,2}, exec_lua [[ return vim.list_extend({1}, {2;a=1}) ]]) eq(true, exec_lua [[ local a = {1} return vim.list_extend(a, {2;a=1}) == a ]]) + eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1) ]]) + eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 2) ]]) + eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1, -1) ]]) + eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, -1, 2) ]]) end) it('vim.tbl_add_reverse_lookup', function() From 6fc409d5939500b038ba281ac1929fd053f35310 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:51:44 -0800 Subject: [PATCH 16/31] Add full text_edit implementation. - Implements textDocument/formatting, textDocument/rangeFormatting, workspace/applyEdit. TODO: - still has edge cases around replacement probably. Only tested with inserts on the same position. --- runtime/lua/vim/lsp/buf.lua | 122 +++++++++++++++++++- runtime/lua/vim/lsp/default_callbacks.lua | 9 ++ runtime/lua/vim/lsp/util.lua | 134 ++++++++++++++++------ 3 files changed, 228 insertions(+), 37 deletions(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 1c2e60f1ca..ea88207b5b 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -259,7 +259,127 @@ function M.completion(context) end) end -function M.range_formatting() +local function apply_edit_to_lines(lines, start_pos, end_pos, new_lines) + -- 0-indexing to 1-indexing makes things look a bit worse. + local i_0 = start_pos[1] + 1 + local i_n = end_pos[1] + 1 + local n = i_n - i_0 + 1 + if not lines[i_0] or not lines[i_n] then + error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) + end + local prefix = "" + local suffix = lines[i_n]:sub(end_pos[2]+1) + lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) + if start_pos[2] > 0 then + prefix = lines[i_0]:sub(1, start_pos[2]) + -- lines[i_0] = lines[i_0]:sub(start.character+1) + end + -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. + new_lines = vim.list_extend({}, new_lines) + if #suffix > 0 then + new_lines[#new_lines] = new_lines[#new_lines]..suffix + end + if #prefix > 0 then + new_lines[1] = prefix..new_lines[1] + end + if #new_lines >= n then + for i = 1, n do + lines[i + i_0 - 1] = new_lines[i] + end + for i = n+1,#new_lines do + table.insert(lines, i_n + 1, new_lines[i]) + end + else + for i = 1, #new_lines do + lines[i + i_0 - 1] = new_lines[i] + end + for _ = #new_lines+1, n do + table.remove(lines, i_0 + #new_lines + 1) + end + end +end + +local function apply_text_edits(text_edits, bufnr) + if not next(text_edits) then return end + -- nvim.print("Start", #text_edits) + local start_line, finish_line = math.huge, -1 + local cleaned = {} + for _, e in ipairs(text_edits) do + start_line = math.min(e.range.start.line, start_line) + finish_line = math.max(e.range["end"].line, finish_line) + table.insert(cleaned, { + A = {e.range.start.line; e.range.start.character}; + B = {e.range["end"].line; e.range["end"].character}; + lines = vim.split(e.newText, '\n', true); + }) + end + local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) + for i, e in ipairs(cleaned) do + -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) + local y = 0 + local x = 0 + -- TODO(ashkan) this could be done in O(n) with dynamic programming + for j = 1, i-1 do + local o = cleaned[j] + -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) + if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then + y = y - (o.B[1] - o.A[1] + 1) + #o.lines + -- Same line + if #o.lines > 1 then + x = -e.A[2] + #o.lines[#o.lines] + else + if o.A[1] == e.A[1] then + -- Try to account for insertions. + -- TODO how to account for deletions? + x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] + end + end + end + end + local A = {e.A[1] + y - start_line, e.A[2] + x} + local B = {e.B[1] + y - start_line, e.B[2] + x} + -- if x ~= 0 or y ~= 0 then + -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) + -- end + apply_edit_to_lines(lines, A, B, e.lines) + end + api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines) +end + +function M.formatting(options) + validate { options = {options, 't', true} } + local params = { + textDocument = { uri = vim.uri_from_bufnr(0) }; + options = options or {}; + } + params.options[vim.type_idx] = vim.types.dictionary + return request('textDocument/formatting', params, function(_, _, result) + if not result then return end + apply_text_edits(result) + end) +end + +function M.range_formatting(options, start_pos, end_pos) + validate { + options = {options, 't', true}; + start_pos = {start_pos, 't', true}; + end_pos = {end_pos, 't', true}; + } + start_pos = start_pos or vim.api.nvim_buf_get_mark(0, '<') + end_pos = end_pos or vim.api.nvim_buf_get_mark(0, '>') + local params = { + textDocument = { uri = vim.uri_from_bufnr(0) }; + range = { + start = { line = start_pos[1]; character = start_pos[2]; }; + ["end"] = { line = end_pos[1]; character = end_pos[2]; }; + }; + options = options or {}; + } + params.options[vim.type_idx] = vim.types.dictionary + return request('textDocument/rangeFormatting', params, function(_, _, result) + if not result then return end + apply_text_edits(result) + end) end function M.rename(new_name) diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index 5e8552307b..d58280e0f2 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -10,6 +10,15 @@ local function err_message(...) api.nvim_command("redraw") end +M['workspace/applyEdit'] = function(_, _, workspace_edit) + if not workspace_edit then return end + -- TODO(ashkan) Do something more with label? + if workspace_edit.label then + print("Workspace edit", workspace_edit.label) + end + util.apply_workspace_edit(workspace_edit.edit) +end + M['textDocument/publishDiagnostics'] = function(_, _, result) if not result then return end local uri = result.uri diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 3a2142a478..004fb81cba 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -33,6 +33,93 @@ local function resolve_bufnr(bufnr) return bufnr end +function M.apply_edit_to_lines(lines, start_pos, end_pos, new_lines) + -- 0-indexing to 1-indexing makes things look a bit worse. + local i_0 = start_pos[1] + 1 + local i_n = end_pos[1] + 1 + local n = i_n - i_0 + 1 + if not lines[i_0] or not lines[i_n] then + error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) + end + local prefix = "" + local suffix = lines[i_n]:sub(end_pos[2]+1) + lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) + if start_pos[2] > 0 then + prefix = lines[i_0]:sub(1, start_pos[2]) + -- lines[i_0] = lines[i_0]:sub(start.character+1) + end + -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. + new_lines = vim.list_extend({}, new_lines) + if #suffix > 0 then + new_lines[#new_lines] = new_lines[#new_lines]..suffix + end + if #prefix > 0 then + new_lines[1] = prefix..new_lines[1] + end + if #new_lines >= n then + for i = 1, n do + lines[i + i_0 - 1] = new_lines[i] + end + for i = n+1,#new_lines do + table.insert(lines, i_n + 1, new_lines[i]) + end + else + for i = 1, #new_lines do + lines[i + i_0 - 1] = new_lines[i] + end + for _ = #new_lines+1, n do + table.remove(lines, i_0 + #new_lines + 1) + end + end +end + +function M.apply_text_edits(text_edits, bufnr) + if not next(text_edits) then return end + -- nvim.print("Start", #text_edits) + local start_line, finish_line = math.huge, -1 + local cleaned = {} + for _, e in ipairs(text_edits) do + start_line = math.min(e.range.start.line, start_line) + finish_line = math.max(e.range["end"].line, finish_line) + table.insert(cleaned, { + A = {e.range.start.line; e.range.start.character}; + B = {e.range["end"].line; e.range["end"].character}; + lines = vim.split(e.newText, '\n', true); + }) + end + local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) + for i, e in ipairs(cleaned) do + -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) + local y = 0 + local x = 0 + -- TODO(ashkan) this could be done in O(n) with dynamic programming + for j = 1, i-1 do + local o = cleaned[j] + -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) + if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then + y = y - (o.B[1] - o.A[1] + 1) + #o.lines + -- Same line + if #o.lines > 1 then + x = -e.A[2] + #o.lines[#o.lines] + else + if o.A[1] == e.A[1] then + -- Try to account for insertions. + -- TODO how to account for deletions? + x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] + end + end + end + end + local A = {e.A[1] + y - start_line, e.A[2] + x} + local B = {e.B[1] + y - start_line, e.B[2] + x} + -- if x ~= 0 or y ~= 0 then + -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) + -- end + M.apply_edit_to_lines(lines, A, B, e.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 @@ -40,26 +127,6 @@ end -- function M.glob_to_regex(glob) -- end ---- Apply the TextEdit response. --- @params TextEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.text_document_apply_text_edit(text_edit, bufnr) - bufnr = resolve_bufnr(bufnr) - local range = text_edit.range - local start = range.start - local finish = range['end'] - local new_lines = split_lines(text_edit.newText) - if start.character == 0 and finish.character == 0 then - api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines) - return - end - local lines = api.nvim_buf_get_lines(bufnr, start.line, finish.line + 1, false) - local suffix = lines[#lines]:sub(finish.character+1) - local prefix = lines[1]:sub(1, start.character) - new_lines[#new_lines] = new_lines[#new_lines]..suffix - new_lines[1] = prefix..new_lines[1] - api.nvim_buf_set_lines(bufnr, start.line, finish.line + 1, false, new_lines) -end - -- textDocument/completion response returns one of CompletionItem[], CompletionList or null. -- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion function M.extract_completion_items(result) @@ -74,18 +141,15 @@ end --- Apply the TextDocumentEdit response. -- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.text_document_apply_text_document_edit(text_document_edit, bufnr) - -- local text_document = text_document_edit.textDocument - -- TODO use text_document_version? - -- local text_document_version = text_document.version - - -- TODO technically, you could do this without doing multiple buf_get/set - -- by getting the full region (smallest line and largest line) and doing - -- the edits on the buffer, and then applying the buffer at the end. - -- I'm not sure if that's better. - for _, text_edit in ipairs(text_document_edit.edits) do - M.text_document_apply_text_edit(text_edit, bufnr) +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) + -- TODO(ashkan) check this is correct. + if api.nvim_buf_get_changedtick(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 function M.get_current_line_to_cursor() @@ -141,14 +205,14 @@ function M.text_document_completion_list_to_complete_items(result, line_prefix) end -- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.workspace_apply_workspace_edit(workspace_edit) +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.text_document_apply_text_document_edit(change) + M.apply_text_document_edit(change) end end return @@ -161,9 +225,7 @@ function M.workspace_apply_workspace_edit(workspace_edit) for uri, changes in pairs(all_changes) do local bufnr = vim.uri_to_bufnr(uri) - for _, change in ipairs(changes) do - M.text_document_apply_text_edit(change, bufnr) - end + M.apply_text_edits(changes, bufnr) end end From 6d9f48ddcf3a4d0fbed0674ce180bcfb9340bb22 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:57:21 -0800 Subject: [PATCH 17/31] Fix reference in rename. --- runtime/lua/vim/lsp/buf.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ea88207b5b..68af34dac6 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -391,7 +391,7 @@ function M.rename(new_name) params.newName = new_name request('textDocument/rename', params, function(_, _, result) if not result then return end - util.workspace_apply_workspace_edit(result) + util.apply_workspace_edit(result) end) end From 7bf766ad0903574b6202af0d2e11d306d656b491 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:59:12 -0800 Subject: [PATCH 18/31] Use the apply_text_edits from util. --- runtime/lua/vim/lsp/buf.lua | 91 +------------------------------------ 1 file changed, 2 insertions(+), 89 deletions(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 68af34dac6..260bca281f 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -259,93 +259,6 @@ function M.completion(context) end) end -local function apply_edit_to_lines(lines, start_pos, end_pos, new_lines) - -- 0-indexing to 1-indexing makes things look a bit worse. - local i_0 = start_pos[1] + 1 - local i_n = end_pos[1] + 1 - local n = i_n - i_0 + 1 - if not lines[i_0] or not lines[i_n] then - error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) - end - local prefix = "" - local suffix = lines[i_n]:sub(end_pos[2]+1) - lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) - if start_pos[2] > 0 then - prefix = lines[i_0]:sub(1, start_pos[2]) - -- lines[i_0] = lines[i_0]:sub(start.character+1) - end - -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. - new_lines = vim.list_extend({}, new_lines) - if #suffix > 0 then - new_lines[#new_lines] = new_lines[#new_lines]..suffix - end - if #prefix > 0 then - new_lines[1] = prefix..new_lines[1] - end - if #new_lines >= n then - for i = 1, n do - lines[i + i_0 - 1] = new_lines[i] - end - for i = n+1,#new_lines do - table.insert(lines, i_n + 1, new_lines[i]) - end - else - for i = 1, #new_lines do - lines[i + i_0 - 1] = new_lines[i] - end - for _ = #new_lines+1, n do - table.remove(lines, i_0 + #new_lines + 1) - end - end -end - -local function apply_text_edits(text_edits, bufnr) - if not next(text_edits) then return end - -- nvim.print("Start", #text_edits) - local start_line, finish_line = math.huge, -1 - local cleaned = {} - for _, e in ipairs(text_edits) do - start_line = math.min(e.range.start.line, start_line) - finish_line = math.max(e.range["end"].line, finish_line) - table.insert(cleaned, { - A = {e.range.start.line; e.range.start.character}; - B = {e.range["end"].line; e.range["end"].character}; - lines = vim.split(e.newText, '\n', true); - }) - end - local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) - for i, e in ipairs(cleaned) do - -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) - local y = 0 - local x = 0 - -- TODO(ashkan) this could be done in O(n) with dynamic programming - for j = 1, i-1 do - local o = cleaned[j] - -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) - if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then - y = y - (o.B[1] - o.A[1] + 1) + #o.lines - -- Same line - if #o.lines > 1 then - x = -e.A[2] + #o.lines[#o.lines] - else - if o.A[1] == e.A[1] then - -- Try to account for insertions. - -- TODO how to account for deletions? - x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] - end - end - end - end - local A = {e.A[1] + y - start_line, e.A[2] + x} - local B = {e.B[1] + y - start_line, e.B[2] + x} - -- if x ~= 0 or y ~= 0 then - -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) - -- end - apply_edit_to_lines(lines, A, B, e.lines) - end - api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines) -end - function M.formatting(options) validate { options = {options, 't', true} } local params = { @@ -355,7 +268,7 @@ function M.formatting(options) params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/formatting', params, function(_, _, result) if not result then return end - apply_text_edits(result) + util.apply_text_edits(result) end) end @@ -378,7 +291,7 @@ function M.range_formatting(options, start_pos, end_pos) params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/rangeFormatting', params, function(_, _, result) if not result then return end - apply_text_edits(result) + util.apply_text_edits(result) end) end From b7170f2d722cee24a62eb74ac836d6192e5475dc Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:59:52 -0800 Subject: [PATCH 19/31] Remove resolve_bufnr/lualint --- runtime/lua/vim/lsp/util.lua | 7 ------- 1 file changed, 7 deletions(-) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 004fb81cba..dcf2c17df4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -26,13 +26,6 @@ local function remove_prefix(prefix, word) return word:sub(prefix_length + 1) end -local function resolve_bufnr(bufnr) - if bufnr == nil or bufnr == 0 then - return api.nvim_get_current_buf() - end - return bufnr -end - function M.apply_edit_to_lines(lines, start_pos, end_pos, new_lines) -- 0-indexing to 1-indexing makes things look a bit worse. local i_0 = start_pos[1] + 1 From bcae04f6c62b23104e85bb08c54a16d0cdc33852 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:19:06 -0800 Subject: [PATCH 20/31] Updates - Use correct implementation of text_edits. - Send indent options to rangeFormatting and formatting. - Remove references to vim bindings and filetype from lsp.txt - Add more examples to docs. - Add before_init to allow changing initialize_params. --- runtime/doc/lsp.txt | 161 ++++++----------------- runtime/lua/vim/lsp.lua | 13 +- runtime/lua/vim/lsp/buf.lua | 14 +- runtime/lua/vim/lsp/util.lua | 108 +++++++-------- test/functional/plugin/lsp/util_spec.lua | 78 +++++++++++ 5 files changed, 187 insertions(+), 187 deletions(-) create mode 100644 test/functional/plugin/lsp/util_spec.lua diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index f4819dc6f8..4f5a9dff9e 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -24,105 +24,14 @@ After installing a language server to your machine, you must let Neovim know how to start and interact with that language server. To do so, you can either: -- Use the |vim.lsp.add_filetype_config()|, which solves the common use-case of - a single server for one or more filetypes. This can also be used from vim - via |lsp#add_filetype_config()|. +- Use https://github.com/neovim/nvim-lsp and one of the existing servers there + or set up a new one using the `nvim_lsp/skeleton` interface (and contribute + it if you find it useful). This uses |vim.lsp.start_client()| under the + hood. - Or |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()|. These are the backbone of the LSP API. These are easy to use enough for basic or more complex configurations such as in |lsp-advanced-js-example|. -================================================================================ - *lsp-filetype-config* - -These are utilities specific to filetype based configurations. - - *lsp#add_filetype_config()* - *vim.lsp.add_filetype_config()* -lsp#add_filetype_config({config}) for Vim. -vim.lsp.add_filetype_config({config}) for Lua - - These are functions which can be used to create a simple configuration which - will start a language server for a list of filetypes based on the |FileType| - event. - It will lazily start start the server, meaning that it will only start once - a matching filetype is encountered. - - The {config} options are the same as |vim.lsp.start_client()|, but - with a few additions and distinctions: - - Additional parameters:~ - `filetype` - {string} or {list} of filetypes to attach to. - `name` - A unique identifying string among all other servers configured with - |vim.lsp.add_filetype_config|. - - Differences:~ - `root_dir` - Will default to |getcwd()| instead of being required. - - NOTE: the function options in {config} like {config.on_init} are for Lua - callbacks, not Vim callbacks. -> - " Go example - call lsp#add_filetype_config({ - \ 'filetype': 'go', - \ 'name': 'gopls', - \ 'cmd': 'gopls' - \ }) - " Python example - call lsp#add_filetype_config({ - \ 'filetype': 'python', - \ 'name': 'pyls', - \ 'cmd': 'pyls' - \ }) - " Rust example - call lsp#add_filetype_config({ - \ 'filetype': 'rust', - \ 'name': 'rls', - \ 'cmd': 'rls', - \ 'capabilities': { - \ 'clippy_preference': 'on', - \ 'all_targets': v:false, - \ 'build_on_save': v:true, - \ 'wait_to_build': 0 - \ }}) -< -> - -- From Lua - vim.lsp.add_filetype_config { - name = "clangd"; - filetype = {"c", "cpp"}; - cmd = "clangd -background-index"; - capabilities = { - offsetEncoding = {"utf-8", "utf-16"}; - }; - on_init = vim.schedule_wrap(function(client, result) - if result.offsetEncoding then - client.offset_encoding = result.offsetEncoding - end - end) - } -< - *vim.lsp.copy_filetype_config()* -vim.lsp.copy_filetype_config({existing_name}, [{override_config}]) - - You can use this to copy an existing filetype configuration and change it by - specifying {override_config} which will override any properties in the - existing configuration. If you don't specify a new unique name with - {override_config.name} then it will try to create one and return it. - - Returns:~ - `name` the new configuration name. - - *vim.lsp.get_filetype_client_by_name()* -vim.lsp.get_filetype_client_by_name({name}) - - Use this to look up a client by its name created from - |vim.lsp.add_filetype_config()|. - - Returns nil if the client is not active or the name is not valid. - ================================================================================ *lsp-core-api* These are the core api functions for working with clients. You will mainly be @@ -203,6 +112,12 @@ vim.lsp.start_client({config}) `vim.lsp.client_errors[code]` can be used to retrieve a human understandable string. + `before_init(initialize_params, config)` + A function which is called *before* the request `initialize` is completed. + `initialize_params` contains the parameters we are sending to the server + and `config` is the config that was passed to `start_client()` for + convenience. You can use this to modify parameters before they are sent. + `on_init(client, initialize_result)` A function which is called after the request `initialize` is completed. `initialize_result` contains `capabilities` and anything else the server @@ -485,18 +400,16 @@ vim.lsp.buf_notify({bufnr}, {method}, {params}) ================================================================================ *lsp-logging* - *lsp#set_log_level()* -lsp#set_log_level({level}) + *vim.lsp.set_log_level()* +vim.lsp.set_log_level({level}) You can set the log level for language server client logging. Possible values: "trace", "debug", "info", "warn", "error" Default: "warn" - Example: `call lsp#set_log_level("debug")` + Example: `lua vim.lsp.set_log_level("debug")` - *lsp#get_log_path()* *vim.lsp.get_log_path()* -lsp#get_log_path() vim.lsp.get_log_path() Returns the path that LSP logs are written. @@ -511,43 +424,43 @@ vim.lsp.log_levels ================================================================================ *lsp-omnifunc* *vim.lsp.omnifunc()* - *lsp#omnifunc* -lsp#omnifunc({findstart}, {base}) vim.lsp.omnifunc({findstart}, {base}) To configure omnifunc, add the following in your init.vim: > - set omnifunc=lsp#omnifunc - - " This is optional, but you may find it useful - autocmd CompleteDone * pclose + " Configure for python + autocmd Filetype python setl omnifunc=v:lua.vim.lsp.omnifunc + + " Or with on_attach + start_client { + ... + on_attach = function(client, bufnr) + vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc') + end; + } + + " This is optional, but you may find it useful + autocmd CompleteDone * pclose < ================================================================================ *lsp-vim-functions* +To use the functions from vim, it is recommended to use |v:lua| to interface +with the Lua functions. No direct vim functions are provided, but the +interface is still easy to use from mappings. + These methods can be used in mappings and are the equivalent of using the request from lua as follows: -> - lua vim.lsp.buf_request(0, "textDocument/hover", vim.lsp.protocol.make_text_document_position_params()) -< - - lsp#text_document_declaration() - lsp#text_document_definition() - lsp#text_document_hover() - lsp#text_document_implementation() - lsp#text_document_signature_help() - lsp#text_document_type_definition() - > " Example config - autocmd Filetype rust,python,go,c,cpp setl omnifunc=lsp#omnifunc - nnoremap ;dc :call lsp#text_document_declaration() - nnoremap ;df :call lsp#text_document_definition() - nnoremap ;h :call lsp#text_document_hover() - nnoremap ;i :call lsp#text_document_implementation() - nnoremap ;s :call lsp#text_document_signature_help() - nnoremap ;td :call lsp#text_document_type_definition() + autocmd Filetype rust,python,go,c,cpp setl omnifunc=v:lua.vim.lsp.omnifunc + nnoremap ;dc lua vim.lsp.buf.declaration() + nnoremap ;df lua vim.lsp.buf.definition() + nnoremap ;h lua vim.lsp.buf.hover() + nnoremap ;i lua vim.lsp.buf.implementation() + nnoremap ;s lua vim.lsp.buf.signature_help() + nnoremap ;td lua vim.lsp.buf.type_definition() < ================================================================================ *lsp-advanced-js-example* diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 5a0b320f4c..790f8f8a9d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -160,13 +160,13 @@ local function validate_client_config(config) root_dir = { config.root_dir, is_dir, "directory" }; callbacks = { config.callbacks, "t", true }; capabilities = { config.capabilities, "t", true }; - -- cmd = { config.cmd, "s", false }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; cmd_env = { config.cmd_env, "f", true }; name = { config.name, 's', true }; on_error = { config.on_error, "f", true }; on_exit = { config.on_exit, "f", true }; on_init = { config.on_init, "f", true }; + before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } local cmd, cmd_args = validate_command(config.cmd) @@ -267,6 +267,12 @@ end -- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a -- human understandable string. -- +-- before_init(initialize_params, config): A function which is called *before* +-- the request `initialize` is completed. `initialize_params` contains +-- the parameters we are sending to the server and `config` is the config that +-- was passed to `start_client()` for convenience. You can use this to modify +-- parameters before they are sent. +-- -- on_init(client, initialize_result): A function which is called after the -- request `initialize` is completed. `initialize_result` contains -- `capabilities` and anything else the server may send. For example, `clangd` @@ -385,7 +391,6 @@ function lsp.start_client(config) -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. rootUri = vim.uri_from_fname(config.root_dir); --- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h")); -- User provided initialization options. initializationOptions = config.init_options; -- The capabilities provided by the client (editor or tool) @@ -409,6 +414,10 @@ function lsp.start_client(config) -- } workspaceFolders = nil; } + if config.before_init then + -- TODO(ashkan) handle errors here. + pcall(config.before_init, initialize_params, config) + end local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 260bca281f..dbc141a906 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -261,11 +261,14 @@ end function M.formatting(options) validate { options = {options, 't', true} } + options = vim.tbl_extend('keep', options or {}, { + tabSize = api.nvim_buf_get_option(0, 'tabstop'); + insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + }) local params = { textDocument = { uri = vim.uri_from_bufnr(0) }; - options = options or {}; + options = options; } - params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/formatting', params, function(_, _, result) if not result then return end util.apply_text_edits(result) @@ -278,6 +281,10 @@ function M.range_formatting(options, start_pos, end_pos) start_pos = {start_pos, 't', true}; end_pos = {end_pos, 't', true}; } + options = vim.tbl_extend('keep', options or {}, { + tabSize = api.nvim_buf_get_option(0, 'tabstop'); + insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + }) start_pos = start_pos or vim.api.nvim_buf_get_mark(0, '<') end_pos = end_pos or vim.api.nvim_buf_get_mark(0, '>') local params = { @@ -286,9 +293,8 @@ function M.range_formatting(options, start_pos, end_pos) start = { line = start_pos[1]; character = start_pos[2]; }; ["end"] = { line = end_pos[1]; character = end_pos[2]; }; }; - options = options or {}; + options = options; } - params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/rangeFormatting', params, function(_, _, result) if not result then return end util.apply_text_edits(result) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index dcf2c17df4..66430fffc7 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -26,89 +26,83 @@ local function remove_prefix(prefix, word) return word:sub(prefix_length + 1) end -function M.apply_edit_to_lines(lines, start_pos, end_pos, new_lines) - -- 0-indexing to 1-indexing makes things look a bit worse. - local i_0 = start_pos[1] + 1 - local i_n = end_pos[1] + 1 - local n = i_n - i_0 + 1 - if not lines[i_0] or not lines[i_n] then - error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) +-- TODO(ashkan) @performance this could do less copying. +function M.set_lines(lines, A, B, new_lines) + -- 0-indexing to 1-indexing + local i_0 = A[1] + 1 + local i_n = B[1] + 1 + 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(end_pos[2]+1) - lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) - if start_pos[2] > 0 then - prefix = lines[i_0]:sub(1, start_pos[2]) - -- lines[i_0] = lines[i_0]:sub(start.character+1) + local suffix = lines[i_n]:sub(B[2]+1) + if A[2] > 0 then + prefix = lines[i_0]:sub(1, A[2]) end - -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. - new_lines = vim.list_extend({}, new_lines) + new_lines = list_extend({}, new_lines) if #suffix > 0 then new_lines[#new_lines] = new_lines[#new_lines]..suffix end if #prefix > 0 then new_lines[1] = prefix..new_lines[1] end - if #new_lines >= n then - for i = 1, n do - lines[i + i_0 - 1] = new_lines[i] - end - for i = n+1,#new_lines do - table.insert(lines, i_n + 1, new_lines[i]) - end - else - for i = 1, #new_lines do - lines[i + i_0 - 1] = new_lines[i] - end - for _ = #new_lines+1, n do - table.remove(lines, i_0 + #new_lines + 1) + local result = list_extend({}, lines, 1, i_0 - 1) + list_extend(result, new_lines) + list_extend(result, lines, i_n + 1) + return result +end + +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 +local edit_sort_key = sort_by_key(function(e) + return {e.A[1], e.A[2], e.i} +end) function M.apply_text_edits(text_edits, bufnr) if not next(text_edits) then return end - -- nvim.print("Start", #text_edits) local start_line, finish_line = math.huge, -1 local cleaned = {} - for _, e in ipairs(text_edits) do + for i, e in ipairs(text_edits) do 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 = {e.range.start.line; e.range.start.character}; B = {e.range["end"].line; e.range["end"].character}; 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) - for i, e in ipairs(cleaned) do - -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) - local y = 0 - local x = 0 - -- TODO(ashkan) this could be done in O(n) with dynamic programming - for j = 1, i-1 do - local o = cleaned[j] - -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) - if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then - y = y - (o.B[1] - o.A[1] + 1) + #o.lines - -- Same line - if #o.lines > 1 then - x = -e.A[2] + #o.lines[#o.lines] - else - if o.A[1] == e.A[1] then - -- Try to account for insertions. - -- TODO how to account for deletions? - x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] - end - end - end - end - local A = {e.A[1] + y - start_line, e.A[2] + x} - local B = {e.B[1] + y - start_line, e.B[2] + x} - -- if x ~= 0 or y ~= 0 then - -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) - -- end - M.apply_edit_to_lines(lines, A, B, e.lines) + 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 diff --git a/test/functional/plugin/lsp/util_spec.lua b/test/functional/plugin/lsp/util_spec.lua new file mode 100644 index 0000000000..6516fa8a08 --- /dev/null +++ b/test/functional/plugin/lsp/util_spec.lua @@ -0,0 +1,78 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq = helpers.eq +local exec_lua = helpers.exec_lua +local dedent = helpers.dedent +local insert = helpers.insert +local clear = helpers.clear +local command = helpers.command +local NIL = helpers.NIL + +describe('LSP util', function() + local test_text = dedent([[ + First line of text + Second line of text + Third line of text + Fourth line of text]]) + + local function reset() + clear() + insert(test_text) + end + + before_each(reset) + + local function make_edit(y_0, x_0, y_1, x_1, text) + return { + range = { + start = { line = y_0, character = x_0 }; + ["end"] = { line = y_1, character = x_1 }; + }; + newText = type(text) == 'table' and table.concat(text, '\n') or (text or ""); + } + end + + local function buf_lines(bufnr) + return exec_lua("return vim.api.nvim_buf_get_lines((...), 0, -1, false)", bufnr) + end + + describe('apply_edits', function() + it('should apply simple edits', function() + local edits = { + make_edit(0, 0, 0, 0, {"123"}); + make_edit(1, 0, 1, 1, {"2"}); + make_edit(2, 0, 2, 2, {"3"}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1) + eq({ + '123First line of text'; + '2econd line of text'; + '3ird line of text'; + 'Fourth line of text'; + }, buf_lines(1)) + end) + + it('should apply complex edits', function() + local edits = { + make_edit(0, 0, 0, 0, {"", "12"}); + make_edit(0, 0, 0, 0, {"3", "foo"}); + make_edit(0, 1, 0, 1, {"bar", "123"}); + make_edit(0, #"First ", 0, #"First line of text", {"guy"}); + make_edit(1, 0, 1, #'Second', {"baz"}); + make_edit(2, #'Th', 2, #"Third", {"e next"}); + make_edit(3, #'', 3, #"Fourth", {"another line of text", "before this"}); + make_edit(3, #'Fourth', 3, #"Fourth line of text", {"!"}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1) + eq({ + ''; + '123'; + 'fooFbar'; + '123irst guy'; + 'baz line of text'; + 'The next line of text'; + 'another line of text'; + 'before this!'; + }, buf_lines(1)) + end) + end) +end) From b27965538dcc3faa8e1c5b3d64a98108c18981ec Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:20:32 -0800 Subject: [PATCH 21/31] Fix hovers staying on bufhidden --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 66430fffc7..77f2ae6fbc 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -335,7 +335,7 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - api.nvim_command("autocmd CursorMoved ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + api.nvim_command("autocmd CursorMoved,BufHidden ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") return floating_bufnr, floating_winnr end From b4ca07896cbf29ac4ca2373f681930be3ac3b882 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:23:43 -0800 Subject: [PATCH 22/31] lualint --- test/functional/plugin/lsp/util_spec.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/functional/plugin/lsp/util_spec.lua b/test/functional/plugin/lsp/util_spec.lua index 6516fa8a08..1cf0e48be4 100644 --- a/test/functional/plugin/lsp/util_spec.lua +++ b/test/functional/plugin/lsp/util_spec.lua @@ -4,8 +4,6 @@ local exec_lua = helpers.exec_lua local dedent = helpers.dedent local insert = helpers.insert local clear = helpers.clear -local command = helpers.command -local NIL = helpers.NIL describe('LSP util', function() local test_text = dedent([[ From da07e71b189a51008a93783b5f7894dc495c277e Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:34:28 -0800 Subject: [PATCH 23/31] Account for character length in jump position. --- runtime/lua/vim/lsp/buf.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index dbc141a906..0b4ac6ca9b 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -134,12 +134,14 @@ local function handle_location(result) err_message('[LSP] Could not find a valid location') return end - local result_file = vim.uri_to_fname(result.uri) - local bufnr = vfn.bufadd(result_file) + local bufnr = vim.uri_to_bufnr(result.uri) update_tagstack() api.nvim_set_current_buf(bufnr) - local start = result.range.start - api.nvim_win_set_cursor(0, {start.line + 1, start.character}) + local row = result.range.start.line + local col = result.range.start.character + local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + col = #line:sub(1, col) + api.nvim_win_set_cursor(0, {row + 1, col}) return true end local function location_callback(_, method, result) From 43d73ee884f97861c44adab4797a8fc185af1436 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:41:32 -0800 Subject: [PATCH 24/31] Fix position params for encoding. --- runtime/doc/lsp.txt | 6 ++---- runtime/lua/vim/lsp/buf.lua | 18 +++++++++--------- runtime/lua/vim/lsp/protocol.lua | 13 ------------- runtime/lua/vim/lsp/util.lua | 11 +++++++++++ 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 4f5a9dff9e..3de2a9e4e6 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -327,12 +327,10 @@ vim.lsp.protocol vim.lsp.protocol.TextDocumentSyncKind[1] == "Full" Utility functions used internally are: - `vim.lsp.make_client_capabilities()` + `vim.lsp.protocol.make_client_capabilities()` Make a ClientCapabilities object. These are the builtin capabilities. - `vim.lsp.make_text_document_position_params()` - Make a TextDocumentPositionParams object. - `vim.lsp.resolve_capabilities(server_capabilites)` + `vim.lsp.protocol.resolve_capabilities(server_capabilites)` Creates a normalized object describing capabilities from the server capabilities. diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 0b4ac6ca9b..ed26e80c3c 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -65,7 +65,7 @@ local function focusable_preview(method, params, fn) end function M.hover() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() focusable_preview('textDocument/hover', params, function(result) if not (result and result.contents) then return end @@ -79,7 +79,7 @@ function M.hover() end function M.peek_definition() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/peekDefinition', params, function(_, _, result, _) if not (result and result[1]) then return end local loc = result[1] @@ -153,22 +153,22 @@ local function location_callback(_, method, result) end function M.declaration() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/declaration', params, location_callback) end function M.definition() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/definition', params, location_callback) end function M.type_definition() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/typeDefinition', params, location_callback) end function M.implementation() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/implementation', params, location_callback) end @@ -232,7 +232,7 @@ local function signature_help_to_preview_contents(input) end function M.signature_help() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() focusable_preview('textDocument/signatureHelp', params, function(result) if not (result and result.signatures and result.signatures[1]) then return end @@ -248,7 +248,7 @@ end -- TODO(ashkan) ? function M.completion(context) - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() params.context = context return request('textDocument/completion', params, function(_, _, result) if vim.tbl_isempty(result or {}) then return end @@ -306,7 +306,7 @@ end function M.rename(new_name) -- TODO(ashkan) use prepareRename -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() new_name = new_name or npcall(vfn.input, "New Name: ") if not (new_name and #new_name > 0) then return end params.newName = new_name diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1f51e7bef7..ead90cc75a 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -680,19 +680,6 @@ function protocol.make_client_capabilities() } end -function protocol.make_text_document_position_params() - local position = vim.api.nvim_win_get_cursor(0) - return { - textDocument = { - uri = vim.uri_from_bufnr() - }; - position = { - line = position[1] - 1; - character = position[2]; - } - } -end - --[=[ export interface DocumentFilter { --A language id, like `typescript`. diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 77f2ae6fbc..76681920bd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -638,6 +638,17 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end +function M.make_position_params() + 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] + col = vim.str_utfindex(line, col) + return { + textDocument = { uri = vim.uri_from_bufnr(0) }; + position = { line = row; character = col; } + } +end + return M -- vim:sw=2 ts=2 et From c055ca00ce61499e9161b8d2fa48568e833ee5b0 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:55:45 -0800 Subject: [PATCH 25/31] Remove comments. --- runtime/lua/vim/lsp.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 790f8f8a9d..17d6001496 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -98,11 +98,7 @@ local function for_each_buffer_client(bufnr, callback) return end for client_id in pairs(client_ids) do - -- This is unlikely to happen. Could only potentially happen in a race - -- condition between literally a single statement. - -- We could skip this error, but let's error for now. local client = active_clients[client_id] - -- or error(string.format("Client %d has already shut down.", client_id)) if client then callback(client, client_id) end From a3d67dac5f9de6099890ae4b9fab2b70b2279a29 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 16:23:12 -0800 Subject: [PATCH 26/31] Fix encoding translation in other places. --- runtime/lua/vim/lsp.lua | 2 +- runtime/lua/vim/lsp/buf.lua | 22 ++++++++++++++++------ runtime/lua/vim/lsp/util.lua | 7 +++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 17d6001496..880d811647 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -862,7 +862,7 @@ function lsp.omnifunc(findstart, base) position = { -- 0-indexed for both line and character line = pos[1] - 1, - character = pos[2], + character = vim.str_utfindex(line, pos[2]), }; -- The completion context. This is only available if the client specifies -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ed26e80c3c..b4e0b9cbfc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -2,8 +2,8 @@ local validate = vim.validate local api = vim.api local vfn = vim.fn local util = require 'vim.lsp.util' -local protocol = require 'vim.lsp.protocol' local log = require 'vim.lsp.log' +local list_extend = vim.list_extend local M = {} @@ -192,7 +192,7 @@ local function signature_help_to_preview_contents(input) if not signature then return end - vim.list_extend(contents, vim.split(signature.label, '\n', true)) + list_extend(contents, vim.split(signature.label, '\n', true)) if signature.documentation then util.convert_input_to_markdown_lines(signature.documentation, contents) end @@ -287,13 +287,23 @@ function M.range_formatting(options, start_pos, end_pos) tabSize = api.nvim_buf_get_option(0, 'tabstop'); insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); }) - start_pos = start_pos or vim.api.nvim_buf_get_mark(0, '<') - end_pos = end_pos or vim.api.nvim_buf_get_mark(0, '>') + 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], util.character_offset(0, unpack(A))} + end + if B[2] > 0 then + B = {B[1], util.character_offset(0, unpack(B))} + end local params = { textDocument = { uri = vim.uri_from_bufnr(0) }; range = { - start = { line = start_pos[1]; character = start_pos[2]; }; - ["end"] = { line = end_pos[1]; character = end_pos[2]; }; + start = { line = A[1]; character = A[2]; }; + ["end"] = { line = B[1]; character = B[2]; }; }; options = options; } diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 76681920bd..570c4df1dd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -649,6 +649,13 @@ function M.make_position_params() } end +-- @param buf buffer handle or 0 for current. +-- @param row 0-indexed line +-- @param col 0-indexed byte offset in line +function M.character_offset(buf, row, col) + local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] + return vim.str_utfindex(line, col) +end return M -- vim:sw=2 ts=2 et From 78991ffbf4357ba1ad477a13991078bb4a0bdc58 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 23:58:32 -0800 Subject: [PATCH 27/31] Improve performance of util.set_lines + bugfix Also permit character_offset for col past the end of line (useful in range formatting). --- runtime/lua/vim/lsp/util.lua | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 570c4df1dd..94b4223a20 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -11,6 +11,14 @@ end local list_extend = vim.list_extend +local function ok_or_nil(status, ...) + if not status then return end + return ... +end +local function npcall(fn, ...) + return ok_or_nil(pcall(fn, ...)) +end + --- Find the longest shared prefix between prefix and word. -- e.g. remove_prefix("123tes", "testing") == "ting" local function remove_prefix(prefix, word) @@ -39,17 +47,22 @@ function M.set_lines(lines, A, B, new_lines) if A[2] > 0 then prefix = lines[i_0]:sub(1, A[2]) end - new_lines = list_extend({}, new_lines) + 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 - new_lines[#new_lines] = new_lines[#new_lines]..suffix + local i = i_0 + #new_lines - 1 + lines[i] = lines[i]..suffix end if #prefix > 0 then - new_lines[1] = prefix..new_lines[1] + lines[i_0] = prefix..lines[i_0] end - local result = list_extend({}, lines, 1, i_0 - 1) - list_extend(result, new_lines) - list_extend(result, lines, i_n + 1) - return result + return lines end local function sort_by_key(fn) @@ -99,7 +112,7 @@ function M.apply_text_edits(text_edits, bufnr) 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) + M.set_lines(lines, A, B, e.lines) end if set_eol and #lines[#lines] == 0 then table.remove(lines) @@ -638,11 +651,12 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end +local str_utfindex = vim.str_utfindex function M.make_position_params() 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] - col = vim.str_utfindex(line, col) + col = str_utfindex(line, col) return { textDocument = { uri = vim.uri_from_bufnr(0) }; position = { line = row; character = col; } @@ -654,7 +668,9 @@ end -- @param col 0-indexed byte offset in line function M.character_offset(buf, row, col) local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] - return vim.str_utfindex(line, col) + -- TODO(ashkan) is there a better way to handle col being past line length? + -- If the col is past the EOL, use the line length. + return npcall(str_utfindex, line, col) or str_utfindex(line) end return M From 73487f4130581da72c9e838189aab39c79c177c5 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Fri, 22 Nov 2019 00:31:10 -0800 Subject: [PATCH 28/31] Improve the character_offset code. --- runtime/lua/vim/lsp/util.lua | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 94b4223a20..e2a8748a20 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -11,14 +11,6 @@ end local list_extend = vim.list_extend -local function ok_or_nil(status, ...) - if not status then return end - return ... -end -local function npcall(fn, ...) - return ok_or_nil(pcall(fn, ...)) -end - --- Find the longest shared prefix between prefix and word. -- e.g. remove_prefix("123tes", "testing") == "ting" local function remove_prefix(prefix, word) @@ -668,9 +660,11 @@ end -- @param col 0-indexed byte offset in line function M.character_offset(buf, row, col) local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] - -- TODO(ashkan) is there a better way to handle col being past line length? -- If the col is past the EOL, use the line length. - return npcall(str_utfindex, line, col) or str_utfindex(line) + if col > #line then + return str_utfindex(line) + end + return str_utfindex(line, col) end return M From d410812311f7b462420690455914ea1316953b3a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sat, 23 Nov 2019 16:14:24 -0800 Subject: [PATCH 29/31] UI tweaks. - Hide diagnostics on client exit - Stop insert on popup focus. - Hide popup on insertchar (for signature_help) --- runtime/lua/vim/lsp.lua | 12 +++++++++++- runtime/lua/vim/lsp/buf.lua | 8 ++++++-- runtime/lua/vim/lsp/util.lua | 5 +++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 880d811647..7076db68d3 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -343,9 +343,19 @@ function lsp.start_client(config) function handlers.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil - for _, client_ids in pairs(all_buffer_active_clients) do + local active_buffers = {} + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + table.insert(active_buffers, bufnr) + end client_ids[client_id] = nil end + -- Buffer level cleanup + vim.schedule(function() + for _, bufnr in ipairs(active_buffers) do + util.buf_clear_diagnostics(bufnr) + end + end) if config.on_exit then pcall(config.on_exit, code, signal, client_id) end diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index b4e0b9cbfc..79d43fda4a 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -51,7 +51,9 @@ local function focusable_preview(method, params, fn) do local win = find_window_by_var(method, bufnr) if win then - return api.nvim_set_current_win(win) + api.nvim_set_current_win(win) + api.nvim_command("stopinsert") + return end end return request(method, params, function(_, _, result, _) @@ -234,7 +236,9 @@ end function M.signature_help() local params = util.make_position_params() focusable_preview('textDocument/signatureHelp', params, function(result) - if not (result and result.signatures and result.signatures[1]) then return end + if not (result and result.signatures and result.signatures[1]) then + return { 'No signature available' } + end -- TODO show empty popup when signatures is empty? local lines = signature_help_to_preview_contents(result) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e2a8748a20..0c53494f02 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -104,7 +104,7 @@ function M.apply_text_edits(text_edits, bufnr) local e = cleaned[i] local A = {e.A[1] - start_line, e.A[2]} local B = {e.B[1] - start_line, e.B[2]} - M.set_lines(lines, A, B, e.lines) + lines = M.set_lines(lines, A, B, e.lines) end if set_eol and #lines[#lines] == 0 then table.remove(lines) @@ -340,7 +340,8 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - api.nvim_command("autocmd CursorMoved,BufHidden ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + -- TODO make InsertCharPre disappearing optional? + api.nvim_command("autocmd CursorMoved,BufHidden,InsertCharPre ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") return floating_bufnr, floating_winnr end From b35f6aa9dd686d082405132e3455cd0aff334361 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 24 Nov 2019 03:01:18 -0800 Subject: [PATCH 30/31] Add support for textDocument/references. Add set_qflist and set_loclist. - Also add locations_to_items, which calculates byte offsets for character positions in files and avoids unnecessary operations. --- runtime/lua/vim/lsp/buf.lua | 15 ++++ runtime/lua/vim/lsp/default_callbacks.lua | 2 +- runtime/lua/vim/lsp/util.lua | 84 +++++++++++++++++------ 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 79d43fda4a..51295e5570 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,3 +1,4 @@ +local vim = vim local validate = vim.validate local api = vim.api local vfn = vim.fn @@ -330,5 +331,19 @@ function M.rename(new_name) end) end +function M.references(context) + validate { context = { context, 't', true } } + local params = util.make_position_params() + params.context = context or { + includeDeclaration = true; + } + params[vim.type_idx] = vim.types.dictionary + request('textDocument/references', params, function(_, _, result) + if not result then return end + util.set_qflist(result) + vim.api.nvim_command("copen") + end) +end + return M -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index d58280e0f2..2a891e7d1d 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -31,7 +31,7 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) util.buf_diagnostics_save_positions(bufnr, result.diagnostics) util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) - -- util.buf_loclist(bufnr, result.diagnostics) + -- util.set_loclist(result.diagnostics) end local function log_message(_, _, result, client_id) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0c53494f02..2dfcdfc70c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1,4 +1,5 @@ local protocol = require 'vim.lsp.protocol' +local vim = vim local validate = vim.validate local api = vim.api @@ -573,30 +574,71 @@ do end end -function M.buf_loclist(bufnr, locations) - local targetwin - for _, winnr in ipairs(api.nvim_list_wins()) do - local winbuf = api.nvim_win_get_buf(winnr) - if winbuf == bufnr then - targetwin = winnr - break +local position_sort = sort_by_key(function(v) + return {v.line, v.character} +end) + +-- Returns the items with the byte position calculated correctly and in sorted +-- order. +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 + local start = d.range.start + local fname = assert(vim.uri_to_fname(d.uri)) + table.insert(grouped[fname], start) + end + local keys = vim.tbl_keys(grouped) + table.sort(keys) + -- TODO(ashkan) I wish we could do this lazily. + for _, fname in ipairs(keys) do + local rows = grouped[fname] + table.sort(rows, position_sort) + local i = 0 + for line in io.lines(fname) do + for _, pos in ipairs(rows) do + local row = pos.line + if i == row then + local col + if pos.character > #line then + col = #line + else + col = vim.str_byteindex(line, pos.character) + end + table.insert(items, { + filename = fname, + lnum = row + 1, + col = col + 1; + }) + end + end + i = i + 1 end end - if not targetwin then return end + return items +end - local items = {} - local path = api.nvim_buf_get_name(bufnr) - for _, d in ipairs(locations) do - -- TODO: URL parsing here? - local start = d.range.start - table.insert(items, { - filename = path, - lnum = start.line + 1, - col = start.character + 1, - text = d.message, - }) - end - vim.fn.setloclist(targetwin, items, ' ', 'Language Server') +-- locations is Location[] +-- Only sets for the current window. +function M.set_loclist(locations) + vim.fn.setloclist(0, {}, ' ', { + title = 'Language Server'; + items = M.locations_to_items(locations); + }) +end + +-- locations is Location[] +function M.set_qflist(locations) + vim.fn.setqflist({}, ' ', { + title = 'Language Server'; + items = M.locations_to_items(locations); + }) end -- Remove empty lines from the beginning and end. From a9036502dca7191c8ada70367e44c398d48dd94a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 24 Nov 2019 03:14:03 -0800 Subject: [PATCH 31/31] Bring vim into local scope --- runtime/lua/vim/lsp.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7076db68d3..928d19177a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -4,6 +4,7 @@ local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' local util = require 'vim.lsp.util' +local vim = vim local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option local uv = vim.loop