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.
This commit is contained in:
Johannes Altmanninger 2023-11-20 20:02:36 +01:00
parent 4499b26ca4
commit 1f11529837
10 changed files with 85 additions and 251 deletions

View File

@ -879,15 +879,6 @@ progressively displaying their result in Kakoune.
See <<doc/pages/buffers#fifo-buffers,`:doc buffers fifo-buffers`>>.
Menus
~~~~~
When a menu is displayed, you can use `j`, `<c-n>` or `<tab>` to select the next
entry, and `k`, `<c-p>` or `<shift-tab>` to select the previous one.
Using the `/` key, you can enter some regex in order to restrict available choices
to the matching ones.
Credits
-------

View File

@ -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* [<switches>] <label1> <commands1> <label2> <commands2> ...::
display a menu using labels, the selected labels 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* [<switches>] <text>::
display text in an information box with the following *switches*:

View File

@ -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: '<space>')

View File

@ -67,11 +67,6 @@ as scrolling or centering the main selection cursor.
See view commands <<keys#view-commands,`:doc keys 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

View File

@ -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"

View File

@ -30,7 +30,7 @@ define-command -params ..1 \
ctags-search [<symbol>]: 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(/</, "<lt>", select); gsub(/\t/, "<c-v><c-i>", 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 "<ret>vc| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "<ret>| & # !"
out = out "%!" menu_item ": " menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|/\\Q" keys "<ret>vc| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "<ret>| & # !"
}
/[^\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 "<ret>| & # !"
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 "<ret>| & # !"
}
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

80
rc/tools/menu.kak Normal file
View File

@ -0,0 +1,80 @@
provide-module menu %§§
define-command menu -params 1.. -docstring %{
menu [<switches>] <name1> <commands1> <name2> <commands2>...: 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")"
}
}

View File

@ -2274,65 +2274,6 @@ const CommandDesc prompt_cmd = {
}
};
const CommandDesc menu_cmd = {
"menu",
nullptr,
"menu [<switches>] <name1> <commands1> <name2> <commands2>...: 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<DisplayLine> choices;
Vector<String> commands;
Vector<String> 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);

View File

@ -632,143 +632,6 @@ private:
const FaceRegistry& m_faces;
};
class Menu : public InputMode
{
public:
Menu(InputHandler& input_handler, Vector<DisplayLine> 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<DisplayLine>;
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<Codepoint> 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<DisplayLine> 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)
{

View File

@ -16,14 +16,6 @@
namespace Kakoune
{
enum class MenuEvent
{
Select,
Abort,
Validate
};
using MenuCallback = std::function<void (int, MenuEvent, Context&)>;
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<DisplayLine> 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,