diff --git a/README.md b/README.md index beb9065..6944fde 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,18 @@ This plugin provides a handy pop-up menu for code actions. Its key competence is to provide the user with more detailed insights for each available code action. Such include meta data as well as a preview diff for certain types of actions. These includes: - - descriptive title (usually shown by all code action "menu" solutions) - - kind of the action (e.g. refactor, command, quickfix, organize imports, ...) - - name of the action - - if the action is the preferred one according to the server (preferred - actions get automatically sorted to the top of the menu) - - if the action is disabled (disabled actions can be used and get - automatically sorted to the bottom of the menu with a special hightlight) - - a preview of the changes this action will do in a diff view for all affected - files (includes a diff count box visualization as GitHub pul requests do) - - ...more will come, especially when servers start to use the `dataSupport` - capability for the code action provider + +- descriptive title (usually shown by all code action "menu" solutions) +- kind of the action (e.g. refactor, command, quickfix, organize imports, ...) +- name of the action +- if the action is the preferred one according to the server (preferred + actions get automatically sorted to the top of the menu) +- if the action is disabled (disabled actions can be used and get + automatically sorted to the bottom of the menu with a special hightlight) +- a preview of the changes this action will do in a diff view for all affected + files (includes a diff count box visualization as GitHub pul requests do) +- ...more will come, especially when servers start to use the `dataSupport` + capability for the code action provider The experience for all these features might vary according to the implementation of the used language server. Especially for the diff view, do some servers still @@ -59,6 +60,17 @@ current cursor location. Please note that LSP and other features for this plugin require you to have at least `v0.5.0` of NeoVim. +## Configuration + +by default + +```lua +require('code_action_menu.config').setup { + source = 'nvim_lsp', -- available values: nvim_lsp, coc + window_options = {}, -- additional options for code-aciton-menu windows +} +``` + ## Usage The plugin has a single command only: `CodeActionMenu` This command works in @@ -72,6 +84,15 @@ apply it right away. In any case the menu window will close itself. If you want to manually close the window without selecting an action, just hit `` or `q`. +If you are using coc.nvim, just change the mappings + +| Original | Now | +| --------------------------------- | ---------------------------------- | +| `(coc-codeaction-selected)` | `(codeaction-selected-menu)` | +| `(coc-codeaction)` | `(codeaction-menu)` | +| `(coc-codeaction-line)` | `(codeaction-line-menu)` | +| `(coc-codeaction-cursor)` | `(codeaction-cursor-menu)` | + ## Customization The plugin allows for a bunch of customization. While there is no classic diff --git a/lua/code_action_menu.lua b/lua/code_action_menu.lua index c0774d2..43e8654 100644 --- a/lua/code_action_menu.lua +++ b/lua/code_action_menu.lua @@ -39,15 +39,8 @@ local function close_warning_message_window() end end -local function open_code_action_menu() - -- Might still be open. - close_code_action_menu() - close_warning_message_window() - - local use_range = vim.api.nvim_get_mode().mode ~= 'n' - local all_actions = action_utils.request_actions_from_all_servers(use_range) - - if #all_actions == 0 then +local function actions_handler(actions) + if actions and #actions == 0 then warning_message_window_instace = WarningMessageWindow:new() warning_message_window_instace:open() vim.api.nvim_command( @@ -55,13 +48,19 @@ local function open_code_action_menu() ) else anchor_window_instance = AnchorWindow:new() - menu_window_instance = MenuWindow:new(all_actions) + menu_window_instance = MenuWindow:new(actions) menu_window_instance:open({ window_stack = { anchor_window_instance }, }) end end +local function open_code_action_menu(...) + close_code_action_menu() + close_warning_message_window() + action_utils.request_actions_from_all_servers(actions_handler, ...) +end + local function update_selected_action() local selected_action = menu_window_instance:get_selected_action() @@ -97,11 +96,7 @@ local function execute_selected_action() local selected_action = menu_window_instance:get_selected_action() if selected_action:is_disabled() then - vim.api.nvim_notify( - 'Can not execute disabled action!', - vim.log.levels.ERROR, - {} - ) + vim.notify('Can not execute disabled action!', vim.log.levels.ERROR) else close_code_action_menu() -- Close first to execute the action in the correct buffer. selected_action:execute() diff --git a/lua/code_action_menu/config.lua b/lua/code_action_menu/config.lua new file mode 100644 index 0000000..f0dfad8 --- /dev/null +++ b/lua/code_action_menu/config.lua @@ -0,0 +1,23 @@ +local settings = { + source = 'nvim_lsp', -- nvim_lsp | coc + window_options = {}, -- additional options for code-aciton-menu windows +} + +local function setup(opts) + settings = vim.tbl_extend('force', settings, opts or {}) + + if vim.g.coc_service_initialized == 1 and settings.source ~= 'coc' then + vim.notify( + ('Detected that you are using COC, but the source is %s'):format( + settings.source + ), + vim.log.levels.WARN + ) + end +end + +return setmetatable({ setup = setup }, { + __index = function(_, key) + return settings[key] + end, +}) diff --git a/lua/code_action_menu/lsp_objects/actions/code_action.lua b/lua/code_action_menu/lsp_objects/actions/code_action.lua index 2118e36..e12706c 100644 --- a/lua/code_action_menu/lsp_objects/actions/code_action.lua +++ b/lua/code_action_menu/lsp_objects/actions/code_action.lua @@ -5,6 +5,7 @@ local TextDocumentEdit = require( local WorkspaceEdit = require( 'code_action_menu.lsp_objects.edits.workspace_edit' ) +local config = require('code_action_menu.config') local CodeAction = BaseAction:new({}) @@ -77,16 +78,22 @@ function CodeAction:get_workspace_edit() end function CodeAction:execute() - if self:is_workspace_edit() then - vim.lsp.util.apply_workspace_edit(self.server_data.edit) - elseif self:is_command() then - vim.lsp.buf.execute_command(self.server_data.command) - else - vim.api.nvim_notify( - 'Failed to execute code action of unknown kind!', - vim.log.levels.ERROR, - {} - ) + local source = config.source + if source == 'nvim_lsp' then + do + if self:is_workspace_edit() then + vim.lsp.util.apply_workspace_edit(self.server_data.edit) + elseif self:is_command() then + vim.lsp.buf.execute_command(self.server_data.command) + else + vim.notify( + 'Failed to execute code action of unknown kind!', + vim.log.levels.ERROR + ) + end + end + elseif source == 'coc' then + vim.fn.CocActionAsync('doCodeAction', self.server_data) end end diff --git a/lua/code_action_menu/lsp_objects/actions/command.lua b/lua/code_action_menu/lsp_objects/actions/command.lua index 2b257c9..e5bf20f 100644 --- a/lua/code_action_menu/lsp_objects/actions/command.lua +++ b/lua/code_action_menu/lsp_objects/actions/command.lua @@ -5,6 +5,7 @@ local TextDocumentEdit = require( local WorkspaceEdit = require( 'code_action_menu.lsp_objects.edits.workspace_edit' ) +local config = require('code_action_menu.config') local Command = BaseAction:new({}) @@ -40,7 +41,12 @@ function Command:get_workspace_edit() end function Command:execute() - vim.lsp.buf.execute_command(self.server_data) + local source = config.source + if source == 'nvim_lsp' then + vim.lsp.buf.execute_command(self.server_data) + elseif source == 'coc' then + vim.fn.CocActionAsync('doCodeAction', self.server_data) + end end return Command diff --git a/lua/code_action_menu/utility_functions/actions.lua b/lua/code_action_menu/utility_functions/actions.lua index 024f3a9..93dac25 100644 --- a/lua/code_action_menu/utility_functions/actions.lua +++ b/lua/code_action_menu/utility_functions/actions.lua @@ -1,5 +1,6 @@ local Command = require('code_action_menu.lsp_objects.actions.command') local CodeAction = require('code_action_menu.lsp_objects.actions.code_action') +local config = require('code_action_menu.config') local function unpack_result_and_error(server_data, client_id) local client = vim.lsp.get_client_by_id(client_id) @@ -7,13 +8,16 @@ local function unpack_result_and_error(server_data, client_id) local error = nil if server_data == nil then - error = "Server for client '" .. client.name .. "' not yet ready!" + error = 'Server for client \'' .. client.name .. '\' not yet ready!' elseif type(server_data) == 'table' and server_data.err ~= nil then - error = "Server of client '" .. client.name .. "' returned error: " + error = 'Server of client \'' .. client.name .. '\' returned error: ' if type(server_data.err) == 'string' then error = error .. server_data.err - elseif type(server_data.err) == 'table' and type(server_data.err.message) == 'string' then + elseif + type(server_data.err) == 'table' + and type(server_data.err.message) == 'string' + then error = error .. server_data.err.message else error = error .. 'unknown error - failed to parse response' @@ -33,14 +37,17 @@ local function resolve_code_action(client_id, code_action_object) local action_object, error = unpack_result_and_error(response, client_id) if error then - vim.api.nvim_notify(error, vim.log.levels.WARN, {}) + vim.notify(error, vim.log.levels.WARN) end return action_object end local function parse_object_as_action(code_action_object) - if type(code_action_object) == 'table' and type(code_action_object.command) == 'string' then + if + type(code_action_object) == 'table' + and type(code_action_object.command) == 'string' + then return Command:new(code_action_object) elseif type(code_action_object) == 'table' @@ -51,8 +58,9 @@ local function parse_object_as_action(code_action_object) then return CodeAction:new(code_action_object) else - local error = 'Failed to parse unknown code action or command data structure! Skipped.' - vim.api.nvim_notify(error, vim.log.levels.WARN, {}) + local error = + 'Failed to parse unknown code action or command data structure! Skipped.' + vim.notify(error, vim.log.levels.WARN) return nil end end @@ -61,7 +69,9 @@ local function parse_action_data_objects(client_id, all_code_action_objects) local all_actions = {} for _, code_action_object in ipairs(all_code_action_objects) do - if type(code_action_object) == 'table' and code_action_object.data ~= nil then + if + type(code_action_object) == 'table' and code_action_object.data ~= nil + then code_action_object = resolve_code_action(client_id, code_action_object) end @@ -86,31 +96,72 @@ local function request_actions_from_server(client_id, parameters) local action_objects, error = unpack_result_and_error(response, client_id) if error then - vim.api.nvim_notify(error, vim.log.levels.WARN, {}) + vim.notify(error, vim.log.levels.WARN) end return action_objects or {} end -local function request_actions_from_all_servers(use_range) - vim.validate({ ['request action for range'] = { use_range, 'boolean', true } }) +local source_map = { + ['nvim_lsp'] = function(handler) + local use_range = vim.api.nvim_get_mode().mode ~= 'n' + local line_diagnostics = vim.lsp.diagnostic.get_line_diagnostics() + local request_parameters = use_range + and vim.lsp.util.make_given_range_params() + or vim.lsp.util.make_range_params() + + request_parameters.context = { diagnostics = line_diagnostics } + + local all_clients = vim.lsp.buf_get_clients() + local all_actions = {} + + for _, client in ipairs(all_clients) do + local action_data_objects = request_actions_from_server( + client.id, + request_parameters + ) + local actions = parse_action_data_objects(client.id, action_data_objects) + vim.list_extend(all_actions, actions) + end - local line_diagnostics = vim.lsp.diagnostic.get_line_diagnostics() - local request_parameters = use_range and vim.lsp.util.make_given_range_params() - or vim.lsp.util.make_range_params() + handler(all_actions) + end, + ['coc'] = function(handler, mode) + if vim.g.coc_service_initialized ~= 1 then + return vim.notify('Coc is not ready!', vim.log.levels.ERROR) + end - request_parameters.context = { diagnostics = line_diagnostics } + vim.fn.CocActionAsync( + 'codeActions', + mode or vim.api.nvim_get_mode().mode, + function(err, res) + if err ~= vim.NIL then + return + end - local all_clients = vim.lsp.buf_get_clients() - local all_actions = {} + local all_actions = {} - for _, client in ipairs(all_clients) do - local action_data_objects = request_actions_from_server(client.id, request_parameters) - local actions = parse_action_data_objects(client.id, action_data_objects) - vim.list_extend(all_actions, actions) - end + for _, data in ipairs(res) do + local action - return all_actions + if type(data.edit) == 'table' or type(data.command) == 'table' then + action = CodeAction:new(data) + else + action = Command:new(data) + end + + table.insert(all_actions, action) + end + + handler(all_actions) + end + ) + end, +} + +---The first argument should be a function that handler all actions +local function request_actions_from_all_servers(...) + source_map[config.source](...) end local function order_actions(action_table, key_a, key_b) diff --git a/lua/code_action_menu/windows/base_window.lua b/lua/code_action_menu/windows/base_window.lua index cce5702..590a808 100644 --- a/lua/code_action_menu/windows/base_window.lua +++ b/lua/code_action_menu/windows/base_window.lua @@ -1,4 +1,5 @@ local buffer_utils = require('code_action_menu.utility_functions.buffers') +local config = require('code_action_menu.config') local BaseWindow = { window_number = -1, @@ -89,6 +90,10 @@ function BaseWindow:open(window_configuration_options) vim.api.nvim_win_set_config(self.window_number, window_configuration) end + for name, value in pairs(config.window_options) do + vim.api.nvim_win_set_option(self.window_number, name, value) + end + self:after_opened() end diff --git a/plugin/code_action_menu.vim b/plugin/code_action_menu.vim index b3aaeda..bdeacbb 100644 --- a/plugin/code_action_menu.vim +++ b/plugin/code_action_menu.vim @@ -1 +1,12 @@ command! CodeActionMenu lua require('code_action_menu').open_code_action_menu() + +" These behave differently only for COC users +function! s:CodeActionFromSelected(type) + call luaeval("require('code_action_menu').open_code_action_menu(_A)", a:type) +endfunction + +vnoremap (codeaction-selected-menu) call luaeval("require('code_action_menu').open_code_action_menu(_A)", visualmode()) +nnoremap (codeaction-selected-menu) set operatorfunc=CodeActionFromSelectedg@ +nnoremap (codeaction-menu) require('code_action_menu').open_code_action_menu('') +nnoremap (codeaction-line-menu) require('code_action_menu').open_code_action_menu('line') +nnoremap (codeaction-cursor-menu) require('code_action_menu').open_code_action_menu('cursor')CR>