Merge remote-tracking branch 'krobelus/fuzzy-menu'
This commit is contained in:
commit
c93c57a46f
|
@ -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
|
||||
-------
|
||||
|
||||
|
|
|
@ -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 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* [<switches>] <text>::
|
||||
display text in an information box with the following *switches*:
|
||||
|
||||
|
|
|
@ -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>')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
85
rc/tools/menu.kak
Normal file
85
rc/tools/menu.kak
Normal file
|
@ -0,0 +1,85 @@
|
|||
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
|
||||
on_abort=
|
||||
while true
|
||||
do
|
||||
case "$1" in
|
||||
(-auto-single) auto_single=true ;;
|
||||
(-select-cmds) select_cmds=true; stride=3 ;;
|
||||
(-on-abort) on_abort="$2"; shift ;;
|
||||
(-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
|
||||
if [ -n "$on_abort" ]; then
|
||||
printf " -on-abort '%s'" "$(printf %s "$on_abort" | sed "s/'/''/g")"
|
||||
fi
|
||||
printf ' -menu -shell-script-candidates %%§
|
||||
printf %%s %s
|
||||
§\n' "$(shellquote "$completion")"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue
Block a user