From 45d8e27ae4bd1adb820a45b4aeeab869f2258961 Mon Sep 17 00:00:00 2001 From: Tim Stack Date: Thu, 18 Apr 2024 21:44:38 -0700 Subject: [PATCH] [mouse] refactor overlay menu --- src/CMakeLists.txt | 2 + src/Makefile.am | 2 + src/base/intern_string.cc | 3 +- src/base/is_utf8.cc | 4 ++ src/base/is_utf8.hh | 1 + src/base/string_util.cc | 74 ++++++++++++++++++++++ src/base/string_util.hh | 12 +++- src/data_scanner.cc | 99 +++++++++++++++++++++++++++++ src/data_scanner.hh | 25 ++++++++ src/data_scanner_re.cc | 4 +- src/data_scanner_re.re | 2 +- src/field_overlay_source.cc | 81 ------------------------ src/field_overlay_source.hh | 22 +------ src/gantt_source.hh | 3 +- src/hotkeys.cc | 21 ++++-- src/init.sql | 2 +- src/listview_curses.cc | 32 +++++++++- src/listview_curses.hh | 31 ++------- src/lnav.cc | 21 ++++++ src/lnav_commands.cc | 2 +- src/logfile.cc | 3 +- src/logfile_sub_source.cc | 20 +----- src/pcrepp/pcre2pp.cc | 32 +--------- src/pcrepp/pcre2pp.hh | 8 --- src/text_overlay_menu.cc | 123 ++++++++++++++++++++++++++++++++++++ src/text_overlay_menu.hh | 55 ++++++++++++++++ src/textfile_sub_source.hh | 3 +- src/textview_curses.cc | 56 ++++++++++++++-- src/textview_curses.hh | 1 + src/top_status_source.cc | 2 +- src/view_curses.cc | 9 ++- 31 files changed, 547 insertions(+), 208 deletions(-) create mode 100644 src/text_overlay_menu.cc create mode 100644 src/text_overlay_menu.hh diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 63c6d633..4600fe84 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -482,6 +482,7 @@ add_library( styling.cc text_anonymizer.cc text_format.cc + text_overlay_menu.cc textfile_highlighters.cc textfile_sub_source.cc textview_curses.cc @@ -608,6 +609,7 @@ add_library( termios_guard.hh text_anonymizer.hh text_format.hh + text_overlay_menu.hh textfile_highlighters.hh textfile_sub_source.hh textview_curses.hh diff --git a/src/Makefile.am b/src/Makefile.am index b4d004af..31fd6b5f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -349,6 +349,7 @@ noinst_HEADERS = \ term_extra.hh \ text_anonymizer.hh \ text_format.hh \ + text_overlay_menu.hh \ textfile_highlighters.hh \ textfile_sub_source.hh \ textview_curses.hh \ @@ -506,6 +507,7 @@ libdiag_a_SOURCES = \ styling.cc \ text_anonymizer.cc \ text_format.cc \ + text_overlay_menu.cc \ textfile_sub_source.cc \ timer.cc \ sql_commands.cc \ diff --git a/src/base/intern_string.cc b/src/base/intern_string.cc index 12dadc23..010da524 100644 --- a/src/base/intern_string.cc +++ b/src/base/intern_string.cc @@ -399,8 +399,9 @@ string_fragment::sub_cell_range(int cell_start, int cell_end) const if (cell_start == cell_index) { byte_start = byte_index; } - if (cell_index == cell_end) { + if (!byte_end && cell_index >= cell_end) { byte_end = byte_index; + break; } auto read_res = ww898::utf::utf8::read( [this, &byte_index]() { return this->sf_string[byte_index++]; }); diff --git a/src/base/is_utf8.cc b/src/base/is_utf8.cc index 714d8c2f..b8fbab03 100644 --- a/src/base/is_utf8.cc +++ b/src/base/is_utf8.cc @@ -76,6 +76,7 @@ is_utf8(string_fragment str, nonstd::optional terminator) break; } + retval.usr_column_width_guess += 1; if (retval.usr_message != nullptr) { i += 1; continue; @@ -83,6 +84,9 @@ is_utf8(string_fragment str, nonstd::optional terminator) valid_end = i; if (ustr[i] <= 0x7F) /* 00..7F */ { + if (ustr[i] == '\t') { + retval.usr_column_width_guess += 7; + } i += 1; } else if (ustr[i] >= 0xC2 && ustr[i] <= 0xDF) /* C2..DF 80..BF */ { if (i + 1 < str.length()) /* Expect a 2nd byte */ { diff --git a/src/base/is_utf8.hh b/src/base/is_utf8.hh index 56a959f5..81d09c69 100644 --- a/src/base/is_utf8.hh +++ b/src/base/is_utf8.hh @@ -40,6 +40,7 @@ struct utf8_scan_result { string_fragment usr_valid_frag{string_fragment::invalid()}; nonstd::optional usr_remaining; bool usr_has_ansi{false}; + size_t usr_column_width_guess{0}; const char* remaining_ptr(const string_fragment& frag) const { diff --git a/src/base/string_util.cc b/src/base/string_util.cc index 90c8f86b..39e80f6c 100644 --- a/src/base/string_util.cc +++ b/src/base/string_util.cc @@ -348,3 +348,77 @@ formatter::format(const lnav::tainted_string& ts, return format_to(ctx.out(), FMT_STRING("{:?}"), ts.ts_str); } } // namespace fmt + +namespace lnav { +namespace pcre2pp { + +static bool +is_meta(char ch) +{ + switch (ch) { + case '\\': + case '^': + case '$': + case '.': + case '[': + case ']': + case '(': + case ')': + case '*': + case '+': + case '?': + case '{': + case '}': + return true; + default: + return false; + } +} + +static nonstd::optional +char_escape_seq(char ch) +{ + switch (ch) { + case '\t': + return "\\t"; + case '\n': + return "\\n"; + } + + return nonstd::nullopt; +} + +std::string +quote(string_fragment str) +{ + std::string retval; + + while (true) { + auto cp_pair_opt = str.consume_codepoint(); + if (!cp_pair_opt) { + break; + } + + auto cp_pair = cp_pair_opt.value(); + if ((cp_pair.first & ~0xff) == 0) { + if (is_meta(cp_pair.first)) { + retval.push_back('\\'); + } else { + auto esc_seq = char_escape_seq(cp_pair.first); + if (esc_seq) { + retval.append(esc_seq.value()); + str = cp_pair_opt->second; + continue; + } + } + } + ww898::utf::utf8::write(cp_pair.first, + [&retval](char ch) { retval.push_back(ch); }); + str = cp_pair_opt->second; + } + + return retval; +} + +} // namespace pcre2pp +} // namespace lnav diff --git a/src/base/string_util.hh b/src/base/string_util.hh index 95cf5310..886fb6c2 100644 --- a/src/base/string_util.hh +++ b/src/base/string_util.hh @@ -282,9 +282,17 @@ private: namespace fmt { template<> struct formatter : formatter { - auto format(const lnav::tainted_string& ts, format_context& ctx) - -> decltype(ctx.out()) const; + auto format(const lnav::tainted_string& ts, + format_context& ctx) -> decltype(ctx.out()) const; }; } // namespace fmt +namespace lnav { +namespace pcre2pp { + +std::string quote(string_fragment sf); + +} +} // namespace lnav + #endif diff --git a/src/data_scanner.cc b/src/data_scanner.cc index 972d1b8e..3727407f 100644 --- a/src/data_scanner.cc +++ b/src/data_scanner.cc @@ -304,3 +304,102 @@ data_scanner::cleanup_end() } } } + +nonstd::optional +data_scanner::tokenize2(text_format_t tf) +{ + auto retval = this->tokenize_int(tf); + + if (this->ds_last_bracket_matched) { + this->ds_matching_brackets.pop_back(); + this->ds_last_bracket_matched = false; + } + if (retval) { + auto dt = retval.value().tr_token; + switch (dt) { + case DT_LSQUARE: + case DT_LCURLY: + case DT_LPAREN: + this->ds_matching_brackets.emplace_back(retval.value()); + break; + case DT_RSQUARE: + case DT_RCURLY: + case DT_RPAREN: + if (!this->ds_matching_brackets.empty() + && this->ds_matching_brackets.back().tr_token + == to_opener(dt)) + { + this->ds_last_bracket_matched = true; + } + break; + default: + break; + } + } + + return retval; +} + +nonstd::optional +data_scanner::find_matching_bracket(text_format_t tf, tokenize_result tr) +{ + switch (tr.tr_token) { + case DT_LSQUARE: + case DT_LCURLY: + case DT_LPAREN: { + auto curr_size = this->ds_matching_brackets.size(); + while (true) { + auto tok_res = this->tokenize2(tf); + if (!tok_res) { + break; + } + + if (this->ds_matching_brackets.size() == curr_size + && this->ds_last_bracket_matched) + { + return tokenize_result{ + DNT_GROUP, + { + tr.tr_capture.c_begin, + tok_res->tr_capture.c_end, + }, + { + tr.tr_capture.c_begin, + tok_res->tr_capture.c_end, + }, + tr.tr_data, + }; + } + } + break; + } + case DT_RSQUARE: + case DT_RCURLY: + case DT_RPAREN: { + for (auto riter = this->ds_matching_brackets.rbegin(); + riter != this->ds_matching_brackets.rend(); + ++riter) + { + if (riter->tr_token == to_opener(tr.tr_token)) { + return data_scanner::tokenize_result{ + DNT_GROUP, + { + riter->tr_capture.c_begin, + tr.tr_capture.c_end, + }, + { + riter->tr_capture.c_begin, + tr.tr_capture.c_end, + }, + tr.tr_data, + }; + } + } + break; + } + default: + break; + } + + return nonstd::nullopt; +} diff --git a/src/data_scanner.hh b/src/data_scanner.hh index 9c07eaaa..86551de6 100644 --- a/src/data_scanner.hh +++ b/src/data_scanner.hh @@ -206,6 +206,9 @@ public: nonstd::optional tokenize2(text_format_t tf = text_format_t::TF_UNKNOWN); + nonstd::optional find_matching_bracket(text_format_t tf, + tokenize_result tr); + void reset() { this->ds_next_offset = this->ds_init_offset; } int get_init_offset() const { return this->ds_init_offset; } @@ -222,6 +225,9 @@ private: bool is_credit_card(string_fragment frag) const; + nonstd::optional tokenize_int(text_format_t tf + = text_format_t::TF_UNKNOWN); + std::string ds_line; shared_buffer_ref ds_sbr; string_fragment ds_input; @@ -229,8 +235,27 @@ private: int ds_next_offset{0}; bool ds_bol{true}; bool ds_units{false}; + std::vector ds_matching_brackets; + bool ds_last_bracket_matched{false}; }; +inline data_token_t +to_opener(data_token_t dt) +{ + switch (dt) { + case DT_XML_CLOSE_TAG: + return DT_XML_OPEN_TAG; + case DT_RCURLY: + return DT_LCURLY; + case DT_RSQUARE: + return DT_LSQUARE; + case DT_RPAREN: + return DT_LPAREN; + default: + ensure(0); + } +} + inline data_token_t to_closer(data_token_t dt) { diff --git a/src/data_scanner_re.cc b/src/data_scanner_re.cc index a0e39989..ae65b3d2 100644 --- a/src/data_scanner_re.cc +++ b/src/data_scanner_re.cc @@ -1,4 +1,4 @@ -/* Generated by re2c 3.1 on Mon Apr 1 11:22:40 2024 */ +/* Generated by re2c 3.1 on Thu Apr 18 13:48:53 2024 */ #line 1 "../../lnav/src/data_scanner_re.re" /** * Copyright (c) 2015, Timothy Stack @@ -48,7 +48,7 @@ enum YYCONDTYPE { #line 38 "../../lnav/src/data_scanner_re.re" -nonstd::optional data_scanner::tokenize2(text_format_t tf) +nonstd::optional data_scanner::tokenize_int(text_format_t tf) { data_token_t token_out = DT_INVALID; capture_t cap_all; diff --git a/src/data_scanner_re.re b/src/data_scanner_re.re index c60c6956..f2871581 100644 --- a/src/data_scanner_re.re +++ b/src/data_scanner_re.re @@ -37,7 +37,7 @@ /*!conditions:re2c*/ -nonstd::optional data_scanner::tokenize2(text_format_t tf) +nonstd::optional data_scanner::tokenize_int(text_format_t tf) { data_token_t token_out = DT_INVALID; capture_t cap_all; diff --git a/src/field_overlay_source.cc b/src/field_overlay_source.cc index c534c3e9..821a8b98 100644 --- a/src/field_overlay_source.cc +++ b/src/field_overlay_source.cc @@ -672,87 +672,6 @@ field_overlay_source::list_value_for_overlay( this->build_meta_line(lv, value_out, row); } -std::vector -field_overlay_source::list_overlay_menu(const listview_curses& lv, - vis_line_t row) -{ - const auto* tc = dynamic_cast(&lv); - std::vector retval; - - if (!tc->tc_text_selection_active && tc->tc_selected_text) { - const auto& sti = tc->tc_selected_text.value(); - - if (sti.sti_line == row) { - auto left = std::max(0, sti.sti_x - 2); - - this->fos_menu_items.clear(); - retval.emplace_back(attr_line_t().pad_to(left).append( - " Filter Other "_status_title)); - { - attr_line_t al; - - al.append(" ").append("\u2714 IN"_ok).append(" "); - int start = left; - this->fos_menu_items.emplace_back( - 1_vl, - line_range{start, start + (int) al.length()}, - [this](const std::string& value) { - auto cmd = fmt::format(FMT_STRING(":filter-in {}"), - lnav::pcre2pp::quote(value)); - this->fos_lss.get_exec_context() - ->with_provenance(exec_context::mouse_input{}) - ->execute(cmd); - }); - start += al.length(); - al.append(":mag_right:"_emoji) - .append(" Search ") - .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)); - this->fos_menu_items.emplace_back( - 1_vl, - line_range{start, start + (int) al.length()}, - [this](const std::string& value) { - auto cmd = fmt::format(FMT_STRING("/{}"), - lnav::pcre2pp::quote(value)); - this->fos_lss.get_exec_context() - ->with_provenance(exec_context::mouse_input{}) - ->execute(cmd); - }); - retval.emplace_back(attr_line_t().pad_to(left).append(al)); - } - { - attr_line_t al; - - al.append(" ").append("\u2718 OUT"_error).append(" "); - int start = left; - this->fos_menu_items.emplace_back( - 2_vl, - line_range{start, start + (int) al.length()}, - [this](const std::string& value) { - auto cmd = fmt::format(FMT_STRING(":filter-out {}"), - lnav::pcre2pp::quote(value)); - this->fos_lss.get_exec_context() - ->with_provenance(exec_context::mouse_input{}) - ->execute(cmd); - }); - start += al.length(); - al.append(":clipboard:"_emoji) - .append(" Copy ") - .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)); - this->fos_menu_items.emplace_back( - 2_vl, - line_range{start, start + (int) al.length()}, - [this](const std::string& value) { - this->fos_lss.get_exec_context()->execute( - "|lnav-copy-text"); - }); - retval.emplace_back(attr_line_t().pad_to(left).append(al)); - } - } - } - - return retval; -} - nonstd::optional field_overlay_source::list_header_for_overlay(const listview_curses& lv, vis_line_t vl) diff --git a/src/field_overlay_source.hh b/src/field_overlay_source.hh index b4570ee6..34410fcd 100644 --- a/src/field_overlay_source.hh +++ b/src/field_overlay_source.hh @@ -36,9 +36,10 @@ #include "listview_curses.hh" #include "log_data_helper.hh" #include "logfile_sub_source.hh" +#include "text_overlay_menu.hh" #include "textfile_sub_source.hh" -class field_overlay_source : public list_overlay_source { +class field_overlay_source : public text_overlay_menu { public: explicit field_overlay_source(logfile_sub_source& lss, textfile_sub_source& tss) @@ -52,12 +53,8 @@ public: { this->fos_lines.clear(); this->fos_meta_lines.clear(); - this->fos_meta_lines_row = -1_vl; } - std::vector list_overlay_menu(const listview_curses& lv, - vis_line_t row) override; - nonstd::optional list_header_for_overlay( const listview_curses& lv, vis_line_t vl) override; @@ -103,23 +100,8 @@ public: int fos_known_key_size{0}; int fos_unknown_key_size{0}; std::vector fos_lines; - vis_line_t fos_meta_lines_row{0_vl}; std::vector fos_meta_lines; std::map fos_row_to_field_meta; - - struct menu_item { - menu_item(vis_line_t line, - line_range range, - std::function action) - : mi_line(line), mi_range(range), mi_action(std::move(action)) - { - } - - vis_line_t mi_line; - line_range mi_range; - std::function mi_action; - }; - std::vector fos_menu_items; }; #endif // LNAV_FIELD_OVERLAY_SOURCE_H diff --git a/src/gantt_source.hh b/src/gantt_source.hh index f3a35246..cc764476 100644 --- a/src/gantt_source.hh +++ b/src/gantt_source.hh @@ -34,6 +34,7 @@ #include "gantt_status_source.hh" #include "logfile_sub_source.hh" #include "plain_text_source.hh" +#include "text_overlay_menu.hh" #include "textview_curses.hh" class gantt_source @@ -164,7 +165,7 @@ public: exec_context* gs_exec_context; }; -class gantt_header_overlay : public list_overlay_source { +class gantt_header_overlay : public text_overlay_menu { public: explicit gantt_header_overlay(std::shared_ptr src); diff --git a/src/hotkeys.cc b/src/hotkeys.cc index 732eb2a8..f48b630c 100644 --- a/src/hotkeys.cc +++ b/src/hotkeys.cc @@ -43,6 +43,7 @@ #include "lnav_config.hh" #include "shlex.hh" #include "sql_util.hh" +#include "sqlitepp.client.hh" #include "xterm_mouse.hh" using namespace lnav::roles::literals; @@ -267,10 +268,22 @@ handle_paging_key(int ch) if (xterm_mouse::is_available()) { auto& mouse_i = injector::get(); mouse_i.set_enabled(!mouse_i.is_enabled()); - auto um = lnav::console::user_message::ok( - attr_line_t("mouse mode -- ") - .append(mouse_i.is_enabled() ? "enabled"_symbol - : "disabled"_symbol)); + + auto al = attr_line_t("mouse mode -- ") + .append(mouse_i.is_enabled() ? "enabled"_symbol + : "disabled"_symbol); + if (mouse_i.is_enabled() + && lnav_config.lc_mouse_mode == lnav_mouse_mode::disabled) + { + al.append(" -- enable permanently with ") + .append(":config /ui/mouse/mode enabled"_quoted_code); + + auto clear_note = prepare_stmt(lnav_data.ld_db, R"( +DELETE FROM lnav_user_notifications WHERE id = 'org.lnav.mouse-support' +)"); + clear_note.unwrap().execute(); + } + auto um = lnav::console::user_message::ok(al); lnav_data.ld_rl_view->set_attr_value(um.to_attr_line()); } else { lnav_data.ld_rl_view->set_value( diff --git a/src/init.sql b/src/init.sql index 6986b5ed..1e8c387c 100644 --- a/src/init.sql +++ b/src/init.sql @@ -128,7 +128,7 @@ CREATE TABLE lnav_user_notifications ); INSERT INTO lnav_user_notifications (id, priority, expiration, message) -VALUES ('org.lnav.breadcrumb.focus', -1, DATETIME('now', '+1 minute'), +VALUES ('org.lnav.breadcrumb.focus', -1, DATETIME('now', '+2 minute'), 'Press ` to focus on the breadcrumb bar'); CREATE TABLE lnav_views_echo AS diff --git a/src/listview_curses.cc b/src/listview_curses.cc index 77bab352..867df34a 100644 --- a/src/listview_curses.cc +++ b/src/listview_curses.cc @@ -843,9 +843,12 @@ listview_curses::handle_mouse(mouse_event& me) default: break; } - if (me.me_button != mouse_button_t::BUTTON_LEFT || inner_height == 0) { + if (me.me_button != mouse_button_t::BUTTON_LEFT || inner_height == 0 + || (me.me_press_x < (int) (width - 2))) + { return false; } + switch (this->lv_mouse_mode) { case lv_mode_t::NONE: { if (me.me_x < (int) (width - 2)) { @@ -1140,6 +1143,33 @@ listview_curses::shift_top(vis_line_t offset, bool suppress_flash) return this->lv_top; } +void +listview_curses::set_left(int left) +{ + if (this->lv_left == left || left < 0) { + return; + } + + if (left > this->lv_left) { + unsigned long width; + vis_line_t height; + + this->get_dimensions(height, width); + if (this->lv_show_scrollbar) { + width -= 1; + } + if ((this->get_inner_width() - this->lv_left) <= width) { + alerter::singleton().chime( + "the maximum width of the view has been reached"); + return; + } + } + + this->lv_left = left; + this->invoke_scroll(); + this->set_needs_update(); +} + size_t listview_curses::get_overlay_height(size_t total, vis_line_t view_height) { diff --git a/src/listview_curses.hh b/src/listview_curses.hh index 982b1414..6e003e40 100644 --- a/src/listview_curses.hh +++ b/src/listview_curses.hh @@ -364,31 +364,10 @@ public: * * @param left The new value for left. */ - void set_left(unsigned int left) - { - if (this->lv_left == left) { - return; - } - - if (left > this->lv_left) { - unsigned long width; - vis_line_t height; - - this->get_dimensions(height, width); - if ((this->get_inner_width() - this->lv_left) <= width) { - alerter::singleton().chime( - "the maximum width of the view has been reached"); - return; - } - } - - this->lv_left = left; - this->invoke_scroll(); - this->set_needs_update(); - } + void set_left(int left); /** @return The column number that is displayed at the left. */ - unsigned int get_left() const { return this->lv_left; } + int get_left() const { return this->lv_left; } /** * Shift the value of left by the given value. @@ -396,7 +375,7 @@ public: * @param offset The amount to change top by. * @return The final value of top. */ - unsigned int shift_left(int offset) + int shift_left(int offset) { if (this->lv_word_wrap) { alerter::singleton().chime( @@ -526,7 +505,7 @@ public: this->lv_title.c_str(), this->vc_y, (int) this->lv_top, - (int) this->lv_left, + this->lv_left, this->lv_height, (int) this->lv_selection, (int) this->get_inner_height()); @@ -582,7 +561,7 @@ protected: action lv_scroll; /*< The scroll action. */ WINDOW* lv_window{nullptr}; /*< The window that contains this view. */ vis_line_t lv_top{0}; /*< The line at the top of the view. */ - unsigned int lv_left{0}; /*< The column at the left of the view. */ + int lv_left{0}; /*< The column at the left of the view. */ vis_line_t lv_height{0}; /*< The abs/rel height of the view. */ bool lv_overlay_focused{false}; vis_line_t lv_focused_overlay_top{0_vl}; diff --git a/src/lnav.cc b/src/lnav.cc index 141dc5aa..2a715eb2 100644 --- a/src/lnav.cc +++ b/src/lnav.cc @@ -1134,6 +1134,27 @@ looper() } auto echo_views_stmt = echo_views_stmt_res.unwrap(); + if (xterm_mouse::is_available() + && lnav_config.lc_mouse_mode == lnav_mouse_mode::disabled) + { + auto mouse_note = prepare_stmt(lnav_data.ld_db, R"( +INSERT INTO lnav_user_notifications (id, priority, expiration, message) +VALUES ('org.lnav.mouse-support', -1, DATETIME('now', '+1 minute'), + 'Press F2 to enable mouse support'); +)"); + if (mouse_note.isErr()) { + lnav::console::print( + stderr, + lnav::console::user_message::error( + "unable to prepare INSERT statement for " + "lnav_user_notifications table") + .with_reason(mouse_note.unwrapErr())); + return; + } + + mouse_note.unwrap().execute(); + } + (void) signal(SIGINT, sigint); (void) signal(SIGTERM, sigint); (void) signal(SIGWINCH, sigwinch); diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index 725d8732..dd41261e 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -5799,7 +5799,7 @@ search_files_prompt(std::vector& args) lnav_data.ld_mode = ln_mode_t::SEARCH_FILES; for (const auto& lf : lnav_data.ld_active_files.fc_files) { - auto path = lnav::pcre2pp::quote(lf->get_unique_path()); + auto path = lnav::pcre2pp::quote(lf->get_unique_path().string()); lnav_data.ld_rl_view->add_possibility( ln_mode_t::SEARCH_FILES, "*", path); } diff --git a/src/logfile.cc b/src/logfile.cc index 432ef4fb..03845359 100644 --- a/src/logfile.cc +++ b/src/logfile.cc @@ -846,7 +846,8 @@ logfile::rebuild_index(nonstd::optional deadline) } this->lf_longest_line - = std::max(this->lf_longest_line, sbr.length()); + = std::max(this->lf_longest_line, + li.li_utf8_scan_result.usr_column_width_guess); this->lf_partial_line = li.li_partial; sort_needed = this->process_prefix(sbr, li, sbc) || sort_needed; diff --git a/src/logfile_sub_source.cc b/src/logfile_sub_source.cc index 531c595a..b78b00cb 100644 --- a/src/logfile_sub_source.cc +++ b/src/logfile_sub_source.cc @@ -993,8 +993,8 @@ logfile_sub_source::rebuild_index( if (lf == nullptr) { continue; } - this->lss_longest_line = std::max(this->lss_longest_line, - lf->get_longest_line_length()); + this->lss_longest_line = std::max( + this->lss_longest_line, lf->get_longest_line_length() + 1); this->lss_basename_width = std::max(this->lss_basename_width, lf->get_unique_path().native().size()); @@ -3066,22 +3066,6 @@ logfile_sub_source::text_handle_mouse( const listview_curses::display_line_content_t& mouse_line, mouse_event& me) { - auto* fos = dynamic_cast(tc.get_overlay_source()); - - if (mouse_line.is() && tc.tc_selected_text) { - auto& om = mouse_line.get(); - auto& sti = tc.tc_selected_text.value(); - - for (const auto& mi : fos->fos_menu_items) { - if (om.om_line == mi.mi_line - && me.is_click_in(mouse_button_t::BUTTON_LEFT, mi.mi_range)) - { - mi.mi_action(sti.sti_value); - break; - } - } - } - if (tc.get_overlay_selection()) { if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 2, 4)) { this->list_input_handle_key(tc, ' '); diff --git a/src/pcrepp/pcre2pp.cc b/src/pcrepp/pcre2pp.cc index afeaeed8..12498768 100644 --- a/src/pcrepp/pcre2pp.cc +++ b/src/pcrepp/pcre2pp.cc @@ -32,41 +32,11 @@ #include "pcre2pp.hh" #include "config.h" +#include "ww898/cp_utf8.hpp" namespace lnav { namespace pcre2pp { -std::string -quote(const char* unquoted) -{ - std::string retval; - - for (int lpc = 0; unquoted[lpc]; lpc++) { - if (isalnum(unquoted[lpc]) || unquoted[lpc] == '_' - || unquoted[lpc] == '-' || unquoted[lpc] == ' ' - || unquoted[lpc] == ':' || unquoted[lpc] == ';' - || unquoted[lpc] & 0x80) - { - retval.push_back(unquoted[lpc]); - } else { - retval.push_back('\\'); - switch (unquoted[lpc]) { - case '\t': - retval.push_back('t'); - break; - case '\n': - retval.push_back('n'); - break; - default: - retval.push_back(unquoted[lpc]); - break; - } - } - } - - return retval; -} - std::string match_data::to_string() const { diff --git a/src/pcrepp/pcre2pp.hh b/src/pcrepp/pcre2pp.hh index 2563e6f9..ae39871b 100644 --- a/src/pcrepp/pcre2pp.hh +++ b/src/pcrepp/pcre2pp.hh @@ -46,14 +46,6 @@ namespace lnav { namespace pcre2pp { -std::string quote(const char* unquoted); - -inline std::string -quote(const std::string& unquoted) -{ - return quote(unquoted.c_str()); -} - class code; struct capture_builder; class matcher; diff --git a/src/text_overlay_menu.cc b/src/text_overlay_menu.cc new file mode 100644 index 00000000..d143e033 --- /dev/null +++ b/src/text_overlay_menu.cc @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2024, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "text_overlay_menu.hh" + +#include "command_executor.hh" +#include "config.h" +#include "lnav.hh" +#include "md4cpp.hh" +#include "textview_curses.hh" + +using namespace md4cpp::literals; +using namespace lnav::roles::literals; + +std::vector +text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row) +{ + const auto* tc = dynamic_cast(&lv); + std::vector retval; + + if (!tc->tc_text_selection_active && tc->tc_selected_text) { + const auto& sti = tc->tc_selected_text.value(); + + if (sti.sti_line == row) { + auto title = " Filter Other "_status_title; + auto left = std::max(0, sti.sti_x - 2); + auto dim = lv.get_dimensions(); + + if (left + title.first.length() >= dim.second) { + left = dim.second - title.first.length() - 2; + } + + this->tom_menu_items.clear(); + retval.emplace_back(attr_line_t().pad_to(left).append(title)); + { + attr_line_t al; + + al.append(" ").append("\u2714 IN"_ok).append(" "); + int start = left; + this->tom_menu_items.emplace_back( + 1_vl, + line_range{start, start + (int) al.length()}, + [](const std::string& value) { + auto cmd = fmt::format(FMT_STRING(":filter-in {}"), + lnav::pcre2pp::quote(value)); + lnav_data.ld_exec_context + .with_provenance(exec_context::mouse_input{}) + ->execute(cmd); + }); + start += al.length(); + al.append(":mag_right:"_emoji) + .append(" Search ") + .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)); + this->tom_menu_items.emplace_back( + 1_vl, + line_range{start, start + (int) al.length()}, + [](const std::string& value) { + auto cmd = fmt::format(FMT_STRING("/{}"), + lnav::pcre2pp::quote(value)); + lnav_data.ld_exec_context + .with_provenance(exec_context::mouse_input{}) + ->execute(cmd); + }); + retval.emplace_back(attr_line_t().pad_to(left).append(al)); + } + { + attr_line_t al; + + al.append(" ").append("\u2718 OUT"_error).append(" "); + int start = left; + this->tom_menu_items.emplace_back( + 2_vl, + line_range{start, start + (int) al.length()}, + [](const std::string& value) { + auto cmd = fmt::format(FMT_STRING(":filter-out {}"), + lnav::pcre2pp::quote(value)); + lnav_data.ld_exec_context + .with_provenance(exec_context::mouse_input{}) + ->execute(cmd); + }); + start += al.length(); + al.append(":clipboard:"_emoji) + .append(" Copy ") + .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)); + this->tom_menu_items.emplace_back( + 2_vl, + line_range{start, start + (int) al.length()}, + [](const std::string& value) { + lnav_data.ld_exec_context.execute("|lnav-copy-text"); + }); + retval.emplace_back(attr_line_t().pad_to(left).append(al)); + } + } + } + + return retval; +} diff --git a/src/text_overlay_menu.hh b/src/text_overlay_menu.hh new file mode 100644 index 00000000..2abf16c5 --- /dev/null +++ b/src/text_overlay_menu.hh @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_text_overlay_menu_hh +#define lnav_text_overlay_menu_hh + +#include "listview_curses.hh" + +class text_overlay_menu : public list_overlay_source { +public: + std::vector list_overlay_menu(const listview_curses& lv, + vis_line_t line) override; + + struct menu_item { + menu_item(vis_line_t line, + line_range range, + std::function action) + : mi_line(line), mi_range(range), mi_action(std::move(action)) + { + } + + vis_line_t mi_line; + line_range mi_range; + std::function mi_action; + }; + std::vector tom_menu_items; +}; + +#endif diff --git a/src/textfile_sub_source.hh b/src/textfile_sub_source.hh index 138082dc..e9b759ee 100644 --- a/src/textfile_sub_source.hh +++ b/src/textfile_sub_source.hh @@ -36,6 +36,7 @@ #include "filter_observer.hh" #include "logfile.hh" #include "plain_text_source.hh" +#include "text_overlay_menu.hh" #include "textview_curses.hh" class textfile_sub_source @@ -198,7 +199,7 @@ private: int64_t tss_content_line{0}; }; -class textfile_header_overlay : public list_overlay_source { +class textfile_header_overlay : public text_overlay_menu { public: explicit textfile_header_overlay(textfile_sub_source* src); diff --git a/src/textview_curses.cc b/src/textview_curses.cc index bc7b1c71..2020e296 100644 --- a/src/textview_curses.cc +++ b/src/textview_curses.cc @@ -43,6 +43,7 @@ #include "log_format_fwd.hh" #include "logfile.hh" #include "shlex.hh" +#include "text_overlay_menu.hh" #include "view_curses.hh" const auto REVERSE_SEARCH_OFFSET = 2000_vl; @@ -461,6 +462,7 @@ textview_curses::handle_mouse(mouse_event& me) case mouse_button_state_t::BUTTON_STATE_PRESSED: { this->tc_text_selection_active = true; this->tc_press_line = mouse_line; + this->tc_press_left = this->lv_left + me.me_press_x; if (!this->lv_selectable) { this->set_selectable(true); } @@ -523,10 +525,27 @@ textview_curses::handle_mouse(mouse_event& me) } auto tok = tok_res.value(); - auto tok_sf = tok.inner_string_fragment(); + auto tok_sf + = (tok.tr_token + == data_token_t::DT_QUOTED_STRING + && (cursor_sf.sf_begin + == tok.to_string_fragment() + .sf_begin + || cursor_sf.sf_begin + == tok.to_string_fragment() + .sf_end + - 1)) + ? tok.to_string_fragment() + : tok.inner_string_fragment(); if (tok_sf.contains(cursor_sf) && tok.tr_token != data_token_t::DT_WHITE) { + auto group_tok + = ds.find_matching_bracket(tf, tok); + if (group_tok) { + tok_sf = group_tok.value() + .to_string_fragment(); + } this->tc_selected_text = selected_text_info{ me.me_x, mc.mc_line, @@ -570,10 +589,10 @@ textview_curses::handle_mouse(mouse_event& me) if (mouse_line.is()) { auto& mc = mouse_line.get(); attr_line_t al; - auto low_x = std::min(this->lv_left + me.me_x, - this->lv_left + me.me_press_x); - auto high_x = std::max(this->lv_left + me.me_x, - this->lv_left + me.me_press_x); + auto low_x = std::min(this->tc_press_left, + (int) this->lv_left + me.me_x); + auto high_x = std::max(this->tc_press_left, + (int) this->lv_left + me.me_x); this->set_selection_without_context(mc.mc_line); if (me.me_button == mouse_button_t::BUTTON_LEFT) { @@ -581,9 +600,14 @@ textview_curses::handle_mouse(mouse_event& me) auto line_sf = string_fragment::from_str(al.get_string()); auto cursor_sf = line_sf.sub_cell_range(low_x, high_x); + if (me.me_x <= 1) { + this->set_left(this->lv_left - 1); + } else if (me.me_x >= width - 1) { + this->set_left(this->lv_left + 1); + } if (!cursor_sf.empty()) { this->tc_selected_text = { - me.me_press_x, + me.me_x, mc.mc_line, line_range{ cursor_sf.sf_begin, @@ -626,6 +650,26 @@ textview_curses::handle_mouse(mouse_event& me) break; } case mouse_button_state_t::BUTTON_STATE_RELEASED: { + auto* ov = this->get_overlay_source(); + if (ov != nullptr && mouse_line.is() + && this->tc_selected_text) + { + auto* tom = dynamic_cast(ov); + if (tom != nullptr) { + auto& om = mouse_line.get(); + auto& sti = this->tc_selected_text.value(); + + for (const auto& mi : tom->tom_menu_items) { + if (om.om_line == mi.mi_line + && me.is_click_in(mouse_button_t::BUTTON_LEFT, + mi.mi_range)) + { + mi.mi_action(sti.sti_value); + break; + } + } + } + } this->tc_text_selection_active = false; if (me.is_click_in(mouse_button_t::BUTTON_RIGHT, 0, INT_MAX)) { auto* lov = this->get_overlay_source(); diff --git a/src/textview_curses.hh b/src/textview_curses.hh index 8b1ef1db..4f80a01e 100644 --- a/src/textview_curses.hh +++ b/src/textview_curses.hh @@ -813,6 +813,7 @@ public: nonstd::optional tc_selected_text; bool tc_text_selection_active{false}; display_line_content_t tc_press_line; + int tc_press_left{0}; protected: class grep_highlighter { diff --git a/src/top_status_source.cc b/src/top_status_source.cc index 71001942..6f7d3838 100644 --- a/src/top_status_source.cc +++ b/src/top_status_source.cc @@ -43,7 +43,7 @@ SELECT message FROM lnav_user_notifications (expiration IS NULL OR expiration > datetime('now')) AND (views IS NULL OR json_contains(views, (SELECT name FROM lnav_top_view))) - ORDER BY priority DESC + ORDER BY priority DESC, expiration ASC LIMIT 1 )"; diff --git a/src/view_curses.cc b/src/view_curses.cc index 6ba0456a..352ee42f 100644 --- a/src/view_curses.cc +++ b/src/view_curses.cc @@ -154,7 +154,7 @@ mouse_event::is_drag_in(mouse_button_t button, line_range lr) const { return this->me_button == button && this->me_state == mouse_button_state_t::BUTTON_STATE_DRAGGED - && lr.contains(this->me_x); + && lr.contains(this->me_press_x) && lr.contains(this->me_x); } bool @@ -264,6 +264,13 @@ view_curses::mvwattrline(WINDOW* window, do { expanded_line.push_back(' '); char_index += 1; + if (char_index == lr_chars.lr_start) { + lr_bytes.lr_start = expanded_line.size(); + } + if (char_index == lr_chars.lr_end) { + lr_bytes.lr_end = expanded_line.size(); + retval.mr_chars_out = char_index; + } } while (expanded_line.size() % 8); utf_adjustments.emplace_back( lpc, expanded_line.size() - exp_start_index - 1);