From 48a59f8f4f3eced84b21a473527b00ef1b9b5bd2 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Fri, 30 Apr 2021 13:40:20 +0200 Subject: Add formatting_seq_sync, change formatting and formatting_sync --- runtime/lua/vim/lsp.lua | 36 +++++++++++++++++ runtime/lua/vim/lsp/buf.lua | 94 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 123 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 26700288af..d54420844e 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -890,6 +890,42 @@ function lsp.start_client(config) end) end + --@private + --- Sends a request to the server and synchronously waits for the response. + --- + --- This is a wrapper around {client.request} + --- + --@param method (string) LSP method name. + --@param params (table) LSP request params. + --@param timeout_ms (number, optional, default=100) Maximum time in + ---milliseconds to wait for a result. + --@param bufnr (number) Buffer handle (0 for current). + --@returns { err, result }, where `err` and `result` come from the |lsp-handler|. + ---On timeout, cancel or error, returns `(nil, err)` where `err` is a + ---string describing the failure reason. If the request was unsuccessful + ---returns `nil`. + --@see |vim.lsp.buf_request_sync()| + function client.request_sync(method, params, timeout_ms, bufnr) + local request_result = nil + local function _sync_handler(err, _, result) + request_result = { error = err, result = result } + end + + local success, request_id = client.request(method, params, _sync_handler, + bufnr) + if not success then return nil end + + local wait_result, reason = vim.wait(timeout_ms or 100, function() + return request_result ~= nil + end, 10) + + if not wait_result then + client.cancel_request(request_id) + return nil, wait_result_reason[reason] + end + return request_result + end + --@private --- Sends a notification to an LSP server. --- diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 31116985e2..d7f3353ad9 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -111,6 +111,39 @@ function M.completion(context) return request('textDocument/completion', params) end +--@private +--- If there is more than one client with formatting capability, asks the user +--- which one to use. +-- +--@returns The client to use for formatting +local function get_formatting_client() + local clients = vim.tbl_values(vim.lsp.buf_get_clients()); + clients = vim.tbl_filter(function (client) + return client.resolved_capabilities.document_formatting + end, clients) + -- better UX when choices are always in the same order (between restarts) + table.sort(clients, function (a, b) return a.name < b.name end) + + if #clients > 1 then + local choices = {} + for k,v in ipairs(clients) do + table.insert(choices, string.format("%d %s", k, v.name)) + end + local user_choice = vim.fn.confirm( + "Select a language server for formatting:", + table.concat(choices, "\n"), + 0, + "Question" + ) + if user_choice == 0 then return nil end + return clients[user_choice] + elseif #clients < 1 then + return nil + else + return clients[1] + end +end + --- Formats the current buffer. --- --@param options (optional, table) Can be used to specify FormattingOptions. @@ -119,8 +152,11 @@ end -- --@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) + local client = get_formatting_client() + if client == nil then return end + local params = util.make_formatting_params(options) - return request('textDocument/formatting', params) + return client.request("textDocument/formatting", params) end --- Performs |vim.lsp.buf.formatting()| synchronously. @@ -134,14 +170,58 @@ end --- --@param options Table with valid `FormattingOptions` entries --@param timeout_ms (number) Request timeout +--@see |vim.lsp.buf.formatting_seq_sync| function M.formatting_sync(options, timeout_ms) + local client = get_formatting_client() + if client == nil then return end + local params = util.make_formatting_params(options) - local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms) - if not result or vim.tbl_isempty(result) then return end - local _, formatting_result = next(result) - result = formatting_result.result - if not result then return end - vim.lsp.util.apply_text_edits(result) + local result = client.request_sync("textDocument/formatting", params, timeout_ms) + if result and result.result then + util.apply_text_edits(result.result) + end +end + +--- Formats the current buffer by sequentially requesting formatting from attached clients. +--- +--- Useful when multiple clients with formatting capability are attached. +--- +--- Since it's synchronous, can be used for running on save, to make sure buffer is formatted +--- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method. +--- Example: +---
+--- vim.api.nvim_command[[autocmd BufWritePre  lua vim.lsp.buf.formatting_seq_sync()]]
+--- 
+--- +--@param options (optional, table) `FormattingOptions` entries +--@param timeout_ms (optional, number) Request timeout +--@param order (optional, table) List of client names. Formatting is requested from clients +---in the following order: first all clients that are not in the `order` list, then +---the remaining clients in the order as they occur in the `order` list. +function M.formatting_seq_sync(options, timeout_ms, order) + local clients = vim.tbl_values(vim.lsp.buf_get_clients()); + + -- sort the clients according to `order` + for _, client_name in ipairs(order or {}) do + -- if the client exists, move to the end of the list + for i, client in ipairs(clients) do + if client.name == client_name then + table.insert(clients, table.remove(clients, i)) + break + end + end + end + + -- loop through the clients and make synchronous formatting requests + for _, client in ipairs(clients) do + if client.resolved_capabilities.document_formatting then + local params = util.make_formatting_params(options) + local result = client.request_sync("textDocument/formatting", params, timeout_ms) + if result and result.result then + util.apply_text_edits(result.result) + end + end + end end --- Formats a given range. -- cgit From 54368736d0aa30284b73bf0a78fa295d8e835c8f Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sun, 2 May 2021 15:23:13 +0200 Subject: doc clarification --- runtime/lua/vim/lsp.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index d54420844e..96b404aa51 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -900,7 +900,7 @@ function lsp.start_client(config) --@param timeout_ms (number, optional, default=100) Maximum time in ---milliseconds to wait for a result. --@param bufnr (number) Buffer handle (0 for current). - --@returns { err, result }, where `err` and `result` come from the |lsp-handler|. + --@returns { err=err, result=result }, a dictionary, where `err` and `result` come from the |lsp-handler|. ---On timeout, cancel or error, returns `(nil, err)` where `err` is a ---string describing the failure reason. If the request was unsuccessful ---returns `nil`. @@ -908,7 +908,7 @@ function lsp.start_client(config) function client.request_sync(method, params, timeout_ms, bufnr) local request_result = nil local function _sync_handler(err, _, result) - request_result = { error = err, result = result } + request_result = { err = err, result = result } end local success, request_id = client.request(method, params, _sync_handler, -- cgit From dc9c6ea2194526d829599fe17768a7a09bbc1f56 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sun, 2 May 2021 15:37:31 +0200 Subject: Support multiple range formatting clients --- runtime/lua/vim/lsp/buf.lua | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index d7f3353ad9..8d4aa50370 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -112,14 +112,14 @@ function M.completion(context) end --@private ---- If there is more than one client with formatting capability, asks the user ---- which one to use. +--- If there is more than one client that supports the given method, +--- asks the user to select one. -- ---@returns The client to use for formatting -local function get_formatting_client() +--@returns The client that the user selected or nil +local function select_client(method) local clients = vim.tbl_values(vim.lsp.buf_get_clients()); clients = vim.tbl_filter(function (client) - return client.resolved_capabilities.document_formatting + return client.supports_method(method) end, clients) -- better UX when choices are always in the same order (between restarts) table.sort(clients, function (a, b) return a.name < b.name end) @@ -130,7 +130,7 @@ local function get_formatting_client() table.insert(choices, string.format("%d %s", k, v.name)) end local user_choice = vim.fn.confirm( - "Select a language server for formatting:", + "Select a language server:", table.concat(choices, "\n"), 0, "Question" @@ -152,7 +152,7 @@ end -- --@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) - local client = get_formatting_client() + local client = select_client("textDocument/formatting") if client == nil then return end local params = util.make_formatting_params(options) @@ -172,7 +172,7 @@ end --@param timeout_ms (number) Request timeout --@see |vim.lsp.buf.formatting_seq_sync| function M.formatting_sync(options, timeout_ms) - local client = get_formatting_client() + local client = select_client("textDocument/formatting") if client == nil then return end local params = util.make_formatting_params(options) @@ -232,15 +232,12 @@ end --@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. function M.range_formatting(options, start_pos, end_pos) - validate { options = {options, 't', true} } - local sts = vim.bo.softtabstop; - options = vim.tbl_extend('keep', options or {}, { - tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; - insertSpaces = vim.bo.expandtab; - }) + local client = select_client("textDocument/rangeFormatting") + if client == nil then return end + local params = util.make_given_range_params(start_pos, end_pos) - params.options = options - return request('textDocument/rangeFormatting', params) + params.options = util.make_formatting_params(options).options + return client.request("textDocument/rangeFormatting", params) end --- Renames all references to the symbol under the cursor. -- cgit From f0f3fddcddf9c01f89a8afaa32bced52632373ee Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sun, 2 May 2021 16:16:49 +0200 Subject: Synchronous formatting methods notify the user on timeout and interrupted --- runtime/lua/vim/lsp/buf.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 8d4aa50370..341a3e82fc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -176,9 +176,11 @@ function M.formatting_sync(options, timeout_ms) if client == nil then return end local params = util.make_formatting_params(options) - local result = client.request_sync("textDocument/formatting", params, timeout_ms) + local result, err = client.request_sync("textDocument/formatting", params, timeout_ms) if result and result.result then util.apply_text_edits(result.result) + elseif err then + vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN) end end @@ -216,9 +218,11 @@ function M.formatting_seq_sync(options, timeout_ms, order) for _, client in ipairs(clients) do if client.resolved_capabilities.document_formatting then local params = util.make_formatting_params(options) - local result = client.request_sync("textDocument/formatting", params, timeout_ms) + local result, err = client.request_sync("textDocument/formatting", params, timeout_ms) if result and result.result then util.apply_text_edits(result.result) + elseif err then + vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) end end end -- cgit From d923f38882d8c6f6966552073147cf35a85fe369 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sun, 2 May 2021 16:24:58 +0200 Subject: Add client.request_sync doc --- runtime/lua/vim/lsp.lua | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 96b404aa51..9091d310dc 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -483,6 +483,13 @@ end --- result. You can use this with `client.cancel_request(request_id)` --- to cancel the request. --- +--- - request_sync(method, params, timeout_ms, bufnr) +--- Sends a request to the server and synchronously waits for the response. +--- This is a wrapper around {client.request} +--- Returns: { err=err, result=result }, a dictionary, where `err` and `result` come from +--- the |lsp-handler|. On timeout, cancel or error, returns `(nil, err)` where `err` is a +--- string describing the failure reason. If the request was unsuccessful returns `nil`. +--- --- - notify(method, params) --- Sends a notification to an LSP server. --- Returns: a boolean to indicate if the notification was successful. If -- cgit From bcf03affbd309c233bd7a2f0d8f30b6e3fb0aa87 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sun, 2 May 2021 17:08:57 +0200 Subject: Increase default LSP sync timeout to 1000ms --- runtime/lua/vim/lsp.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9091d310dc..93ec9ed624 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -904,7 +904,7 @@ function lsp.start_client(config) --- --@param method (string) LSP method name. --@param params (table) LSP request params. - --@param timeout_ms (number, optional, default=100) Maximum time in + --@param timeout_ms (number, optional, default=1000) Maximum time in ---milliseconds to wait for a result. --@param bufnr (number) Buffer handle (0 for current). --@returns { err=err, result=result }, a dictionary, where `err` and `result` come from the |lsp-handler|. @@ -922,7 +922,7 @@ function lsp.start_client(config) bufnr) if not success then return nil end - local wait_result, reason = vim.wait(timeout_ms or 100, function() + local wait_result, reason = vim.wait(timeout_ms or 1000, function() return request_result ~= nil end, 10) @@ -1332,12 +1332,12 @@ end --- --- Calls |vim.lsp.buf_request_all()| but blocks Nvim while awaiting the result. --- Parameters are the same as |vim.lsp.buf_request()| but the return result is ---- different. Wait maximum of {timeout_ms} (default 100) ms. +--- different. Wait maximum of {timeout_ms} (default 1000) ms. --- --@param bufnr (number) Buffer handle, or 0 for current. --@param method (string) LSP method name --@param params (optional, table) Parameters to send to the server ---@param timeout_ms (optional, number, default=100) Maximum time in +--@param timeout_ms (optional, number, default=1000) Maximum time in --- milliseconds to wait for a result. --- --@returns Map of client_id:request_result. On timeout, cancel or error, @@ -1350,7 +1350,7 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) request_results = it end) - local wait_result, reason = vim.wait(timeout_ms or 100, function() + local wait_result, reason = vim.wait(timeout_ms or 1000, function() return request_results ~= nil end, 10) -- cgit