diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index a1783fe..c495466 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -31,6 +31,8 @@ jobs: -readability-magic-numbers,\ -readability-uppercase-literal-suffix,\ -readability-function-cognitive-complexity,\ + -readability-identifier-length,\ + -bugprone-easily-swappable-parameters,\ -cppcoreguidelines-avoid-magic-numbers,\ -cppcoreguidelines-macro-usage,\ -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ diff --git a/scripts/choose.bash b/scripts/choose.bash index dfd2555..61b0ff5 100644 --- a/scripts/choose.bash +++ b/scripts/choose.bash @@ -29,7 +29,7 @@ ch_hist() { IFS= read -r -d '' cat } | grep -zi -- "$*" | - choose -r "\x00" -uet --delimit-not-at-end --flip -p "Select a line to edit then run.") + choose -r "\x00" -ue --delimit-not-at-end --flip -p "Select a line to edit then run.") if [ -z "$LINE" ]; then echo "empty line" @@ -53,3 +53,10 @@ ch_hist() { # run on current shell source -- "$TEMPFILE" } + +ch_branch() { + local branch="$(git branch | grep -i -- "$*" | choose -re --tui-select '^\*' --sub '^[ *] ' '' --sort-reverse --delimit-not-at-end -p 'swap branch')" + if [ -n "$branch" ]; then + git checkout "$branch" + fi +} diff --git a/src/args.hpp b/src/args.hpp index 311d3d4..a05cff5 100644 --- a/src/args.hpp +++ b/src/args.hpp @@ -282,6 +282,10 @@ void print_help_message() { #ifndef PCRE2_SUBSTITUTE_LITERAL " WARNING PCRE2 version old: replacement is never literal\n" #endif + " --tui-select \n" + " place the tui cursor at the last matched token. inherits the\n" + " same match options as the positional argument. has a higher\n" + " priority than --end. implies --tui\n" "options:\n" " --auto-completion-strings\n" " -b, --batch-delimiter >\n" @@ -318,7 +322,7 @@ void print_help_message() { " --delimit-on-empty\n" " even if the output would be empty, place a batch delimiter\n" " -e, --end\n" - " begin cursor and prompt at the bottom of the tui\n" + " begin cursor and prompt at the bottom of the tui. implies --tui\n" " --flush\n" " makes the input unbuffered, and the output is flushed after each\n" " token is written. this is useful for long running inputs with -u\n" @@ -517,6 +521,7 @@ Arguments handle_args(int argc, char* const* argv, FILE* input = NULL, FILE* out {"prompt", required_argument, NULL, 'p'}, {"sub", required_argument, NULL, 0}, {"substitute", required_argument, NULL, 0}, + {"tui-select", required_argument, NULL, 0}, {"filter", required_argument, NULL, 'f'}, {"field", required_argument, NULL, 0}, {"remove", required_argument, NULL, 0}, @@ -582,13 +587,13 @@ Arguments handle_args(int argc, char* const* argv, FILE* input = NULL, FILE* out case '?': arg_has_errors = true; #ifndef CHOOSE_FUZZING_APPLIED - printf("Unknown option: %c\n", optopt); + printf("Unknown option: %s\n", argv[optind - 1]); #endif break; case ':': arg_has_errors = true; #ifndef CHOOSE_FUZZING_APPLIED - printf("Mising arg for: %c\n", optopt); + printf("Missing arg for: %s\n", argv[optind - 1]); #endif break; default: @@ -721,6 +726,9 @@ Arguments handle_args(int argc, char* const* argv, FILE* input = NULL, FILE* out ++optind; uncompiled_output.ordered_ops.push_back(uncompiled::UncompiledSubOp{argv[optind - 2], argv[optind - 1]}); } + } else if (strcmp("tui-select", name) == 0) { + uncompiled_output.ordered_ops.push_back(uncompiled::UncompiledTuiSelectOp{optarg}); + ret.tui = true; } else if (strcmp("load-factor", name) == 0) { char* end_ptr; // NOLINT ret.unique_load_factor = strtof(optarg, &end_ptr); @@ -849,6 +857,7 @@ Arguments handle_args(int argc, char* const* argv, FILE* input = NULL, FILE* out break; case 'e': ret.end = true; + ret.tui = true; break; case 'g': ret.sort_type = general_numeric; diff --git a/src/main.cpp b/src/main.cpp index 9e9a998..c384d71 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -449,9 +449,9 @@ struct UIState { int main(int argc, char* const* argv) { choose::Arguments args = choose::handle_args(argc, argv); setlocale(LC_ALL, args.locale); - std::vector tokens; + choose::CreateTokensResult tokens_result; try { - tokens = choose::create_tokens(args); + tokens_result = choose::create_tokens(args); } catch (const choose::termination_request&) { return EXIT_SUCCESS; } @@ -470,8 +470,8 @@ int main(int argc, char* const* argv) { choose::nc::screen screen; UIState state{ - std::move(args), // - std::move(tokens), // + std::move(args), // + std::move(tokens_result.tokens), // BatchOutputStream(state.args), }; @@ -483,18 +483,30 @@ int main(int argc, char* const* argv) { choose::nc::noecho(); curs_set(0); // invisible cursor - // I don't handle ERR for anything color or attribute related since - // the application still works, even on failure (just without color) - // I also don't check ERR for ncurses printing, since if that stuff - // is not working, it will be very apparent to the user + // ERR isn't handled for anything color or attribute related since the + // application still works, even on failure (just without color) similar + // thinking for ncurses printing, in that case it will be very apparent to + // the user start_color(); use_default_colors(); init_pair(UIState::PAIR_SELECTED, COLOR_GREEN, -1); state.scroll_position = 0; - state.selection_position = state.args.end ? (int)state.tokens.size() - 1 : 0; + if (tokens_result.initial_selected_token.has_value()) { + // best to do this association at the end, as the indices are moved + // around by sorting and uniqueness + for (int i = 0; i < (int)state.tokens.size(); ++i) { + if (std::equal(state.tokens[i].buffer.cbegin(), state.tokens[i].buffer.cend(), // + tokens_result.initial_selected_token->buffer.cbegin(), tokens_result.initial_selected_token->buffer.cend())) { + state.selection_position = i; + break; + } + } + } else { + // --tui-select has a higher priority than --end + state.selection_position = state.args.end ? (int)state.tokens.size() - 1 : 0; + } state.tenacious_single_select_indicator = 0; - state.loop(); } catch (...) { // a note on ncurses: @@ -507,5 +519,5 @@ int main(int argc, char* const* argv) { } return EXIT_FAILURE; } - return sigint_occurred ? 128 + 2 : EXIT_SUCCESS; + return sigint_occurred ? 128 + SIGINT : EXIT_SUCCESS; } diff --git a/src/ordered_op.hpp b/src/ordered_op.hpp index d9365cd..c9a8560 100644 --- a/src/ordered_op.hpp +++ b/src/ordered_op.hpp @@ -6,6 +6,18 @@ namespace choose { +struct TuiSelectOp { + regex::code target; + regex::match_data match_data; + + TuiSelectOp(regex::code&& target) : target(std::move(target)), match_data(regex::create_match_data(this->target)) {} + + bool matches(const char* begin, const char* end) const { + int rc = regex::match(this->target, begin, end - begin, this->match_data, "tui selection target"); + return rc > 0; + } +}; + struct RmOrFilterOp { enum Type { REMOVE, FILTER }; Type type; @@ -183,10 +195,14 @@ struct IndexOp { } }; -using OrderedOp = std::variant; +using OrderedOp = std::variant; namespace uncompiled { +struct UncompiledTuiSelectOp { + const char* target; +}; + struct UncompiledRmOrFilterOp { RmOrFilterOp::Type type; const char* arg; @@ -204,7 +220,12 @@ using UncompiledIndexOp = IndexOp; // uncompiled ops are exclusively used in the args. They hold information as all the // args are parsed. once the args are fully known, they are converted to // there compiled counterparts. -using UncompiledOrderedOp = std::variant; +using UncompiledOrderedOp = std::variant; OrderedOp compile(UncompiledOrderedOp op, uint32_t options) { if (UncompiledRmOrFilterOp* rf_op = std::get_if(&op)) { @@ -216,6 +237,8 @@ OrderedOp compile(UncompiledOrderedOp op, uint32_t options) { return *o; } else if (UncompiledInLimitOp* o = std::get_if(&op)) { return *o; + } else if (UncompiledTuiSelectOp* o = std::get_if(&op)) { + return TuiSelectOp(regex::compile(o->target, options, "tui select")); } else { return std::get(op); } diff --git a/src/regex.hpp b/src/regex.hpp index a44e2bf..63f7e8e 100644 --- a/src/regex.hpp +++ b/src/regex.hpp @@ -302,7 +302,7 @@ template bool get_match_and_groups(const char* subject, int rc, const match_data& match_data, T handler, const char* identification) { PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data.get()); for (int i = 0; i < rc; ++i) { - auto m = Match{subject + ovector[2 * i], subject + ovector[2 * i + 1]}; + auto m = Match{subject + ovector[2 * i], subject + ovector[2 * i + 1]}; // NOLINT m.ensure_sane(identification); if (handler(m)) { return true; diff --git a/src/test.cpp b/src/test.cpp index c3514c7..f85550c 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -359,7 +359,7 @@ BOOST_AUTO_TEST_SUITE_END() // choose either sends to stdout, or creates an interface that displays tokens struct choose_output { - std::variant, std::vector> o; + std::variant, choose::CreateTokensResult> o; bool operator==(const choose_output& other) const { if (o.index() != other.o.index()) { @@ -370,9 +370,17 @@ struct choose_output { const std::vector& second = std::get>(other.o); return first == second; } else { - const std::vector& first = std::get>(o); - const std::vector& second = std::get>(other.o); - return std::equal(first.begin(), first.end(), second.begin(), second.end(), [](const choose::Token& lhs, const choose::Token& rhs) -> bool { // + const choose::CreateTokensResult& first = std::get(o); + const choose::CreateTokensResult& second = std::get(other.o); + if (first.initial_selected_token.has_value() != second.initial_selected_token.has_value()) { + return false; + } + if (first.initial_selected_token.has_value()) { + if (first.initial_selected_token->buffer != second.initial_selected_token->buffer) { + return false; + } + } + return std::equal(first.tokens.begin(), first.tokens.end(), second.tokens.begin(), second.tokens.end(), [](const choose::Token& lhs, const choose::Token& rhs) -> bool { // return lhs.buffer == rhs.buffer; }); } @@ -398,10 +406,28 @@ std::ostream& operator<<(std::ostream& os, const choose_output& out) { } } } else { - const std::vector& out_tokens = std::get>(out.o); - os << "\ntokens: "; + const CreateTokensResult& out_tokens = std::get(out.o); + os << "\n"; + if (out_tokens.initial_selected_token.has_value()) { + os << "(cursor:"; + bool first = true; + for (char ch : out_tokens.initial_selected_token->buffer) { + if (!first) { + os << ','; + } + first = false; + const char* escape_sequence = str::get_escape_sequence(ch); + if (escape_sequence) { + os << escape_sequence; + } else { + os << ch; + } + } + os << ") "; + } + os << "tokens: "; bool first_token = true; - for (const Token& t : out_tokens) { + for (const Token& t : out_tokens.tokens) { if (!first_token) { os << '|'; os << '|'; @@ -510,13 +536,13 @@ BOOST_AUTO_TEST_SUITE(create_tokens_test_suite) BOOST_AUTO_TEST_CASE(simple) { choose_output out = run_choose("a\na\nb\nc", {"-t"}); - choose_output correct_output{std::vector{"a", "a", "b", "c"}}; + choose_output correct_output{CreateTokensResult{std::vector{"a", "a", "b", "c"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(simple_ignore_case) { choose_output out = run_choose("1a2A3", {"-t", "-i", "a"}); - choose_output correct_output{std::vector{"1", "2", "3"}}; + choose_output correct_output{CreateTokensResult{std::vector{"1", "2", "3"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -553,7 +579,7 @@ BOOST_AUTO_TEST_CASE(output_rm_filter) { BOOST_AUTO_TEST_CASE(zero_with_tui) { choose_output out = run_choose("anything", {"--out=0", "-t"}); - choose_output correct_output{std::vector{}}; + choose_output correct_output{CreateTokensResult{std::vector{}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -563,6 +589,19 @@ BOOST_AUTO_TEST_CASE(zero_no_tui) { BOOST_REQUIRE_EQUAL(out, correct_output); } +BOOST_AUTO_TEST_CASE(tui_cursor_selection) { + choose_output out = run_choose("1\n2\n3", {"--tui-select", "2"}); + choose_output correct_output{CreateTokensResult{std::vector{"1", "2", "3"}, "2"}}; + BOOST_REQUIRE_EQUAL(out, correct_output); +} + +BOOST_AUTO_TEST_CASE(tui_cursor_selection_at_end) { + // ensure proper logic for a selected token which was discarded by later op + choose_output out = run_choose("1\n2\n3", {"-r", "--tui-select", "3", "--filter", "[^3]"}); + choose_output correct_output{CreateTokensResult{std::vector{"1", "2"}}}; + BOOST_REQUIRE_EQUAL(out, correct_output); +} + BOOST_AUTO_TEST_CASE(output_accumulation) { // the output avoids a copy when it can, but it still accumulates the input on no/partial delimiter match. // this is needed because an entire token needs to be accumulated before a filter can be applied @@ -619,6 +658,12 @@ BOOST_AUTO_TEST_CASE(general_numeric_unique_use_set) { BOOST_REQUIRE_EQUAL(out, correct_output); } +BOOST_AUTO_TEST_CASE(partial_stable_sort_full_coverage) { + choose_output out = run_choose("d\na\nb\nc", {"--stable", "--sort-reverse", "--out", "3", "--truncate-no-bound"}); + choose_output correct_output{to_vec("d\nc\nb\n")}; + BOOST_REQUIRE_EQUAL(out, correct_output); +} + BOOST_AUTO_TEST_CASE(numeric_sort) { choose_output out = run_choose("17\n-0\n.0\n1\n0001.0", {"--sort-numeric", "--stable"}); choose_output correct_output{to_vec("-0\n.0\n1\n0001.0\n17\n")}; @@ -679,7 +724,7 @@ BOOST_AUTO_TEST_CASE(out) { BOOST_AUTO_TEST_CASE(out_tui) { choose_output out = run_choose("this\nis\na\ntest", {"--out=3", "-t"}); - choose_output correct_output{std::vector{"this", "is", "a"}}; + choose_output correct_output{CreateTokensResult{std::vector{"this", "is", "a"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -692,7 +737,7 @@ BOOST_AUTO_TEST_CASE(out_min) { BOOST_AUTO_TEST_CASE(out_min_tui) { choose_output out = run_choose("this\nis\na\ntest", {"--out=1,3", "-t"}); - choose_output correct_output{std::vector{"is", "a"}}; + choose_output correct_output{CreateTokensResult{std::vector{"is", "a"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -741,6 +786,12 @@ BOOST_AUTO_TEST_CASE(truncate_no_bound_out) { BOOST_REQUIRE_EQUAL(out, correct_output); } +BOOST_AUTO_TEST_CASE(sorted_truncate_no_bound_out_of_bound) { + choose_output out = run_choose("3\n2\n1", {"--sort", "--out=10000", "--truncate-no-bound"}); + choose_output correct_output{to_vec("1\n2\n3\n")}; + BOOST_REQUIRE_EQUAL(out, correct_output); +} + BOOST_AUTO_TEST_CASE(sort_out) { OutputSizeBoundFixture f(5); choose_output out = run_choose("i\nh\ng\nf\ne\nd\nc\nb\na\n", {"--sort", "--out=5"}); @@ -751,7 +802,7 @@ BOOST_AUTO_TEST_CASE(sort_out) { BOOST_AUTO_TEST_CASE(sort_out_tui) { OutputSizeBoundFixture f(5); choose_output out = run_choose("i\nh\ng\nf\ne\nd\nc\nb\na\n", {"--sort", "--out=5", "-t"}); - choose_output correct_output{std::vector{"a", "b", "c", "d", "e"}}; + choose_output correct_output{CreateTokensResult{std::vector{"a", "b", "c", "d", "e"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -854,26 +905,26 @@ BOOST_AUTO_TEST_CASE(sort_unique_tail_min) { BOOST_AUTO_TEST_CASE(unique_with_set) { choose_output out = run_choose("this\nis\nis\na\na\ntest", {"--unique", "--unique-use-set", "-t"}); - choose_output correct_output{std::vector{"this", "is", "a", "test"}}; + choose_output correct_output{CreateTokensResult{std::vector{"this", "is", "a", "test"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } #ifndef CHOOSE_DISABLE_FIELD BOOST_AUTO_TEST_CASE(unique_by_field) { choose_output out = run_choose("alpha,tester\nbeta,tester\ngamma,tester,abcde", {"-t", "--unique", "--field", "^[^,]*+.\\K[^,]*+"}); - choose_output correct_output{std::vector{"alpha,tester"}}; + choose_output correct_output{CreateTokensResult{std::vector{"alpha,tester"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(sort_by_field) { choose_output out = run_choose("a,z\nb,y\nc,x", {"-t", "--sort", "--field", "^[^,]*+.\\K[^,]*+"}); - choose_output correct_output{std::vector{"c,x", "b,y", "a,z"}}; + choose_output correct_output{CreateTokensResult{std::vector{"c,x", "b,y", "a,z"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(field_with_no_matches) { choose_output out = run_choose("abc 1245\nzzz 123\nno match!", {"-t", "-s", "--field", "\\d+"}); - choose_output correct_output{std::vector{"no match!", "zzz 123", "abc 1245"}}; + choose_output correct_output{CreateTokensResult{std::vector{"no match!", "zzz 123", "abc 1245"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } #endif @@ -901,7 +952,7 @@ BOOST_AUTO_TEST_CASE(no_delimit_delimit_on_empty) { BOOST_AUTO_TEST_CASE(in_limit) { choose_output out = run_choose("d\nc\nb\na", {"--head=3", "--sort", "-t"}); - choose_output correct_output{std::vector{"b", "c", "d"}}; + choose_output correct_output{CreateTokensResult{std::vector{"b", "c", "d"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -961,13 +1012,13 @@ BOOST_AUTO_TEST_CASE(out_limit_with_sort_past_end_start) { BOOST_AUTO_TEST_CASE(out_unique_sort_with_limit_past_bound) { choose_output out = run_choose("this\nis\na\ntest", {"-t", "--unique", "--sort", "--out=100000"}); - choose_output correct_output{std::vector{"a", "is", "test", "this"}}; + choose_output correct_output{CreateTokensResult{std::vector{"a", "is", "test", "this"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(tail_unique_with_min_limit_past_bound) { choose_output out = run_choose("this\nis\na\ntest", {"-t", "--unique", "--tail=100000,100000"}); - choose_output correct_output{std::vector{}}; + choose_output correct_output{CreateTokensResult{std::vector{}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -979,7 +1030,7 @@ BOOST_AUTO_TEST_CASE(out_limit_unique) { BOOST_AUTO_TEST_CASE(ordered_ops) { choose_output out = run_choose("this\nis\nrra\ntest", {"-r", "--sub", "is", "rr", "--rm", "test", "--filter", "rr$", "-t"}); - choose_output correct_output{std::vector{"thrr", "rr"}}; + choose_output correct_output{CreateTokensResult{std::vector{"thrr", "rr"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1049,7 +1100,7 @@ BOOST_AUTO_TEST_CASE(index_op_after_last) { BOOST_AUTO_TEST_CASE(literal_sub) { choose_output out = run_choose("literal substitution", {"--sub", "literal", "good", "-t"}); - choose_output correct_output{std::vector{"good substitution"}}; + choose_output correct_output{CreateTokensResult{std::vector{"good substitution"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1061,7 +1112,7 @@ BOOST_AUTO_TEST_CASE(delimiter_sub) { BOOST_AUTO_TEST_CASE(index_ops) { choose_output out = run_choose("every\nother\nword\nis\nremoved\n5\n6\n7\n8\n9\n10", {"-r", "--index=after", "-f", "[02468]$", "--sub", "(.*) [0-9]+", "$1", "--index", "-t"}); - choose_output correct_output{std::vector{"0 every", "1 word", "2 removed", "3 6", "4 8", "5 10"}}; + choose_output correct_output{CreateTokensResult{std::vector{"0 every", "1 word", "2 removed", "3 6", "4 8", "5 10"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1073,44 +1124,44 @@ BOOST_AUTO_TEST_CASE(check_empty_input) { BOOST_AUTO_TEST_CASE(check_match_with_groups) { choose_output out = run_choose("abcde", {"-r", "--read=1", "--match", "b(c)(d)", "-t"}); - choose_output correct_output{std::vector{"bcd", "c", "d"}}; + choose_output correct_output{CreateTokensResult{std::vector{"bcd", "c", "d"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(check_match_with_groups_limit) { choose_output out = run_choose("abcde", {"-r", "--read=1", "--match", "b(c)(d)", "--head=2", "-t"}); - choose_output correct_output{std::vector{"bcd", "c"}}; + choose_output correct_output{CreateTokensResult{std::vector{"bcd", "c"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(check_no_match_lookbehind_retained) { // if there is no match, then the input is discarded but it keep enough for the lookbehind choose_output out = run_choose("aaabbbccc", {"--read=3", "-r", "--match", "(?<=aaa)bbb", "-t"}); - choose_output correct_output{std::vector{"bbb"}}; + choose_output correct_output{CreateTokensResult{std::vector{"bbb"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(check_partial_match_lookbehind_retained) { choose_output out = run_choose("aaabbbccc", {"--read=4", "-r", "--match", "(?<=aaa)bbb", "-t"}); - choose_output correct_output{std::vector{"bbb"}}; + choose_output correct_output{CreateTokensResult{std::vector{"bbb"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(check_no_delimiter_lookbehind_retained) { choose_output out = run_choose("aaabbbccc", {"--read=3", "-r", "(?<=aaa)bbb", "-t"}); - choose_output correct_output{std::vector{"aaa", "ccc"}}; + choose_output correct_output{CreateTokensResult{std::vector{"aaa", "ccc"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(check_partial_delimiter_lookbehind_retained) { choose_output out = run_choose("aaabbbccc", {"--read=4", "-r", "(?<=aaa)bbb", "-t"}); - choose_output correct_output{std::vector{"aaa", "ccc"}}; + choose_output correct_output{CreateTokensResult{std::vector{"aaa", "ccc"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(delimiter_no_match) { choose_output out = run_choose("aaabbbccc", {"zzzz", "--read=1", "-t"}); - choose_output correct_output{std::vector{"aaabbbccc"}}; + choose_output correct_output{CreateTokensResult{std::vector{"aaabbbccc"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1131,38 +1182,38 @@ BOOST_AUTO_TEST_CASE(empty_match_target) { // important since PCRE2_NOTEMPTY_ATSTART is used to prevent infinite loop; ensures progress choose_output out = run_choose("1234", {"--match", "", "-t"}); // one empty match in between each character and the end - choose_output correct_output{std::vector{"", "", "", "", ""}}; + choose_output correct_output{CreateTokensResult{std::vector{"", "", "", "", ""}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(input_is_delimiter) { choose_output out = run_choose("\n", {"-t"}); - choose_output correct_output{std::vector{""}}; + choose_output correct_output{CreateTokensResult{std::vector{""}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(input_is_delimiter_use_delimit) { choose_output out = run_choose("\n", {"--use-delimiter", "-t"}); - choose_output correct_output{std::vector{"", ""}}; + choose_output correct_output{CreateTokensResult{std::vector{"", ""}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(check_shrink_excess) { // creates a large subject but resizes internal buffer to remove the bytes that weren't written to choose_output out = run_choose("12345", {"zzzz", "--read=10000", "-t"}); - choose_output correct_output{std::vector{"12345"}}; + choose_output correct_output{CreateTokensResult{std::vector{"12345"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(no_multiline) { choose_output out = run_choose("this\nis\na\ntest", {"-r", "--match", "^t", "-t"}); - choose_output correct_output{std::vector{"t"}}; + choose_output correct_output{CreateTokensResult{std::vector{"t"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(yes_multiline) { choose_output out = run_choose("this\nis\na\ntest", {"-r", "--multiline", "--match", "^t", "-t"}); - choose_output correct_output{std::vector{"t", "t"}}; + choose_output correct_output{CreateTokensResult{std::vector{"t", "t"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1173,44 +1224,44 @@ BOOST_AUTO_TEST_CASE(begin_of_string) { // lookbehind of 1 allows characters to be retained, so it is correctly // recognized as not the beginning of the string. choose_output out = run_choose("uaaat", {"-r", "--match", "--read=1", "\\A[ut]", "-t"}); - choose_output correct_output{std::vector{"u"}}; + choose_output correct_output{CreateTokensResult{std::vector{"u"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(begin_of_line) { choose_output out = run_choose("abcd\nefgh", {"-r", "--multiline", "--match", "^.", "-t"}); - choose_output correct_output{std::vector{"a", "e"}}; + choose_output correct_output{CreateTokensResult{std::vector{"a", "e"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(end_of_string) { choose_output out = run_choose("uaaat", {"-r", "--match", "--read=6", "[ut]\\Z", "-t"}); - choose_output correct_output{std::vector{"t"}}; + choose_output correct_output{CreateTokensResult{std::vector{"t"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(end_of_line) { choose_output out = run_choose("abcd\nefgh", {"-r", "--multiline", ".$", "-t"}); - choose_output correct_output{std::vector{"abc", "\nefg"}}; + choose_output correct_output{CreateTokensResult{std::vector{"abc", "\nefg"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(buf_size_less_than_read) { // read takes the minimum of the available space in buffer left and the read amount choose_output out = run_choose("aaa1234aaa", {"--match", "1234", "--read=1000000", "--buf-size=3", "-t"}); - choose_output correct_output{std::vector{}}; + choose_output correct_output{CreateTokensResult{std::vector{}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(buf_size_match) { choose_output out = run_choose("aaa1234aaa", {"--match", "1234", "--read=1", "--buf-size=3", "-t"}); - choose_output correct_output{std::vector{}}; + choose_output correct_output{CreateTokensResult{std::vector{}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(buf_size_match_enough) { choose_output out = run_choose("aaaa1234aaaa", {"--match", "1234", "--read=1", "--buf-size=4", "-t"}); - choose_output correct_output{std::vector{"1234"}}; + choose_output correct_output{CreateTokensResult{std::vector{"1234"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1240,7 +1291,7 @@ BOOST_AUTO_TEST_CASE(buf_size_entirely_composed_incomplete_multibyte) { BOOST_AUTO_TEST_CASE(buf_size_partial_match_enough) { choose_output out = run_choose("aaa1234aaaa1234aaaa", {"--match", "1234", "--read=4", "--buf-size=4", "-t"}); - choose_output correct_output{std::vector{"1234", "1234"}}; + choose_output correct_output{CreateTokensResult{std::vector{"1234", "1234"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1254,26 +1305,26 @@ BOOST_AUTO_TEST_CASE(frag_flush_in_process_token) { BOOST_AUTO_TEST_CASE(frag_prev_sep_offset_not_zero) { // when the buffer is filled because of lookbehind bytes, not from the previous delimiter end choose_output out = run_choose("123123", {"(?<=123)?123", "-r", "--read=1", "--buf-size=3", "-t"}); - choose_output correct_output{std::vector{"", ""}}; + choose_output correct_output{CreateTokensResult{std::vector{"", ""}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(frag_prev_sep_offset_not_zero_2) { choose_output out = run_choose("123123", {"(?<=123)123", "-r", "--read=1", "--buf-size=3", "-t"}); - choose_output correct_output{std::vector{"123123"}}; + choose_output correct_output{CreateTokensResult{std::vector{"123123"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(frag_buffer_too_small_appending) { choose_output out = run_choose("123123123abc", {"abc", "--read=1", "--buf-size=3", "--buf-size-frag=3", "-t"}); - choose_output correct_output{std::vector{"123"}}; + choose_output correct_output{CreateTokensResult{std::vector{"123"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(frag_buffer_too_small_process_token) { // checks that process_token discards the fragment if it would exceed the fragment size choose_output out = run_choose("12341abc", {"abc", "--read=4", "--buf-size=4", "--buf-size-frag=4", "-t"}); - choose_output correct_output{std::vector{""}}; + choose_output correct_output{CreateTokensResult{std::vector{""}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1293,21 +1344,21 @@ BOOST_AUTO_TEST_CASE(process_fragment_in_count) { BOOST_AUTO_TEST_CASE(buf_size_delimiter_limit) { // ensure match failure behaviour on buffer full choose_output out = run_choose("qwerty123testerabqwerty123tester", {"-r", "(?:123)|(?:ab)", "--read=1", "--buf-size=2", "--buf-size-frag=1000", "-t"}); - choose_output correct_output{std::vector{"qwerty123tester", "qwerty123tester"}}; + choose_output correct_output{CreateTokensResult{std::vector{"qwerty123tester", "qwerty123tester"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(buf_size_delimiter_limit_from_lookbehind_enough) { // same as above, but because the lookbehind is too big choose_output out = run_choose("abcd12abcd12abcd", {"-r", "(?<=cd)12", "--read=1", "--buf-size=4", "--buf-size-frag=1000", "-t"}); - choose_output correct_output{std::vector{"abcd", "abcd", "abcd"}}; + choose_output correct_output{CreateTokensResult{std::vector{"abcd", "abcd", "abcd"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(buf_size_delimiter_limit_from_lookbehind) { // same as above, but because the lookbehind is too big choose_output out = run_choose("abcd12abcd12abcd", {"-r", "(?<=cd)12", "--read=1", "--buf-size=3", "--buf-size-frag=1000", "-t"}); - choose_output correct_output{std::vector{"abcd12abcd12abcd"}}; + choose_output correct_output{CreateTokensResult{std::vector{"abcd12abcd12abcd"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1315,7 +1366,7 @@ BOOST_AUTO_TEST_CASE(complete_utf8) { // checks that the last utf8 char is completed before sending it to pcre2 const char ch[] = {(char)0xE6, (char)0xBC, (char)0xA2, 0}; choose_output out = run_choose(ch, {"--read=1", "--utf", "-t"}); - choose_output correct_output{std::vector{ch}}; + choose_output correct_output{CreateTokensResult{std::vector{ch}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1325,14 +1376,14 @@ BOOST_AUTO_TEST_CASE(utf8_lookback_separates_multibyte) { // the lookbehind must be correctly decremented to include the 0xE6 byte const char pattern[] = {'(', '?', '<', '=', (char)0xE6, (char)0xBC, (char)0xA2, 't', 'e', ')', 's', 't', 0}; choose_output out = run_choose(ch, {"-r", "--max-lookbehind=1", "--read=1", "--utf", "--match", pattern, "-t"}); - choose_output correct_output{std::vector{"st"}}; + choose_output correct_output{CreateTokensResult{std::vector{"st"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(invalid_utf8) { const char ch[] = {(char)0xFF, (char)0b11000000, (char)0b10000000, (char)0b10000000, 't', 'e', 's', 't', 0}; choose_output out = run_choose(ch, {"-r", "--read=1", "--utf-allow-invalid", "--match", "test", "-t"}); - choose_output correct_output{std::vector{"test"}}; + choose_output correct_output{CreateTokensResult{std::vector{"test"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1342,14 +1393,14 @@ BOOST_AUTO_TEST_CASE(invalid_utf8_separating_multibyte_not_ok) { // the subject_effective_end logic const char ch[] = {(char)0xE6, (char)0xBC, (char)0xA2, 't', 'e', 's', 't', 0}; choose_output out = run_choose(ch, {"-r", "--read=1", "--utf-allow-invalid", "--match", ch, "-t"}); - choose_output correct_output{std::vector{ch}}; + choose_output correct_output{CreateTokensResult{std::vector{ch}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(invalid_utf8_overlong_byte) { const char ch[] = {continuation, continuation, continuation, continuation, 0}; choose_output out = run_choose(ch, {"-r", "--read=1", "--utf-allow-invalid", "--match", "anything", "-t"}); - choose_output correct_output{std::vector{}}; + choose_output correct_output{CreateTokensResult{std::vector{}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } @@ -1372,13 +1423,13 @@ BOOST_AUTO_TEST_CASE(null_output_delimiters) { BOOST_AUTO_TEST_CASE(null_input_delimiter) { choose_output out = run_choose(std::vector{'a', '\0', 'b', '\0', 'c'}, {"--read0", "-t"}); - choose_output correct_output{std::vector{"a", "b", "c"}}; + choose_output correct_output{CreateTokensResult{std::vector{"a", "b", "c"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } BOOST_AUTO_TEST_CASE(in_index_before) { choose_output out = run_choose("this\nis\na\ntest", {"--index=before", "-t"}); - choose_output correct_output{std::vector{"0 this", "1 is", "2 a", "3 test"}}; + choose_output correct_output{CreateTokensResult{std::vector{"0 this", "1 is", "2 a", "3 test"}}}; BOOST_REQUIRE_EQUAL(out, correct_output); } diff --git a/src/token.hpp b/src/token.hpp index 175b6d9..f085df7 100644 --- a/src/token.hpp +++ b/src/token.hpp @@ -92,7 +92,6 @@ struct Token { } } - private: // point to range in buffer, a special field of interest const char* field_begin = 0; const char* field_end = 0; @@ -210,13 +209,19 @@ bool general_numeric_comparison(const Token& lhs, const Token& rhs) { // } // namespace +struct CreateTokensResult { + std::vector tokens; + // used for --tui-select + std::optional initial_selected_token = {}; +}; + // reads from args.input // if args.tui: // returns the tokens // else // writes to args.output, then throws a termination_request exception, // which the caller should handle (exit unless unit test) -std::vector create_tokens(choose::Arguments& args) { +CreateTokensResult create_tokens(choose::Arguments& args) { const bool single_byte_delimiter = args.in_byte_delimiter.has_value(); const bool is_utf = args.primary ? regex::options(args.primary) & PCRE2_UTF : false; const bool is_invalid_utf = args.primary ? regex::options(args.primary) & PCRE2_MATCH_INVALID_UTF : false; @@ -249,7 +254,10 @@ std::vector create_tokens(choose::Arguments& args) { uint32_t match_options = PCRE2_PARTIAL_HARD; TokenOutputStream direct_output(args); // if is_direct_output, this is used - std::vector output; // !tokens_not_stored, this is used + + // fields for CreateTokensResult + std::optional initial_selected_token = {}; // !tokens_not_stored, these two are used + std::vector output; if (args.out_end == 0) { // edge case on logic. it adds a token, then checks if the out limit has been hit @@ -369,6 +377,8 @@ std::vector create_tokens(choose::Arguments& args) { bool t_is_set = false; Token t; + bool token_is_selected = false; // for --tui-select + if (!fragment.empty()) { if (fragment.size() + (end - begin) > args.buf_size_frag) { args.drop_warning(); @@ -448,6 +458,8 @@ std::vector create_tokens(choose::Arguments& args) { return true; }; + bool ret = false; // return value + for (OrderedOp& op : args.ordered_ops) { if (RmOrFilterOp* rf_op = std::get_if(&op)) { if (rf_op->removes(begin, end)) { @@ -464,11 +476,14 @@ std::vector create_tokens(choose::Arguments& args) { default: break; } + } else if (TuiSelectOp* tui_select_op = std::get_if(&op)) { + if (!initial_selected_token.has_value() && tui_select_op->matches(begin, end)) { + // set the cursor to here, only if it makes it through all following ops + token_is_selected = true; + } } else { if (tokens_not_stored && &op == &*args.ordered_ops.rbegin()) { if (ReplaceOp* rep_op = std::get_if(&op)) { - // placing this on the stack instead had no noticable difference - // in performance. perhaps elided std::vector out; rep_op->apply(out, subject, subject + subject_size, primary_data, args.primary); direct_output.write_output(&*out.cbegin(), &*out.cend()); @@ -516,7 +531,8 @@ std::vector create_tokens(choose::Arguments& args) { if (is_direct_output) { if (!tokens_not_stored) { if (!check_unique_then_append()) { - return false; + ret = false; + goto end; } } direct_output.write_output(begin, end); @@ -530,17 +546,33 @@ std::vector create_tokens(choose::Arguments& args) { direct_output.finish_output(); throw termination_request(); } - return false; + ret = false; + goto end; } else { check_unique_then_append(); // result ignored // handle the case mentioned in check_unique_then_append if (mem_is_bounded && !sort && !tail) { if (output.size() == *args.out_end) { - return true; + ret = true; + goto end; } } - return false; + ret = false; + goto end; + } + +end: + if (unlikely(token_is_selected && !initial_selected_token.has_value())) { + Token selected; + // manual copy here (since copying is disabled on tokens otherwise) + selected.buffer = output.rbegin()->buffer; +#ifndef CHOOSE_DISABLE_FIELD + selected.field_begin = output.rbegin()->field_begin; + selected.field_end = output.rbegin()->field_end; +#endif + initial_selected_token = selected; } + return ret; }; while (1) { @@ -813,7 +845,7 @@ std::vector create_tokens(choose::Arguments& args) { } } } else { - // truncate the ends, leaving only the beginning elements + // truncate the ends if (mem_is_bounded) { // sort and end truncation has already been applied } else { @@ -875,7 +907,7 @@ std::vector create_tokens(choose::Arguments& args) { throw termination_request(); } - return output; + return CreateTokensResult{std::move(output), std::move(initial_selected_token)}; } } // namespace choose