diff --git a/src/builtin_commandline.cpp b/src/builtin_commandline.cpp index 5f7c27a16..a4fb80f46 100644 --- a/src/builtin_commandline.cpp +++ b/src/builtin_commandline.cpp @@ -325,11 +325,10 @@ int builtin_commandline(parser_t &parser, io_streams_t &streams, wchar_t **argv) } for (i = w.woptind; i < argc; i++) { - wchar_t c = input_function_get_code(argv[i]); - if (c != INPUT_CODE_NONE) { + if (auto mc = input_function_get_code(argv[i])) { // input_unreadch inserts the specified keypress or readline function at the back of // the queue of unused keypresses. - input_queue_ch(c); + input_queue_ch(*mc); } else { streams.err.append_format(_(L"%ls: Unknown input function '%ls'"), cmd, argv[i]); builtin_print_help(parser, streams, cmd, streams.err); diff --git a/src/common.cpp b/src/common.cpp index b50bcac37..d097f5844 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -2382,8 +2382,7 @@ char **make_null_terminated_array(const std::vector &lst) { // TODO: Actually implement the replacement as documented above. bool fish_reserved_codepoint(wchar_t c) { return (c >= RESERVED_CHAR_BASE && c < RESERVED_CHAR_END) || - (c >= ENCODE_DIRECT_BASE && c < ENCODE_DIRECT_END) || - (c >= INPUT_COMMON_BASE && c < INPUT_COMMON_END); + (c >= ENCODE_DIRECT_BASE && c < ENCODE_DIRECT_END); } /// Reopen stdin, stdout and/or stderr on /dev/null. This is invoked when we find that our tty has diff --git a/src/common.h b/src/common.h index e84edaa50..cf6963349 100644 --- a/src/common.h +++ b/src/common.h @@ -87,8 +87,6 @@ typedef std::vector wcstring_list_t; // on Mac OS X. See http://www.unicode.org/faq/private_use.html. #define ENCODE_DIRECT_BASE (wchar_t)0xF600 #define ENCODE_DIRECT_END (ENCODE_DIRECT_BASE + 256) -#define INPUT_COMMON_BASE (wchar_t)0xF700 -#define INPUT_COMMON_END (INPUT_COMMON_BASE + 64) // NAME_MAX is not defined on Solaris #if !defined(NAME_MAX) diff --git a/src/fish_key_reader.cpp b/src/fish_key_reader.cpp index be845c63b..ba688b4e7 100644 --- a/src/fish_key_reader.cpp +++ b/src/fish_key_reader.cpp @@ -204,13 +204,13 @@ static void process_input(bool continuous_mode) { std::fwprintf(stderr, L"Press a key\n\n"); while (keep_running) { - wchar_t wc; + char_event_t evt{0}; if (reader_test_and_clear_interrupted()) { - wc = shell_modes.c_cc[VINTR]; + evt = char_event_t{shell_modes.c_cc[VINTR]}; } else { - wc = input_common_readch(true); + evt = input_common_readch_timed(true); } - if (wc == R_TIMEOUT || wc == R_EOF) { + if (!evt.is_char()) { output_bind_command(bind_chars); if (first_char_seen && !continuous_mode) { return; @@ -218,6 +218,7 @@ static void process_input(bool continuous_mode) { continue; } + wchar_t wc = evt.get_char(); prev_tstamp = output_elapsed_time(prev_tstamp, first_char_seen); add_char_to_bind_command(wc, bind_chars); output_info_about_char(wc); diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index 364701fe0..dfee85398 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -2996,9 +2996,11 @@ static void test_input() { } // Now test. - wint_t c = input_readch(); - if (c != R_DOWN_LINE) { - err(L"Expected to read char R_DOWN_LINE, but instead got %ls\n", describe_char(c).c_str()); + auto evt = input_readch(); + if (!evt.is_readline()) { + err(L"Event is not a readline"); + } else if (evt.get_readline() != readline_cmd_t::down_line) { + err(L"Expected to read char down_line"); } } diff --git a/src/input.cpp b/src/input.cpp index 5cece3eeb..256e0f62c 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -51,6 +51,9 @@ struct input_mapping_t { static unsigned int s_last_input_map_spec_order = 0; specification_order = ++s_last_input_map_spec_order; } + + /// \return true if this is a generic mapping, i.e. acts as a fallback. + bool is_generic() const { return seq.empty(); } }; /// A struct representing the mapping from a terminfo key name to a terminfo character sequence. @@ -64,68 +67,69 @@ static constexpr size_t input_function_count = R_END_INPUT_FUNCTIONS - R_BEGIN_I /// Input function metadata. This list should be kept in sync with the key code list in /// input_common.h. struct input_function_metadata_t { - wchar_t code; + readline_cmd_t code; const wchar_t *name; }; + static const input_function_metadata_t input_function_metadata[] = { - {R_BEGINNING_OF_LINE, L"beginning-of-line"}, - {R_END_OF_LINE, L"end-of-line"}, - {R_FORWARD_CHAR, L"forward-char"}, - {R_BACKWARD_CHAR, L"backward-char"}, - {R_FORWARD_WORD, L"forward-word"}, - {R_BACKWARD_WORD, L"backward-word"}, - {R_FORWARD_BIGWORD, L"forward-bigword"}, - {R_BACKWARD_BIGWORD, L"backward-bigword"}, - {R_HISTORY_SEARCH_BACKWARD, L"history-search-backward"}, - {R_HISTORY_SEARCH_FORWARD, L"history-search-forward"}, - {R_DELETE_CHAR, L"delete-char"}, - {R_BACKWARD_DELETE_CHAR, L"backward-delete-char"}, - {R_KILL_LINE, L"kill-line"}, - {R_YANK, L"yank"}, - {R_YANK_POP, L"yank-pop"}, - {R_COMPLETE, L"complete"}, - {R_COMPLETE_AND_SEARCH, L"complete-and-search"}, - {R_PAGER_TOGGLE_SEARCH, L"pager-toggle-search"}, - {R_BEGINNING_OF_HISTORY, L"beginning-of-history"}, - {R_END_OF_HISTORY, L"end-of-history"}, - {R_BACKWARD_KILL_LINE, L"backward-kill-line"}, - {R_KILL_WHOLE_LINE, L"kill-whole-line"}, - {R_KILL_WORD, L"kill-word"}, - {R_KILL_BIGWORD, L"kill-bigword"}, - {R_BACKWARD_KILL_WORD, L"backward-kill-word"}, - {R_BACKWARD_KILL_PATH_COMPONENT, L"backward-kill-path-component"}, - {R_BACKWARD_KILL_BIGWORD, L"backward-kill-bigword"}, - {R_HISTORY_TOKEN_SEARCH_BACKWARD, L"history-token-search-backward"}, - {R_HISTORY_TOKEN_SEARCH_FORWARD, L"history-token-search-forward"}, - {R_SELF_INSERT, L"self-insert"}, - {R_TRANSPOSE_CHARS, L"transpose-chars"}, - {R_TRANSPOSE_WORDS, L"transpose-words"}, - {R_UPCASE_WORD, L"upcase-word"}, - {R_DOWNCASE_WORD, L"downcase-word"}, - {R_CAPITALIZE_WORD, L"capitalize-word"}, - {R_VI_ARG_DIGIT, L"vi-arg-digit"}, - {R_VI_DELETE_TO, L"vi-delete-to"}, - {R_EXECUTE, L"execute"}, - {R_BEGINNING_OF_BUFFER, L"beginning-of-buffer"}, - {R_END_OF_BUFFER, L"end-of-buffer"}, - {R_REPAINT, L"repaint"}, - {R_FORCE_REPAINT, L"force-repaint"}, - {R_UP_LINE, L"up-line"}, - {R_DOWN_LINE, L"down-line"}, - {R_SUPPRESS_AUTOSUGGESTION, L"suppress-autosuggestion"}, - {R_ACCEPT_AUTOSUGGESTION, L"accept-autosuggestion"}, - {R_BEGIN_SELECTION, L"begin-selection"}, - {R_SWAP_SELECTION_START_STOP, L"swap-selection-start-stop"}, - {R_END_SELECTION, L"end-selection"}, - {R_KILL_SELECTION, L"kill-selection"}, - {R_FORWARD_JUMP, L"forward-jump"}, - {R_BACKWARD_JUMP, L"backward-jump"}, - {R_FORWARD_JUMP_TILL, L"forward-jump-till"}, - {R_BACKWARD_JUMP_TILL, L"backward-jump-till"}, - {R_REPEAT_JUMP, L"repeat-jump"}, - {R_REVERSE_REPEAT_JUMP, L"repeat-jump-reverse"}, - {R_AND, L"and"}, - {R_CANCEL, L"cancel"}}; + {readline_cmd_t::beginning_of_line, L"beginning-of-line"}, + {readline_cmd_t::end_of_line, L"end-of-line"}, + {readline_cmd_t::forward_char, L"forward-char"}, + {readline_cmd_t::backward_char, L"backward-char"}, + {readline_cmd_t::forward_word, L"forward-word"}, + {readline_cmd_t::backward_word, L"backward-word"}, + {readline_cmd_t::forward_bigword, L"forward-bigword"}, + {readline_cmd_t::backward_bigword, L"backward-bigword"}, + {readline_cmd_t::history_search_backward, L"history-search-backward"}, + {readline_cmd_t::history_search_forward, L"history-search-forward"}, + {readline_cmd_t::delete_char, L"delete-char"}, + {readline_cmd_t::backward_delete_char, L"backward-delete-char"}, + {readline_cmd_t::kill_line, L"kill-line"}, + {readline_cmd_t::yank, L"yank"}, + {readline_cmd_t::yank_POP, L"yank-pop"}, + {readline_cmd_t::complete, L"complete"}, + {readline_cmd_t::complete_AND_SEARCH, L"complete-and-search"}, + {readline_cmd_t::pager_toggle_search, L"pager-toggle-search"}, + {readline_cmd_t::beginning_of_history, L"beginning-of-history"}, + {readline_cmd_t::end_of_history, L"end-of-history"}, + {readline_cmd_t::backward_kill_line, L"backward-kill-line"}, + {readline_cmd_t::kill_whole_line, L"kill-whole-line"}, + {readline_cmd_t::kill_word, L"kill-word"}, + {readline_cmd_t::kill_bigword, L"kill-bigword"}, + {readline_cmd_t::backward_kill_word, L"backward-kill-word"}, + {readline_cmd_t::backward_kill_path_component, L"backward-kill-path-component"}, + {readline_cmd_t::backward_kill_bigword, L"backward-kill-bigword"}, + {readline_cmd_t::history_token_search_backward, L"history-token-search-backward"}, + {readline_cmd_t::history_token_search_forward, L"history-token-search-forward"}, + {readline_cmd_t::self_insert, L"self-insert"}, + {readline_cmd_t::transpose_chars, L"transpose-chars"}, + {readline_cmd_t::transpose_words, L"transpose-words"}, + {readline_cmd_t::upcase_word, L"upcase-word"}, + {readline_cmd_t::downcase_word, L"downcase-word"}, + {readline_cmd_t::capitalize_word, L"capitalize-word"}, + {readline_cmd_t::vi_arg_digit, L"vi-arg-digit"}, + {readline_cmd_t::vi_delete_to, L"vi-delete-to"}, + {readline_cmd_t::execute, L"execute"}, + {readline_cmd_t::beginning_of_buffer, L"beginning-of-buffer"}, + {readline_cmd_t::end_of_buffer, L"end-of-buffer"}, + {readline_cmd_t::repaint, L"repaint"}, + {readline_cmd_t::force_repaint, L"force-repaint"}, + {readline_cmd_t::up_line, L"up-line"}, + {readline_cmd_t::down_line, L"down-line"}, + {readline_cmd_t::suppress_autosuggestion, L"suppress-autosuggestion"}, + {readline_cmd_t::accept_autosuggestion, L"accept-autosuggestion"}, + {readline_cmd_t::begin_selection, L"begin-selection"}, + {readline_cmd_t::swap_selection_start_stop, L"swap-selection-start-stop"}, + {readline_cmd_t::end_selection, L"end-selection"}, + {readline_cmd_t::kill_selection, L"kill-selection"}, + {readline_cmd_t::forward_jump, L"forward-jump"}, + {readline_cmd_t::backward_jump, L"backward-jump"}, + {readline_cmd_t::forward_jump_till, L"forward-jump-till"}, + {readline_cmd_t::backward_jump_till, L"backward-jump-till"}, + {readline_cmd_t::repeat_jump, L"repeat-jump"}, + {readline_cmd_t::reverse_repeat_jump, L"repeat-jump-reverse"}, + {readline_cmd_t::func_and, L"and"}, + {readline_cmd_t::cancel, L"cancel"}}; static_assert(sizeof(input_function_metadata) / sizeof(input_function_metadata[0]) == input_function_count, @@ -179,12 +183,12 @@ void input_set_bind_mode(const wcstring &bm) { } /// Returns the arity of a given input function. -static int input_function_arity(int function) { +static int input_function_arity(readline_cmd_t function) { switch (function) { - case R_FORWARD_JUMP: - case R_BACKWARD_JUMP: - case R_FORWARD_JUMP_TILL: - case R_BACKWARD_JUMP_TILL: + case readline_cmd_t::forward_jump: + case readline_cmd_t::backward_jump: + case readline_cmd_t::forward_jump_till: + case readline_cmd_t::backward_jump_till: return 1; default: return 0; @@ -246,17 +250,21 @@ void input_mapping_add(const wchar_t *sequence, const wchar_t *command, const wc /// Handle interruptions to key reading by reaping finshed jobs and propagating the interrupt to the /// reader. -static int interrupt_handler() { +static maybe_t interrupt_handler() { // Fire any pending events. event_fire_delayed(); // Reap stray processes, including printing exit status messages. if (job_reap(true)) reader_repaint_needed(); // Tell the reader an event occured. if (reader_reading_interrupted()) { - return shell_modes.c_cc[VINTR]; + auto vintr = shell_modes.c_cc[VINTR]; + if (vintr == 0) { + return none(); + } + return char_event_t{vintr}; } - return R_NULL; + return char_event_t{char_event_type_t::check_exit}; } static std::atomic input_initialized{false}; @@ -295,19 +303,21 @@ void input_function_push_arg(wchar_t arg) { wchar_t input_function_pop_arg() { return input_function_args[--input_function_args_index]; } -void input_function_push_args(int code) { +void input_function_push_args(readline_cmd_t code) { int arity = input_function_arity(code); - std::vector skipped; + std::vector skipped; for (int i = 0; i < arity; i++) { - wchar_t arg; - // Skip and queue up any function codes. See issue #2357. - while ((arg = input_common_readch(0)) >= R_BEGIN_INPUT_FUNCTIONS && - arg < R_END_INPUT_FUNCTIONS) { - skipped.push_back(arg); + wchar_t arg{}; + for (;;) { + auto evt = input_common_readch(); + if (evt.is_char()) { + arg = evt.get_char(); + break; + } + skipped.push_back(evt); } - input_function_push_arg(arg); } @@ -326,7 +336,7 @@ static void input_mapping_execute(const input_mapping_t &m, bool allow_commands) bool has_commands = false, has_functions = false; for (const wcstring &cmd : m.commands) { - if (input_function_get_code(cmd) != INPUT_CODE_NONE) + if (input_function_get_code(cmd)) has_functions = true; else has_commands = true; @@ -339,19 +349,19 @@ static void input_mapping_execute(const input_mapping_t &m, bool allow_commands) } if (has_commands && !allow_commands) { - // We don't want to run commands yet. Put the characters back and return R_NULL. + // We don't want to run commands yet. Put the characters back and return check_exit. for (wcstring::const_reverse_iterator it = m.seq.rbegin(), end = m.seq.rend(); it != end; ++it) { input_common_next_ch(*it); } - input_common_next_ch(R_NULL); + input_common_next_ch(char_event_type_t::check_exit); return; // skip the input_set_bind_mode } else if (has_functions && !has_commands) { // Functions are added at the head of the input queue. for (wcstring_list_t::const_reverse_iterator it = m.commands.rbegin(), end = m.commands.rend(); it != end; ++it) { - wchar_t code = input_function_get_code(*it); + readline_cmd_t code = input_function_get_code(*it).value(); input_function_push_args(code); input_common_next_ch(code); } @@ -365,11 +375,11 @@ static void input_mapping_execute(const input_mapping_t &m, bool allow_commands) parser_t::principal_parser().eval(cmd, io_chain_t(), TOP); } proc_set_last_statuses(std::move(last_statuses)); - input_common_next_ch(R_NULL); + input_common_next_ch(char_event_type_t::check_exit); } else { // Invalid binding, mixed commands and functions. We would need to execute these one by // one. - input_common_next_ch(R_NULL); + input_common_next_ch(char_event_type_t::check_exit); } // Empty bind mode indicates to not reset the mode (#2871) @@ -384,15 +394,13 @@ static bool input_mapping_is_match(const input_mapping_t &m) { bool timed = false; for (size_t i = 0; i < str.size(); ++i) { - wchar_t read = input_common_readch(timed); - - if (read != str[i]) { - // We didn't match the bind sequence/input mapping, (it timed out or they entered something else) - // Undo consumption of the read characters since we didn't match the bind sequence and abort. - input_common_next_ch(read); - while (i--) { - input_common_next_ch(str[i]); - } + auto evt = timed ? input_common_readch_timed() : input_common_readch(); + if (!evt.is_char() || evt.get_char() != str[i]) { + // We didn't match the bind sequence/input mapping, (it timed out or they entered + // something else) Undo consumption of the read characters since we didn't match the + // bind sequence and abort. + input_common_next_ch(evt); + while (i--) input_common_next_ch(str[i]); return false; } @@ -404,115 +412,102 @@ static bool input_mapping_is_match(const input_mapping_t &m) { return true; } -void input_queue_ch(wint_t ch) { input_common_queue_ch(ch); } +void input_queue_ch(char_event_t ch) { input_common_queue_ch(ch); } -static void input_mapping_execute_matching_or_generic(bool allow_commands) { +/// \return the first mapping that matches, walking first over the user's mapping list, then the +/// preset list. \return null if nothing matches. +static const input_mapping_t *find_mapping() { const input_mapping_t *generic = NULL; - const auto &vars = parser_t::principal_parser().vars(); const wcstring bind_mode = input_get_bind_mode(vars); - for (auto& m : mapping_list) { - if (m.mode != bind_mode) { - continue; - } + const auto lists = {&mapping_list, &preset_mapping_list}; + for (const auto *listp : lists) { + for (const auto &m : *listp) { + if (m.mode != bind_mode) { + continue; + } - if (m.seq.length() == 0) { - generic = &m; - } else if (input_mapping_is_match(m)) { - input_mapping_execute(m, allow_commands); - return; + if (m.is_generic()) { + if (!generic) generic = &m; + } else if (input_mapping_is_match(m)) { + return &m; + } } } + return generic; +} - // HACK: This is ugly duplication. - for (auto& m : preset_mapping_list) { - if (m.mode != bind_mode) { - continue; - } - - if (m.seq.length() == 0) { - // Only use this generic if the user list didn't have one. - if (!generic) generic = &m; - } else if (input_mapping_is_match(m)) { - input_mapping_execute(m, allow_commands); - return; - } - } - - if (generic) { - input_mapping_execute(*generic, allow_commands); +static void input_mapping_execute_matching_or_generic(bool allow_commands) { + const input_mapping_t *mapping = find_mapping(); + if (mapping) { + input_mapping_execute(*mapping, allow_commands); } else { debug(2, L"no generic found, ignoring char..."); - wchar_t c = input_common_readch(0); - if (c == R_EOF) { - input_common_next_ch(c); + auto evt = input_common_readch(); + if (evt.is_eof()) { + input_common_next_ch(evt); } } } /// Helper function. Picks through the queue of incoming characters until we get to one that's not a /// readline function. -static wchar_t input_read_characters_only() { - std::vector functions_to_put_back; - wchar_t char_to_return; +static char_event_t input_read_characters_no_readline() { + std::vector saved_events; + char_event_t evt_to_return{0}; for (;;) { - char_to_return = input_common_readch(0); - bool is_readline_function = - (char_to_return >= R_BEGIN_INPUT_FUNCTIONS && char_to_return < R_END_INPUT_FUNCTIONS); - // R_NULL and R_EOF are more control characters than readline functions, so check specially - // for those. - if (!is_readline_function || char_to_return == R_NULL || char_to_return == R_EOF) { + auto evt = input_common_readch(); + if (evt.is_readline()) { + saved_events.push_back(evt); + } else { + evt_to_return = evt; break; } - // This is a readline function; save it off for later re-enqueing and try again. - functions_to_put_back.push_back(char_to_return); } // Restore any readline functions, in reverse to preserve their original order. - size_t idx = functions_to_put_back.size(); - while (idx--) { - input_common_next_ch(functions_to_put_back.at(idx)); + for (auto iter = saved_events.rbegin(); iter != saved_events.rend(); ++iter) { + input_common_next_ch(*iter); } - return char_to_return; + return evt_to_return; } -wint_t input_readch(bool allow_commands) { +char_event_t input_readch(bool allow_commands) { // Clear the interrupted flag. reader_reset_interrupted(); // Search for sequence in mapping tables. while (true) { - wchar_t c = input_common_readch(0); + auto evt = input_common_readch(); - if (c >= R_BEGIN_INPUT_FUNCTIONS && c < R_END_INPUT_FUNCTIONS) { - switch (c) { - case R_SELF_INSERT: { + if (evt.is_readline()) { + switch (evt.get_readline()) { + case readline_cmd_t::self_insert: { // Issue #1595: ensure we only insert characters, not readline functions. The // common case is that this will be empty. - return input_read_characters_only(); + return input_read_characters_no_readline(); } - case R_AND: { + case readline_cmd_t::func_and: { if (input_function_status) { return input_readch(); } - c = input_common_readch(0); - while (c >= R_BEGIN_INPUT_FUNCTIONS && c < R_END_INPUT_FUNCTIONS) { - c = input_common_readch(0); - } - input_common_next_ch(c); + do { + evt = input_common_readch(); + } while (evt.is_readline()); + input_common_next_ch(evt); return input_readch(); } - default: { return c; } + default: { return evt; } } - } else if (c == R_EOF) { - // If we have R_EOF, we need to immediately quit. + } else if (evt.is_eof()) { + // If we have EOF, we need to immediately quit. // There's no need to go through the input functions. - return R_EOF; + return evt; } else { - input_common_next_ch(c); + input_common_next_ch(evt); input_mapping_execute_matching_or_generic(allow_commands); - // Regarding allow_commands, we're in a loop, but if a fish command - // is executed, R_NULL is unread, so the next pass through the loop - // we'll break out and return it. + // Regarding allow_commands, we're in a loop, but if a fish command is executed, + // check_exit is unread, so the next pass through the loop we'll break out and return + // it. } } } @@ -800,11 +795,11 @@ wcstring_list_t input_function_get_names() { return result; } -wchar_t input_function_get_code(const wcstring &name) { +maybe_t input_function_get_code(const wcstring &name) { for (const auto &md : input_function_metadata) { if (name == md.name) { return md.code; } } - return INPUT_CODE_NONE; + return none(); } diff --git a/src/input.h b/src/input.h index 3ae272239..f76cede6a 100644 --- a/src/input.h +++ b/src/input.h @@ -9,6 +9,7 @@ #include "builtin_bind.h" #include "common.h" +#include "input_common.h" #define FISH_BIND_MODE_VAR L"fish_bind_mode" @@ -30,13 +31,13 @@ void init_input(); /// key press, and is returned as such. /// /// The argument determines whether fish commands are allowed to be run as bindings. If false, when -/// a character is encountered that would invoke a fish command, it is unread and R_NULL is -/// returned. -wint_t input_readch(bool allow_commands = true); +/// a character is encountered that would invoke a fish command, it is unread and +/// char_event_type_t::check_exit is returned. +char_event_t input_readch(bool allow_commands = true); /// Enqueue a character or a readline function to the queue of unread characters that input_readch /// will return before actually reading from fd 0. -void input_queue_ch(wint_t ch); +void input_queue_ch(char_event_t ch); /// Add a key mapping from the specified sequence to the specified command. /// @@ -94,8 +95,7 @@ bool input_terminfo_get_name(const wcstring &seq, wcstring *out_name); wcstring_list_t input_terminfo_get_names(bool skip_null); /// Returns the input function code for the given input function name. -#define INPUT_CODE_NONE (wchar_t(-1)) -wchar_t input_function_get_code(const wcstring &name); +maybe_t input_function_get_code(const wcstring &name); /// Returns a list of all existing input function names. wcstring_list_t input_function_get_names(void); diff --git a/src/input_common.cpp b/src/input_common.cpp index 765a9d667..df3bbfcd1 100644 --- a/src/input_common.cpp +++ b/src/input_common.cpp @@ -33,8 +33,8 @@ #define WAIT_ON_ESCAPE_DEFAULT 30 static int wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT; -/// Characters that have been read and returned by the sequence matching code. -static std::deque lookahead_list; +/// Events which have been read and returned by the sequence matching code. +static std::deque lookahead_list; // Queue of pairs of (function pointer, argument) to be invoked. Expected to be mostly empty. typedef std::list> callback_queue_t; @@ -43,26 +43,35 @@ static void input_flush_callbacks(); static bool has_lookahead() { return !lookahead_list.empty(); } -static wint_t lookahead_pop() { - wint_t result = lookahead_list.front(); +static char_event_t lookahead_pop() { + auto result = lookahead_list.front(); lookahead_list.pop_front(); return result; } -static void lookahead_push_back(wint_t c) { lookahead_list.push_back(c); } +/// \return the next lookahead char, or none if none. Discards timeouts. +static maybe_t lookahead_pop_evt() { + while (has_lookahead()) { + auto evt = lookahead_pop(); + if (! evt.is_timeout()) { + return evt; + } + } + return none(); +} -static void lookahead_push_front(wint_t c) { lookahead_list.push_front(c); } +static void lookahead_push_back(char_event_t c) { lookahead_list.push_back(c); } -static wint_t lookahead_front() { return lookahead_list.front(); } +static void lookahead_push_front(char_event_t c) { lookahead_list.push_front(c); } /// Callback function for handling interrupts on reading. -static int (*interrupt_handler)(); +static interrupt_func_t interrupt_handler; -void input_common_init(int (*ih)()) { interrupt_handler = ih; } +void input_common_init(interrupt_func_t func) { interrupt_handler = func; } /// Internal function used by input_common_readch to read one byte from fd 0. This function should /// only be called by input_common_readch(). -static wint_t readb() { +static char_event_t readb() { // do_loop must be set on every path through the loop; leaving it uninitialized allows the // static analyzer to assist in catching mistakes. unsigned char arr[1]; @@ -107,15 +116,17 @@ static wint_t readb() { if (res == -1) { if (errno == EINTR || errno == EAGAIN) { if (interrupt_handler) { - int res = interrupt_handler(); - if (res) return res; - if (has_lookahead()) return lookahead_pop(); + if (auto interrupt_evt = interrupt_handler()) { + return *interrupt_evt; + } else if (auto mc = lookahead_pop_evt()) { + return *mc; + } } do_loop = true; } else { - // The terminal has been closed. Save and exit. - return R_EOF; + // The terminal has been closed. + return char_event_type_t::eof; } } else { // Assume we loop unless we see a character in stdin. @@ -133,15 +144,15 @@ static wint_t readb() { if (ioport > 0 && FD_ISSET(ioport, &fdset)) { iothread_service_completion(); - if (has_lookahead()) { - return lookahead_pop(); + if (auto mc = lookahead_pop_evt()) { + return *mc; } } if (FD_ISSET(STDIN_FILENO, &fdset)) { if (read_blocked(0, arr, 1) != 1) { - // The teminal has been closed. Save and exit. - return R_EOF; + // The teminal has been closed. + return char_event_type_t::eof; } // We read from stdin, so don't loop. @@ -173,63 +184,67 @@ void update_wait_on_escape_ms(const environment_t &vars) { } } -wchar_t input_common_readch(int timed) { - if (!has_lookahead()) { - if (timed) { - fd_set fds; - FD_ZERO(&fds); - FD_SET(0, &fds); - struct timeval tm = {wait_on_escape_ms / 1000, 1000 * (wait_on_escape_ms % 1000)}; - int count = select(1, &fds, 0, 0, &tm); - if (count <= 0) { - return R_TIMEOUT; - } +char_event_t input_common_readch() { + if (auto mc = lookahead_pop_evt()) { + return *mc; + } + wchar_t res; + mbstate_t state = {}; + while (1) { + auto evt = readb(); + if (!evt.is_char()) { + return evt; } - wchar_t res; - mbstate_t state = {}; - - while (1) { - wint_t b = readb(); - - if (b >= R_NULL && b < R_END_INPUT_FUNCTIONS) return b; - - if (MB_CUR_MAX == 1) { - // return (unsigned char)b; // single-byte locale, all values are legal - return b; // single-byte locale, all values are legal - } - - char bb = b; - size_t sz = std::mbrtowc(&res, &bb, 1, &state); - - switch (sz) { - case (size_t)(-1): { - std::memset(&state, '\0', sizeof(state)); - debug(2, L"Illegal input"); - return R_NULL; - } - case (size_t)(-2): { - break; - } - case 0: { - return 0; - } - default: { return res; } - } - } - } else { - if (!timed) { - while (has_lookahead() && lookahead_front() == R_TIMEOUT) lookahead_pop(); - if (!has_lookahead()) return input_common_readch(0); + wint_t b = evt.get_char(); + if (MB_CUR_MAX == 1) { + return b; // single-byte locale, all values are legal } - return lookahead_pop(); + char bb = b; + size_t sz = std::mbrtowc(&res, &bb, 1, &state); + + switch (sz) { + case (size_t)(-1): { + std::memset(&state, '\0', sizeof(state)); + debug(2, L"Illegal input"); + return char_event_type_t::check_exit; + } + case (size_t)(-2): { + break; + } + case 0: { + return 0; + } + default: { return res; } + } } } -void input_common_queue_ch(wint_t ch) { lookahead_push_back(ch); } +char_event_t input_common_readch_timed(bool dequeue_timeouts) { + char_event_t result{char_event_type_t::timeout}; + if (has_lookahead()) { + result = lookahead_pop(); + } else { + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + struct timeval tm = {wait_on_escape_ms / 1000, 1000 * (wait_on_escape_ms % 1000)}; + if (select(1, &fds, 0, 0, &tm) > 0) { + result = input_common_readch(); + } + } + // If we got a timeout, either through dequeuing or creating, ensure it stays on the queue. + if (result.is_timeout()) { + if (!dequeue_timeouts) lookahead_push_front(char_event_type_t::timeout); + return char_event_type_t::timeout; + } + return result; +} -void input_common_next_ch(wint_t ch) { lookahead_push_front(ch); } +void input_common_queue_ch(char_event_t ch) { lookahead_push_back(ch); } + +void input_common_next_ch(char_event_t ch) { lookahead_push_front(ch); } void input_common_add_callback(std::function callback) { ASSERT_IS_MAIN_THREAD(); diff --git a/src/input_common.h b/src/input_common.h index 58d1ac6f6..7489f0e96 100644 --- a/src/input_common.h +++ b/src/input_common.h @@ -7,102 +7,168 @@ #include #include "common.h" +#include "maybe.h" -enum { - R_MIN = INPUT_COMMON_BASE, - // R_NULL is sometimes returned by the input when a character was requested but none could be - // delivered, or when an exception happened. - R_NULL = R_MIN, - R_EOF, - - R_BEGINNING_OF_LINE, - R_END_OF_LINE, - R_FORWARD_CHAR, - R_BACKWARD_CHAR, - R_FORWARD_WORD, - R_BACKWARD_WORD, - R_FORWARD_BIGWORD, - R_BACKWARD_BIGWORD, - R_HISTORY_SEARCH_BACKWARD, - R_HISTORY_SEARCH_FORWARD, - R_DELETE_CHAR, - R_BACKWARD_DELETE_CHAR, - R_KILL_LINE, - R_YANK, - R_YANK_POP, - R_COMPLETE, - R_COMPLETE_AND_SEARCH, - R_PAGER_TOGGLE_SEARCH, - R_BEGINNING_OF_HISTORY, - R_END_OF_HISTORY, - R_BACKWARD_KILL_LINE, - R_KILL_WHOLE_LINE, - R_KILL_WORD, - R_KILL_BIGWORD, - R_BACKWARD_KILL_WORD, - R_BACKWARD_KILL_PATH_COMPONENT, - R_BACKWARD_KILL_BIGWORD, - R_HISTORY_TOKEN_SEARCH_BACKWARD, - R_HISTORY_TOKEN_SEARCH_FORWARD, - R_SELF_INSERT, - R_TRANSPOSE_CHARS, - R_TRANSPOSE_WORDS, - R_UPCASE_WORD, - R_DOWNCASE_WORD, - R_CAPITALIZE_WORD, - R_VI_ARG_DIGIT, - R_VI_DELETE_TO, - R_EXECUTE, - R_BEGINNING_OF_BUFFER, - R_END_OF_BUFFER, - R_REPAINT, - R_FORCE_REPAINT, - R_UP_LINE, - R_DOWN_LINE, - R_SUPPRESS_AUTOSUGGESTION, - R_ACCEPT_AUTOSUGGESTION, - R_BEGIN_SELECTION, - R_SWAP_SELECTION_START_STOP, - R_END_SELECTION, - R_KILL_SELECTION, - R_FORWARD_JUMP, - R_BACKWARD_JUMP, - R_FORWARD_JUMP_TILL, - R_BACKWARD_JUMP_TILL, - R_AND, - R_CANCEL, - R_REPEAT_JUMP, - R_REVERSE_REPEAT_JUMP, - - R_TIMEOUT, // we didn't get interactive input within wait_on_escape_ms - - // The range of key codes for inputrc-style keyboard functions that are passed on to the caller - // of input_read(). - R_BEGIN_INPUT_FUNCTIONS = R_BEGINNING_OF_LINE, - R_END_INPUT_FUNCTIONS = R_REVERSE_REPEAT_JUMP + 1 +enum class readline_cmd_t { + beginning_of_line, + end_of_line, + forward_char, + backward_char, + forward_word, + backward_word, + forward_bigword, + backward_bigword, + history_search_backward, + history_search_forward, + delete_char, + backward_delete_char, + kill_line, + yank, + yank_POP, + complete, + complete_AND_SEARCH, + pager_toggle_search, + beginning_of_history, + end_of_history, + backward_kill_line, + kill_whole_line, + kill_word, + kill_bigword, + backward_kill_word, + backward_kill_path_component, + backward_kill_bigword, + history_token_search_backward, + history_token_search_forward, + self_insert, + transpose_chars, + transpose_words, + upcase_word, + downcase_word, + capitalize_word, + vi_arg_digit, + vi_delete_to, + execute, + beginning_of_buffer, + end_of_buffer, + repaint, + force_repaint, + up_line, + down_line, + suppress_autosuggestion, + accept_autosuggestion, + begin_selection, + swap_selection_start_stop, + end_selection, + kill_selection, + forward_jump, + backward_jump, + forward_jump_till, + backward_jump_till, + func_and, + cancel, + repeat_jump, + reverse_repeat_jump, }; -/// Init the library. -void input_common_init(int (*ih)()); +// The range of key codes for inputrc-style keyboard functions. +enum { + R_BEGIN_INPUT_FUNCTIONS = static_cast(readline_cmd_t::beginning_of_line), + R_END_INPUT_FUNCTIONS = static_cast(readline_cmd_t::reverse_repeat_jump) + 1 +}; + +/// Represents an event on the character input stream. +enum class char_event_type_t : uint8_t { + /// A character was entered. + charc, + + /// A readline event. + readline, + + /// A timeout was hit. + timeout, + + /// end-of-file was reached. + eof, + + /// An event was handled internally, or an interrupt was received. Check to see if the reader + /// loop should exit. + check_exit, +}; + +class char_event_t { + union { + /// Set if the type is charc. + wchar_t c; + + /// Set if the type is readline. + readline_cmd_t rl; + } v_{}; + + public: + char_event_type_t type; + + bool is_timeout() const { return type == char_event_type_t::timeout; } + + bool is_char() const { return type == char_event_type_t::charc; } + + bool is_eof() const { return type == char_event_type_t::eof; } + + bool is_check_exit() const { return type == char_event_type_t::check_exit; } + + bool is_readline() const { return type == char_event_type_t::readline; } + + wchar_t get_char() const { + assert(type == char_event_type_t::charc && "Not a char type"); + return v_.c; + } + + readline_cmd_t get_readline() const { + assert(type == char_event_type_t::readline && "Not a readline type"); + return v_.rl; + } + + /* implicit */ char_event_t(wchar_t c) : type(char_event_type_t::charc) { v_.c = c; } + + /* implicit */ char_event_t(readline_cmd_t rl) : type(char_event_type_t::readline) { + v_.rl = rl; + } + + /* implicit */ char_event_t(char_event_type_t type) : type(type) { + assert(type != char_event_type_t::charc && type != char_event_type_t::readline && + "Cannot create a char event with this constructor"); + } +}; + +/// A type of function invoked on interrupt. +/// \return the event which is to be returned to the reader loop, or none if VINTR is 0. +using interrupt_func_t = maybe_t (*)(); + +/// Init the library with an interrupt function. +void input_common_init(interrupt_func_t func); /// Adjust the escape timeout. +class environment_t; void update_wait_on_escape_ms(const environment_t &vars); /// Function used by input_readch to read bytes from stdin until enough bytes have been read to /// convert them to a wchar_t. Conversion is done using mbrtowc. If a character has previously been -/// read and then 'unread' using \c input_common_unreadch, that character is returned. If timed is -/// true, readch2 will wait at most WAIT_ON_ESCAPE milliseconds for a character to be available for -/// reading before returning with the value R_EOF. -wchar_t input_common_readch(int timed); +/// read and then 'unread' using \c input_common_unreadch, that character is returned. +/// This function never returns a timeout. +char_event_t input_common_readch(); + +/// Like input_common_readch(), except it will wait at most WAIT_ON_ESCAPE milliseconds for a +/// character to be available for reading. +/// If \p dequeue_timeouts is set, remove any timeout from the queue; otherwise retain them. +char_event_t input_common_readch_timed(bool dequeue_timeouts = false); /// Enqueue a character or a readline function to the queue of unread characters that input_readch /// will return before actually reading from fd 0. -void input_common_queue_ch(wint_t ch); +void input_common_queue_ch(char_event_t ch); /// Add a character or a readline function to the front of the queue of unread characters. This /// will be the first character returned by input_readch (unless this function is called more than /// once). -void input_common_next_ch(wint_t ch); +void input_common_next_ch(char_event_t ch); /// Adds a callback to be invoked at the next turn of the "event loop." The callback function will /// be invoked and passed arg. diff --git a/src/kill.cpp b/src/kill.cpp index b005ba824..89fa8124c 100644 --- a/src/kill.cpp +++ b/src/kill.cpp @@ -17,10 +17,11 @@ typedef std::list kill_list_t; static kill_list_t kill_list; -void kill_add(const wcstring &str) { +void kill_add(wcstring str) { ASSERT_IS_MAIN_THREAD(); - if (str.empty()) return; - kill_list.push_front(str); + if (!str.empty()) { + kill_list.push_front(std::move(str)); + } } /// Remove first match for specified string from circular list. @@ -35,19 +36,19 @@ void kill_replace(const wcstring &old, const wcstring &newv) { kill_add(newv); } -const wchar_t *kill_yank_rotate() { +wcstring kill_yank_rotate() { ASSERT_IS_MAIN_THREAD(); // Move the first element to the end. if (kill_list.empty()) { - return NULL; + return {}; } kill_list.splice(kill_list.end(), kill_list, kill_list.begin()); - return kill_list.front().c_str(); + return kill_list.front(); } -const wchar_t *kill_yank() { +wcstring kill_yank() { if (kill_list.empty()) { - return L""; + return {}; } - return kill_list.front().c_str(); + return kill_list.front(); } diff --git a/src/kill.h b/src/kill.h index e22fb57f6..8e959ca50 100644 --- a/src/kill.h +++ b/src/kill.h @@ -11,12 +11,12 @@ void kill_replace(const wcstring &old, const wcstring &newv); /// Add a string to the top of the killring. -void kill_add(const wcstring &str); +void kill_add(wcstring str); /// Rotate the killring. -const wchar_t *kill_yank_rotate(); +wcstring kill_yank_rotate(); /// Paste from the killring. -const wchar_t *kill_yank(); +wcstring kill_yank(); #endif diff --git a/src/maybe.h b/src/maybe.h index a3e5cebda..b86b1c1ee 100644 --- a/src/maybe.h +++ b/src/maybe.h @@ -203,6 +203,12 @@ class maybe_t : private maybe_detail::conditionally_copyable_t { } bool operator!=(const maybe_t &rhs) const { return !(*this == rhs); } + + bool operator==(const T &rhs) const { return this->has_value() && this->value() == rhs; } + + bool operator!=(const T &rhs) const { return !(*this == rhs); } + + ~maybe_t() { reset(); } }; #endif diff --git a/src/reader.cpp b/src/reader.cpp index 02c5a11bc..409e69b46 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -105,7 +105,7 @@ /// The maximum number of characters to read from the keyboard without repainting. Note that this /// readahead will only occur if new characters are available for reading, fish will never block for /// more input without repainting. -#define READAHEAD_MAX 256 +static constexpr size_t READAHEAD_MAX = 256; /// A mode for calling the reader_kill function. In this mode, the new string is appended to the /// current contents of the kill buffer. @@ -316,6 +316,8 @@ struct highlight_result_t { } // namespace +struct readline_loop_state_t; + /// A struct describing the state of the interactive reader. These states can be stacked, in case /// reader_readline() calls are nested. This happens when the 'read' builtin is used. class reader_data_t : public std::enable_shared_from_this { @@ -441,6 +443,8 @@ class reader_data_t : public std::enable_shared_from_this { bool newv); maybe_t readline(int nchars); + maybe_t read_normal_chars(readline_loop_state_t &rls); + void handle_readline_command(readline_cmd_t cmd, readline_loop_state_t &rls); void clear_pager(); void select_completion_in_direction(enum selection_direction_t dir); @@ -1027,68 +1031,83 @@ void reader_force_exit() { } /// Indicates if the given command char ends paging. -static bool command_ends_paging(wchar_t c, bool focused_on_search_field) { +static bool command_ends_paging(readline_cmd_t c, bool focused_on_search_field) { + using rl = readline_cmd_t; switch (c) { - case R_HISTORY_SEARCH_BACKWARD: - case R_HISTORY_SEARCH_FORWARD: - case R_HISTORY_TOKEN_SEARCH_BACKWARD: - case R_HISTORY_TOKEN_SEARCH_FORWARD: - case R_ACCEPT_AUTOSUGGESTION: - case R_CANCEL: { + case rl::history_search_backward: + case rl::history_search_forward: + case rl::history_token_search_backward: + case rl::history_token_search_forward: + case rl::accept_autosuggestion: + case rl::cancel: { // These commands always end paging. return true; } - case R_COMPLETE: - case R_COMPLETE_AND_SEARCH: - case R_BACKWARD_CHAR: - case R_FORWARD_CHAR: - case R_UP_LINE: - case R_DOWN_LINE: - case R_NULL: - case R_REPAINT: - case R_SUPPRESS_AUTOSUGGESTION: - case R_BEGINNING_OF_HISTORY: - case R_END_OF_HISTORY: { + case rl::complete: + case rl::complete_AND_SEARCH: + case rl::backward_char: + case rl::forward_char: + case rl::up_line: + case rl::down_line: + case rl::repaint: + case rl::suppress_autosuggestion: + case rl::beginning_of_history: + case rl::end_of_history: { // These commands never end paging. return false; } - case R_EXECUTE: { - // R_EXECUTE does end paging, but only executes if it was not paging. So it's handled + case rl::execute: { + // execute does end paging, but only executes if it was not paging. So it's handled // specially. return false; } - case R_BEGINNING_OF_LINE: - case R_END_OF_LINE: - case R_FORWARD_WORD: - case R_BACKWARD_WORD: - case R_FORWARD_BIGWORD: - case R_BACKWARD_BIGWORD: - case R_DELETE_CHAR: - case R_BACKWARD_DELETE_CHAR: - case R_KILL_LINE: - case R_YANK: - case R_YANK_POP: - case R_BACKWARD_KILL_LINE: - case R_KILL_WHOLE_LINE: - case R_KILL_WORD: - case R_KILL_BIGWORD: - case R_BACKWARD_KILL_WORD: - case R_BACKWARD_KILL_PATH_COMPONENT: - case R_BACKWARD_KILL_BIGWORD: - case R_SELF_INSERT: - case R_TRANSPOSE_CHARS: - case R_TRANSPOSE_WORDS: - case R_UPCASE_WORD: - case R_DOWNCASE_WORD: - case R_CAPITALIZE_WORD: - case R_VI_ARG_DIGIT: - case R_VI_DELETE_TO: - case R_BEGINNING_OF_BUFFER: - case R_END_OF_BUFFER: { + case rl::beginning_of_line: + case rl::end_of_line: + case rl::forward_word: + case rl::backward_word: + case rl::forward_bigword: + case rl::backward_bigword: + case rl::delete_char: + case rl::backward_delete_char: + case rl::kill_line: + case rl::yank: + case rl::yank_POP: + case rl::backward_kill_line: + case rl::kill_whole_line: + case rl::kill_word: + case rl::kill_bigword: + case rl::backward_kill_word: + case rl::backward_kill_path_component: + case rl::backward_kill_bigword: + case rl::self_insert: + case rl::transpose_chars: + case rl::transpose_words: + case rl::upcase_word: + case rl::downcase_word: + case rl::capitalize_word: + case rl::vi_arg_digit: + case rl::vi_delete_to: + case rl::beginning_of_buffer: + case rl::end_of_buffer: // These commands operate on the search field if that's where the focus is. return !focused_on_search_field; - } - default: { return false; } + default: + return false; + } +} + +/// Indicates if the given command ends the history search. +static bool command_ends_history_search(readline_cmd_t c) { + switch (c) { + case readline_cmd_t::history_search_backward: + case readline_cmd_t::history_search_forward: + case readline_cmd_t::history_token_search_backward: + case readline_cmd_t::history_token_search_forward: + case readline_cmd_t::repaint: + case readline_cmd_t::force_repaint: + return false; + default: + return true; } } @@ -2382,19 +2401,825 @@ static bool text_ends_in_comment(const wcstring &text) { return token.type == TOK_COMMENT; } -maybe_t reader_data_t::readline(int nchars) { - wint_t c; - int last_char = 0; - size_t yank_len = 0; - const wchar_t *yank_str; - bool comp_empty = true; +/// \return true if an event is a normal character that should be inserted into the buffer. +static bool event_is_normal_char(const char_event_t &evt) { + if (!evt.is_char()) return false; + auto c = evt.get_char(); + return !fish_reserved_codepoint(c) && c > 31 && c != 127; +} + +/// readline_loop_state_t encapsulates the state used in a readline loop. +/// It is always stack allocated transient. This state should not be "publicly visible;" public +/// state should be in reader_data_t. +struct readline_loop_state_t { + /// The last command that was executed. + maybe_t last_cmd{}; + + /// If the last command was a yank, the length of yanking that occurred. + size_t yank_len{0}; + + /// If set, it means nothing has been inserted into the command line via completion machinery. + bool comp_empty{true}; + + /// List of completions. std::vector comp; - int finished = 0; + + /// Whether we are skipping redundant repaints. + bool coalescing_repaints = false; + + /// Whether the loop has finished, due to reaching the character limit or through executing a + /// command. + bool finished{false}; + + /// Maximum number of characters to read. + size_t nchars{std::numeric_limits::max()}; +}; + +/// Read normal characters, inserting them into the command line. +/// \return the next unhandled event. +maybe_t reader_data_t::read_normal_chars(readline_loop_state_t &rls) { + int was_interactive_read = is_interactive_read; + is_interactive_read = 1; + maybe_t event_needing_handling = input_readch(); + is_interactive_read = was_interactive_read; + + if (!event_is_normal_char(*event_needing_handling) || !can_read(STDIN_FILENO)) + return event_needing_handling; + + // This is a normal character input. + // We are going to handle it directly, accumulating more. + char_event_t evt = event_needing_handling.acquire(); + size_t limit = std::min(rls.nchars - command_line.size(), READAHEAD_MAX); + + wchar_t arr[READAHEAD_MAX + 1] = {}; + arr[0] = evt.get_char(); + + for (size_t i = 1; i < limit; ++i) { + if (!can_read(0)) { + break; + } + // Only allow commands on the first key; otherwise, we might have data we + // need to insert on the commandline that the commmand might need to be able + // to see. + auto next_event = input_readch(false); + if (event_is_normal_char(next_event)) { + arr[i] = next_event.get_char(); + } else { + // We need to process this in the outer loop. + assert(!event_needing_handling && "Should not have an unhandled event"); + event_needing_handling = next_event; + break; + } + } + + editable_line_t *el = active_edit_line(); + insert_string(el, arr, true); + + // End paging upon inserting into the normal command line. + if (el == &command_line) { + clear_pager(); + } + + // Since we handled a normal character, we don't have a last command. + rls.last_cmd.reset(); + return event_needing_handling; +} + +/// Handle a readline command \p c, updating the state \p rls. +void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_state_t &rls) { + const auto &vars = parser_t::principal_parser().vars(); + using rl = readline_cmd_t; + switch (c) { + // Go to beginning of line. + case rl::beginning_of_line: { + editable_line_t *el = active_edit_line(); + while (el->position > 0 && el->text.at(el->position - 1) != L'\n') { + update_buff_pos(el, el->position - 1); + } + + reader_repaint_needed(); + break; + } + case rl::end_of_line: { + editable_line_t *el = active_edit_line(); + if (el->position < el->size()) { + const wchar_t *buff = el->text.c_str(); + while (buff[el->position] && buff[el->position] != L'\n') { + update_buff_pos(el, el->position + 1); + } + } else { + accept_autosuggestion(true); + } + + reader_repaint_needed(); + break; + } + case rl::beginning_of_buffer: { + update_buff_pos(&command_line, 0); + reader_repaint_needed(); + break; + } + case rl::end_of_buffer: { + update_buff_pos(&command_line, command_line.size()); + reader_repaint_needed(); + break; + } + case rl::cancel: { + // The only thing we can cancel right now is paging, which we handled up above. + break; + } + case rl::force_repaint: + case rl::repaint: { + if (!rls.coalescing_repaints) { + rls.coalescing_repaints = true; + exec_prompt(); + s_reset(&screen, screen_reset_current_line_and_prompt); + screen_reset_needed = false; + repaint(); + } + break; + } + case rl::complete: + case rl::complete_AND_SEARCH: { + if (!complete_func) break; + + // Use the command line only; it doesn't make sense to complete in any other line. + editable_line_t *el = &command_line; + if (is_navigating_pager_contents() || + (!rls.comp_empty && rls.last_cmd == rl::complete)) { + // The user typed complete more than once in a row. If we are not yet fully + // disclosed, then become so; otherwise cycle through our available completions. + if (current_page_rendering.remaining_to_disclose > 0) { + pager.set_fully_disclosed(true); + reader_repaint_needed(); + } else { + select_completion_in_direction(c == rl::complete ? direction_next + : direction_prev); + } + } else { + // Either the user hit tab only once, or we had no visible completion list. + // Remove a trailing backslash. This may trigger an extra repaint, but this is + // rare. + if (is_backslashed(el->text, el->position)) { + remove_backward(); + } + + // Get the string; we have to do this after removing any trailing backslash. + const wchar_t *const buff = el->text.c_str(); + + // Clear the completion list. + rls.comp.clear(); + + // Figure out the extent of the command substitution surrounding the cursor. + // This is because we only look at the current command substitution to form + // completions - stuff happening outside of it is not interesting. + const wchar_t *cmdsub_begin, *cmdsub_end; + parse_util_cmdsubst_extent(buff, el->position, &cmdsub_begin, &cmdsub_end); + + // Figure out the extent of the token within the command substitution. Note we + // pass cmdsub_begin here, not buff. + const wchar_t *token_begin, *token_end; + parse_util_token_extent(cmdsub_begin, el->position - (cmdsub_begin - buff), + &token_begin, &token_end, 0, 0); + + // Hack: the token may extend past the end of the command substitution, e.g. in + // (echo foo) the last token is 'foo)'. Don't let that happen. + if (token_end > cmdsub_end) token_end = cmdsub_end; + + // Figure out how many steps to get from the current position to the end of the + // current token. + size_t end_of_token_offset = token_end - buff; + + // Move the cursor to the end. + if (el->position != end_of_token_offset) { + update_buff_pos(el, end_of_token_offset); + repaint(); + } + + // Construct a copy of the string from the beginning of the command substitution + // up to the end of the token we're completing. + const wcstring buffcpy = wcstring(cmdsub_begin, token_end); + + // std::fwprintf(stderr, L"Complete (%ls)\n", buffcpy.c_str()); + complete_flags_t complete_flags = COMPLETION_REQUEST_DEFAULT | + COMPLETION_REQUEST_DESCRIPTIONS | + COMPLETION_REQUEST_FUZZY_MATCH; + complete_func(buffcpy, &rls.comp, complete_flags, vars); + + // Munge our completions. + completions_sort_and_prioritize(&rls.comp); + + // Record our cycle_command_line. + cycle_command_line = el->text; + cycle_cursor_pos = el->position; + + bool cont_after_prefix_insertion = (c == rl::complete_AND_SEARCH); + rls.comp_empty = handle_completions(rls.comp, cont_after_prefix_insertion); + + // Show the search field if requested and if we printed a list of completions. + if (c == rl::complete_AND_SEARCH && !rls.comp_empty && !pager.empty()) { + pager.set_search_field_shown(true); + select_completion_in_direction(direction_next); + reader_repaint_needed(); + } + } + break; + } + case rl::pager_toggle_search: { + if (!pager.empty()) { + // Toggle search, and begin navigating if we are now searching. + bool sfs = pager.is_search_field_shown(); + pager.set_search_field_shown(!sfs); + pager.set_fully_disclosed(true); + if (pager.is_search_field_shown() && !is_navigating_pager_contents()) { + select_completion_in_direction(direction_south); + } + reader_repaint_needed(); + } + break; + } + case rl::kill_line: { + editable_line_t *el = active_edit_line(); + const wchar_t *buff = el->text.c_str(); + const wchar_t *begin = &buff[el->position]; + const wchar_t *end = begin; + + while (*end && *end != L'\n') end++; + + if (end == begin && *end) end++; + + size_t len = end - begin; + if (len) { + kill(el, begin - buff, len, KILL_APPEND, rls.last_cmd != rl::kill_line); + } + break; + } + case rl::backward_kill_line: { + editable_line_t *el = active_edit_line(); + if (el->position <= 0) { + break; + } + const wchar_t *buff = el->text.c_str(); + const wchar_t *end = &buff[el->position]; + const wchar_t *begin = end; + + begin--; // make sure we delete at least one character (see issue #580) + + // Delete until we hit a newline, or the beginning of the string. + while (begin > buff && *begin != L'\n') begin--; + + // If we landed on a newline, don't delete it. + if (*begin == L'\n') begin++; + assert(end >= begin); + size_t len = std::max(end - begin, 1); + begin = end - len; + kill(el, begin - buff, len, KILL_PREPEND, rls.last_cmd != rl::backward_kill_line); + break; + } + case rl::kill_whole_line: { + // We match the emacs behavior here: "kills the entire line including the following + // newline". + editable_line_t *el = active_edit_line(); + const wchar_t *buff = el->text.c_str(); + + // Back up to the character just past the previous newline, or go to the beginning + // of the command line. Note that if the position is on a newline, visually this + // looks like the cursor is at the end of a line. Therefore that newline is NOT the + // beginning of a line; this justifies the -1 check. + size_t begin = el->position; + while (begin > 0 && buff[begin - 1] != L'\n') { + begin--; + } + + // Push end forwards to just past the next newline, or just past the last char. + size_t end = el->position; + while (buff[end] != L'\0') { + end++; + if (buff[end - 1] == L'\n') { + break; + } + } + assert(end >= begin); + + if (end > begin) { + kill(el, begin, end - begin, KILL_APPEND, rls.last_cmd != rl::kill_whole_line); + } + break; + } + case rl::yank: { + wcstring yank_str = kill_yank(); + insert_string(active_edit_line(), yank_str); + rls.yank_len = yank_str.size(); + break; + } + case rl::yank_POP: { + if (rls.yank_len) { + for (size_t i = 0; i < rls.yank_len; i++) remove_backward(); + + wcstring yank_str = kill_yank_rotate(); + insert_string(active_edit_line(), yank_str); + rls.yank_len = yank_str.size(); + } + break; + } + case rl::backward_delete_char: { + remove_backward(); + break; + } + case rl::delete_char: { + // Remove the current character in the character buffer and on the screen using + // syntax highlighting, etc. + editable_line_t *el = active_edit_line(); + if (el->position < el->size()) { + update_buff_pos(el, el->position + 1); + remove_backward(); + } + break; + } + // Evaluate. If the current command is unfinished, or if the charater is escaped + // using a backslash, insert a newline. + case rl::execute: { + // If the user hits return while navigating the pager, it only clears the pager. + if (is_navigating_pager_contents()) { + clear_pager(); + break; + } + + // Delete any autosuggestion. + autosuggestion.clear(); + + // The user may have hit return with pager contents, but while not navigating them. + // Clear the pager in that event. + clear_pager(); + + // We only execute the command line. + editable_line_t *el = &command_line; + + // Allow backslash-escaped newlines. + bool continue_on_next_line = false; + if (el->position >= el->size()) { + // We're at the end of the text and not in a comment (issue #1225). + continue_on_next_line = + is_backslashed(el->text, el->position) && !text_ends_in_comment(el->text); + } else { + // Allow mid line split if the following character is whitespace (issue #613). + if (is_backslashed(el->text, el->position) && iswspace(el->text.at(el->position))) { + continue_on_next_line = true; + // Check if the end of the line is backslashed (issue #4467). + } else if (is_backslashed(el->text, el->size()) && + !text_ends_in_comment(el->text)) { + // Move the cursor to the end of the line. + el->position = el->size(); + continue_on_next_line = true; + } + } + // If the conditions are met, insert a new line at the position of the cursor. + if (continue_on_next_line) { + insert_char(el, '\n'); + break; + } + + // See if this command is valid. + int command_test_result = test_func(el->text); + if (command_test_result == 0 || command_test_result == PARSER_TEST_INCOMPLETE) { + // This command is valid, but an abbreviation may make it invalid. If so, we + // will have to test again. + bool abbreviation_expanded = expand_abbreviation_as_necessary(1); + if (abbreviation_expanded) { + // It's our reponsibility to rehighlight and repaint. But everything we do + // below triggers a repaint. + command_test_result = test_func(el->text.c_str()); + + // If the command is OK, then we're going to execute it. We still want to do + // syntax highlighting, but a synchronous variant that performs no I/O, so + // as not to block the user. + bool skip_io = (command_test_result == 0); + super_highlight_me_plenty(0, skip_io); + } + } + + if (command_test_result == 0) { + // Finished command, execute it. Don't add items that start with a leading + // space. + const editable_line_t *el = &command_line; + if (history != NULL && !el->empty() && el->text.at(0) != L' ') { + history->add_pending_with_file_detection(el->text, vars.get_pwd_slash()); + } + rls.finished = true; + update_buff_pos(&command_line, command_line.size()); + repaint(); + } else if (command_test_result == PARSER_TEST_INCOMPLETE) { + // We are incomplete, continue editing. + insert_char(el, '\n'); + } else { + // Result must be some combination including an error. The error message will + // already be printed, all we need to do is repaint. + s_reset(&screen, screen_reset_abandon_line); + reader_repaint_needed(); + } + + break; + } + + case rl::history_search_backward: + case rl::history_token_search_backward: + case rl::history_search_forward: + case rl::history_token_search_forward: { + if (history_search.is_at_end()) { + const editable_line_t *el = &command_line; + bool by_token = (c == rl::history_token_search_backward) || + (c == rl::history_token_search_forward); + if (by_token) { + // Searching by token. + const wchar_t *begin, *end; + const wchar_t *buff = el->text.c_str(); + parse_util_token_extent(buff, el->position, &begin, &end, 0, 0); + if (begin) { + wcstring token(begin, end); + history_search.reset_to_mode(token, history, + reader_history_search_t::token); + } else { + // No current token, refuse to do a token search. + history_search.reset(); + } + } else { + // Searching by line. + history_search.reset_to_mode(el->text, history, reader_history_search_t::line); + + // Skip the autosuggestion in the history unless it was truncated. + const wcstring &suggest = autosuggestion; + if (!suggest.empty() && !screen.autosuggestion_is_truncated) { + history_search.add_skip(suggest); + } + } + } + if (history_search.active()) { + history_search_direction_t dir = + (c == rl::history_search_backward || c == rl::history_token_search_backward) + ? history_search_direction_t::backward + : history_search_direction_t::forward; + history_search.move_in_direction(dir); + update_command_line_from_history_search(); + } + break; + } + case rl::backward_char: { + editable_line_t *el = active_edit_line(); + if (is_navigating_pager_contents()) { + select_completion_in_direction(direction_west); + } else if (el->position > 0) { + update_buff_pos(el, el->position - 1); + reader_repaint_needed(); + } + break; + } + case rl::forward_char: { + editable_line_t *el = active_edit_line(); + if (is_navigating_pager_contents()) { + select_completion_in_direction(direction_east); + } else if (el->position < el->size()) { + update_buff_pos(el, el->position + 1); + reader_repaint_needed(); + } else { + accept_autosuggestion(true); + } + break; + } + case rl::backward_kill_word: + case rl::backward_kill_path_component: + case rl::backward_kill_bigword: { + move_word_style_t style = + (c == rl::backward_kill_bigword + ? move_word_style_whitespace + : c == rl::backward_kill_path_component ? move_word_style_path_components + : move_word_style_punctuation); + // Is this the same killring item as the last kill? + bool newv = (rls.last_cmd != rl::backward_kill_word && + rls.last_cmd != rl::backward_kill_path_component && + rls.last_cmd != rl::backward_kill_bigword); + move_word(active_edit_line(), MOVE_DIR_LEFT, true /* erase */, style, newv); + break; + } + case rl::kill_word: + case rl::kill_bigword: { + // The "bigword" functions differ only in that they move to the next whitespace, not + // punctuation. + auto move_style = + (c == rl::kill_word) ? move_word_style_punctuation : move_word_style_whitespace; + move_word(active_edit_line(), MOVE_DIR_RIGHT, true /* erase */, move_style, + rls.last_cmd != c /* same kill item if same movement */); + break; + } + case rl::backward_word: + case rl::backward_bigword: { + auto move_style = (c == rl::backward_word) ? move_word_style_punctuation + : move_word_style_whitespace; + move_word(active_edit_line(), MOVE_DIR_LEFT, false /* do not erase */, move_style, + false); + break; + } + case rl::forward_word: + case rl::forward_bigword: { + auto move_style = (c == rl::forward_word) ? move_word_style_punctuation + : move_word_style_whitespace; + editable_line_t *el = active_edit_line(); + if (el->position < el->size()) { + move_word(el, MOVE_DIR_RIGHT, false /* do not erase */, move_style, false); + } else { + accept_autosuggestion(false, move_style); + } + break; + } + case rl::beginning_of_history: + case rl::end_of_history: { + bool up = (c == rl::beginning_of_history); + if (is_navigating_pager_contents()) { + select_completion_in_direction(up ? direction_page_north : direction_page_south); + } else { + if (up) { + history_search.go_to_beginning(); + } else { + history_search.go_to_end(); + } + update_command_line_from_history_search(); + } + break; + } + case rl::up_line: + case rl::down_line: { + if (is_navigating_pager_contents()) { + // We are already navigating pager contents. + selection_direction_t direction; + if (c == rl::down_line) { + // Down arrow is always south. + direction = direction_south; + } else if (selection_is_at_top()) { + // Up arrow, but we are in the first column and first row. End navigation. + direction = direction_deselect; + } else { + // Up arrow, go north. + direction = direction_north; + } + + // Now do the selection. + select_completion_in_direction(direction); + } else if (!pager.empty()) { + // We pressed a direction with a non-empty pager, begin navigation. + select_completion_in_direction(c == rl::down_line ? direction_south + : direction_north); + } else { + // Not navigating the pager contents. + editable_line_t *el = active_edit_line(); + int line_old = parse_util_get_line_from_offset(el->text, el->position); + int line_new; + + if (c == rl::up_line) + line_new = line_old - 1; + else + line_new = line_old + 1; + + int line_count = parse_util_lineno(el->text.c_str(), el->size()) - 1; + + if (line_new >= 0 && line_new <= line_count) { + size_t base_pos_new; + size_t base_pos_old; + + int indent_old; + int indent_new; + size_t line_offset_old; + size_t total_offset_new; + + base_pos_new = parse_util_get_offset_from_line(el->text, line_new); + + base_pos_old = parse_util_get_offset_from_line(el->text, line_old); + + assert(base_pos_new != (size_t)(-1) && base_pos_old != (size_t)(-1)); + indent_old = indents.at(base_pos_old); + indent_new = indents.at(base_pos_new); + + line_offset_old = + el->position - parse_util_get_offset_from_line(el->text, line_old); + total_offset_new = parse_util_get_offset( + el->text, line_new, line_offset_old - 4 * (indent_new - indent_old)); + update_buff_pos(el, total_offset_new); + reader_repaint_needed(); + } + } + break; + } + case rl::suppress_autosuggestion: { + suppress_autosuggestion = true; + autosuggestion.clear(); + reader_repaint_needed(); + break; + } + case rl::accept_autosuggestion: { + accept_autosuggestion(true); + break; + } + case rl::transpose_chars: { + editable_line_t *el = active_edit_line(); + if (el->size() < 2) { + break; + } + + // If the cursor is at the end, transpose the last two characters of the line. + if (el->position == el->size()) { + update_buff_pos(el, el->position - 1); + } + + // Drag the character before the cursor forward over the character at the cursor, + // moving the cursor forward as well. + if (el->position > 0) { + wcstring local_cmd = el->text; + std::swap(local_cmd.at(el->position), local_cmd.at(el->position - 1)); + set_command_line_and_position(el, local_cmd, el->position + 1); + } + break; + } + case rl::transpose_words: { + editable_line_t *el = active_edit_line(); + size_t len = el->size(); + const wchar_t *buff = el->text.c_str(); + const wchar_t *tok_begin, *tok_end, *prev_begin, *prev_end; + + // If we are not in a token, look for one ahead. + size_t buff_pos = el->position; + while (buff_pos != len && !iswalnum(buff[buff_pos])) buff_pos++; + + update_buff_pos(el, buff_pos); + + parse_util_token_extent(buff, el->position, &tok_begin, &tok_end, &prev_begin, + &prev_end); + + // In case we didn't find a token at or after the cursor... + if (tok_begin == &buff[len]) { + // ...retry beginning from the previous token. + size_t pos = prev_end - &buff[0]; + parse_util_token_extent(buff, pos, &tok_begin, &tok_end, &prev_begin, &prev_end); + } + + // Make sure we have two tokens. + if (prev_begin < prev_end && tok_begin < tok_end && tok_begin > prev_begin) { + const wcstring prev(prev_begin, prev_end - prev_begin); + const wcstring sep(prev_end, tok_begin - prev_end); + const wcstring tok(tok_begin, tok_end - tok_begin); + const wcstring trail(tok_end, &buff[len] - tok_end); + + // Compose new command line with swapped tokens. + wcstring new_buff(buff, prev_begin - buff); + new_buff.append(tok); + new_buff.append(sep); + new_buff.append(prev); + new_buff.append(trail); + // Put cursor right after the second token. + set_command_line_and_position(el, new_buff, tok_end - buff); + } + break; + } + case rl::upcase_word: + case rl::downcase_word: + case rl::capitalize_word: { + editable_line_t *el = active_edit_line(); + // For capitalize_word, whether we've capitalized a character so far. + bool capitalized_first = false; + + // We apply the operation from the current location to the end of the word. + size_t pos = el->position; + move_word(el, MOVE_DIR_RIGHT, false, move_word_style_punctuation, false); + for (; pos < el->position; pos++) { + wchar_t chr = el->text.at(pos); + + // We always change the case; this decides whether we go uppercase (true) or + // lowercase (false). + bool make_uppercase; + if (c == rl::capitalize_word) + make_uppercase = !capitalized_first && iswalnum(chr); + else + make_uppercase = (c == rl::upcase_word); + + // Apply the operation and then record what we did. + if (make_uppercase) + chr = towupper(chr); + else + chr = towlower(chr); + + command_line.text.at(pos) = chr; + capitalized_first = capitalized_first || make_uppercase; + } + command_line_changed(el); + super_highlight_me_plenty(); + reader_repaint_needed(); + break; + } + case rl::begin_selection: + case rl::end_selection: { + sel_start_pos = command_line.position; + sel_stop_pos = command_line.position; + if (c == rl::begin_selection) { + sel_active = true; + sel_begin_pos = command_line.position; + } else { + sel_active = false; + } + + break; + } + case rl::swap_selection_start_stop: { + if (!sel_active) break; + size_t tmp = sel_begin_pos; + sel_begin_pos = command_line.position; + sel_start_pos = command_line.position; + editable_line_t *el = active_edit_line(); + update_buff_pos(el, tmp); + break; + } + case rl::kill_selection: { + bool newv = (rls.last_cmd != rl::kill_selection); + size_t start, len; + if (reader_get_selection(&start, &len)) { + kill(&command_line, start, len, KILL_APPEND, newv); + } + break; + } + case rl::forward_jump: + case rl::backward_jump: + case rl::forward_jump_till: + case rl::backward_jump_till: { + auto direction = (c == rl::forward_jump || c == rl::forward_jump_till) + ? jump_direction_t::forward + : jump_direction_t::backward; + auto precision = (c == rl::forward_jump || c == rl::backward_jump) + ? jump_precision_t::to + : jump_precision_t::till; + editable_line_t *el = active_edit_line(); + wchar_t target = input_function_pop_arg(); + bool success = jump(direction, precision, el, target); + + input_function_set_status(success); + reader_repaint_needed(); + break; + } + case rl::repeat_jump: { + editable_line_t *el = active_edit_line(); + bool success = false; + + if (last_jump_target) { + success = jump(last_jump_direction, last_jump_precision, el, last_jump_target); + } + + input_function_set_status(success); + reader_repaint_needed(); + break; + } + case rl::reverse_repeat_jump: { + editable_line_t *el = active_edit_line(); + bool success = false; + jump_direction_t original_dir, dir; + original_dir = dir = last_jump_direction; + + if (last_jump_direction == jump_direction_t::forward) { + dir = jump_direction_t::backward; + } else { + dir = jump_direction_t::forward; + } + + if (last_jump_target) { + success = jump(dir, last_jump_precision, el, last_jump_target); + } + + last_jump_direction = original_dir; + + input_function_set_status(success); + reader_repaint_needed(); + break; + } + + // Some commands should have been handled internally by input_readch(). + case rl::self_insert: { + DIE("self-insert should have been handled by input_readch"); + } + case rl::func_and: { + DIE("self-insert should have been handled by input_readch"); + } + case rl::vi_arg_digit: + case rl::vi_delete_to: { + // TODO: what needs to happen with these? + break; + } + } +} + +maybe_t reader_data_t::readline(int nchars_or_0) { + using rl = readline_cmd_t; + readline_loop_state_t rls{}; struct termios old_modes; - // Coalesce redundant repaints. When we get a repaint, we set this to true, and skip repaints - // until we get something else. - bool coalescing_repaints = false; + // If nchars_or_0 is positive, then that's the maximum number of chars. Otherwise keep it at + // SIZE_MAX. + if (nchars_or_0 > 0) { + rls.nchars = static_cast(nchars_or_0); + } // The command line before completion. cycle_command_line.clear(); @@ -2408,10 +3233,9 @@ maybe_t reader_data_t::readline(int nchars) { s_reset(&screen, screen_reset_abandon_line); repaint(); - const auto &vars = parser_t::principal_parser().vars(); - // Get the current terminal modes. These will be restored when the function returns. if (tcgetattr(STDIN_FILENO, &old_modes) == -1 && errno == EIO) redirect_tty_output(); + // Set the new modes. if (tcsetattr(0, TCSANOW, &shell_modes) == -1) { int err = errno; @@ -2426,834 +3250,89 @@ maybe_t reader_data_t::readline(int nchars) { } } - while (!finished && !shell_is_exiting()) { - if (0 < nchars && (size_t)nchars <= command_line.size()) { + while (!rls.finished && !shell_is_exiting()) { + if (rls.nchars <= command_line.size()) { // We've already hit the specified character limit. - finished = 1; + rls.finished = true; break; } - // Sometimes strange input sequences seem to generate a zero byte. I believe these simply - // mean a character was pressed but it should be ignored. (Example: Trying to add a tilde - // (~) to digit). + maybe_t event_needing_handling{}; while (1) { - int was_interactive_read = is_interactive_read; - is_interactive_read = 1; - c = input_readch(); - is_interactive_read = was_interactive_read; - // std::fwprintf(stderr, L"C: %lx\n", (long)c); + event_needing_handling = read_normal_chars(rls); + if (event_needing_handling.has_value()) break; - if (((!fish_reserved_codepoint(c))) && (c > 31) && (c != 127) && can_read(0)) { - wchar_t arr[READAHEAD_MAX + 1]; - size_t i; - size_t limit = 0 < nchars ? std::min((size_t)nchars - command_line.size(), - (size_t)READAHEAD_MAX) - : READAHEAD_MAX; - - std::memset(arr, 0, sizeof(arr)); - arr[0] = c; - - for (i = 1; i < limit; ++i) { - if (!can_read(0)) { - c = 0; - break; - } - // Only allow commands on the first key; otherwise, we might have data we - // need to insert on the commandline that the commmand might need to be able - // to see. - c = input_readch(false); - if (!fish_reserved_codepoint(c) && c > 31 && c != 127) { - arr[i] = c; - c = 0; - } else - break; - } - - editable_line_t *el = active_edit_line(); - insert_string(el, arr, true); - - // End paging upon inserting into the normal command line. - if (el == &command_line) { - clear_pager(); - } - last_char = c; - } - - if (c != 0) break; - - if (0 < nchars && (size_t)nchars <= command_line.size()) { - c = R_NULL; + if (rls.nchars <= command_line.size()) { + event_needing_handling.reset(); break; } } - // If we get something other than a repaint, then stop coalescing them. - if (c != R_REPAINT) coalescing_repaints = false; + if (!event_needing_handling || event_needing_handling->is_check_exit()) { + repaint_if_needed(); + continue; + } else if (event_needing_handling->is_eof()) { + reader_force_exit(); + continue; + } + assert((event_needing_handling->is_char() || event_needing_handling->is_readline()) && + "Should have a char or readline"); - if (last_char != R_YANK && last_char != R_YANK_POP) yank_len = 0; - - // Restore the text. - if (c == R_CANCEL && is_navigating_pager_contents()) { - set_command_line_and_position(&command_line, cycle_command_line, cycle_cursor_pos); + if (rls.last_cmd != rl::yank && rls.last_cmd != rl::yank_POP) { + rls.yank_len = 0; } - // Clear the pager if necessary. - bool focused_on_search_field = (active_edit_line() == &pager.search_field_line); - if (command_ends_paging(c, focused_on_search_field)) { - clear_pager(); - } - // std::fwprintf(stderr, L"\n\nchar: %ls\n\n", describe_char(c).c_str()); + if (event_needing_handling->is_readline()) { + readline_cmd_t readline_cmd = event_needing_handling->get_readline(); + // If we get something other than a repaint, then stop coalescing them. + if (readline_cmd != rl::repaint) rls.coalescing_repaints = false; - switch (c) { - // Go to beginning of line. - case R_BEGINNING_OF_LINE: { - editable_line_t *el = active_edit_line(); - while (el->position > 0 && el->text.at(el->position - 1) != L'\n') { - update_buff_pos(el, el->position - 1); - } - - reader_repaint_needed(); - break; + if (readline_cmd == rl::cancel && is_navigating_pager_contents()) { + set_command_line_and_position(&command_line, cycle_command_line, cycle_cursor_pos); } - case R_END_OF_LINE: { - editable_line_t *el = active_edit_line(); - if (el->position < el->size()) { - const wchar_t *buff = el->text.c_str(); - while (buff[el->position] && buff[el->position] != L'\n') { - update_buff_pos(el, el->position + 1); - } - } else { - accept_autosuggestion(true); - } - reader_repaint_needed(); - break; + // Clear the pager if necessary. + bool focused_on_search_field = (active_edit_line() == &pager.search_field_line); + if (command_ends_paging(readline_cmd, focused_on_search_field)) { + clear_pager(); } - case R_BEGINNING_OF_BUFFER: { - update_buff_pos(&command_line, 0); - reader_repaint_needed(); - break; + + handle_readline_command(readline_cmd, rls); + + if (command_ends_history_search(readline_cmd)) { + history_search.reset(); } - case R_END_OF_BUFFER: { - update_buff_pos(&command_line, command_line.size()); - reader_repaint_needed(); - break; - } - case R_NULL: { - break; - } - case R_CANCEL: { - // The only thing we can cancel right now is paging, which we handled up above. - break; - } - case R_FORCE_REPAINT: - case R_REPAINT: { - if (!coalescing_repaints) { - coalescing_repaints = true; - exec_prompt(); - s_reset(&screen, screen_reset_current_line_and_prompt); - screen_reset_needed = false; - repaint(); - } - break; - } - case R_EOF: { - reader_force_exit(); - break; - } - case R_COMPLETE: - case R_COMPLETE_AND_SEARCH: { - if (!complete_func) break; - - // Use the command line only; it doesn't make sense to complete in any other line. - editable_line_t *el = &command_line; - if (is_navigating_pager_contents() || (!comp_empty && last_char == R_COMPLETE)) { - // The user typed R_COMPLETE more than once in a row. If we are not yet fully - // disclosed, then become so; otherwise cycle through our available completions. - if (current_page_rendering.remaining_to_disclose > 0) { - pager.set_fully_disclosed(true); - reader_repaint_needed(); - } else { - select_completion_in_direction(c == R_COMPLETE ? direction_next - : direction_prev); - } - } else { - // Either the user hit tab only once, or we had no visible completion list. - // Remove a trailing backslash. This may trigger an extra repaint, but this is - // rare. - if (is_backslashed(el->text, el->position)) { - remove_backward(); - } - - // Get the string; we have to do this after removing any trailing backslash. - const wchar_t *const buff = el->text.c_str(); - - // Clear the completion list. - comp.clear(); - - // Figure out the extent of the command substitution surrounding the cursor. - // This is because we only look at the current command substitution to form - // completions - stuff happening outside of it is not interesting. - const wchar_t *cmdsub_begin, *cmdsub_end; - parse_util_cmdsubst_extent(buff, el->position, &cmdsub_begin, &cmdsub_end); - - // Figure out the extent of the token within the command substitution. Note we - // pass cmdsub_begin here, not buff. - const wchar_t *token_begin, *token_end; - parse_util_token_extent(cmdsub_begin, el->position - (cmdsub_begin - buff), - &token_begin, &token_end, 0, 0); - - // Hack: the token may extend past the end of the command substitution, e.g. in - // (echo foo) the last token is 'foo)'. Don't let that happen. - if (token_end > cmdsub_end) token_end = cmdsub_end; - - // Figure out how many steps to get from the current position to the end of the - // current token. - size_t end_of_token_offset = token_end - buff; - - // Move the cursor to the end. - if (el->position != end_of_token_offset) { - update_buff_pos(el, end_of_token_offset); - repaint(); - } - - // Construct a copy of the string from the beginning of the command substitution - // up to the end of the token we're completing. - const wcstring buffcpy = wcstring(cmdsub_begin, token_end); - - // std::fwprintf(stderr, L"Complete (%ls)\n", buffcpy.c_str()); - complete_flags_t complete_flags = COMPLETION_REQUEST_DEFAULT | - COMPLETION_REQUEST_DESCRIPTIONS | - COMPLETION_REQUEST_FUZZY_MATCH; - complete_func(buffcpy, &comp, complete_flags, vars); - - // Munge our completions. - completions_sort_and_prioritize(&comp); - - // Record our cycle_command_line. - cycle_command_line = el->text; - cycle_cursor_pos = el->position; - - bool cont_after_prefix_insertion = (c == R_COMPLETE_AND_SEARCH); - comp_empty = handle_completions(comp, cont_after_prefix_insertion); - - // Show the search field if requested and if we printed a list of completions. - if (c == R_COMPLETE_AND_SEARCH && !comp_empty && !pager.empty()) { - pager.set_search_field_shown(true); - select_completion_in_direction(direction_next); - reader_repaint_needed(); - } - } - break; - } - case R_PAGER_TOGGLE_SEARCH: { - if (!pager.empty()) { - // Toggle search, and begin navigating if we are now searching. - bool sfs = pager.is_search_field_shown(); - pager.set_search_field_shown(!sfs); - pager.set_fully_disclosed(true); - if (pager.is_search_field_shown() && !is_navigating_pager_contents()) { - select_completion_in_direction(direction_south); - } - reader_repaint_needed(); - } - break; - } - case R_KILL_LINE: { - editable_line_t *el = active_edit_line(); - const wchar_t *buff = el->text.c_str(); - const wchar_t *begin = &buff[el->position]; - const wchar_t *end = begin; - - while (*end && *end != L'\n') end++; - - if (end == begin && *end) end++; - - size_t len = end - begin; - if (len) { - kill(el, begin - buff, len, KILL_APPEND, last_char != R_KILL_LINE); - } - break; - } - case R_BACKWARD_KILL_LINE: { - editable_line_t *el = active_edit_line(); - if (el->position <= 0) { - break; - } - const wchar_t *buff = el->text.c_str(); - const wchar_t *end = &buff[el->position]; - const wchar_t *begin = end; - - begin--; // make sure we delete at least one character (see issue #580) - - // Delete until we hit a newline, or the beginning of the string. - while (begin > buff && *begin != L'\n') begin--; - - // If we landed on a newline, don't delete it. - if (*begin == L'\n') begin++; - assert(end >= begin); - size_t len = std::max(end - begin, 1); - begin = end - len; - kill(el, begin - buff, len, KILL_PREPEND, last_char != R_BACKWARD_KILL_LINE); - break; - } - case R_KILL_WHOLE_LINE: { - // We match the emacs behavior here: "kills the entire line including the following - // newline". - editable_line_t *el = active_edit_line(); - const wchar_t *buff = el->text.c_str(); - - // Back up to the character just past the previous newline, or go to the beginning - // of the command line. Note that if the position is on a newline, visually this - // looks like the cursor is at the end of a line. Therefore that newline is NOT the - // beginning of a line; this justifies the -1 check. - size_t begin = el->position; - while (begin > 0 && buff[begin - 1] != L'\n') { - begin--; - } - - // Push end forwards to just past the next newline, or just past the last char. - size_t end = el->position; - while (buff[end] != L'\0') { - end++; - if (buff[end - 1] == L'\n') { - break; - } - } - assert(end >= begin); - - if (end > begin) { - kill(el, begin, end - begin, KILL_APPEND, last_char != R_KILL_WHOLE_LINE); - } - break; - } - case R_YANK: { - yank_str = kill_yank(); - insert_string(active_edit_line(), yank_str); - yank_len = std::wcslen(yank_str); - break; - } - case R_YANK_POP: { - if (yank_len) { - for (size_t i = 0; i < yank_len; i++) remove_backward(); - - yank_str = kill_yank_rotate(); - insert_string(active_edit_line(), yank_str); - yank_len = std::wcslen(yank_str); - } - break; - } - // Escape was pressed. - case L'\x1B': { + rls.last_cmd = readline_cmd; + } else { + // Ordinary char. + wchar_t c = event_needing_handling->get_char(); + if (c == L'\x1B') { + // Escape was pressed. if (history_search.active()) { history_search.go_to_end(); update_command_line_from_history_search(); history_search.reset(); } assert(!history_search.active()); - break; - } - case R_BACKWARD_DELETE_CHAR: { - remove_backward(); - break; - } - case R_DELETE_CHAR: { - // Remove the current character in the character buffer and on the screen using - // syntax highlighting, etc. + } else if (!fish_reserved_codepoint(c) && (c >= L' ' || c == L'\n' || c == L'\r') && + c != 0x7F) { + // Regular character. editable_line_t *el = active_edit_line(); - if (el->position < el->size()) { - update_buff_pos(el, el->position + 1); - remove_backward(); - } - break; - } - // Evaluate. If the current command is unfinished, or if the charater is escaped using a - // backslash, insert a newline. - case R_EXECUTE: { - // If the user hits return while navigating the pager, it only clears the pager. - if (is_navigating_pager_contents()) { + bool allow_expand_abbreviations = (el == &command_line); + insert_char(active_edit_line(), c, allow_expand_abbreviations); + + // End paging upon inserting into the normal command line. + if (el == &command_line) { clear_pager(); - break; } - - // Delete any autosuggestion. - autosuggestion.clear(); - - // The user may have hit return with pager contents, but while not navigating them. - // Clear the pager in that event. - clear_pager(); - - // We only execute the command line. - editable_line_t *el = &command_line; - - // Allow backslash-escaped newlines. - bool continue_on_next_line = false; - if (el->position >= el->size()) { - // We're at the end of the text and not in a comment (issue #1225). - continue_on_next_line = - is_backslashed(el->text, el->position) && !text_ends_in_comment(el->text); - } else { - // Allow mid line split if the following character is whitespace (issue #613). - if (is_backslashed(el->text, el->position) && - iswspace(el->text.at(el->position))) { - continue_on_next_line = true; - // Check if the end of the line is backslashed (issue #4467). - } else if (is_backslashed(el->text, el->size()) && - !text_ends_in_comment(el->text)) { - // Move the cursor to the end of the line. - el->position = el->size(); - continue_on_next_line = true; - } - } - // If the conditions are met, insert a new line at the position of the cursor. - if (continue_on_next_line) { - insert_char(el, '\n'); - break; - } - - // See if this command is valid. - int command_test_result = test_func(el->text); - if (command_test_result == 0 || command_test_result == PARSER_TEST_INCOMPLETE) { - // This command is valid, but an abbreviation may make it invalid. If so, we - // will have to test again. - bool abbreviation_expanded = expand_abbreviation_as_necessary(1); - if (abbreviation_expanded) { - // It's our reponsibility to rehighlight and repaint. But everything we do - // below triggers a repaint. - command_test_result = test_func(el->text.c_str()); - - // If the command is OK, then we're going to execute it. We still want to do - // syntax highlighting, but a synchronous variant that performs no I/O, so - // as not to block the user. - bool skip_io = (command_test_result == 0); - super_highlight_me_plenty(0, skip_io); - } - } - - if (command_test_result == 0) { - // Finished command, execute it. Don't add items that start with a leading - // space. - const editable_line_t *el = &command_line; - if (history != NULL && !el->empty() && el->text.at(0) != L' ') { - history->add_pending_with_file_detection(el->text, vars.get_pwd_slash()); - } - finished = 1; - update_buff_pos(&command_line, command_line.size()); - repaint(); - } else if (command_test_result == PARSER_TEST_INCOMPLETE) { - // We are incomplete, continue editing. - insert_char(el, '\n'); - } else { - // Result must be some combination including an error. The error message will - // already be printed, all we need to do is repaint. - s_reset(&screen, screen_reset_abandon_line); - reader_repaint_needed(); - } - - break; - } - - case R_HISTORY_SEARCH_BACKWARD: - case R_HISTORY_TOKEN_SEARCH_BACKWARD: - case R_HISTORY_SEARCH_FORWARD: - case R_HISTORY_TOKEN_SEARCH_FORWARD: { - if (history_search.is_at_end()) { - const editable_line_t *el = &command_line; - bool by_token = (c == R_HISTORY_TOKEN_SEARCH_BACKWARD) || - (c == R_HISTORY_TOKEN_SEARCH_FORWARD); - if (by_token) { - // Searching by token. - const wchar_t *begin, *end; - const wchar_t *buff = el->text.c_str(); - parse_util_token_extent(buff, el->position, &begin, &end, 0, 0); - if (begin) { - wcstring token(begin, end); - history_search.reset_to_mode(token, history, - reader_history_search_t::token); - } else { - // No current token, refuse to do a token search. - history_search.reset(); - } - } else { - // Searching by line. - history_search.reset_to_mode(el->text, history, - reader_history_search_t::line); - - // Skip the autosuggestion in the history unless it was truncated. - const wcstring &suggest = autosuggestion; - if (!suggest.empty() && !screen.autosuggestion_is_truncated) { - history_search.add_skip(suggest); - } - } - } - if (history_search.active()) { - history_search_direction_t dir = - (c == R_HISTORY_SEARCH_BACKWARD || c == R_HISTORY_TOKEN_SEARCH_BACKWARD) - ? history_search_direction_t::backward - : history_search_direction_t::forward; - history_search.move_in_direction(dir); - update_command_line_from_history_search(); - } - break; - } - case R_BACKWARD_CHAR: { - editable_line_t *el = active_edit_line(); - if (is_navigating_pager_contents()) { - select_completion_in_direction(direction_west); - } else if (el->position > 0) { - update_buff_pos(el, el->position - 1); - reader_repaint_needed(); - } - break; - } - case R_FORWARD_CHAR: { - editable_line_t *el = active_edit_line(); - if (is_navigating_pager_contents()) { - select_completion_in_direction(direction_east); - } else if (el->position < el->size()) { - update_buff_pos(el, el->position + 1); - reader_repaint_needed(); - } else { - accept_autosuggestion(true); - } - break; - } - case R_BACKWARD_KILL_WORD: - case R_BACKWARD_KILL_PATH_COMPONENT: - case R_BACKWARD_KILL_BIGWORD: { - move_word_style_t style = - (c == R_BACKWARD_KILL_BIGWORD - ? move_word_style_whitespace - : c == R_BACKWARD_KILL_PATH_COMPONENT ? move_word_style_path_components - : move_word_style_punctuation); - // Is this the same killring item as the last kill? - bool newv = (last_char != R_BACKWARD_KILL_WORD && - last_char != R_BACKWARD_KILL_PATH_COMPONENT && - last_char != R_BACKWARD_KILL_BIGWORD); - move_word(active_edit_line(), MOVE_DIR_LEFT, true /* erase */, style, newv); - break; - } - case R_KILL_WORD: - case R_KILL_BIGWORD: { - // The "bigword" functions differ only in that they move to the next whitespace, not punctuation. - auto move_style = (c == R_KILL_WORD) ? move_word_style_punctuation : move_word_style_whitespace; - move_word(active_edit_line(), MOVE_DIR_RIGHT, true /* erase */, move_style, - last_char != c /* same kill item if same movement */); - break; - } - case R_BACKWARD_WORD: - case R_BACKWARD_BIGWORD: { - auto move_style = (c == R_BACKWARD_WORD) ? move_word_style_punctuation : move_word_style_whitespace; - move_word(active_edit_line(), MOVE_DIR_LEFT, false /* do not erase */, move_style, - false); - break; - } - case R_FORWARD_WORD: - case R_FORWARD_BIGWORD: { - auto move_style = (c == R_FORWARD_WORD) ? move_word_style_punctuation : move_word_style_whitespace; - editable_line_t *el = active_edit_line(); - if (el->position < el->size()) { - move_word(el, MOVE_DIR_RIGHT, false /* do not erase */, - move_style, false); - } else { - accept_autosuggestion(false, move_style); - } - break; - } - case R_BEGINNING_OF_HISTORY: - case R_END_OF_HISTORY: { - bool up = (c == R_BEGINNING_OF_HISTORY); - if (is_navigating_pager_contents()) { - select_completion_in_direction(up ? direction_page_north - : direction_page_south); - } else { - if (up) { - history_search.go_to_beginning(); - } else { - history_search.go_to_end(); - } - update_command_line_from_history_search(); - } - break; - } - case R_UP_LINE: - case R_DOWN_LINE: { - if (is_navigating_pager_contents()) { - // We are already navigating pager contents. - selection_direction_t direction; - if (c == R_DOWN_LINE) { - // Down arrow is always south. - direction = direction_south; - } else if (selection_is_at_top()) { - // Up arrow, but we are in the first column and first row. End navigation. - direction = direction_deselect; - } else { - // Up arrow, go north. - direction = direction_north; - } - - // Now do the selection. - select_completion_in_direction(direction); - } else if (!pager.empty()) { - // We pressed a direction with a non-empty pager, begin navigation. - select_completion_in_direction(c == R_DOWN_LINE ? direction_south - : direction_north); - } else { - // Not navigating the pager contents. - editable_line_t *el = active_edit_line(); - int line_old = parse_util_get_line_from_offset(el->text, el->position); - int line_new; - - if (c == R_UP_LINE) - line_new = line_old - 1; - else - line_new = line_old + 1; - - int line_count = parse_util_lineno(el->text.c_str(), el->size()) - 1; - - if (line_new >= 0 && line_new <= line_count) { - size_t base_pos_new; - size_t base_pos_old; - - int indent_old; - int indent_new; - size_t line_offset_old; - size_t total_offset_new; - - base_pos_new = parse_util_get_offset_from_line(el->text, line_new); - - base_pos_old = parse_util_get_offset_from_line(el->text, line_old); - - assert(base_pos_new != (size_t)(-1) && base_pos_old != (size_t)(-1)); - indent_old = indents.at(base_pos_old); - indent_new = indents.at(base_pos_new); - - line_offset_old = - el->position - parse_util_get_offset_from_line(el->text, line_old); - total_offset_new = parse_util_get_offset( - el->text, line_new, line_offset_old - 4 * (indent_new - indent_old)); - update_buff_pos(el, total_offset_new); - reader_repaint_needed(); - } - } - break; - } - case R_SUPPRESS_AUTOSUGGESTION: { - suppress_autosuggestion = true; - autosuggestion.clear(); - reader_repaint_needed(); - break; - } - case R_ACCEPT_AUTOSUGGESTION: { - accept_autosuggestion(true); - break; - } - case R_TRANSPOSE_CHARS: { - editable_line_t *el = active_edit_line(); - if (el->size() < 2) { - break; - } - - // If the cursor is at the end, transpose the last two characters of the line. - if (el->position == el->size()) { - update_buff_pos(el, el->position - 1); - } - - // Drag the character before the cursor forward over the character at the cursor, - // moving the cursor forward as well. - if (el->position > 0) { - wcstring local_cmd = el->text; - std::swap(local_cmd.at(el->position), local_cmd.at(el->position - 1)); - set_command_line_and_position(el, local_cmd, el->position + 1); - } - break; - } - case R_TRANSPOSE_WORDS: { - editable_line_t *el = active_edit_line(); - size_t len = el->size(); - const wchar_t *buff = el->text.c_str(); - const wchar_t *tok_begin, *tok_end, *prev_begin, *prev_end; - - // If we are not in a token, look for one ahead. - size_t buff_pos = el->position; - while (buff_pos != len && !iswalnum(buff[buff_pos])) buff_pos++; - - update_buff_pos(el, buff_pos); - - parse_util_token_extent(buff, el->position, &tok_begin, &tok_end, &prev_begin, - &prev_end); - - // In case we didn't find a token at or after the cursor... - if (tok_begin == &buff[len]) { - // ...retry beginning from the previous token. - size_t pos = prev_end - &buff[0]; - parse_util_token_extent(buff, pos, &tok_begin, &tok_end, &prev_begin, - &prev_end); - } - - // Make sure we have two tokens. - if (prev_begin < prev_end && tok_begin < tok_end && tok_begin > prev_begin) { - const wcstring prev(prev_begin, prev_end - prev_begin); - const wcstring sep(prev_end, tok_begin - prev_end); - const wcstring tok(tok_begin, tok_end - tok_begin); - const wcstring trail(tok_end, &buff[len] - tok_end); - - // Compose new command line with swapped tokens. - wcstring new_buff(buff, prev_begin - buff); - new_buff.append(tok); - new_buff.append(sep); - new_buff.append(prev); - new_buff.append(trail); - // Put cursor right after the second token. - set_command_line_and_position(el, new_buff, tok_end - buff); - } - break; - } - case R_UPCASE_WORD: - case R_DOWNCASE_WORD: - case R_CAPITALIZE_WORD: { - editable_line_t *el = active_edit_line(); - // For capitalize_word, whether we've capitalized a character so far. - bool capitalized_first = false; - - // We apply the operation from the current location to the end of the word. - size_t pos = el->position; - move_word(el, MOVE_DIR_RIGHT, false, move_word_style_punctuation, false); - for (; pos < el->position; pos++) { - wchar_t chr = el->text.at(pos); - - // We always change the case; this decides whether we go uppercase (true) or - // lowercase (false). - bool make_uppercase; - if (c == R_CAPITALIZE_WORD) - make_uppercase = !capitalized_first && iswalnum(chr); - else - make_uppercase = (c == R_UPCASE_WORD); - - // Apply the operation and then record what we did. - if (make_uppercase) - chr = towupper(chr); - else - chr = towlower(chr); - - command_line.text.at(pos) = chr; - capitalized_first = capitalized_first || make_uppercase; - } - command_line_changed(el); - super_highlight_me_plenty(); - reader_repaint_needed(); - break; - } - case R_BEGIN_SELECTION: - case R_END_SELECTION: { - sel_start_pos = command_line.position; - sel_stop_pos = command_line.position; - if (c == R_BEGIN_SELECTION) { - sel_active = true; - sel_begin_pos = command_line.position; - } else { - sel_active = false; - } - - break; - } - case R_SWAP_SELECTION_START_STOP: { - if (!sel_active) break; - size_t tmp = sel_begin_pos; - sel_begin_pos = command_line.position; - sel_start_pos = command_line.position; - editable_line_t *el = active_edit_line(); - update_buff_pos(el, tmp); - break; - } - case R_KILL_SELECTION: { - bool newv = (last_char != R_KILL_SELECTION); - size_t start, len; - if (reader_get_selection(&start, &len)) { - kill(&command_line, start, len, KILL_APPEND, newv); - } - break; - } - case R_FORWARD_JUMP: - case R_BACKWARD_JUMP: - case R_FORWARD_JUMP_TILL: - case R_BACKWARD_JUMP_TILL: { - auto direction = (c == R_FORWARD_JUMP || c == R_FORWARD_JUMP_TILL) ? - jump_direction_t::forward : jump_direction_t::backward; - auto precision = (c == R_FORWARD_JUMP || c == R_BACKWARD_JUMP) ? - jump_precision_t::to : jump_precision_t::till; - editable_line_t *el = active_edit_line(); - wchar_t target = input_function_pop_arg(); - bool success = jump(direction, precision, el, target); - - input_function_set_status(success); - reader_repaint_needed(); - break; - } - case R_REPEAT_JUMP: { - editable_line_t *el = active_edit_line(); - bool success = false; - - if (last_jump_target) { - success = jump(last_jump_direction, last_jump_precision, el, last_jump_target); - } - - input_function_set_status(success); - reader_repaint_needed(); - break; - } - case R_REVERSE_REPEAT_JUMP: { - editable_line_t *el = active_edit_line(); - bool success = false; - jump_direction_t original_dir, dir; - original_dir = dir = last_jump_direction; - - if (last_jump_direction == jump_direction_t::forward) { - dir = jump_direction_t::backward; - } else { - dir = jump_direction_t::forward; - } - - if (last_jump_target) { - success = jump(dir, last_jump_precision, el, last_jump_target); - } - - last_jump_direction = original_dir; - - input_function_set_status(success); - reader_repaint_needed(); - break; - } - default: { - // Other, if a normal character, we add it to the command. - if (!fish_reserved_codepoint(c) && (c >= L' ' || c == L'\n' || c == L'\r') && - c != 0x7F) { - // Regular character. - editable_line_t *el = active_edit_line(); - bool allow_expand_abbreviations = (el == &command_line); - insert_char(active_edit_line(), c, allow_expand_abbreviations); - - // End paging upon inserting into the normal command line. - if (el == &command_line) { - clear_pager(); - } - } else { - // This can happen if the user presses a control char we don't recognize. No - // reason to report this to the user unless they've enabled debugging output. - debug(2, _(L"Unknown key binding 0x%X"), c); - } - break; + } else { + // This can happen if the user presses a control char we don't recognize. No + // reason to report this to the user unless they've enabled debugging output. + debug(2, _(L"Unknown key binding 0x%X"), c); } + rls.last_cmd = none(); } - if ((c != R_HISTORY_SEARCH_BACKWARD) && (c != R_HISTORY_SEARCH_FORWARD) && - (c != R_HISTORY_TOKEN_SEARCH_BACKWARD) && (c != R_HISTORY_TOKEN_SEARCH_FORWARD) && - (c != R_NULL) && (c != R_REPAINT) && (c != R_FORCE_REPAINT)) { - history_search.reset(); - } - - last_char = c; - repaint_if_needed(); } @@ -3277,7 +3356,7 @@ maybe_t reader_data_t::readline(int nchars) { outputter_t::stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset()); } - return finished ? maybe_t{command_line.text} : none(); + return rls.finished ? maybe_t{command_line.text} : none(); } bool reader_data_t::jump(jump_direction_t dir, jump_precision_t precision, editable_line_t *el,