aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/api.txt11
-rw-r--r--runtime/lua/vim/lsp.lua95
-rw-r--r--runtime/lua/vim/lsp/protocol.lua14
-rw-r--r--src/nvim/api/buffer.c49
-rw-r--r--src/nvim/api/private/helpers.c14
-rw-r--r--src/nvim/screen.c2
-rw-r--r--test/functional/api/buffer_spec.lua20
-rw-r--r--test/functional/fixtures/fake-lsp-server.lua20
-rw-r--r--test/functional/plugin/lsp_spec.lua64
9 files changed, 252 insertions, 37 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index 083128ab80..0c726ddd86 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -1783,6 +1783,17 @@ nvim_buf_del_var({buffer}, {name}) *nvim_buf_del_var()*
{buffer} Buffer handle, or 0 for current buffer
{name} Variable name
+nvim_buf_delete({buffer}, {opts}) *nvim_buf_delete()*
+ Deletes the buffer. See |:bwipeout|
+
+ Parameters: ~
+ {buffer} Buffer handle, or 0 for current buffer
+ {opts} Optional parameters. Keys:
+ • force: Force deletion and ignore unsaved
+ changes.
+ • unload: Unloaded only, do not delete. See
+ |:bunload|
+
nvim_buf_detach({buffer}) *nvim_buf_detach()*
Deactivates buffer-update events on the channel.
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 95d9c585ee..fad213212a 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -25,6 +25,27 @@ local lsp = {
-- format_rpc_error = lsp_rpc.format_rpc_error;
}
+-- maps request name to the required resolved_capability in the client.
+lsp._request_name_to_capability = {
+ ['textDocument/hover'] = 'hover';
+ ['textDocument/signatureHelp'] = 'signature_help';
+ ['textDocument/definition'] = 'goto_definition';
+ ['textDocument/implementation'] = 'implementation';
+ ['textDocument/declaration'] = 'declaration';
+ ['textDocument/typeDefinition'] = 'type_definition';
+ ['textDocument/documentSymbol'] = 'document_symbol';
+ ['textDocument/workspaceSymbol'] = 'workspace_symbol';
+ ['textDocument/prepareCallHierarchy'] = 'call_hierarchy';
+ ['textDocument/rename'] = 'rename';
+ ['textDocument/codeAction'] = 'code_action';
+ ['workspace/executeCommand'] = 'execute_command';
+ ['textDocument/references'] = 'find_references';
+ ['textDocument/rangeFormatting'] = 'document_range_formatting';
+ ['textDocument/formatting'] = 'document_formatting';
+ ['textDocument/completion'] = 'completion';
+ ['textDocument/documentHighlight'] = 'document_highlight';
+}
+
-- TODO improve handling of scratch buffers with LSP attached.
--@private
@@ -51,6 +72,16 @@ local function resolve_bufnr(bufnr)
end
--@private
+--- callback called by the client when trying to call a method that's not
+--- supported in any of the servers registered for the current buffer.
+--@param method (string) name of the method
+function lsp._unsupported_method(method)
+ local msg = string.format("method %s is not supported by any of the servers registered for the current buffer", method)
+ log.warn(msg)
+ return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg)
+end
+
+--@private
--- Checks whether a given path is a directory.
---
--@param filename (string) path to check
@@ -575,6 +606,15 @@ function lsp.start_client(config)
-- These are the cleaned up capabilities we use for dynamically deciding
-- when to send certain events to clients.
client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities)
+ client.supports_method = function(method)
+ local required_capability = lsp._request_name_to_capability[method]
+ -- if we don't know about the method, assume that the client supports it.
+ if not required_capability then
+ return true
+ end
+
+ return client.resolved_capabilities[required_capability]
+ end
if config.on_init then
local status, err = pcall(config.on_init, client, result)
if not status then
@@ -598,19 +638,6 @@ function lsp.start_client(config)
end
--@private
- --- Throws error for a method that is not supported by the current LSP
- --- server.
- ---
- --@param method (string) an LSP method name not supported by the LSP server.
- --@returns (error) a 'MethodNotFound' JSON-RPC error response.
- local function unsupported_method(method)
- local msg = "server doesn't support "..method
- local _ = log.warn() and log.warn(msg)
- err_message(msg)
- return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg)
- end
-
- --@private
--- Sends a request to the server.
---
--- This is a thin wrapper around {client.rpc.request} with some additional
@@ -637,20 +664,6 @@ function lsp.start_client(config)
or error(string.format("not found: %q request callback for client %q.", method, client.name))
end
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr)
- -- TODO keep these checks or just let it go anyway?
- if (not client.resolved_capabilities.hover and method == 'textDocument/hover')
- or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp')
- or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition')
- or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation')
- or (not client.resolved_capabilities.declaration and method == 'textDocument/declaration')
- or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition')
- or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol')
- or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol')
- or (not client.resolved_capabilities.call_hierarchy and method == 'textDocument/prepareCallHierarchy')
- then
- callback(unsupported_method(method), method, nil, client_id, bufnr)
- return
- end
return rpc.request(method, params, function(err, result)
callback(err, method, result, client_id, bufnr)
end)
@@ -997,16 +1010,32 @@ function lsp.buf_request(bufnr, method, params, callback)
callback = { callback, 'f', true };
}
local client_request_ids = {}
- for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr)
- local request_success, request_id = client.request(method, params, callback, resolved_bufnr)
- -- This could only fail if the client shut down in the time since we looked
- -- it up and we did the request, which should be rare.
- if request_success then
- client_request_ids[client_id] = request_id
+ local method_supported = false
+ for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr)
+ if client.supports_method(method) then
+ method_supported = true
+ local request_success, request_id = client.request(method, params, callback, resolved_bufnr)
+
+ -- This could only fail if the client shut down in the time since we looked
+ -- it up and we did the request, which should be rare.
+ if request_success then
+ client_request_ids[client_id] = request_id
+ end
end
end)
+ -- if no clients support the given method, call the callback with the proper
+ -- error message.
+ if not method_supported then
+ local unsupported_err = lsp._unsupported_method(method)
+ local cb = callback or lsp.callbacks['method']
+ if cb then
+ cb(unsupported_err, method, bufnr)
+ end
+ return
+ end
+
local function _cancel_all_requests()
for client_id, request_id in pairs(client_request_ids) do
local client = active_clients[client_id]
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 4e926381e0..2773f59b45 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -703,6 +703,10 @@ function protocol.make_client_capabilities()
};
hierarchicalDocumentSymbolSupport = true;
};
+ rename = {
+ dynamicRegistration = false;
+ prepareSupport = true;
+ };
};
workspace = {
symbol = {
@@ -914,6 +918,7 @@ function protocol.resolve_capabilities(server_capabilities)
return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync))
end
end
+ general_properties.completion = server_capabilities.completionProvider ~= nil
general_properties.hover = server_capabilities.hoverProvider or false
general_properties.goto_definition = server_capabilities.definitionProvider or false
general_properties.find_references = server_capabilities.referencesProvider or false
@@ -923,6 +928,15 @@ function protocol.resolve_capabilities(server_capabilities)
general_properties.document_formatting = server_capabilities.documentFormattingProvider or false
general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false
general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false
+ general_properties.execute_command = server_capabilities.executeCommandProvider ~= nil
+
+ if server_capabilities.renameProvider == nil then
+ general_properties.rename = false
+ elseif type(server_capabilities.renameProvider) == 'boolean' then
+ general_properties.rename = server_capabilities.renameProvider
+ else
+ general_properties.rename = true
+ end
if server_capabilities.codeActionProvider == nil then
general_properties.code_action = false
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c
index c8dd85b39d..cad4c8314f 100644
--- a/src/nvim/api/buffer.c
+++ b/src/nvim/api/buffer.c
@@ -953,6 +953,53 @@ Boolean nvim_buf_is_loaded(Buffer buffer)
return buf && buf->b_ml.ml_mfp != NULL;
}
+/// Deletes the buffer. See |:bwipeout|
+///
+/// @param buffer Buffer handle, or 0 for current buffer
+/// @param opts Optional parameters. Keys:
+/// - force: Force deletion and ignore unsaved changes.
+/// - unload: Unloaded only, do not delete. See |:bunload|
+void nvim_buf_delete(Buffer buffer, Dictionary opts, Error *err)
+ FUNC_API_SINCE(7)
+{
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+
+ if (ERROR_SET(err)) {
+ return;
+ }
+
+ bool force = false;
+ bool unload = false;
+ for (size_t i = 0; i < opts.size; i++) {
+ String k = opts.items[i].key;
+ Object v = opts.items[i].value;
+ if (strequal("force", k.data)) {
+ force = api_coerce_to_bool(v, "force", false, err);
+ } else if (strequal("unload", k.data)) {
+ unload = api_coerce_to_bool(v, "unload", false, err);
+ } else {
+ api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
+ return;
+ }
+ }
+
+ if (ERROR_SET(err)) {
+ return;
+ }
+
+ int result = do_buffer(
+ unload ? DOBUF_UNLOAD : DOBUF_WIPE,
+ DOBUF_FIRST,
+ FORWARD,
+ buf->handle,
+ force);
+
+ if (result == FAIL) {
+ api_set_error(err, kErrorTypeException, "Failed to unload buffer.");
+ return;
+ }
+}
+
/// Checks if a buffer is valid.
///
/// @note Even if a buffer is valid it may have been unloaded. See |api-buffer|
@@ -1394,7 +1441,7 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
goto error;
}
} else if (strequal("ephemeral", k.data)) {
- ephemeral = api_is_truthy(*v, "ephemeral", false, err);
+ ephemeral = api_coerce_to_bool(*v, "ephemeral", false, err);
if (ERROR_SET(err)) {
goto error;
}
diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c
index 84517c99fc..981d41ae6e 100644
--- a/src/nvim/api/private/helpers.c
+++ b/src/nvim/api/private/helpers.c
@@ -1619,14 +1619,24 @@ free_exit:
return virt_text;
}
-bool api_is_truthy(Object obj, const char *what, bool nil_truthy, Error *err)
+/// Force obj to bool.
+/// If it fails, returns false and sets err
+/// @param obj The object to coerce to a boolean
+/// @param what The name of the object, used for error message
+/// @param nil_value What to return if the type is nil.
+/// @param err Set if there was an error in converting to a bool
+bool api_coerce_to_bool(
+ Object obj,
+ const char *what,
+ bool nil_value,
+ Error *err)
{
if (obj.type == kObjectTypeBoolean) {
return obj.data.boolean;
} else if (obj.type == kObjectTypeInteger) {
return obj.data.integer; // C semantics: non-zero int is true
} else if (obj.type == kObjectTypeNil) {
- return nil_truthy; // caller decides what NIL (missing retval in lua) means
+ return nil_value; // caller decides what NIL (missing retval in lua) means
} else {
api_set_error(err, kErrorTypeValidation, "%s is not an boolean", what);
return false;
diff --git a/src/nvim/screen.c b/src/nvim/screen.c
index 9f450888a2..df0c5ce791 100644
--- a/src/nvim/screen.c
+++ b/src/nvim/screen.c
@@ -176,7 +176,7 @@ static bool provider_invoke(NS ns_id, const char *name, LuaRef ref,
textlock--;
if (!ERROR_SET(&err)
- && api_is_truthy(ret, "provider %s retval", default_true, &err)) {
+ && api_coerce_to_bool(ret, "provider %s retval", default_true, &err)) {
return true;
}
diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua
index da7515f012..8ed642b43e 100644
--- a/test/functional/api/buffer_spec.lua
+++ b/test/functional/api/buffer_spec.lua
@@ -534,6 +534,26 @@ describe('api/buf', function()
end)
end)
+ describe('nvim_buf_delete', function()
+ it('allows for just deleting', function()
+ nvim('command', 'new')
+ local b = nvim('get_current_buf')
+ ok(buffer('is_valid', b))
+ nvim('buf_delete', b, {})
+ ok(not buffer('is_loaded', b))
+ ok(not buffer('is_valid', b))
+ end)
+
+ it('allows for just unloading', function()
+ nvim('command', 'new')
+ local b = nvim('get_current_buf')
+ ok(buffer('is_valid', b))
+ nvim('buf_delete', b, { unload = true })
+ ok(not buffer('is_loaded', b))
+ ok(buffer('is_valid', b))
+ end)
+ end)
+
describe('nvim_buf_get_mark', function()
it('works', function()
curbuf('set_lines', -1, -1, true, {'a', 'bit of', 'text'})
diff --git a/test/functional/fixtures/fake-lsp-server.lua b/test/functional/fixtures/fake-lsp-server.lua
index dca7f35923..a30eb748d0 100644
--- a/test/functional/fixtures/fake-lsp-server.lua
+++ b/test/functional/fixtures/fake-lsp-server.lua
@@ -125,6 +125,26 @@ function tests.basic_check_capabilities()
}
end
+function tests.capabilities_for_client_supports_method()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ completionProvider = true;
+ hoverProvider = true;
+ definitionProvider = false;
+ referencesProvider = false;
+ }
+ }
+ end;
+ body = function()
+ end;
+ }
+end
+
function tests.basic_finish()
skeleton {
on_init = function(params)
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index f07a2d18a2..067a13ce68 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -270,6 +270,70 @@ describe('LSP', function()
test_name = "basic_check_capabilities";
on_init = function(client)
client.stop()
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code", fake_lsp_logfile)
+ eq(0, signal, "exit signal", fake_lsp_logfile)
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('client.supports_methods() should validate capabilities', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "capabilities_for_client_supports_method";
+ on_init = function(client)
+ client.stop()
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().completion)
+ eq(true, client.resolved_capabilities().hover)
+ eq(false, client.resolved_capabilities().goto_definition)
+ eq(false, client.resolved_capabilities().rename)
+
+ -- known methods for resolved capabilities
+ eq(true, client.supports_method("textDocument/hover"))
+ eq(false, client.supports_method("textDocument/definition"))
+
+ -- unknown methods are assumed to be supported.
+ eq(true, client.supports_method("unknown-method"))
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code", fake_lsp_logfile)
+ eq(0, signal, "exit signal", fake_lsp_logfile)
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('should call unsupported_method when trying to call an unsupported method', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "capabilities_for_client_supports_method";
+ on_setup = function()
+ exec_lua([=[
+ vim.lsp._unsupported_method = function(method)
+ vim.lsp._last_unsupported_method = method
+ return 'fake-error'
+ end
+ vim.lsp.buf.hover()
+ ]=])
+ end;
+ on_init = function(client)
+ client.stop()
+ local method = exec_lua("return vim.lsp._last_unsupported_method")
+ eq("textDocument/hover", method)
end;
on_exit = function(code, signal)
eq(0, code, "exit code", fake_lsp_logfile)