diff --git a/src/builtin_commandline.cpp b/src/builtin_commandline.cpp index 07089da5d..dff824e31 100644 --- a/src/builtin_commandline.cpp +++ b/src/builtin_commandline.cpp @@ -80,7 +80,7 @@ static void replace_part(const wchar_t *begin, const wchar_t *end, const wchar_t } } out.append(end); - reader_set_buffer(out, out_pos); + commandline_set_buffer(out, out_pos); } /// Output the specified selection. @@ -125,13 +125,7 @@ static void write_part(const wchar_t *begin, const wchar_t *end, int cut_at_curs /// The commandline builtin. It is used for specifying a new value for the commandline. maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - // Pointer to what the commandline builtin considers to be the current contents of the command - // line buffer. - const wchar_t *current_buffer = nullptr; - - // What the commandline builtin considers to be the current cursor position. - auto current_cursor_pos = static_cast(-1); - + const commandline_state_t rstate = commandline_get_state(); const wchar_t *cmd = argv[0]; int buffer_part = 0; bool cut_at_cursor = false; @@ -149,30 +143,9 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const bool search_mode = false; bool paging_mode = false; const wchar_t *begin = nullptr, *end = nullptr; + const wchar_t *override_buffer = nullptr; const auto &ld = parser.libdata(); - wcstring transient_commandline; - if (!ld.transient_commandlines.empty()) { - transient_commandline = ld.transient_commandlines.back(); - current_buffer = transient_commandline.c_str(); - current_cursor_pos = transient_commandline.size(); - } else { - current_buffer = reader_get_buffer(); - current_cursor_pos = reader_get_cursor_pos(); - } - - if (!current_buffer) { - if (is_interactive_session()) { - // Prompt change requested while we don't have a prompt, most probably while reading the - // init files. Just ignore it. - return STATUS_CMD_ERROR; - } - - streams.err.append(argv[0]); - streams.err.append(L": Can not set commandline in non-interactive mode\n"); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_CMD_ERROR; - } static const wchar_t *const short_options = L":abijpctforhI:CLSsP"; static const struct woption long_options[] = {{L"append", no_argument, nullptr, 'a'}, @@ -239,8 +212,7 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const break; } case 'I': { - current_buffer = w.woptarg; - current_cursor_pos = std::wcslen(w.woptarg); + override_buffer = w.woptarg; break; } case 'C': { @@ -328,10 +300,9 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const } if (selection_mode) { - size_t start, len; - const wchar_t *buffer = reader_get_buffer(); - if (reader_get_selection(&start, &len)) { - streams.out.append(buffer + start, len); + if (rstate.selection) { + streams.out.append(rstate.text.c_str() + rstate.selection->start, + rstate.selection->length); } return STATUS_CMD_OK; } @@ -375,19 +346,42 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const } if (line_mode) { - size_t pos = reader_get_cursor_pos(); - const wchar_t *buff = reader_get_buffer(); - streams.out.append_format(L"%lu\n", - static_cast(parse_util_lineno(buff, pos))); + streams.out.append_format(L"%d\n", + parse_util_lineno(rstate.text.c_str(), rstate.cursor_pos)); return STATUS_CMD_OK; } if (search_mode) { - return reader_is_in_search_mode() ? 0 : 1; + return commandline_get_state().search_mode ? 0 : 1; } if (paging_mode) { - return reader_has_pager_contents() ? 0 : 1; + return commandline_get_state().pager_mode ? 0 : 1; + } + + // At this point we have (nearly) exhausted the options which always operate on the true command + // line. Now we respect the possibility of a transient command line due to evaluating a wrapped + // completion. Don't do this in cursor_mode: it makes no sense to move the cursor based on a + // transient commandline. + const wchar_t *current_buffer = nullptr; + size_t current_cursor_pos{0}; + wcstring transient; + if (!ld.transient_commandlines.empty() && !cursor_mode) { + transient = ld.transient_commandlines.back(); + current_buffer = transient.c_str(); + current_cursor_pos = transient.size(); + } else if (rstate.initialized) { + current_buffer = rstate.text.c_str(); + current_cursor_pos = rstate.cursor_pos; + } else { + // There is no command line, either because we are not interactive, or because we are + // interactive and are still reading init files (in which case we silently ignore this). + if (!is_interactive_session()) { + streams.err.append(cmd); + streams.err.append(L": Can not set commandline in non-interactive mode\n"); + builtin_print_error_trailer(parser, streams.err, cmd); + } + return STATUS_CMD_ERROR; } switch (buffer_part) { @@ -422,12 +416,11 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const builtin_print_error_trailer(parser, streams.err, cmd); } - current_buffer = reader_get_buffer(); new_pos = std::max(0L, std::min(new_pos, static_cast(std::wcslen(current_buffer)))); - reader_set_buffer(current_buffer, static_cast(new_pos)); + commandline_set_buffer(current_buffer, static_cast(new_pos)); } else { - size_t pos = reader_get_cursor_pos() - (begin - current_buffer); + size_t pos = current_cursor_pos - (begin - current_buffer); streams.out.append_format(L"%lu\n", static_cast(pos)); } return STATUS_CMD_OK; diff --git a/src/builtin_complete.cpp b/src/builtin_complete.cpp index 964e9370f..dbd6f3506 100644 --- a/src/builtin_complete.cpp +++ b/src/builtin_complete.cpp @@ -342,14 +342,14 @@ maybe_t builtin_complete(parser_t &parser, io_streams_t &streams, const wch if (do_complete) { if (!have_do_complete_param) { // No argument given, try to use the current commandline. - const wchar_t *cmd = reader_get_buffer(); - if (cmd == nullptr) { + auto state = commandline_get_state(); + if (!state.initialized) { // This corresponds to using 'complete -C' in non-interactive mode. // See #2361 . builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); return STATUS_INVALID_ARGS; } - do_complete_param = cmd; + do_complete_param = std::move(state.text); } const wchar_t *token; diff --git a/src/builtin_history.cpp b/src/builtin_history.cpp index 2f8cde9de..0c9b49461 100644 --- a/src/builtin_history.cpp +++ b/src/builtin_history.cpp @@ -216,7 +216,7 @@ maybe_t builtin_history(parser_t &parser, io_streams_t &streams, const wcha // Use the default history if we have none (which happens if invoked non-interactively, e.g. // from webconfig.py. - std::shared_ptr history = reader_get_history(); + std::shared_ptr history = commandline_get_state().history; if (!history) history = history_t::with_name(history_session_id(parser.vars())); // If a history command hasn't already been specified via a flag check the first word. diff --git a/src/builtin_read.cpp b/src/builtin_read.cpp index d2c2e9a14..1a9d50b79 100644 --- a/src/builtin_read.cpp +++ b/src/builtin_read.cpp @@ -223,7 +223,7 @@ static int read_interactive(parser_t &parser, wcstring &buff, int nchars, bool s // Keep in-memory history only. reader_push(parser, wcstring{}, std::move(conf)); - reader_set_buffer(commandline, std::wcslen(commandline)); + commandline_set_buffer(commandline, std::wcslen(commandline)); scoped_push interactive{&parser.libdata().is_interactive, true}; event_fire_generic(parser, L"fish_read"); diff --git a/src/env.cpp b/src/env.cpp index 735412b45..0bbe27fb4 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -729,7 +729,7 @@ maybe_t env_scoped_impl_t::try_get_computed(const wcstring &key) cons return none(); } - std::shared_ptr history = reader_get_history(); + std::shared_ptr history = commandline_get_state().history; if (!history) { history = history_t::with_name(history_session_id(*this)); } diff --git a/src/reader.cpp b/src/reader.cpp index ebeac39e7..551830f03 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -123,6 +123,22 @@ enum class history_search_direction_t { forward, backward }; enum class jump_direction_t { forward, backward }; enum class jump_precision_t { till, to }; +/// A singleton snapshot of the reader state. This is updated when the reader changes. This is +/// factored out for thread-safety reasons: it may be fetched on a background thread. +static acquired_lock commandline_state_snapshot() { + // Deliberately leaked to avoid shutdown dtors. + static owning_lock *const s_state = new owning_lock(); + return s_state->acquire(); +} + +commandline_state_t commandline_get_state() { return *commandline_state_snapshot(); } + +void commandline_set_buffer(wcstring text, size_t cursor_pos) { + auto state = commandline_state_snapshot(); + state->cursor_pos = std::min(cursor_pos, text.size()); + state->text = std::move(text); +} + /// Any time the contents of a buffer changes, we update the generation count. This allows for our /// background threads to notice it and skip doing work that they would otherwise have to do. static std::atomic s_generation; @@ -715,6 +731,15 @@ class reader_data_t : public std::enable_shared_from_this { /// \return the command, or none if we were asked to cancel (e.g. SIGHUP). maybe_t readline(int nchars); + /// Reflect our current data in the command line state snapshot. + /// This is called before we run any fish script, so that the commandline builtin can see our + /// state. + void update_commandline_state() const; + + /// Apply any changes from the reader snapshot. This is called after running fish script, + /// incorporating changes from the commandline builtin. + void apply_commandline_state_changes(); + void move_word(editable_line_t *el, bool move_right, bool erase, enum move_word_style_t style, bool newv); @@ -725,6 +750,8 @@ class reader_data_t : public std::enable_shared_from_this { void select_completion_in_direction(selection_motion_t dir); void flash(); + maybe_t get_selection() const; + void completion_insert(const wcstring &val, size_t token_end, complete_flags_t flags); bool can_autosuggest() const; @@ -1103,6 +1130,8 @@ void reader_data_t::command_line_changed(const editable_line_t *el) { this->pager.refilter_completions(); this->pager_selection_changed(); } + // Ensure that the commandline builtin sees our new state. + update_commandline_state(); } void reader_data_t::pager_selection_changed() { @@ -1849,6 +1878,14 @@ void reader_data_t::flash() { paint_layout(L"unflash"); } +maybe_t reader_data_t::get_selection() const { + if (!this->selection.has_value()) return none(); + size_t start = this->selection->start; + size_t len = + std::min(this->selection->stop, this->command_line.size()) - this->selection->start; + return source_range_t{static_cast(start), static_cast(len)}; +} + /// Characters that may not be part of a token that is to be replaced by a case insensitive /// completion. #define REPLACE_UNCLEAN L"$*?({})" @@ -2559,6 +2596,7 @@ void reader_change_history(const wcstring &name) { if (data && data->history) { data->history->save(); data->history = history_t::with_name(name); + commandline_state_snapshot()->history = data->history; } } @@ -2575,6 +2613,7 @@ static std::shared_ptr reader_push_ret(parser_t &parser, if (reader_data_stack.size() == 1) { reader_interactive_init(parser); } + data->update_commandline_state(); return data; } @@ -2589,8 +2628,10 @@ void reader_pop() { reader_data_t *new_reader = current_data_or_null(); if (new_reader == nullptr) { reader_interactive_destroy(); + *commandline_state_snapshot() = commandline_state_t{}; } else { s_reset_abandoning_line(&new_reader->screen, termsize_last().width); + new_reader->update_commandline_state(); } } @@ -2661,6 +2702,29 @@ static bool selection_is_at_top(const reader_data_t *data) { return !(col != 0 && col != PAGER_SELECTION_NONE); } +void reader_data_t::update_commandline_state() const { + auto snapshot = commandline_state_snapshot(); + snapshot->text = this->command_line.text(); + snapshot->cursor_pos = this->command_line.position(); + snapshot->history = this->history; + snapshot->selection = this->get_selection(); + snapshot->pager_mode = !this->current_page_rendering.screen_data.empty(); + snapshot->search_mode = this->history_search.active(); + snapshot->initialized = true; +} + +void reader_data_t::apply_commandline_state_changes() { + // Only the text and cursor position may be changed. + commandline_state_t state = *commandline_state_snapshot(); + if (state.text != this->command_line.text() || + state.cursor_pos != this->command_line.position()) { + // The commandline builtin changed our contents. + this->pager.clear(); + this->set_buffer_maintaining_pager(state.text, state.cursor_pos); + this->reset_loop_state = true; + } +} + static relaxed_atomic_t run_count{0}; /// Returns the current interactive loop count @@ -2820,11 +2884,19 @@ struct readline_loop_state_t { size_t nchars{std::numeric_limits::max()}; }; +std::shared_ptr reader_get_history() { + ASSERT_IS_MAIN_THREAD(); + reader_data_t *data = current_data_or_null(); + return data ? data->history : nullptr; +} + /// Run a sequence of commands from an input binding. void reader_data_t::run_input_command_scripts(const wcstring_list_t &cmds) { auto last_statuses = parser().get_last_statuses(); for (const wcstring &cmd : cmds) { + update_commandline_state(); parser().eval(cmd, io_chain_t{}); + apply_commandline_state_changes(); } parser().set_last_statuses(std::move(last_statuses)); @@ -3623,8 +3695,10 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat editable_line_t *el = active_edit_line(); // Check that we have an active selection and get the bounds. - size_t start, len; - if (reader_get_selection(&start, &len)) { + if (auto selection = this->get_selection()) { + size_t start = selection->start; + size_t len = selection->length; + size_t buff_pos = el->position(); wcstring replacement; @@ -3714,9 +3788,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat 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); + if (auto selection = this->get_selection()) { + kill(&command_line, selection->start, selection->length, KILL_APPEND, newv); } break; } @@ -4089,15 +4162,6 @@ bool reader_data_t::jump(jump_direction_t dir, jump_precision_t precision, edita maybe_t reader_readline(int nchars) { return current_data()->readline(nchars); } -bool reader_is_in_search_mode() { - reader_data_t *data = current_data_or_null(); - return data && data->history_search.active(); -} - -bool reader_has_pager_contents() { - reader_data_t *data = current_data_or_null(); - return data && !data->current_page_rendering.screen_data.empty(); -} int reader_reading_interrupted() { int res = reader_test_and_clear_interrupted(); @@ -4132,18 +4196,6 @@ void reader_queue_ch(const char_event_t &ch) { } } -const wchar_t *reader_get_buffer() { - ASSERT_IS_MAIN_THREAD(); - reader_data_t *data = current_data_or_null(); - return data ? data->command_line.text().c_str() : nullptr; -} - -std::shared_ptr reader_get_history() { - ASSERT_IS_MAIN_THREAD(); - reader_data_t *data = current_data_or_null(); - return data ? data->history : nullptr; -} - /// Sets the command line contents, clearing the pager. void reader_set_buffer(const wcstring &b, size_t pos) { reader_data_t *data = current_data_or_null(); @@ -4154,23 +4206,6 @@ void reader_set_buffer(const wcstring &b, size_t pos) { data->reset_loop_state = true; } -size_t reader_get_cursor_pos() { - reader_data_t *data = current_data_or_null(); - if (!data) return static_cast(-1); - - return data->command_line.position(); -} - -bool reader_get_selection(size_t *start, size_t *len) { - bool result = false; - reader_data_t *data = current_data_or_null(); - if (data != nullptr && data->selection.has_value()) { - *start = data->selection->start; - *len = std::min(data->selection->stop, data->command_line.size()) - data->selection->start; - result = true; - } - return result; -} /// Read non-interactively. Read input from stdin without displaying the prompt, using syntax /// highlighting. This is used for reading scripts and init files. diff --git a/src/reader.h b/src/reader.h index 6a49ea54d..911510847 100644 --- a/src/reader.h +++ b/src/reader.h @@ -165,28 +165,6 @@ void reader_schedule_prompt_repaint(); class char_event_t; void reader_queue_ch(const char_event_t &ch); -/// Get the string of character currently entered into the command buffer, or 0 if interactive mode -/// is uninitialized. -const wchar_t *reader_get_buffer(); - -/// Returns the current reader's history. -std::shared_ptr reader_get_history(); - -/// Set the string of characters in the command buffer, as well as the cursor position. -/// -/// \param b the new buffer value -/// \param p the cursor position. If \c p is larger than the length of the command line, the cursor -/// is placed on the last character. -void reader_set_buffer(const wcstring &b, size_t p = -1); - -/// Get the current cursor position in the command line. If interactive mode is uninitialized, -/// return (size_t)-1. -size_t reader_get_cursor_pos(); - -/// Get the current selection range in the command line. Returns false if there is no active -/// selection, true otherwise. -bool reader_get_selection(size_t *start, size_t *len); - /// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it /// was set. In practice this will return 0 or SIGINT. int reader_test_and_clear_interrupted(); @@ -254,12 +232,6 @@ void reader_handle_sigint(); /// TODO: this doesn't belong in reader. bool check_cancel_from_fish_signal(); -/// Test whether the interactive reader is in search mode. -bool reader_is_in_search_mode(); - -/// Test whether the interactive reader has visible pager contents. -bool reader_has_pager_contents(); - /// Given a command line and an autosuggestion, return the string that gets shown to the user. /// Exposed for testing purposes only. wcstring combine_command_and_autosuggestion(const wcstring &cmdline, @@ -275,6 +247,24 @@ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flag const wcstring &command_line, size_t *inout_cursor_pos, bool append_only); +/// Snapshotted state from the reader. +struct commandline_state_t { + wcstring text; // command line text, or empty if not interactive + size_t cursor_pos{0}; // position of the cursor, may be as large as text.size() + maybe_t selection{}; // visual selection, or none if none + std::shared_ptr history{}; // current reader history, or null if not interactive + bool pager_mode{false}; // pager is visible + bool search_mode{false}; // pager is visible and search is active + bool initialized{false}; // if false, the reader has not yet been entered +}; + +/// Get the command line state. This may be fetched on a background thread. +commandline_state_t commandline_get_state(); + +/// Set the command line text and position. This may be called on a background thread; the reader +/// will pick it up when it is done executing. +void commandline_set_buffer(wcstring text, size_t cursor_pos = -1); + /// Return the current interactive reads loop count. Useful for determining how many commands have /// been executed between invocations of code. uint64_t reader_run_count(); diff --git a/tests/pexpects/commandline.py b/tests/pexpects/commandline.py index 0c544dd0f..321013c23 100644 --- a/tests/pexpects/commandline.py +++ b/tests/pexpects/commandline.py @@ -30,3 +30,9 @@ expect_prompt() sendline("echo foo") expect_prompt("foo") + +# commandline is empty when a command is executed. +sendline("set what (commandline)") +expect_prompt() +sendline('echo "<$what>"') +expect_prompt("<>")