From 1f11529837c69781179c7deea832f0b8b266305f Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Mon, 20 Nov 2023 20:02:36 +0100 Subject: [PATCH] rc tools menu: replace menu builtin with a prompt-based implementation prompt has fuzzy filtering which is more discoverable than the menu mode's regex filtering (because that one needs / to trigger it). There are no important differences left, so replace the menu builtin with a prompt-based command. prompt does not support markup in the completion menu, so drop that feature for now. --- README.asciidoc | 9 --- doc/pages/commands.asciidoc | 13 ---- doc/pages/mapping.asciidoc | 3 - doc/pages/modes.asciidoc | 5 -- rc/filetype/kakrc.kak | 4 +- rc/tools/ctags.kak | 6 +- rc/tools/menu.kak | 80 ++++++++++++++++++++ src/commands.cc | 60 --------------- src/input_handler.cc | 142 ------------------------------------ src/input_handler.hh | 14 ---- 10 files changed, 85 insertions(+), 251 deletions(-) create mode 100644 rc/tools/menu.kak diff --git a/README.asciidoc b/README.asciidoc index f759a3e39..7b960245e 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -879,15 +879,6 @@ progressively displaying their result in Kakoune. See <>. -Menus -~~~~~ - -When a menu is displayed, you can use `j`, `` or `` to select the next -entry, and `k`, `` or `` to select the previous one. - -Using the `/` key, you can enter some regex in order to restrict available choices -to the matching ones. - Credits ------- diff --git a/doc/pages/commands.asciidoc b/doc/pages/commands.asciidoc index 5b158ef83..e88094007 100644 --- a/doc/pages/commands.asciidoc +++ b/doc/pages/commands.asciidoc @@ -342,19 +342,6 @@ but not really useful in that context. so inside a draft context like `evaluate-commands -draft`, it only responds to an `execute-keys` command in the same context. -*menu* [] ...:: - display a menu using labels, the selected label’s commands are - executed. The *menu* command can take an *-auto-single* argument, to automatically - run commands when only one choice is provided, and a *-select-cmds* - argument, in which case menu takes three argument per item, the - last one being a command to execute when the item is selected (but - not validated) - - NOTE: The menu is displayed in and receives input from the - current client context, so inside a draft context like - `evaluate-commands -draft`, it is invisible and only responds to - an `execute-keys` command in the same context. - *info* [] :: display text in an information box with the following *switches*: diff --git a/doc/pages/mapping.asciidoc b/doc/pages/mapping.asciidoc index e96845ca9..5b32c1c28 100644 --- a/doc/pages/mapping.asciidoc +++ b/doc/pages/mapping.asciidoc @@ -23,9 +23,6 @@ The *map* command makes *key* behave as if the *keys* sequence was typed. *prompt*:: prompts, such as when entering a command through *:*, or a regex through */* - *menu*:: - mode entered when a menu is displayed with the 'menu' command - *user*:: mode entered when the user prefix is hit (default: '') diff --git a/doc/pages/modes.asciidoc b/doc/pages/modes.asciidoc index f739ae98e..8a73ae403 100644 --- a/doc/pages/modes.asciidoc +++ b/doc/pages/modes.asciidoc @@ -67,11 +67,6 @@ as scrolling or centering the main selection cursor. See view commands <>. -=== Menu mode - -Menu mode is entered when a menu is displayed with the `menu` command. -Mappings are used to filter and select intended items. - === Prompt mode Mode entered with `:`, `/` or the `prompt` command. During prompt mode a diff --git a/rc/filetype/kakrc.kak b/rc/filetype/kakrc.kak index c58b6966b..1914e4645 100644 --- a/rc/filetype/kakrc.kak +++ b/rc/filetype/kakrc.kak @@ -58,13 +58,13 @@ evaluate-commands %sh{ keywords="add-highlighter alias arrange-buffers buffer buffer-next buffer-previous catch change-directory colorscheme debug declare-option declare-user-mode define-command complete-command delete-buffer delete-buffer! echo edit edit! enter-user-mode evaluate-commands execute-keys - fail hook info kill kill! map menu nop on-key prompt provide-module quit quit! + fail hook info kill kill! map nop on-key prompt provide-module quit quit! remove-highlighter remove-hooks rename-buffer rename-client rename-session require-module select set-face set-option set-register source trigger-user-hook try unalias unmap unset-face unset-option update-option write write! write-all write-all-quit write-quit write-quit!" attributes="global buffer window current - normal insert menu prompt goto view user object + normal insert prompt goto view user object number-lines show-matching show-whitespaces fill regex dynregex group flag-lines ranges line column wrap ref regions region default-region replace-ranges" types="int bool str regex int-list str-list completions line-specs range-specs str-to-str-map" diff --git a/rc/tools/ctags.kak b/rc/tools/ctags.kak index 7653562a8..59992c018 100644 --- a/rc/tools/ctags.kak +++ b/rc/tools/ctags.kak @@ -30,7 +30,7 @@ define-command -params ..1 \ ctags-search []: jump to a symbol's definition If no symbol is passed then the current selection is used as symbol name } \ - ctags-search %[ evaluate-commands %sh[ + ctags-search %[ require-module menu; evaluate-commands %sh[ realpath() { ( cd "$(dirname "$1")"; printf "%s/%s\n" "$(pwd -P)" "$(basename "$1")" ) } export tagname="${1:-${kak_selection}}" eval "set -- $kak_quoted_opt_ctagsfiles" @@ -49,7 +49,7 @@ define-command -params ..1 \ menu_item = $2; gsub("!", "!!", menu_item); edit_path = path($2); gsub("&", "&&", edit_path); gsub("#", "##", edit_path); gsub("\\|", "||", edit_path); select = $1; gsub(/", select); gsub(/\t/, "", select); gsub("!", "!!", select); gsub("&", "&&", select); gsub("#", "##", select); gsub("\\|", "||", select); - out = out "%!" menu_item ": {MenuInfo}{\\}" menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|/\\Q" keys "vc| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "| & # !" + out = out "%!" menu_item ": " menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|/\\Q" keys "vc| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "| & # !" } /[^\t]+\t[^\t]+\t[0-9]+/ { menu_item = $2; gsub("!", "!!", menu_item); @@ -57,7 +57,7 @@ define-command -params ..1 \ menu_info = $3; gsub("!", "!!", menu_info); edit_path = path($2); gsub("!", "!!", edit_path); gsub("#", "##", edit_path); gsub("&", "&&", edit_path); gsub("\\|", "||", edit_path); line_number = $3; - out = out "%!" menu_item ": {MenuInfo}{\\}" menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|" line_number "gx| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "| & # !" + out = out "%!" menu_item ": " menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|" line_number "gx| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "| & # !" } END { print ( length(out) == 0 ? "fail no such tag " ENVIRON["tagname"] : "menu -markup -auto-single " out ) } # Ensure x is an absolute file path, by prepending with tagroot diff --git a/rc/tools/menu.kak b/rc/tools/menu.kak new file mode 100644 index 000000000..8615afe0f --- /dev/null +++ b/rc/tools/menu.kak @@ -0,0 +1,80 @@ +provide-module menu %§§ + +define-command menu -params 1.. -docstring %{ + menu [] ...: display a + menu and execute commands for the selected item + + -auto-single instantly validate if only one item is available + -select-cmds each item specify an additional command to run when selected +} %{ + evaluate-commands %sh{ + auto_single=false + select_cmds=false + stride=2 + while true + do + case "$1" in + (-auto-single) auto_single=true ;; + (-select-cmds) select_cmds=true; stride=3 ;; + (-markup) ;; # no longer supported + (*) break ;; + esac + shift + done + if [ $(( $# % $stride )) -ne 0 ]; then + echo fail "wrong argument count" + exit + fi + if $auto_single && [ $# -eq $stride ]; then + printf %s "$2" + exit + fi + shellquote() { + printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g; s/§/§§/g; $2")" + } + cases= + select_cases= + completion= + nl=$(printf '\n.'); nl=${nl%.} + while [ $# -gt 0 ]; do + title=$1 + command=$2 + completion="${completion}${title}${nl}" + cases="${cases} + ($(shellquote "$title" s/¶/¶¶/g)) + printf '%s\\n' $(shellquote "$command" s/¶/¶¶/g) + ;;" + if $select_cmds; then + select_command=$3 + select_cases="${select_cases} + ($(shellquote "$title" s/¶/¶¶/g)) + printf '%s\\n' $(shellquote "$select_command" s/¶/¶¶/g) + ;;" + fi + shift $stride + done + printf "\ + prompt '' %%§ + evaluate-commands %%sh¶ + case \"\$kak_text\" in \ + %s + (*) echo fail -- no such item: \"'\$(printf %%s \"\$kak_text\" | sed \"s/'/''/g\")'\" ;; + esac + ¶ + §" "$cases" + if $select_cmds; then + printf " \ + -on-change %%§ + evaluate-commands %%sh¶ + case \"\$kak_text\" in \ + %s + (*) : ;; + esac + ¶ + §" "$select_cases" + fi + printf ' -menu -shell-script-candidates %%§ + printf %%s %s + §\n' "$(shellquote "$completion")" + } +} diff --git a/src/commands.cc b/src/commands.cc index 1c6ea09ad..5c3b5f39d 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -2274,65 +2274,6 @@ const CommandDesc prompt_cmd = { } }; -const CommandDesc menu_cmd = { - "menu", - nullptr, - "menu [] ...: display a " - "menu and execute commands for the selected item", - ParameterDesc{ - { { "auto-single", { {}, "instantly validate if only one item is available" } }, - { "select-cmds", { {}, "each item specify an additional command to run when selected" } }, - { "markup", { {}, "parse menu entries as markup text" } } } - }, - CommandFlags::None, - CommandHelper{}, - CommandCompleter{}, - [](const ParametersParser& parser, Context& context, const ShellContext& shell_context) - { - const bool with_select_cmds = (bool)parser.get_switch("select-cmds"); - const bool markup = (bool)parser.get_switch("markup"); - const size_t modulo = with_select_cmds ? 3 : 2; - - const size_t count = parser.positional_count(); - if (count == 0 or (count % modulo) != 0) - throw wrong_argument_count(); - - if (count == modulo and parser.get_switch("auto-single")) - { - ScopedSetBool noninteractive{context.noninteractive()}; - - CommandManager::instance().execute(parser[1], context); - return; - } - - Vector choices; - Vector commands; - Vector select_cmds; - for (int i = 0; i < count; i += modulo) - { - if (parser[i].empty()) - throw runtime_error(format("entry #{} is empty", i+1)); - - choices.push_back(markup ? parse_display_line(parser[i], context.faces()) - : DisplayLine{ parser[i], {} }); - commands.push_back(parser[i+1]); - if (with_select_cmds) - select_cmds.push_back(parser[i+2]); - } - - CapturedShellContext sc{shell_context}; - context.input_handler().menu(std::move(choices), - [=](int choice, MenuEvent event, Context& context) { - ScopedSetBool noninteractive{context.noninteractive()}; - - if (event == MenuEvent::Validate and choice >= 0 and choice < commands.size()) - CommandManager::instance().execute(commands[choice], context, sc); - if (event == MenuEvent::Select and choice >= 0 and choice < select_cmds.size()) - CommandManager::instance().execute(select_cmds[choice], context, sc); - }); - } -}; - const CommandDesc on_key_cmd = { "on-key", nullptr, @@ -2820,7 +2761,6 @@ void register_commands() register_command(execute_keys_cmd); register_command(evaluate_commands_cmd); register_command(prompt_cmd); - register_command(menu_cmd); register_command(on_key_cmd); register_command(info_cmd); register_command(try_catch_cmd); diff --git a/src/input_handler.cc b/src/input_handler.cc index 9bc495a2d..dc9cd35e4 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -632,143 +632,6 @@ private: const FaceRegistry& m_faces; }; -class Menu : public InputMode -{ -public: - Menu(InputHandler& input_handler, Vector choices, - MenuCallback callback) - : InputMode(input_handler), - m_callback(std::move(callback)), m_choices(choices.begin(), choices.end()), - m_selected(m_choices.begin()), - m_filter_editor{context().faces()} - { - if (not context().has_client()) - return; - context().client().menu_show(std::move(choices), {}, MenuStyle::Prompt); - context().client().menu_select(0); - } - - void on_key(Key key, bool) override - { - auto match_filter = [this](const DisplayLine& choice) { - for (auto& atom : choice) - { - const auto& contents = atom.content(); - if (regex_match(contents.begin(), contents.end(), m_filter)) - return true; - } - return false; - }; - - if (key == Key::Return) - { - if (context().has_client()) - context().client().menu_hide(); - context().print_status(DisplayLine{}); - - // Maintain hooks disabled in callback if they were before pop_mode - ScopedSetBool disable_hooks(context().hooks_disabled(), - context().hooks_disabled()); - pop_mode(); - int selected = m_selected - m_choices.begin(); - m_callback(selected, MenuEvent::Validate, context()); - return; - } - else if (key == Key::Escape or key == ctrl('c')) - { - if (m_edit_filter) - { - m_edit_filter = false; - m_filter = Regex{".*"}; - m_filter_editor.reset("", ""); - context().print_status(DisplayLine{}); - } - else - { - if (context().has_client()) - context().client().menu_hide(); - - // Maintain hooks disabled in callback if they were before pop_mode - ScopedSetBool disable_hooks(context().hooks_disabled(), - context().hooks_disabled()); - pop_mode(); - int selected = m_selected - m_choices.begin(); - m_callback(selected, MenuEvent::Abort, context()); - } - } - else if (key == Key::Down or key == Key::Tab or - key == ctrl('n') or (not m_edit_filter and key == 'j')) - { - auto it = std::find_if(m_selected+1, m_choices.end(), match_filter); - if (it == m_choices.end()) - it = std::find_if(m_choices.begin(), m_selected, match_filter); - select(it); - } - else if (key == Key::Up or key == shift(Key::Tab) or - key == ctrl('p') or (not m_edit_filter and key == 'k')) - { - ChoiceList::const_reverse_iterator selected(m_selected+1); - auto it = std::find_if(selected+1, m_choices.rend(), match_filter); - if (it == m_choices.rend()) - it = std::find_if(m_choices.rbegin(), selected, match_filter); - select(it.base()-1); - } - else if (key == '/' and not m_edit_filter) - { - m_edit_filter = true; - } - else if (m_edit_filter) - { - m_filter_editor.handle_key(key); - - auto search = ".*" + m_filter_editor.line() + ".*"; - m_filter = Regex{search}; - auto it = std::find_if(m_selected, m_choices.end(), match_filter); - if (it == m_choices.end()) - it = std::find_if(m_choices.begin(), m_selected, match_filter); - select(it); - } - - if (m_edit_filter and context().has_client()) - { - auto prompt = "filter:"_str; - auto width = context().client().dimensions().column - prompt.column_length(); - auto display_line = m_filter_editor.build_display_line(width); - display_line.insert(display_line.begin(), { prompt, context().faces()["Prompt"] }); - context().print_status(display_line); - } - } - - DisplayLine mode_line() const override - { - return { "menu", context().faces()["StatusLineMode"] }; - } - - KeymapMode keymap_mode() const override { return KeymapMode::Menu; } - - StringView name() const override { return "menu"; } - -private: - MenuCallback m_callback; - - using ChoiceList = Vector; - const ChoiceList m_choices; - ChoiceList::const_iterator m_selected; - - void select(ChoiceList::const_iterator it) - { - m_selected = it; - int selected = m_selected - m_choices.begin(); - if (context().has_client()) - context().client().menu_select(selected); - m_callback(selected, MenuEvent::Select, context()); - } - - Regex m_filter = Regex{".*"}; - bool m_edit_filter = false; - LineEditor m_filter_editor; -}; - static Optional get_raw_codepoint(Key key) { if (auto cp = key.codepoint()) @@ -1744,11 +1607,6 @@ void InputHandler::set_prompt_face(Face prompt_face) prompt->set_prompt_face(prompt_face); } -void InputHandler::menu(Vector choices, MenuCallback callback) -{ - push_mode(new InputModes::Menu(*this, std::move(choices), std::move(callback))); -} - void InputHandler::on_next_key(StringView mode_name, KeymapMode keymap_mode, KeyCallback callback, Timer::Callback idle_callback) { diff --git a/src/input_handler.hh b/src/input_handler.hh index a26d392c5..eeb42464d 100644 --- a/src/input_handler.hh +++ b/src/input_handler.hh @@ -16,14 +16,6 @@ namespace Kakoune { -enum class MenuEvent -{ - Select, - Abort, - Validate -}; -using MenuCallback = std::function; - enum class PromptEvent { Change, @@ -85,12 +77,6 @@ public: void set_prompt_face(Face prompt_face); bool history_enabled() const; - // enter menu mode, callback is called on each selection change, - // abort or validation with corresponding MenuEvent value - // returns to normal mode after validation if callback does - // not change the mode itself - void menu(Vector choices, MenuCallback callback); - // execute callback on next keypress and returns to normal mode // if callback does not change the mode itself void on_next_key(StringView mode_name, KeymapMode mode, KeyCallback callback,