diff options
-rw-r--r-- | runtime/doc/lua.txt | 21 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/codelens.lua | 16 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 70 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 39 | ||||
-rw-r--r-- | runtime/lua/vim/ui.lua | 36 | ||||
-rwxr-xr-x | scripts/finddeclarations.pl | 50 | ||||
-rwxr-xr-x | scripts/gen_vimdoc.py | 3 | ||||
-rw-r--r-- | src/nvim/lua/vim.lua | 3 | ||||
-rw-r--r-- | test/functional/lua/ui_spec.lua | 46 | ||||
-rw-r--r-- | test/functional/plugin/lsp/codelens_spec.lua | 28 |
10 files changed, 194 insertions, 118 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 53d68fa5e6..22e323baa7 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1645,4 +1645,25 @@ uri_to_fname({uri}) *vim.uri_to_fname()* Return: ~ Filename + +============================================================================== +Lua module: ui *lua-ui* + +select({items}, {opts}, {on_choice}) *vim.ui.select()* + Prompts the user to pick a single item from a collection of + entries + + Parameters: ~ + {items} table Arbitrary items + {opts} table Additional options + • prompt (string|nil) Text of the prompt. + Defaults to `Select one of:` + • format_item (function item -> text) + Function to format an individual item from + `items` . Defaults to `tostring` . + {on_choice} function ((item|nil, idx|nil) -> ()) Called + once the user made a choice. `idx` is the + 1-based index of `item` within `item` . `nil` + if the user aborted the dialog. + vim:tw=78:ts=8:ft=help:norl: diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 9cedb2f1db..20b203fe99 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -31,10 +31,24 @@ local function execute_lens(lens, bufnr, client_id) local line = lens.range.start.line api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1) + local command = lens.command + local fn = vim.lsp.commands[command.command] + if fn then + fn(command, { bufnr = bufnr, client_id = client_id }) + return + end -- Need to use the client that returned the lens → must not use buf_request local client = vim.lsp.get_client_by_id(client_id) assert(client, 'Client is required to execute lens, client_id=' .. client_id) - client.request('workspace/executeCommand', lens.command, function(...) + local command_provider = client.server_capabilities.executeCommandProvider + local commands = type(command_provider) == 'table' and command_provider.commands or {} + if not vim.tbl_contains(commands, command.command) then + vim.notify(string.format( + "Language server does not support command `%s`. This command may require a client extension.", command.command), + vim.log.levels.WARN) + return + end + client.request('workspace/executeCommand', command, function(...) local result = vim.lsp.handlers['workspace/executeCommand'](...) M.refresh() return result diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 624f8b5462..def83a7320 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -116,42 +116,44 @@ M['textDocument/codeAction'] = function(_, result, ctx) return end - local option_strings = {"Code actions:"} - for i, action in ipairs(result) do - local title = action.title:gsub('\r\n', '\\r\\n') - title = title:gsub('\n', '\\n') - table.insert(option_strings, string.format("%d. %s", i, title)) - end - - local choice = vim.fn.inputlist(option_strings) - if choice < 1 or choice > #result then - return - end - local action = result[choice] - -- textDocument/codeAction can return either Command[] or CodeAction[] - -- - -- CodeAction - -- ... - -- edit?: WorkspaceEdit -- <- must be applied before command - -- command?: Command - -- - -- Command: - -- title: string - -- command: string - -- arguments?: any[] - -- - if action.edit then - util.apply_workspace_edit(action.edit) - end - if action.command then - local command = type(action.command) == 'table' and action.command or action - local fn = vim.lsp.commands[command.command] - if fn then - fn(command, ctx) - else - buf.execute_command(command) + ---@private + local function on_user_choice(action) + if not action then + return + end + -- textDocument/codeAction can return either Command[] or CodeAction[] + -- + -- CodeAction + -- ... + -- edit?: WorkspaceEdit -- <- must be applied before command + -- command?: Command + -- + -- Command: + -- title: string + -- command: string + -- arguments?: any[] + -- + if action.edit then + util.apply_workspace_edit(action.edit) + end + if action.command then + local command = type(action.command) == 'table' and action.command or action + local fn = vim.lsp.commands[command.command] + if fn then + fn(command, ctx) + else + buf.execute_command(command) + end end end + + vim.ui.select(result, { + prompt = 'Code actions:', + format_item = function(action) + local title = action.title:gsub('\r\n', '\\r\\n') + return title:gsub('\n', '\\n') + end, + }, on_user_choice) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 716f42faf9..255eb65dfe 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -4,34 +4,6 @@ local log = require('vim.lsp.log') local protocol = require('vim.lsp.protocol') local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap --- TODO replace with a better implementation. ----@private ---- Encodes to JSON. ---- ----@param data (table) Data to encode ----@returns (string) Encoded object -local function json_encode(data) - local status, result = pcall(vim.json.encode, data) - if status then - return result - else - return nil, result - end -end ----@private ---- Decodes from JSON. ---- ----@param data (string) Data to decode ----@returns (table) Decoded JSON object -local function json_decode(data) - local status, result = pcall(vim.json.decode, data) - if status then - return result - else - return nil, result - end -end - ---@private --- Checks whether a given path exists and is a directory. ---@param filename (string) path to check @@ -389,12 +361,12 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) --- Encodes {payload} into a JSON-RPC message and sends it to the remote --- process. --- - ---@param payload (table) Converted into a JSON string, see |json_encode()| + ---@param payload table ---@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing. local function encode_and_send(payload) local _ = log.debug() and log.debug("rpc.send", payload) if handle == nil or handle:is_closing() then return false end - local encoded = assert(json_encode(payload)) + local encoded = vim.json.encode(payload) stdin:write(format_message_with_content_length(encoded)) return true end @@ -485,14 +457,15 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) ---@private local function handle_body(body) - local decoded, err = json_decode(body) - if not decoded then - -- on_error(client_errors.INVALID_SERVER_JSON, err) + local ok, decoded = pcall(vim.json.decode, body) + if not ok then + on_error(client_errors.INVALID_SERVER_JSON, decoded) return end local _ = log.debug() and log.debug("rpc.receive", decoded) if type(decoded.method) == 'string' and decoded.id then + local err -- Server Request decoded.params = convert_NIL(decoded.params) -- Schedule here so that the users functions don't trigger an error and diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua new file mode 100644 index 0000000000..5eab20fc54 --- /dev/null +++ b/runtime/lua/vim/ui.lua @@ -0,0 +1,36 @@ +local M = {} + +--- Prompts the user to pick a single item from a collection of entries +--- +---@param items table Arbitrary items +---@param opts table Additional options +--- - prompt (string|nil) +--- Text of the prompt. Defaults to `Select one of:` +--- - format_item (function item -> text) +--- Function to format an +--- individual item from `items`. Defaults to `tostring`. +---@param on_choice function ((item|nil, idx|nil) -> ()) +--- Called once the user made a choice. +--- `idx` is the 1-based index of `item` within `item`. +--- `nil` if the user aborted the dialog. +function M.select(items, opts, on_choice) + vim.validate { + items = { items, 'table', false }, + on_choice = { on_choice, 'function', false }, + } + opts = opts or {} + local choices = {opts.prompt or 'Select one of:'} + local format_item = opts.format_item or tostring + for i, item in pairs(items) do + table.insert(choices, string.format('%d: %s', i, format_item(item))) + end + local choice = vim.fn.inputlist(choices) + if choice < 1 or choice > #items then + on_choice(nil, nil) + else + on_choice(items[choice], choice) + end +end + + +return M diff --git a/scripts/finddeclarations.pl b/scripts/finddeclarations.pl deleted file mode 100755 index 1b1a57b9b7..0000000000 --- a/scripts/finddeclarations.pl +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; - -if ($ARGV[0] eq '--help') { - print << "EOF"; -Usage: - - $0 definitions.c -EOF - exit; -} - -my ($cfname, $sfname, $gfname, $cpp) = @ARGV; - -my $F; - -open $F, "<", $cfname; - -my $text = join "", <$F>; - -close $F; - -my $s = qr/(?>\s*)/aso; -my $w = qr/(?>\w+)/aso; -my $argname = qr/$w(?:\[(?>\w+)\])?/aso; -my $type_regex = qr/(?:$w$s\**$s)+/aso; -my $arg_regex = qr/(?:$type_regex$s$argname)/aso; - -while ($text =~ / - (?<=\n) # Definition starts at the start of line - $type_regex # Return type - $s$w # Function name - $s\($s - (?: - $arg_regex(?:$s,$s$arg_regex)*+ - ($s,$s\.\.\.)? # varargs function - |void - )? - $s\) - (?:$s FUNC_ATTR_$w(?:\((?>[^)]*)\))?)*+ # Optional attributes - (?=$s;) # Ending semicolon - /axsogp) { - my $match = "${^MATCH}"; - my $s = "${^PREMATCH}"; - $s =~ s/[^\n]++//g; - my $line = 1 + length $s; - print "${cfname}:${line}: $match\n"; -} diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index 64ed8d61f6..36e01153f1 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -123,11 +123,13 @@ CONFIG = { 'vim.lua', 'shared.lua', 'uri.lua', + 'ui.lua', ], 'files': ' '.join([ os.path.join(base_dir, 'src/nvim/lua/vim.lua'), os.path.join(base_dir, 'runtime/lua/vim/shared.lua'), os.path.join(base_dir, 'runtime/lua/vim/uri.lua'), + os.path.join(base_dir, 'runtime/lua/vim/ui.lua'), ]), 'file_patterns': '*.lua', 'fn_name_prefix': '', @@ -141,6 +143,7 @@ CONFIG = { # `shared` functions are exposed on the `vim` module. 'shared': 'vim', 'uri': 'vim', + 'ui': 'vim.ui', }, 'append_only': [ 'shared.lua', diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index ba124c41ad..7a209f2d79 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -108,6 +108,9 @@ setmetatable(vim, { elseif key == 'diagnostic' then t.diagnostic = require('vim.diagnostic') return t.diagnostic + elseif key == 'ui' then + t.ui = require('vim.ui') + return t.ui end end }) diff --git a/test/functional/lua/ui_spec.lua b/test/functional/lua/ui_spec.lua new file mode 100644 index 0000000000..94f1b5840b --- /dev/null +++ b/test/functional/lua/ui_spec.lua @@ -0,0 +1,46 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq = helpers.eq +local exec_lua = helpers.exec_lua +local clear = helpers.clear + +describe('vim.ui', function() + before_each(function() + clear() + end) + + + describe('select', function() + it('can select an item', function() + local result = exec_lua[[ + local items = { + { name = 'Item 1' }, + { name = 'Item 2' }, + } + local opts = { + format_item = function(entry) + return entry.name + end + } + local selected + local cb = function(item) + selected = item + end + -- inputlist would require input and block the test; + local choices + vim.fn.inputlist = function(x) + choices = x + return 1 + end + vim.ui.select(items, opts, cb) + vim.wait(100, function() return selected ~= nil end) + return {selected, choices} + ]] + eq({ name = 'Item 1' }, result[1]) + eq({ + 'Select one of:', + '1: Item 1', + '2: Item 2', + }, result[2]) + end) + end) +end) diff --git a/test/functional/plugin/lsp/codelens_spec.lua b/test/functional/plugin/lsp/codelens_spec.lua index e48a0ad260..c8b75e65fc 100644 --- a/test/functional/plugin/lsp/codelens_spec.lua +++ b/test/functional/plugin/lsp/codelens_spec.lua @@ -58,5 +58,33 @@ describe('vim.lsp.codelens', function() ]], bufnr) eq({[1] = {'Lens1', 'LspCodeLens'}}, virtual_text_chunks) + + end) + it('codelens uses client commands', function() + local fake_uri = "file:///fake/uri" + local cmd = exec_lua([[ + fake_uri = ... + local bufnr = vim.uri_to_bufnr(fake_uri) + vim.fn.bufload(bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {'One line'}) + local lenses = { + { + range = { + start = { line = 0, character = 0, }, + ['end'] = { line = 0, character = 8 } + }, + command = { title = 'Lens1', command = 'Dummy' } + }, + } + vim.lsp.codelens.on_codelens(nil, lenses, {method='textDocument/codeLens', client_id=1, bufnr=bufnr}) + local cmd_called = nil + vim.lsp.commands['Dummy'] = function(command) + cmd_called = command + end + vim.api.nvim_set_current_buf(bufnr) + vim.lsp.codelens.run() + return cmd_called + ]], fake_uri) + eq({ command = 'Dummy', title = 'Lens1' }, cmd) end) end) |