From 908f1803ee15717d69b55b14e1e2b02fb923d6c2 Mon Sep 17 00:00:00 2001 From: Tim Stack Date: Sun, 16 Feb 2025 22:52:04 -0800 Subject: [PATCH] [textinput] up arrow for history --- src/base/intern_string.hh | 4 +-- src/cmd.parser.cc | 51 ++++++++++++++++++++++++-- src/itertools.similar.hh | 8 +++++ src/lnav.prompt.cc | 69 ++++++++++++++++++++++++++++-------- src/lnav.prompt.hh | 3 +- src/log_vtab_impl.cc | 44 +++++++++++------------ src/readline_callbacks.cc | 32 +++++++++++------ src/readline_highlighters.cc | 8 ++--- src/textinput.history.cc | 4 +-- src/textinput_curses.cc | 27 +++++++++----- 10 files changed, 185 insertions(+), 65 deletions(-) diff --git a/src/base/intern_string.hh b/src/base/intern_string.hh index d0416bb3b4a..a76f1083c3c 100644 --- a/src/base/intern_string.hh +++ b/src/base/intern_string.hh @@ -982,10 +982,10 @@ operator==(const string_fragment& left, const intern_string_t& right) && (memcmp(left.data(), right.get(), left.length()) == 0); } -inline string_fragment +constexpr string_fragment operator"" _frag(const char* str, std::size_t len) { - return string_fragment::from_byte_range(str, 0, len); + return string_fragment{str, 0, (int) len}; } namespace std { diff --git a/src/cmd.parser.cc b/src/cmd.parser.cc index 67688f38bba..4b82d71b085 100644 --- a/src/cmd.parser.cc +++ b/src/cmd.parser.cc @@ -32,6 +32,7 @@ #include "cmd.parser.hh" +#include "data_scanner.hh" #include "shlex.hh" namespace lnav::command { @@ -42,7 +43,40 @@ parsed::arg_at(int x) const for (const auto& arg : this->p_args) { for (const auto& se : arg.second.a_values) { if (se.se_origin.sf_begin <= x && x <= se.se_origin.sf_end) { - return std::make_pair(arg.second.a_help, se); + switch (arg.second.a_help->ht_format) { + case help_parameter_format_t::HPF_TEXT: + case help_parameter_format_t::HPF_REGEX: { + data_scanner ds(se.se_value); + + while (true) { + auto tok_res = ds.tokenize2(); + + if (!tok_res) { + break; + } + auto tok = tok_res.value(); + + log_debug("cap b:%d x:%d e:%d", + tok.tr_capture.c_begin, + x, + tok.tr_capture.c_end); + if (tok.tr_capture.c_begin <= x + && x <= tok.tr_capture.c_end) + { + return std::make_pair( + arg.second.a_help, + shlex::split_element_t{ + tok.to_string_fragment(), + tok.to_string(), + }); + } + } + return std::make_pair(arg.second.a_help, + shlex::split_element_t{}); + } + default: + return std::make_pair(arg.second.a_help, se); + } } } } @@ -114,12 +148,25 @@ parse_for(mode_t mode, } do { + const auto& se = split_args[split_index]; switch (param.ht_format) { + case help_parameter_format_t::HPF_TEXT: + case help_parameter_format_t::HPF_REGEX: { + const auto& last_se = split_args.back(); + auto sf = string_fragment{ + se.se_origin.sf_string, + se.se_origin.sf_begin, + last_se.se_origin.sf_end, + }; + arg.a_values.emplace_back( + shlex::split_element_t{sf, sf.to_string()}); + split_index = split_args.size() - 1; + break; + } case help_parameter_format_t::HPF_STRING: case help_parameter_format_t::HPF_FILENAME: case help_parameter_format_t::HPF_LOADED_FILE: case help_parameter_format_t::HPF_FORMAT_FIELD: { - const auto& se = split_args[split_index]; if (!param.ht_enum_values.empty()) { auto enum_iter = std::find(param.ht_enum_values.begin(), param.ht_enum_values.end(), diff --git a/src/itertools.similar.hh b/src/itertools.similar.hh index b6e7dab6775..79055f4680c 100644 --- a/src/itertools.similar.hh +++ b/src/itertools.similar.hh @@ -98,6 +98,7 @@ operator|(const T& in, const lnav::itertools::details::similar_to& st) } std::priority_queue, score_cmp> pq; + auto exact_match = false; for (const auto& elem : in) { int score = 0; @@ -117,6 +118,9 @@ operator|(const T& in, const lnav::itertools::details::similar_to& st) if (score <= 0) { continue; } + if (st.st_pattern == estr) { + exact_match = true; + } pq.push(std::make_pair(score, elem)); if (pq.size() > st.st_count) { @@ -130,6 +134,10 @@ operator|(const T& in, const lnav::itertools::details::similar_to& st) } std::reverse(retval.begin(), retval.end()); + if (retval.size() == 1 && exact_match) { + retval.pop_back(); + } + return retval; } diff --git a/src/lnav.prompt.cc b/src/lnav.prompt.cc index b1aa77d84b2..86f2d191671 100644 --- a/src/lnav.prompt.cc +++ b/src/lnav.prompt.cc @@ -39,6 +39,7 @@ #include "itertools.similar.hh" #include "lnav.hh" #include "log_format_ext.hh" +#include "readline_highlighters.hh" #include "readline_possibilities.hh" #include "scn/scan.h" #include "shlex.hh" @@ -165,10 +166,10 @@ prompt& prompt::get() { static prompt retval = { - textinput::history::for_context(string_fragment::from_const("sql")), - textinput::history::for_context(string_fragment::from_const("cmd")), - textinput::history::for_context(string_fragment::from_const("search")), - textinput::history::for_context(string_fragment::from_const("script")), + textinput::history::for_context("sql"_frag), + textinput::history::for_context("cmd"_frag), + textinput::history::for_context("search"_frag), + textinput::history::for_context("script"_frag), }; return retval; @@ -245,14 +246,30 @@ prompt::refresh_sql_completions(textview_curses& tc) void prompt::rl_history(textinput_curses& tc) { - auto& hist = this->get_history_for(tc.tc_prefix.al_string.front()); + auto sigil = tc.tc_prefix.al_string.front(); + auto& hist = this->get_history_for(sigil); std::vector poss; - hist.query_entries(tc.get_content(), [&poss](const auto& e) { - poss.emplace_back( - attr_line_t() - .append(e.e_content) - .with_attr_for_all(SUBST_TEXT.value(e.e_content))); - }); + auto cb = [&poss, sigil](const auto& e) { + auto al = attr_line_t() + .append(e.e_content) + .with_attr_for_all(SUBST_TEXT.value(e.e_content)); + switch (sigil) { + case ':': + readline_command_highlighter(al, std::nullopt); + break; + case ';': + readline_sqlite_highlighter(al, std::nullopt); + break; + case '/': + readline_regex_highlighter(al, std::nullopt); + break; + } + poss.emplace_back(al.move()); + }; + hist.query_entries(tc.get_content(), cb); + if (poss.empty()) { + hist.query_entries(""_frag, cb); + } tc.open_popup_for_history(poss); } @@ -278,6 +295,7 @@ sql_item_visitor::operator()(const prompt::sql_keyword_t&) const static constexpr auto retval = prompt::sql_item_meta{ " ", "", + " ", role_t::VCR_KEYWORD, }; @@ -291,6 +309,7 @@ sql_item_visitor::operator()(const prompt::sql_collation_t&) const static constexpr auto retval = prompt::sql_item_meta{ " ", "", + " ", role_t::VCR_IDENTIFIER, }; @@ -304,6 +323,7 @@ sql_item_visitor::operator()(const prompt::sql_db_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\u26c1", "", + ".", role_t::VCR_IDENTIFIER, }; @@ -317,6 +337,7 @@ sql_item_visitor::operator()(const prompt::sql_table_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\U0001f143", "", + " ", role_t::VCR_IDENTIFIER, }; @@ -330,6 +351,7 @@ sql_item_visitor::operator()(const prompt::sql_table_valued_function_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\U0001D453", "()", + "(", role_t::VCR_FUNCTION, }; @@ -343,6 +365,7 @@ sql_item_visitor::operator()(const prompt::sql_function_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\U0001D453", "()", + "(", role_t::VCR_FUNCTION, }; @@ -356,6 +379,7 @@ sql_item_visitor::operator()(const prompt::sql_column_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\U0001F132", "", + "", role_t::VCR_IDENTIFIER, }; @@ -369,6 +393,7 @@ sql_item_visitor::operator()(const prompt::sql_number_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\U0001F13D", "", + " ", role_t::VCR_NUMBER, }; @@ -382,6 +407,7 @@ sql_item_visitor::operator()(const prompt::sql_string_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\U0001f142", "", + " ", role_t::VCR_STRING, }; @@ -395,6 +421,7 @@ sql_item_visitor::operator()(const prompt::sql_var_t&) const static constexpr auto retval = prompt::sql_item_meta{ "\U0001f145", "", + " ", role_t::VCR_VARIABLE, }; @@ -416,16 +443,30 @@ prompt::get_sql_completion_text(const std::pair& p) .append(" ") .append(p.first, VC_ROLE.value(item_meta.sim_role)) .append(item_meta.sim_display_suffix) - .with_attr_for_all(SUBST_TEXT.value(p.first)); + .with_attr_for_all( + SUBST_TEXT.value(p.first + item_meta.sim_replace_suffix)); } std::vector -prompt::get_cmd_parameter_completion(const help_text* ht, +prompt::get_cmd_parameter_completion(textview_curses& tc, + const help_text* ht, const std::string& str) { log_debug("cmd comp %s", str.c_str()); if (ht->ht_enum_values.empty()) { switch (ht->ht_format) { + case help_parameter_format_t::HPF_REGEX: { + std::vector retval; + auto poss_str = view_text_possibilities(tc) + | lnav::itertools::similar_to(str, 10); + + for (const auto& str : poss_str) { + retval.emplace_back( + attr_line_t().append(str).with_attr_for_all( + SUBST_TEXT.value(lnav::pcre2pp::quote(str)))); + } + return retval; + } case help_parameter_format_t::HPF_FILENAME: { auto str_as_path = std::filesystem::path{str}; auto parent = str_as_path.parent_path(); @@ -499,7 +540,7 @@ prompt::get_cmd_parameter_completion(const help_text* ht, return ht->ht_enum_values | lnav::itertools::similar_to(str, 10) | lnav::itertools::map([](const auto& x) { return attr_line_t(x).with_attr_for_all( - lnav::prompt::SUBST_TEXT.value(std::string(x) + " ")); + SUBST_TEXT.value(std::string(x) + " ")); }); } diff --git a/src/lnav.prompt.hh b/src/lnav.prompt.hh index 4c7968b9887..55c07c6a9b8 100644 --- a/src/lnav.prompt.hh +++ b/src/lnav.prompt.hh @@ -74,6 +74,7 @@ struct prompt { struct sql_item_meta { const char* sim_type_hint; const char* sim_display_suffix; + const char* sim_replace_suffix; role_t sim_role; }; @@ -108,7 +109,7 @@ struct prompt { const std::pair& p); std::vector get_cmd_parameter_completion( - const help_text* ht, const std::string& str); + textview_curses& tc, const help_text* ht, const std::string& str); void rl_history(textinput_curses& tc); void rl_completion(textinput_curses& tc); diff --git a/src/log_vtab_impl.cc b/src/log_vtab_impl.cc index b108c2cf401..e65739694d8 100644 --- a/src/log_vtab_impl.cc +++ b/src/log_vtab_impl.cc @@ -57,28 +57,28 @@ thread_local _log_vtab_data log_vtab_data; const std::unordered_set log_vtab_impl::RESERVED_COLUMNS = { - string_fragment::from_const("log_line"), - string_fragment::from_const("log_time"), - string_fragment::from_const("log_level"), - string_fragment::from_const("log_part"), - string_fragment::from_const("log_actual_time"), - string_fragment::from_const("log_idle_msecs"), - string_fragment::from_const("log_mark"), - string_fragment::from_const("log_comment"), - string_fragment::from_const("log_tags"), - string_fragment::from_const("log_annotations"), - string_fragment::from_const("log_filters"), - string_fragment::from_const("log_opid"), - string_fragment::from_const("log_user_opid"), - string_fragment::from_const("log_format"), - string_fragment::from_const("log_format_regex"), - string_fragment::from_const("log_time_msecs"), - string_fragment::from_const("log_path"), - string_fragment::from_const("log_unique_path"), - string_fragment::from_const("log_text"), - string_fragment::from_const("log_body"), - string_fragment::from_const("log_raw_text"), - string_fragment::from_const("log_line_hash"), + "log_line"_frag, + "log_time"_frag, + "log_level"_frag, + "log_part"_frag, + "log_actual_time"_frag, + "log_idle_msecs"_frag, + "log_mark"_frag, + "log_comment"_frag, + "log_tags"_frag, + "log_annotations"_frag, + "log_filters"_frag, + "log_opid"_frag, + "log_user_opid"_frag, + "log_format"_frag, + "log_format_regex"_frag, + "log_time_msecs"_frag, + "log_path"_frag, + "log_unique_path"_frag, + "log_text"_frag, + "log_body"_frag, + "log_raw_text"_frag, + "log_line_hash"_frag, }; static const char* const LOG_COLUMNS = R"( ( diff --git a/src/readline_callbacks.cc b/src/readline_callbacks.cc index b76adc1dd2a..4080a7e17fb 100644 --- a/src/readline_callbacks.cc +++ b/src/readline_callbacks.cc @@ -451,21 +451,30 @@ rl_change(textinput_curses& rc) if (iter == lnav_commands.end() || (args.size() == 1 && !endswith(line, " "))) { - log_debug("unknown command: %s", args[0].c_str()); switch (rc.tc_popup_type) { case textinput_curses::popup_type_t::history: { rc.tc_on_history(rc); break; } default: { - auto poss - = lnav_commands | lnav::itertools::first() + auto poss_str = lnav_commands | lnav::itertools::first() | lnav::itertools::similar_to( - args.empty() ? "" : args[0], 10) - | lnav::itertools::map([](const auto& x) { + args.empty() ? "" : args[0], 10); + auto poss_width = poss_str + | lnav::itertools::map(&std::string::size) + | lnav::itertools::max(); + + auto poss + = poss_str + | lnav::itertools::map([&poss_width]( + const auto& x) { return attr_line_t() .append( x, VC_ROLE.value(role_t::VCR_KEYWORD)) + .append(" ") + .pad_to(poss_width.value_or(0) + 1) + .append( + lnav_commands[x]->c_help.ht_summary) .with_attr_for_all( lnav::prompt::SUBST_TEXT.value( x + " ")); @@ -548,12 +557,13 @@ rl_change(textinput_curses& rc) if (arg_pair_opt) { auto arg_pair = arg_pair_opt.value(); - log_debug("apair %s [%d:%d)", + log_debug("apair %s [%d:%d) -- %s", arg_pair.first->ht_name, arg_pair.second.se_origin.sf_begin, - arg_pair.second.se_origin.sf_end); + arg_pair.second.se_origin.sf_end, + arg_pair.second.se_value.c_str()); auto poss = prompt.get_cmd_parameter_completion( - arg_pair.first, arg_pair.second.se_value); + *tc, arg_pair.first, arg_pair.second.se_value); auto left = arg_pair.second.se_value.empty() ? rc.tc_cursor.x : line_sf.byte_to_column_index( @@ -1048,7 +1058,8 @@ rl_callback_int(textinput_curses& rc, bool is_alt) auto cmdline = rc.get_content(); rl_search_internal(rc, old_mode, true); if (!cmdline.empty()) { - auto hist_guard = prompt.p_search_history.start_operation(cmdline); + auto hist_guard + = prompt.p_search_history.start_operation(cmdline); auto& bm = tc->get_bookmarks(); const auto& bv = bm[&textview_curses::BM_SEARCH]; auto vl = is_alt ? bv.prev(tc->get_selection()) @@ -1173,7 +1184,8 @@ rl_callback_int(textinput_curses& rc, bool is_alt) fclose)); auto src_guard = lnav_data.ld_exec_context.enter_source( SRC, 1, fmt::format(FMT_STRING("|{}"), path_and_args)); - auto hist_guard = prompt.p_script_history.start_operation(path_and_args); + auto hist_guard = prompt.p_script_history.start_operation( + path_and_args); auto exec_res = execute_file(ec, path_and_args); if (exec_res.isOk()) { rc.tc_inactive_value = exec_res.unwrap(); diff --git a/src/readline_highlighters.cc b/src/readline_highlighters.cc index 15bad2a9dc5..b1a1449cc6e 100644 --- a/src/readline_highlighters.cc +++ b/src/readline_highlighters.cc @@ -524,20 +524,20 @@ readline_lnav_highlighter(attr_line_t& al, std::optional x) void highlight_syntax(text_format_t tf, attr_line_t& al) { - switch (tf) { + switch (tf) { case text_format_t::TF_SQL: { readline_sqlite_highlighter(al, std::nullopt); break; } - case text_format_t::TF_PCRE: { + case text_format_t::TF_PCRE: { readline_regex_highlighter(al, std::nullopt); break; } - case text_format_t::TF_SHELL_SCRIPT: { + case text_format_t::TF_SHELL_SCRIPT: { readline_shlex_highlighter(al, std::nullopt); break; } - case text_format_t::TF_LNAV_SCRIPT: { + case text_format_t::TF_LNAV_SCRIPT: { readline_lnav_highlighter(al, std::nullopt); break; } diff --git a/src/textinput.history.cc b/src/textinput.history.cc index 98ef21e8596..1423d219750 100644 --- a/src/textinput.history.cc +++ b/src/textinput.history.cc @@ -237,7 +237,7 @@ history::insert_plain_content(string_fragment content) constexpr auto FUZZY_QUERY = R"( SELECT session_id, - max(create_time_us), + max(create_time_us) as max_create_time, NULL, content, status @@ -245,7 +245,7 @@ SELECT WHERE context = ?1 AND fuzzy_match(?2, content) IS NOT NULL GROUP BY content - ORDER BY fuzzy_match(?2, content) DESC + ORDER BY fuzzy_match(?2, content), max_create_time DESC LIMIT 50 )"; diff --git a/src/textinput_curses.cc b/src/textinput_curses.cc index bc1eb3754ec..015f908a67e 100644 --- a/src/textinput_curses.cc +++ b/src/textinput_curses.cc @@ -970,6 +970,10 @@ textinput_curses::handle_key(const ncinput& ch) case NCKEY_UP: { if (this->tc_popup.is_visible()) { this->tc_popup.handle_key(ch); + } else if (this->tc_height == 1) { + if (this->tc_on_history) { + this->tc_on_history(*this); + } } else { if (ncinput_shift_p(&ch)) { log_debug("up shift"); @@ -1644,8 +1648,9 @@ textinput_curses::open_popup_for_completion( | lnav::itertools::max(); auto full_width = std::min((int) max_width.value_or(1) + 2, dim.dr_width); - auto popup_height - = vis_line_t(std::min(this->tc_max_popup_height, possibilities.size())); + auto new_sel = 0_vl; + auto popup_height = vis_line_t( + std::min(this->tc_max_popup_height, possibilities.size() + 1)); auto rel_x = left; if (this->tc_cursor.y == 0) { rel_x += this->tc_prefix.column_width(); @@ -1656,6 +1661,8 @@ textinput_curses::open_popup_for_completion( auto rel_y = this->tc_cursor.y - this->tc_top + 1; if (this->vc_y + rel_y + popup_height > dim.dr_full_height) { rel_y = this->tc_cursor.y - this->tc_top - popup_height; + std::reverse(possibilities.begin(), possibilities.end()); + new_sel = vis_line_t(possibilities.size() - 1); } this->tc_complete_range = selected_range::from_key( @@ -1667,7 +1674,8 @@ textinput_curses::open_popup_for_completion( this->tc_popup.set_width(full_width); this->tc_popup.set_height(popup_height); this->tc_popup.set_visible(true); - this->tc_popup.set_selection(0_vl); + this->tc_popup.set_top(0_vl); + this->tc_popup.set_selection(new_sel); this->set_needs_update(); } @@ -1681,11 +1689,14 @@ textinput_curses::open_popup_for_history(std::vector possibilities) this->tc_popup_type = popup_type_t::history; auto dim = this->get_visible_dimensions(); - auto popup_height - = vis_line_t(std::min(this->tc_max_popup_height, possibilities.size())); + auto new_sel = 0_vl; + auto popup_height = vis_line_t( + std::min(this->tc_max_popup_height, possibilities.size() + 1)); auto rel_y = this->tc_cursor.y - this->tc_top + 1; - if (this->vc_y + rel_y + popup_height > dim.dr_full_height) { + if (this->vc_y + rel_y + popup_height >= dim.dr_full_height) { rel_y = this->tc_cursor.y - this->tc_top - popup_height; + std::reverse(possibilities.begin(), possibilities.end()); + new_sel = vis_line_t(possibilities.size() - 1); } this->tc_complete_range = selected_range::from_key( @@ -1698,10 +1709,10 @@ textinput_curses::open_popup_for_history(std::vector possibilities) this->tc_popup.set_window(this->tc_window); this->tc_popup.set_x(this->vc_x); this->tc_popup.set_y(this->vc_y + rel_y); - log_debug("pop width %d", this->vc_width); this->tc_popup.set_width(this->vc_width); this->tc_popup.set_height(popup_height); + this->tc_popup.set_top(0_vl); + this->tc_popup.set_selection(new_sel); this->tc_popup.set_visible(true); - this->tc_popup.set_selection(0_vl); this->set_needs_update(); }