diff --git a/doc/telescope.txt b/doc/telescope.txt index fc36ca5..30f6c5d 100644 --- a/doc/telescope.txt +++ b/doc/telescope.txt @@ -1643,6 +1643,18 @@ actions.remove_selected_picker({prompt_bufnr})*actions.remove_selected_picker()* {prompt_bufnr} (number) The prompt bufnr +actions.which_key({prompt_bufnr}) *actions.which_key()* + Display the keymaps of registered actions similar to which-key.nvim. + + - Notes: + - The defaults can be overridden via + |action_generate.toggle_registered_actions|. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + ================================================================================ *telescope.actions.state* @@ -1785,6 +1797,91 @@ utils.map_selections({prompt_bufnr}, {f}) *utils.map_selections()* that takes (selection) as a viable argument +utils.get_registered_mappings({prompt_bufnr})*utils.get_registered_mappings()* + Utility to collect mappings of prompt buffer in array of `{mode, keybind, + name}`. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + + +================================================================================ + *telescope.actions.generate* + +Module for convenience to override defaults of corresponding +|telescope.actions| at |telescope.setup()|. + +General usage: + require("telescope").setup { + defaults = { + mappings = { + n = { + ["?"] = action_generate.toggle_registered_actions { + name_width = 20, -- typically leads to smaller floats + max_height = 0.5, -- increase potential maximum height + seperator = " > ", -- change sep between mode, keybind, and name + close_with_action = false, -- do not close float on action + }, + }, + }, + }, + } + +action_generate.which_key({opts}) *action_generate.which_key()* + Display the keymaps of registered actions similar to which-key.nvim. + + - Floating window: + - Appears on the opposite side of the prompt. + - Resolves to minimum required number of lines to show hints with `opts` + or truncates entries at `max_height`. + - Closes automatically on action call and can be disabled with by setting + `close_with_action` to false. + + + Parameters: ~ + {opts} (table) options to pass to toggling registered actions + + Fields: ~ + {max_height} (number) % of max. height or no. of rows + for hints (default: 0.4), see + |resolver.resolve_height()| + {only_show_current_mode} (boolean) only show keymaps for the current + mode (default: true) + {mode_width} (number) fixed width of mode to be shown + (default: 1) + {keybind_width} (number) fixed width of keybind to be shown + (default: 7) + {name_width} (number) fixed width of action name to be + shown (default: 30) + {column_padding} (string) string to split; can be used for + vertical seperator (default: " ") + {mode_hl} (string) hl group of mode (default: + TelescopeResultsConstant) + {keybind_hl} (string) hl group of keybind (default: + TelescopeResultsVariable) + {name_hl} (string) hl group of action name (default: + TelescopeResultsFunction) + {column_indent} (number) number of left-most spaces before + keybinds are shown (default: 4) + {line_padding} (number) row padding in top and bottom of + float (default: 1) + {separator} (string) seperator string between mode, key + bindings, and action (default: " + -> ") + {close_with_action} (boolean) registered action will close + keymap float (default: true) + {normal_hl} (string) winhl of "Normal" for keymap hints + floating window (default: + "TelescopePrompt") + {border_hl} (string) winhl of "Normal" for keymap + borders (default: + "TelescopePromptBorder") + {winblend} (number) pseudo-transparency of keymap + hints floating window + + ================================================================================ *telescope.previewers* diff --git a/lua/telescope/actions/generate.lua b/lua/telescope/actions/generate.lua new file mode 100644 index 0000000..fafce8c --- /dev/null +++ b/lua/telescope/actions/generate.lua @@ -0,0 +1,56 @@ +---@tag telescope.actions.generate + +---@brief [[ +--- Module for convenience to override defaults of corresponding |telescope.actions| at |telescope.setup()|. +--- +---
+--- General usage:
+---   require("telescope").setup {
+---     defaults = {
+---       mappings = {
+---         n = {
+---           ["?"] = action_generate.toggle_registered_actions {
+---             name_width = 20, -- typically leads to smaller floats
+---             max_height = 0.5, -- increase potential maximum height
+---             seperator = " > ", -- change sep between mode, keybind, and name
+---             close_with_action = false, -- do not close float on action
+---           },
+---         },
+---       },
+---     },
+---   }
+--- 
+---@brief ]] + +local actions = require "telescope.actions" +local action_generate = {} + +--- Display the keymaps of registered actions similar to which-key.nvim.
+--- - Floating window: +--- - Appears on the opposite side of the prompt. +--- - Resolves to minimum required number of lines to show hints with `opts` or truncates entries at `max_height`. +--- - Closes automatically on action call and can be disabled with by setting `close_with_action` to false. +---@param opts table: options to pass to toggling registered actions +---@field max_height number: % of max. height or no. of rows for hints (default: 0.4), see |resolver.resolve_height()| +---@field only_show_current_mode boolean: only show keymaps for the current mode (default: true) +---@field mode_width number: fixed width of mode to be shown (default: 1) +---@field keybind_width number: fixed width of keybind to be shown (default: 7) +---@field name_width number: fixed width of action name to be shown (default: 30) +---@field column_padding string: string to split; can be used for vertical seperator (default: " ") +---@field mode_hl string: hl group of mode (default: TelescopeResultsConstant) +---@field keybind_hl string: hl group of keybind (default: TelescopeResultsVariable) +---@field name_hl string: hl group of action name (default: TelescopeResultsFunction) +---@field column_indent number: number of left-most spaces before keybinds are shown (default: 4) +---@field line_padding number: row padding in top and bottom of float (default: 1) +---@field separator string: seperator string between mode, key bindings, and action (default: " -> ") +---@field close_with_action boolean: registered action will close keymap float (default: true) +---@field normal_hl string: winhl of "Normal" for keymap hints floating window (default: "TelescopePrompt") +---@field border_hl string: winhl of "Normal" for keymap borders (default: "TelescopePromptBorder") +---@field winblend number: pseudo-transparency of keymap hints floating window +action_generate.which_key = function(opts) + return function(prompt_bufnr) + actions.which_key(prompt_bufnr, opts) + end +end + +return action_generate diff --git a/lua/telescope/actions/init.lua b/lua/telescope/actions/init.lua index aa930dc..39a76ee 100644 --- a/lua/telescope/actions/init.lua +++ b/lua/telescope/actions/init.lua @@ -10,16 +10,20 @@ local a = vim.api local log = require "telescope.log" +local config = require "telescope.config" local state = require "telescope.state" local utils = require "telescope.utils" +local popup = require "plenary.popup" local p_scroller = require "telescope.pickers.scroller" local action_state = require "telescope.actions.state" local action_utils = require "telescope.actions.utils" local action_set = require "telescope.actions.set" +local entry_display = require "telescope.pickers.entry_display" local from_entry = require "telescope.from_entry" local transform_mod = require("telescope.actions.mt").transform_mod +local resolver = require "telescope.config.resolve" local actions = setmetatable({}, { __index = function(_, k) @@ -847,8 +851,205 @@ actions.remove_selected_picker = function(prompt_bufnr) end end +--- Display the keymaps of registered actions similar to which-key.nvim.
+--- - Notes: +--- - The defaults can be overridden via |action_generate.toggle_registered_actions|. +---@param prompt_bufnr number: The prompt bufnr +actions.which_key = function(prompt_bufnr, opts) + opts = opts or {} + opts.max_height = utils.get_default(opts.max_height, 0.4) + opts.only_show_current_mode = utils.get_default(opts.only_show_current_mode, true) + opts.mode_width = utils.get_default(opts.mode_width, 1) + opts.keybind_width = utils.get_default(opts.keybind_width, 7) + opts.name_width = utils.get_default(opts.name_width, 30) + opts.column_padding = utils.get_default(opts.column_padding, " ") + opts.column_indent = table.concat(utils.repeated_table(utils.get_default(opts.column_indent, 4), " ")) + opts.line_padding = utils.get_default(opts.line_padding, 1) + opts.separator = utils.get_default(opts.separator, " -> ") + opts.close_with_action = utils.get_default(opts.close_with_action, true) + opts.normal_hl = utils.get_default(opts.normal_hl, "TelescopePrompt") + opts.border_hl = utils.get_default(opts.border_hl, "TelescopePromptBorder") + opts.winblend = utils.get_default(opts.winblend, config.values.winblend) + + -- close on repeated keypress + local km_bufs = (function() + local ret = {} + local bufs = a.nvim_list_bufs() + for _, buf in ipairs(bufs) do + for _, bufname in ipairs { "_TelescopeWhichKey", "_TelescopeWhichKeyBorder" } do + if string.find(a.nvim_buf_get_name(buf), bufname) then + table.insert(ret, buf) + end + end + end + return ret + end)() + if not vim.tbl_isempty(km_bufs) then + for _, buf in ipairs(km_bufs) do + utils.buf_delete(buf) + local win_ids = vim.fn.win_findbuf(buf) + for _, win_id in ipairs(win_ids) do + pcall(a.nvim_win_close, win_id, true) + end + end + return + end + + local displayer = entry_display.create { + separator = opts.separator, + items = { + { width = opts.mode_with }, + { width = opts.keybind_width }, + { width = opts.name_width }, + }, + } + + local make_display = function(mapping) + return displayer { + { mapping.mode, utils.get_default(opts.mode_hl, "TelescopeResultsConstant") }, + { mapping.keybind, utils.get_default(opts.keybind_hl, "TelescopeResultsVariable") }, + { mapping.name, utils.get_default(opts.name_hl, "TelescopeResultsFunction") }, + } + end + + local mappings = {} + local mode = a.nvim_get_mode().mode + for _, v in pairs(action_utils.get_registered_mappings(prompt_bufnr)) do + -- holds true for registered keymaps + if type(v.func) == "table" then + local name = "" + for _, action in ipairs(v.func) do + if type(action) == "string" then + name = name == "" and action or name .. " + " .. action + end + end + if name and name ~= "which_key" then + if not opts.only_show_current_mode or mode == v.mode then + table.insert(mappings, { mode = v.mode, keybind = v.keybind, name = name }) + end + end + end + end + + table.sort(mappings, function(x, y) + if x.name < y.name then + return true + elseif x.name == y.name then + -- show normal mode as the standard mode first + if x.mode > y.mode then + return true + else + return false + end + else + return false + end + end) + + local entry_width = #opts.column_padding + + opts.mode_width + + opts.keybind_width + + opts.name_width + + (3 * #opts.separator) + local num_total_columns = math.floor((vim.o.columns - #opts.column_indent) / entry_width) + opts.num_rows = math.min( + math.ceil(#mappings / num_total_columns), + resolver.resolve_height(opts.max_height)(_, _, vim.o.lines) + ) + local total_available_entries = opts.num_rows * num_total_columns + local winheight = opts.num_rows + 2 * opts.line_padding + + -- place hints at top or bottom relative to prompt + local picker = action_state.get_current_picker(prompt_bufnr) + local prompt_win = picker.prompt_win + local prompt_row = a.nvim_win_get_position(prompt_win)[1] + local prompt_pos = prompt_row <= 0.5 * vim.o.lines + + local modes = { n = "Normal", i = "Insert" } + local title_mode = opts.only_show_current_mode and modes[mode] .. " Mode " or "" + local title_text = title_mode .. "Keymaps" + local popup_opts = { + relative = "editor", + enter = false, + minwidth = vim.o.columns, + maxwidth = vim.o.columns, + minheight = winheight, + maxheight = winheight, + line = prompt_pos == true and vim.o.lines - winheight or 0, + col = 1, + border = { prompt_pos and 1 or 0, 0, not prompt_pos and 1 or 0, 0 }, + borderchars = { prompt_pos and "─" or " ", "", not prompt_pos and "─" or " ", "", "", "", "", "" }, + noautocmd = true, + title = { { text = title_text, pos = prompt_pos and "N" or "S" } }, + } + local km_win_id, km_opts = popup.create("", popup_opts) + local km_buf = a.nvim_win_get_buf(km_win_id) + a.nvim_buf_set_name(km_buf, "_TelescopeWhichKey") + a.nvim_buf_set_name(km_opts.border.bufnr, "_TelescopeTelescopeWhichKeyBorder") + a.nvim_win_set_option(km_win_id, "winhl", "Normal:" .. opts.normal_hl) + a.nvim_win_set_option(km_opts.border.win_id, "winhl", "Normal:" .. opts.border_hl) + a.nvim_win_set_option(km_win_id, "winblend", opts.winblend) + + vim.cmd(string.format( + "autocmd BufLeave ++once lua %s", + table.concat({ + string.format("pcall(vim.api.nvim_win_close, %s, true)", km_win_id), + string.format("pcall(vim.api.nvim_win_close, %s, true)", km_opts.border.win_id), + string.format("require 'telescope.utils'.buf_delete(%s)", km_buf), + }, ";") + )) + + a.nvim_buf_set_lines( + km_buf, + 0, + -1, + false, + utils.repeated_table(opts.num_rows + 2 * opts.line_padding, opts.column_indent) + ) + + local keymap_highlights = a.nvim_create_namespace "telescope_whichkey" + local highlights = {} + for index, mapping in ipairs(mappings) do + local row = utils.cycle(index, opts.num_rows) - 1 + opts.line_padding + local prev_line = a.nvim_buf_get_lines(km_buf, row, row + 1, false)[1] + if index == total_available_entries and total_available_entries > #mappings then + local new_line = prev_line .. "..." + a.nvim_buf_set_lines(km_buf, row, row + 1, false, { new_line }) + break + end + local display, display_hl = make_display(mapping) + local new_line = prev_line .. display .. opts.column_padding -- incl. padding + a.nvim_buf_set_lines(km_buf, row, row + 1, false, { new_line }) + table.insert(highlights, { hl = display_hl, row = row, col = #prev_line }) + end + + -- highlighting only after line setting as vim.api.nvim_buf_set_lines removes hl otherwise + for _, highlight_tbl in pairs(highlights) do + local highlight = highlight_tbl.hl + local row_ = highlight_tbl.row + local col = highlight_tbl.col + for _, hl_block in ipairs(highlight) do + a.nvim_buf_add_highlight(km_buf, keymap_highlights, hl_block[2], row_, col + hl_block[1][1], col + hl_block[1][2]) + end + end + + -- only set up autocommand after showing preview completed + if opts.close_with_action then + vim.schedule(function() + vim.cmd(string.format( + "autocmd User TelescopeKeymap ++once lua %s", + table.concat({ + string.format("pcall(vim.api.nvim_win_close, %s, true)", km_win_id), + string.format("pcall(vim.api.nvim_win_close, %s, true)", km_opts.border.win_id), + string.format("require 'telescope.utils'.buf_delete(%s)", km_buf), + }, ";") + )) + end) + end +end + -- ================================================== --- Transforms modules and sets the corect metatables. +-- Transforms modules and sets the correct metatables. -- ================================================== actions = transform_mod(actions) return actions diff --git a/lua/telescope/actions/utils.lua b/lua/telescope/actions/utils.lua index f2abfe1..7c8a3ac 100644 --- a/lua/telescope/actions/utils.lua +++ b/lua/telescope/actions/utils.lua @@ -78,4 +78,26 @@ function utils.map_selections(prompt_bufnr, f) end end +local findnth = function(str, nth) + local array = {} + for i in string.gmatch(str, "%d+") do + table.insert(array, tonumber(i)) + end + return array[nth] +end + +--- Utility to collect mappings of prompt buffer in array of `{mode, keybind, name}`. +---@param prompt_bufnr number: The prompt bufnr +function utils.get_registered_mappings(prompt_bufnr) + local ret = {} + for _, mode in ipairs { "n", "i" } do + local mode_mappings = vim.api.nvim_buf_get_keymap(prompt_bufnr, mode) + for _, mapping in ipairs(mode_mappings) do + local funcid = findnth(mapping.rhs, 2) + table.insert(ret, { mode = mode, keybind = mapping.lhs, func = __TelescopeKeymapStore[prompt_bufnr][funcid] }) + end + end + return ret +end + return utils diff --git a/lua/telescope/mappings.lua b/lua/telescope/mappings.lua index f5f3655..fa2e2f5 100644 --- a/lua/telescope/mappings.lua +++ b/lua/telescope/mappings.lua @@ -30,6 +30,7 @@ mappings.default_mappings = config.values.default_mappings [""] = actions.send_to_qflist + actions.open_qflist, [""] = actions.send_selected_to_qflist + actions.open_qflist, [""] = actions.complete_tag, + [""] = actions.which_key, -- keys from pressing }, n = { @@ -56,6 +57,7 @@ mappings.default_mappings = config.values.default_mappings [""] = actions.preview_scrolling_up, [""] = actions.preview_scrolling_down, + ["?"] = actions.which_key, }, } @@ -222,6 +224,7 @@ mappings.execute_keymap = function(prompt_bufnr, keymap_identifier) assert(key_func, string.format("Unsure of how we got this failure: %s %s", prompt_bufnr, keymap_identifier)) key_func(prompt_bufnr) + vim.cmd [[ doautocmd User TelescopeKeymap ]] end mappings.clear = function(prompt_bufnr) diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index a13a424..b5be5ef 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -21,6 +21,10 @@ utils.get_default = function(x, default) return utils.if_nil(x, default, x) end +utils.cycle = function(i, n) + return i % n == 0 and n or i % n +end + utils.get_lazy_default = function(x, defaulter, ...) if x == nil then return defaulter(...) diff --git a/scripts/gendocs.lua b/scripts/gendocs.lua index 6972b21..025e077 100644 --- a/scripts/gendocs.lua +++ b/scripts/gendocs.lua @@ -19,6 +19,7 @@ docs.test = function() "./lua/telescope/actions/state.lua", "./lua/telescope/actions/set.lua", "./lua/telescope/actions/utils.lua", + "./lua/telescope/actions/generate.lua", "./lua/telescope/previewers/init.lua", "./lua/telescope/actions/history.lua", }