diff --git a/src/env_dispatch.cpp b/src/env_dispatch.cpp index f415f7067..2db92817d 100644 --- a/src/env_dispatch.cpp +++ b/src/env_dispatch.cpp @@ -197,7 +197,7 @@ void env_dispatch_var_change(const wcstring &key, env_stack_t &vars) { // Eww. if (string_prefixes_string(L"fish_color_", key)) { - reader_schedule_prompt_repaint(); + reader_react_to_color_change(); } } @@ -220,7 +220,7 @@ void env_universal_callbacks(env_stack_t *stack, const callback_data_list_t &cal static void handle_fish_term_change(const env_stack_t &vars) { update_fish_color_support(vars); - reader_schedule_prompt_repaint(); + reader_react_to_color_change(); } static void handle_change_ambiguous_width(const env_stack_t &vars) { diff --git a/src/flog.h b/src/flog.h index bc6f0af29..30bdb4cb8 100644 --- a/src/flog.h +++ b/src/flog.h @@ -99,7 +99,6 @@ class category_list_t { category_t term_support{L"term-support", L"Terminal feature detection"}; category_t reader{L"reader", L"The interactive reader/input system"}; - category_t reader_render{L"reader-render", L"Rendering the command line"}; category_t complete{L"complete", L"The completion system"}; category_t path{L"path", L"Searching/using paths"}; diff --git a/src/input.cpp b/src/input.cpp index 910167a9e..4a6b2201c 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -271,7 +271,7 @@ static maybe_t interrupt_handler() { event_fire_delayed(parser); // Reap stray processes, including printing exit status messages. // TODO: shouldn't need this parser here. - if (job_reap(parser, true)) reader_schedule_prompt_repaint(); + if (job_reap(parser, true)) reader_repaint_needed(); // Tell the reader an event occurred. if (reader_reading_interrupted()) { auto vintr = shell_modes.c_cc[VINTR]; diff --git a/src/pager.cpp b/src/pager.cpp index 37c833e4a..1132467bb 100644 --- a/src/pager.cpp +++ b/src/pager.cpp @@ -14,7 +14,6 @@ #include "common.h" #include "complete.h" #include "fallback.h" -#include "flog.h" #include "highlight.h" #include "pager.h" #include "reader.h" @@ -576,28 +575,26 @@ page_rendering_t pager_t::render() const { return rendering; } -bool pager_t::rendering_needs_update(const page_rendering_t &rendering) const { - // Common case is no pager. - if (this->empty() && rendering.screen_data.empty()) return false; - - return rendering.term_width != this->available_term_width || // - rendering.term_height != this->available_term_height || // - rendering.selected_completion_idx != - this->visual_selected_completion_index(rendering.rows, rendering.cols) || // - rendering.search_field_shown != this->search_field_shown || // - rendering.search_field_line.text() != this->search_field_line.text() || // - rendering.search_field_line.position() != this->search_field_line.position() || // - (rendering.remaining_to_disclose > 0 && this->fully_disclosed); -} - void pager_t::update_rendering(page_rendering_t *rendering) const { - if (rendering_needs_update(*rendering)) { + if (rendering->term_width != this->available_term_width || + rendering->term_height != this->available_term_height || + rendering->selected_completion_idx != + this->visual_selected_completion_index(rendering->rows, rendering->cols) || + rendering->search_field_shown != this->search_field_shown || + rendering->search_field_line.text() != this->search_field_line.text() || + rendering->search_field_line.position() != this->search_field_line.position() || + (rendering->remaining_to_disclose > 0 && this->fully_disclosed)) { *rendering = this->render(); } } -pager_t::pager_t() = default; -pager_t::~pager_t() = default; +pager_t::pager_t() + : available_term_width(0), + available_term_height(0), + selected_completion_idx(PAGER_SELECTION_NONE), + suggested_row_start(0), + fully_disclosed(false), + search_field_shown(false) {} bool pager_t::empty() const { return unfiltered_completion_infos.empty(); } @@ -858,4 +855,5 @@ size_t pager_t::cursor_position() const { return result; } +// Constructor page_rendering_t::page_rendering_t() = default; diff --git a/src/pager.h b/src/pager.h index 7ea45aafa..426dd61a4 100644 --- a/src/pager.h +++ b/src/pager.h @@ -62,17 +62,17 @@ enum class selection_motion_t { #define PAGER_UNDISCLOSED_MAX_ROWS 4 class pager_t { - size_t available_term_width{0}; - size_t available_term_height{0}; + size_t available_term_width; + size_t available_term_height; - size_t selected_completion_idx{PAGER_SELECTION_NONE}; - size_t suggested_row_start{0}; + size_t selected_completion_idx; + size_t suggested_row_start; // Fully disclosed means that we show all completions. - bool fully_disclosed{false}; + bool fully_disclosed; // Whether we show the search field. - bool search_field_shown{false}; + bool search_field_shown; // Returns the index of the completion that should draw selected, using the given number of // columns. @@ -82,15 +82,19 @@ class pager_t { /// Data structure describing one or a group of related completions. struct comp_t { /// The list of all completion strings this entry applies to. - wcstring_list_t comp{}; + wcstring_list_t comp; /// The description. - wcstring desc{}; + wcstring desc; /// The representative completion. - completion_t representative{L""}; + completion_t representative; /// On-screen width of the completion string. - size_t comp_width{0}; + size_t comp_width; /// On-screen width of the description information. - size_t desc_width{0}; + size_t desc_width; + /// Minimum acceptable width. + // size_t min_width; + + comp_t() : comp(), desc(), representative(L""), comp_width(0), desc_width(0) {} // Our text looks like this: // completion (description) @@ -108,7 +112,7 @@ class pager_t { }; private: - using comp_info_list_t = std::vector; + typedef std::vector comp_info_list_t; // The filtered list of completion infos. comp_info_list_t completion_infos; @@ -161,10 +165,7 @@ class pager_t { // Produces a rendering of the completions, at the given term size. page_rendering_t render() const; - // \return true if the given rendering needs to be updated. - bool rendering_needs_update(const page_rendering_t &rendering) const; - - // Updates the rendering. + // Updates the rendering if it's stale. void update_rendering(page_rendering_t *rendering) const; // Indicates if there are no completions, and therefore nothing to render. @@ -191,8 +192,8 @@ class pager_t { // Position of the cursor. size_t cursor_position() const; + // Constructor pager_t(); - ~pager_t(); }; #endif diff --git a/src/reader.cpp b/src/reader.cpp index cdb568052..f5245b290 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -456,44 +456,6 @@ struct selection_data_t { /// The start and stop position of the current selection. size_t start{0}; size_t stop{0}; - - bool operator==(const selection_data_t &rhs) const { - return begin == rhs.begin && start == rhs.start && stop == rhs.stop; - } - - bool operator!=(const selection_data_t &rhs) const { return !(*this == rhs); } -}; - -/// A value-type struct representing a layout from which we can call to s_write(). -/// The intent is that everything we send to the screen is encapsulated in this struct. -struct layout_data_t { - /// Text of the command line. - wcstring text{}; - - /// The colors. This has the same length as 'text'. - std::vector colors{}; - - /// Position of the cursor in the command line. - size_t position{}; - - /// Whether the cursor is focused on the pager or not. - bool focused_on_pager{false}; - - /// Visual selection of the command line, or none if none. - maybe_t selection{}; - - /// String containing the autosuggestion. - wcstring autosuggestion{}; - - /// String containing the history search. If non-empty, then highlight the found range within - /// the text. - wcstring history_search_text{}; - - /// The result of evaluating the left, mode and right prompt commands. - /// That is, this the text of the prompts, not the commands to produce them. - wcstring left_prompt_buff{}; - wcstring mode_prompt_buff{}; - wcstring right_prompt_buff{}; }; /// A struct describing the state of the interactive reader. These states can be stacked, in case @@ -510,20 +472,16 @@ class reader_data_t : public std::enable_shared_from_this { /// or a pager selection change. When this is true and another transient change is made, the /// old transient change will be removed from the undo history. bool command_line_has_transient_edit = false; - /// The most recent layout data sent to the screen. - layout_data_t rendered_layout; /// String containing the autosuggestion. wcstring autosuggestion; /// Current pager. pager_t pager; - /// The output of the pager. + /// Current page rendering. page_rendering_t current_page_rendering; /// When backspacing, we temporarily suppress autosuggestions. bool suppress_autosuggestion{false}; - /// The representation of the current screen contents. screen_t screen; - /// The source of input events. inputter_t inputter; /// The history. @@ -538,12 +496,12 @@ class reader_data_t : public std::enable_shared_from_this { wcstring mode_prompt_buff; /// The output of the last evaluation of the right prompt command. wcstring right_prompt_buff; - - /// When navigating the pager, we modify the command line. - /// This is the saved command line before modification. + /// Completion support. wcstring cycle_command_line; size_t cycle_cursor_pos{0}; - + /// Color is the syntax highlighting for buff. The format is that color[i] is the + /// classification (according to the enum in highlight.h) of buff[i]. + std::vector colors; /// If set, a key binding or the 'exit' command has asked us to exit our read loop. bool exit_loop_requested{false}; /// If this is true, exit reader even if there are running jobs. This happens if we press e.g. @@ -551,21 +509,16 @@ class reader_data_t : public std::enable_shared_from_this { bool did_warn_for_bg_jobs{false}; /// The current contents of the top item in the kill ring. wcstring kill_item; - - /// A flag which may be set to force re-execing all prompts and re-rendering. - /// This may come about when a color like $fish_color... has changed. - bool force_exec_prompt_and_repaint{false}; - + /// Keep track of whether any internal code has done something which is known to require a + /// repaint. + bool repaint_needed{false}; + /// Whether a screen reset is needed after a repaint. + bool screen_reset_needed{false}; /// The target character of the last jump command. wchar_t last_jump_target{0}; jump_direction_t last_jump_direction{jump_direction_t::forward}; jump_precision_t last_jump_precision{jump_precision_t::to}; - /// The text of the most recent asynchronous highlight and autosuggestion requests. - /// If these differs from the text of the command line, then we must kick off a new request. - wcstring in_flight_highlight_request; - wcstring in_flight_autosuggest_request; - bool is_navigating_pager_contents() const { return this->pager.is_navigating_contents(); } /// The line that is currently being edited. Typically the command line, but may be the search @@ -591,28 +544,7 @@ class reader_data_t : public std::enable_shared_from_this { /// Expand abbreviations at the current cursor position, minus backtrack_amt. bool expand_abbreviation_as_necessary(size_t cursor_backtrack); - /// \return the string used for history search, or an empty string if none. - wcstring history_search_text_if_active() const; - - /// \return true if the command line has changed and repainting is needed. If \p colors is not - /// null, then also return true if the colors have changed. - using highlight_list_t = std::vector; - bool is_repaint_needed(const highlight_list_t *mcolors = nullptr) const; - - /// Generate a new layout data from the current state of the world. - /// If \p mcolors has a value, then apply it; otherwise extend existing colors. - layout_data_t make_layout_data(maybe_t mcolors = none()) const; - - /// Generate a new layout data from the current state of the world, and paint with it. - /// If \p mcolors has a value, then apply it; otherwise extend existing colors. - void layout_and_repaint(const wchar_t *reason, maybe_t mcolors = none()) { - this->rendered_layout = make_layout_data(std::move(mcolors)); - paint_layout(reason); - } - - /// Paint the last rendered layout. - /// \p reason is used in FLOG to explain why. - void paint_layout(const wchar_t *reason); + void repaint_if_needed(); /// Return the variable set used for e.g. command duration. env_stack_t &vars() { return parser_ref->vars(); } @@ -628,21 +560,18 @@ class reader_data_t : public std::enable_shared_from_this { history(hist) {} void update_buff_pos(editable_line_t *el, maybe_t new_pos = none_t()); - + void repaint(); void kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv); - void insert_string(editable_line_t *el, const wcstring &str); + bool insert_string(editable_line_t *el, const wcstring &str); /// Insert the character into the command line buffer and print it to the screen using syntax /// highlighting, etc. - void insert_char(editable_line_t *el, wchar_t c) { insert_string(el, wcstring{c}); } - - /// Read a command to execute, respecting input bindings. - /// \return the command, or none if we were asked to cancel (e.g. SIGHUP). - maybe_t readline(int nchars); + bool insert_char(editable_line_t *el, wchar_t c) { return insert_string(el, wcstring{c}); } void move_word(editable_line_t *el, bool move_right, bool erase, enum move_word_style_t style, 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); @@ -650,6 +579,8 @@ class reader_data_t : public std::enable_shared_from_this { void select_completion_in_direction(selection_motion_t dir); void flash(); + void mark_repaint_needed() { repaint_needed = true; } + void completion_insert(const wchar_t *val, size_t token_end, complete_flags_t flags); bool can_autosuggest() const; @@ -659,6 +590,7 @@ class reader_data_t : public std::enable_shared_from_this { move_word_style_t style = move_word_style_punctuation); void super_highlight_me_plenty(bool no_io = false); + void highlight_search(); void highlight_complete(highlight_result_t result); void exec_mode_prompt(); void exec_prompt(); @@ -857,63 +789,10 @@ void reader_data_t::update_buff_pos(editable_line_t *el, maybe_t new_pos } } -bool reader_data_t::is_repaint_needed(const std::vector *mcolors) const { - // Note: this function is responsible for detecting all of the ways that the command line may - // change, by comparing it to what is present in rendered_layout. - // The pager is the problem child, it has its own update logic. - auto check = [](bool val, const wchar_t *reason) { - if (val) FLOG(reader_render, L"repaint needed because", reason, L"change"); - return val; - }; - - bool focused_on_pager = active_edit_line() == &pager.search_field_line; - const layout_data_t &last = this->rendered_layout; - return check(force_exec_prompt_and_repaint, L"forced") || - check(command_line.text() != last.text, L"text") || - check(mcolors && *mcolors != last.colors, L"highlight") || - check(selection != last.selection, L"selection") || - check(focused_on_pager != last.focused_on_pager, L"focus") || - check(command_line.position() != last.position, L"position") || - check(history_search_text_if_active() != last.history_search_text, L"history search") || - check(autosuggestion != last.autosuggestion, L"autosuggestion") || - check(left_prompt_buff != last.left_prompt_buff, L"left_prompt") || - check(mode_prompt_buff != last.mode_prompt_buff, L"mode_prompt") || - check(right_prompt_buff != last.right_prompt_buff, L"right_prompt") || - check(pager.rendering_needs_update(current_page_rendering), L"pager"); -} - -layout_data_t reader_data_t::make_layout_data(maybe_t mcolors) const { - layout_data_t result{}; - bool focused_on_pager = active_edit_line() == &pager.search_field_line; - result.text = command_line.text(); - - if (mcolors.has_value()) { - result.colors = mcolors.acquire(); - } else { - result.colors = rendered_layout.colors; - } - - result.position = focused_on_pager ? pager.cursor_position() : command_line.position(); - result.selection = selection; - result.focused_on_pager = (active_edit_line() == &pager.search_field_line); - result.history_search_text = history_search_text_if_active(); - result.autosuggestion = autosuggestion; - result.left_prompt_buff = left_prompt_buff; - result.mode_prompt_buff = mode_prompt_buff; - result.right_prompt_buff = right_prompt_buff; - - // Ensure our color list has the same length as the command line, by extending it with the last - // color. This typically reduces redraws; e.g. if the user continues types into an argument, we - // guess it's still an argument, while the highlighting proceeds in the background. - highlight_spec_t last_color = result.colors.empty() ? highlight_spec_t{} : result.colors.back(); - result.colors.resize(result.text.size(), last_color); - return result; -} - -void reader_data_t::paint_layout(const wchar_t *reason) { - FLOGF(reader_render, L"Repainting from %ls", reason); - const layout_data_t &data = this->rendered_layout; - const editable_line_t *cmd_line = &command_line; +/// Repaint the entire commandline. This means reset and clear the commandline, write the prompt, +/// perform syntax highlighting, write the commandline and move the cursor. +void reader_data_t::repaint() { + editable_line_t *cmd_line = &command_line; wcstring full_line; if (conf.in_silent_mode) { @@ -923,41 +802,33 @@ void reader_data_t::paint_layout(const wchar_t *reason) { full_line = combine_command_and_autosuggestion(cmd_line->text(), autosuggestion); } - // Copy the colors and extend them with autosuggestion color. - std::vector colors = data.colors; + size_t len = full_line.size(); + if (len < 1) len = 1; - // Highlight any history search. - if (!conf.in_silent_mode && !data.history_search_text.empty()) { - const wcstring &needle = data.history_search_text; - const wcstring &haystack = cmd_line->text(); - size_t match_pos = haystack.find(needle); - if (match_pos != wcstring::npos) { - for (size_t i = 0; i < needle.size(); i++) { - colors.at(match_pos + i).background = highlight_role_t::search_match; - } - } - } + std::vector colors = this->colors; + colors.resize(len, highlight_role_t::autosuggestion); - // Apply any selection. - if (data.selection.has_value()) { + if (selection.has_value()) { highlight_spec_t selection_color = {highlight_role_t::normal, highlight_role_t::selection}; - for (size_t i = data.selection->start; i < std::min(selection->stop, colors.size()); i++) { - colors.at(i) = selection_color; + for (size_t i = selection->start; i < std::min(len, selection->stop); i++) { + colors[i] = selection_color; } } - // Extend our colors with the autosuggestion. - colors.resize(full_line.size(), highlight_role_t::autosuggestion); - // Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion // always conceptually has an indent of 0. std::vector indents = parse_util_compute_indents(cmd_line->text()); - indents.resize(full_line.size(), 0); + indents.resize(len, 0); + + bool focused_on_pager = active_edit_line() == &pager.search_field_line; + size_t cursor_position = focused_on_pager ? pager.cursor_position() : cmd_line->position(); // Prepend the mode prompt to the left prompt. s_write(&screen, mode_prompt_buff + left_prompt_buff, right_prompt_buff, full_line, - cmd_line->size(), colors, indents, data.position, pager, current_page_rendering, - data.focused_on_pager); + cmd_line->size(), colors, indents, cursor_position, pager, current_page_rendering, + focused_on_pager); + + repaint_needed = false; } /// Internal helper function for handling killing parts of text. @@ -978,6 +849,10 @@ void reader_data_t::kill(editable_line_t *el, size_t begin_idx, size_t length, i kill_replace(old, kill_item); } el->erase_substring(begin_idx, length); + update_buff_pos(el); + command_line_changed(el); + super_highlight_me_plenty(); + repaint(); } // This is called from a signal handler! @@ -987,6 +862,13 @@ void reader_handle_sigint() { interrupted = SIGINT; } void reader_data_t::command_line_changed(const editable_line_t *el) { ASSERT_IS_MAIN_THREAD(); if (el == &this->command_line) { + size_t len = this->command_line.size(); + + // When we grow colors, propagate the last color (if any), under the assumption that usually + // it will be correct. If it is, it avoids a repaint. + highlight_spec_t last_color = colors.empty() ? highlight_spec_t() : colors.back(); + colors.resize(len, last_color); + // Update the gen count. s_generation.store(1 + read_generation_count(), std::memory_order_relaxed); } else if (el == &this->pager.search_field_line) { @@ -1016,6 +898,9 @@ void reader_data_t::pager_selection_changed() { if (new_cmd_line != command_line.text()) { set_buffer_maintaining_pager(new_cmd_line, cursor_pos, true /* transient */); } + + // Trigger repaint (see issue #765). + mark_repaint_needed(); } /// Expand abbreviations at the given cursor position. Does NOT inspect 'data'. @@ -1078,7 +963,7 @@ maybe_t reader_expand_abbreviation_in_command(const wcstring &cmdline, s return result; } -/// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This may +/// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This may /// change the command line but does NOT repaint it. This is to allow the caller to coalesce /// repaints. bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) { @@ -1093,12 +978,28 @@ bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) { el->push_edit(std::move(*edit)); update_buff_pos(el); el->undo_history.may_coalesce = false; + command_line_changed(el); result = true; } } return result; } +void reader_data_t::repaint_if_needed() { + bool needs_reset = screen_reset_needed; + bool needs_repaint = needs_reset || repaint_needed; + + if (needs_reset) { + exec_prompt(); + s_reset_line(&screen, true /* repaint prompt */); + screen_reset_needed = false; + } + + if (needs_repaint) { + repaint(); // reader_repaint clears repaint_needed + } +} + void reader_reset_interrupted() { interrupted = 0; } int reader_test_and_clear_interrupted() { @@ -1357,17 +1258,34 @@ void reader_data_t::delete_char(bool backward) { } while (width == 0 && pos > 0); el->erase_substring(pos, pos_end - pos); update_buff_pos(el); + command_line_changed(el); suppress_autosuggestion = true; + + super_highlight_me_plenty(); + mark_repaint_needed(); } /// Insert the characters of the string into the command line buffer and print them to the screen /// using syntax highlighting, etc. /// Returns true if the string changed. -void reader_data_t::insert_string(editable_line_t *el, const wcstring &str) { - if (!str.empty()) { - el->insert_string(str, 0, str.size()); - if (el == &command_line) suppress_autosuggestion = false; +bool reader_data_t::insert_string(editable_line_t *el, const wcstring &str) { + if (str.empty()) return false; + + el->insert_string(str, 0, str.size()); + update_buff_pos(el); + command_line_changed(el); + + if (el == &command_line) { + suppress_autosuggestion = false; + + // Syntax highlight. Note we must have that buff_pos > 0 because we just added something + // nonzero to its length. + assert(el->position() > 0); + super_highlight_me_plenty(); } + + repaint(); + return true; } /// Insert the string in the given command line at the given cursor position. The function checks if @@ -1495,7 +1413,9 @@ void reader_data_t::completion_insert(const wchar_t *val, size_t token_end, editable_line_t *el = active_edit_line(); // Move the cursor to the end of the token. - if (el->position() != token_end) update_buff_pos(el, token_end); + if (el->position() != token_end) { + update_buff_pos(el, token_end); // repaint() is done later + } size_t cursor = el->position(); wcstring new_command_line = completion_apply_to_command_line(val, flags, el->text(), &cursor, @@ -1584,53 +1504,31 @@ bool reader_data_t::can_autosuggest() const { el == &command_line && el->text().find_first_not_of(whitespace) != wcstring::npos; } -// Called after an autosuggestion has been computed on a background thread. +// Called after an autosuggestion has been computed on a background thread void reader_data_t::autosuggest_completed(autosuggestion_result_t result) { - ASSERT_IS_MAIN_THREAD(); - if (result.search_string == in_flight_autosuggest_request) - in_flight_autosuggest_request.clear(); if (!result.suggestion.empty() && can_autosuggest() && result.search_string == command_line.text() && string_prefixes_string_case_insensitive(result.search_string, result.suggestion)) { // Autosuggestion is active and the search term has not changed, so we're good to go. autosuggestion = std::move(result.suggestion); - if (this->is_repaint_needed()) { - this->layout_and_repaint(L"autosuggest"); - } + repaint(); } } void reader_data_t::update_autosuggestion() { - // If we can't autosuggest, just clear it. - if (!can_autosuggest()) { - in_flight_autosuggest_request.clear(); - autosuggestion.clear(); - return; - } - - // Check to see if our autosuggestion still applies; if so, don't recompute it. - // Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into - // the autosuggestion. - // This is also the main mechanism by which readline commands that don't change the command line - // text avoid recomputing the autosuggestion. - const editable_line_t &el = command_line; - if (!autosuggestion.empty() && - string_prefixes_string_case_insensitive(el.text(), autosuggestion)) { - return; - } - - // Do nothing if we've already kicked off this autosuggest request. - if (el.text() == in_flight_autosuggest_request) return; - in_flight_autosuggest_request = el.text(); - - // Clear the autosuggestion and kick it off in the background. - FLOG(reader_render, L"Autosuggesting"); + // Updates autosuggestion. We look for an autosuggestion if the command line is non-empty and if + // we're not doing a history search. autosuggestion.clear(); - auto performer = get_autosuggestion_performer(parser(), el.text(), el.position(), history); - auto shared_this = this->shared_from_this(); - debounce_autosuggestions().perform(performer, [shared_this](autosuggestion_result_t result) { - shared_this->autosuggest_completed(std::move(result)); - }); + if (can_autosuggest()) { + const editable_line_t *el = active_edit_line(); + auto performer = + get_autosuggestion_performer(parser(), el->text(), el->position(), history); + auto shared_this = this->shared_from_this(); + debounce_autosuggestions().perform( + performer, [shared_this](autosuggestion_result_t result) { + shared_this->autosuggest_completed(std::move(result)); + }); + } } // Accept any autosuggestion by replacing the command line with it. If full is true, take the whole @@ -1659,6 +1557,10 @@ void reader_data_t::accept_autosuggestion(bool full, bool single, move_word_styl command_line.replace_substring(command_line.size(), 0, autosuggestion.substr(have, want - have)); } + update_buff_pos(&command_line); + command_line_changed(&command_line); + super_highlight_me_plenty(); + repaint(); } } @@ -1666,6 +1568,7 @@ void reader_data_t::accept_autosuggestion(bool full, bool single, move_word_styl void reader_data_t::clear_pager() { pager.clear(); current_page_rendering = page_rendering_t(); + mark_repaint_needed(); } void reader_data_t::select_completion_in_direction(selection_motion_t dir) { @@ -1680,18 +1583,11 @@ void reader_data_t::select_completion_in_direction(selection_motion_t dir) { void reader_data_t::flash() { struct timespec pollint; editable_line_t *el = &command_line; - layout_data_t data = make_layout_data(); - - // Save off the colors and set the background. - highlight_list_t saved_colors = data.colors; for (size_t i = 0; i < el->position(); i++) { - data.colors.at(i) = highlight_spec_t::make_background(highlight_role_t::search_match); + colors.at(i) = highlight_spec_t::make_background(highlight_role_t::search_match); } - this->rendered_layout = data; // need to copy the data since we will use it again. - paint_layout(L"flash"); - - layout_data_t old_data = std::move(rendered_layout); + repaint(); ignore_result(write(STDOUT_FILENO, "\a", 1)); // The write above changed the timestamp of stdout; ensure we don't therefore reset our screen. // See #3693. @@ -1701,10 +1597,8 @@ void reader_data_t::flash() { pollint.tv_nsec = 100 * 1000000; nanosleep(&pollint, nullptr); - // Re-render with our saved data. - data.colors = std::move(saved_colors); - this->rendered_layout = std::move(data); - paint_layout(L"unflash"); + super_highlight_me_plenty(); + repaint(); } /// Characters that may not be part of a token that is to be replaced by a case insensitive @@ -1906,6 +1800,7 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok current_page_rendering = page_rendering_t(); // Modify the command line to reflect the new pager. pager_selection_changed(); + mark_repaint_needed(); return false; } @@ -2103,6 +1998,9 @@ void reader_data_t::set_command_line_and_position(editable_line_t *el, wcstring el->set_position(pos); el->undo_history.may_coalesce = false; update_buff_pos(el, pos); + command_line_changed(el); + super_highlight_me_plenty(); + mark_repaint_needed(); } /// Undo the transient edit und update commandline accordingly. @@ -2112,6 +2010,9 @@ void reader_data_t::clear_transient_edit() { } command_line.undo(); update_buff_pos(&command_line); + command_line_changed(&command_line); + super_highlight_me_plenty(); + mark_repaint_needed(); command_line_has_transient_edit = false; } @@ -2147,6 +2048,9 @@ void reader_data_t::update_command_line_from_history_search() { command_line_has_transient_edit = true; assert(el == &command_line); update_buff_pos(el); + command_line_changed(el); + super_highlight_me_plenty(); + mark_repaint_needed(); } enum move_word_dir_t { MOVE_DIR_LEFT, MOVE_DIR_RIGHT }; @@ -2194,6 +2098,7 @@ void reader_data_t::move_word(editable_line_t *el, bool move_right, bool erase, } } else { update_buff_pos(el, buff_pos); + repaint(); } } @@ -2213,10 +2118,13 @@ void reader_data_t::set_buffer_maintaining_pager(const wcstring &b, size_t pos, // Don't set a position past the command line length. if (pos > command_line_len) pos = command_line_len; //!OCLINT(parameter reassignment) + update_buff_pos(&command_line, pos); // Clear history search and pager contents. history_search.reset(); + super_highlight_me_plenty(); + mark_repaint_needed(); } void set_env_cmd_duration(struct timeval *after, struct timeval *before, env_stack_t &vars) { @@ -2290,20 +2198,32 @@ static parser_test_error_bits_t reader_shell_test(parser_t &parser, const wcstri return res; } -wcstring reader_data_t::history_search_text_if_active() const { - if (!history_search.active() || history_search.is_at_end()) { - return wcstring{}; +/// Called to set the highlight flag for search results. +void reader_data_t::highlight_search() { + if (history_search.is_at_end()) { + return; + } + const wcstring &needle = history_search.search_string(); + const editable_line_t *el = &command_line; + size_t match_pos = el->text().find(needle); + if (match_pos != wcstring::npos) { + size_t end = match_pos + needle.size(); + for (size_t i = match_pos; i < end; i++) { + colors.at(i).background = highlight_role_t::search_match; + } } - return history_search.search_string(); } void reader_data_t::highlight_complete(highlight_result_t result) { ASSERT_IS_MAIN_THREAD(); - in_flight_highlight_request.clear(); if (result.text == command_line.text()) { + // The data hasn't changed, so swap in our colors. The colors may not have changed, so do + // nothing if they have not. assert(result.colors.size() == command_line.size()); - if (this->is_repaint_needed(&result.colors)) { - this->layout_and_repaint(L"highlight", std::move(result.colors)); + if (colors != result.colors) { + colors = std::move(result.colors); + highlight_search(); + repaint(); } } } @@ -2324,7 +2244,10 @@ static std::function get_highlight_performer(parser_t }; } -/// Highlight the command line in a super, plentiful way. +/// Call specified external highlighting function and then do search highlighting. Lastly, clear the +/// background color under the cursor to avoid repaint issues on terminals where e.g. syntax +/// highlighting maykes characters under the sursor unreadable. +/// /// \param no_io if true, do a highlight that does not perform I/O, synchronously. If false, perform /// an asynchronous highlight in the background, which may perform disk I/O. void reader_data_t::super_highlight_me_plenty(bool no_io) { @@ -2332,11 +2255,6 @@ void reader_data_t::super_highlight_me_plenty(bool no_io) { const editable_line_t *el = &command_line; - // Do nothing if this text is already in flight. - if (el->text() == in_flight_highlight_request) return; - in_flight_highlight_request = el->text(); - - FLOG(reader_render, L"Highlighting"); auto highlight_performer = get_highlight_performer(parser(), el->text(), !no_io); if (no_io) { // Highlighting without IO, we just do it. @@ -2349,6 +2267,18 @@ void reader_data_t::super_highlight_me_plenty(bool no_io) { shared_this->highlight_complete(std::move(result)); }); } + highlight_search(); + + // Here's a hack. Check to see if our autosuggestion still applies; if so, don't recompute it. + // Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into + // the autosuggestion. + const wcstring &cmd = el->text(), &suggest = autosuggestion; + if (can_autosuggest() && !suggest.empty() && + string_prefixes_string_case_insensitive(cmd, suggest)) { + // the autosuggestion is still reasonable, so do nothing + } else { + update_autosuggestion(); + } } /// The stack of current interactive reading contexts. @@ -2515,7 +2445,7 @@ static int read_i(parser_t &parser) { while (!check_exit_loop_maybe_warning(data.get())) { run_count++; - maybe_t tmp = data->readline(0); + maybe_t tmp = reader_readline(0); if (tmp && !tmp->empty()) { const wcstring command = tmp.acquire(); data->update_buff_pos(&data->command_line, 0); @@ -2639,15 +2569,15 @@ struct readline_loop_state_t { /// List of completions. completion_list_t comp; + /// 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()}; - - /// \return whether the last readline command was a repaint. - bool last_was_repaint() const { return last_cmd && *last_cmd == readline_cmd_t::repaint; } }; /// Read normal characters, inserting them into the command line. @@ -2697,6 +2627,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat while (el->position() > 0 && el->text().at(el->position() - 1) != L'\n') { update_buff_pos(el, el->position() - 1); } + mark_repaint_needed(); break; } case rl::end_of_line: { @@ -2709,14 +2640,18 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } else { accept_autosuggestion(true); } + + mark_repaint_needed(); break; } case rl::beginning_of_buffer: { update_buff_pos(&command_line, 0); + mark_repaint_needed(); break; } case rl::end_of_buffer: { update_buff_pos(&command_line, command_line.size()); + mark_repaint_needed(); break; } case rl::cancel_commandline: { @@ -2726,7 +2661,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat update_buff_pos(&command_line, command_line.size()); autosuggestion.clear(); // Repaint also changes the actual cursor position - if (this->is_repaint_needed()) this->layout_and_repaint(L"cancel"); + repaint(); auto fish_color_cancel = vars.get(L"fish_color_cancel"); if (fish_color_cancel) { @@ -2759,7 +2694,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat exec_mode_prompt(); if (!mode_prompt_buff.empty()) { s_reset_line(&screen, true /* redraw prompt */); - if (this->is_repaint_needed()) this->layout_and_repaint(L"mode"); + screen_reset_needed = false; + repaint(); break; } // Else we repaint as normal. @@ -2767,11 +2703,12 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::force_repaint: case rl::repaint: { - if (force_exec_prompt_and_repaint || !rls.last_was_repaint()) { + if (!rls.coalescing_repaints) { + rls.coalescing_repaints = true; exec_prompt(); s_reset_line(&screen, true /* redraw prompt */); - this->layout_and_repaint(L"readline"); - force_exec_prompt_and_repaint = false; + screen_reset_needed = false; + repaint(); } break; } @@ -2787,6 +2724,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // disclosed, then become so; otherwise cycle through our available completions. if (current_page_rendering.remaining_to_disclose > 0) { pager.set_fully_disclosed(true); + mark_repaint_needed(); } else { select_completion_in_direction(c == rl::complete ? selection_motion_t::next : selection_motion_t::prev); @@ -2846,6 +2784,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat if (c == rl::complete_and_search && !rls.complete_did_insert && !pager.empty()) { pager.set_search_field_shown(true); select_completion_in_direction(selection_motion_t::next); + mark_repaint_needed(); } } break; @@ -2859,6 +2798,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat if (pager.is_search_field_shown() && !is_navigating_pager_contents()) { select_completion_in_direction(selection_motion_t::south); } + mark_repaint_needed(); } break; } @@ -2945,7 +2885,10 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat std::move(yank_str)); update_buff_pos(el); rls.yank_len = new_yank_len; + command_line_changed(el); suppress_autosuggestion = true; + super_highlight_me_plenty(); + mark_repaint_needed(); } break; } @@ -3019,13 +2962,19 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // 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 && conf.syntax_check_ok) { - command_test_result = reader_shell_test(parser(), el->text()); + if (abbreviation_expanded) { + // It's our reponsibility to rehighlight and repaint. But everything we do + // below triggers a repaint. + if (conf.syntax_check_ok) { + command_test_result = reader_shell_test(parser(), el->text()); + } + + // 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(skip_io); } - // 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. - if (command_test_result == 0) super_highlight_me_plenty(true); } if (command_test_result == 0) { @@ -3037,6 +2986,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } 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, L'\n'); @@ -3046,7 +2996,9 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat wcstring_list_t argv(1, el->text()); event_fire_generic(parser(), L"fish_posterror", &argv); s_reset_abandoning_line(&screen, termsize_last().width); + mark_repaint_needed(); } + break; } @@ -3110,6 +3062,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat select_completion_in_direction(selection_motion_t::west); } else if (el->position() > 0) { update_buff_pos(el, el->position() - 1); + mark_repaint_needed(); } break; } @@ -3119,6 +3072,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat select_completion_in_direction(selection_motion_t::east); } else if (el->position() < el->size()) { update_buff_pos(el, el->position() + 1); + mark_repaint_needed(); } else { accept_autosuggestion(true); } @@ -3130,6 +3084,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat select_completion_in_direction(selection_motion_t::east); } else if (el->position() < el->size()) { update_buff_pos(el, el->position() + 1); + mark_repaint_needed(); } else { accept_autosuggestion(false, true); } @@ -3247,6 +3202,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat size_t 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); + mark_repaint_needed(); } } break; @@ -3254,6 +3210,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::suppress_autosuggestion: { suppress_autosuggestion = true; autosuggestion.clear(); + mark_repaint_needed(); break; } case rl::accept_autosuggestion: { @@ -3343,6 +3300,10 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Restore the buffer position since replace_substring moves // the buffer position ahead of the replaced text. update_buff_pos(el, buff_pos); + + command_line_changed(el); + super_highlight_me_plenty(); + mark_repaint_needed(); } break; } @@ -3375,6 +3336,10 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Restore the buffer position since replace_substring moves // the buffer position ahead of the replaced text. update_buff_pos(el, buff_pos); + + command_line_changed(el); + super_highlight_me_plenty(); + mark_repaint_needed(); } break; } @@ -3412,6 +3377,9 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } el->replace_substring(init_pos, pos - init_pos, std::move(replacement)); update_buff_pos(el); + command_line_changed(el); + super_highlight_me_plenty(); + mark_repaint_needed(); break; } @@ -3462,6 +3430,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat bool success = jump(direction, precision, el, target); inputter.function_set_status(success); + mark_repaint_needed(); break; } case rl::repeat_jump: { @@ -3473,6 +3442,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } inputter.function_set_status(success); + mark_repaint_needed(); break; } case rl::reverse_repeat_jump: { @@ -3494,11 +3464,14 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat last_jump_direction = original_dir; inputter.function_set_status(success); + mark_repaint_needed(); break; } case rl::expand_abbr: { if (expand_abbreviation_as_necessary(1)) { + super_highlight_me_plenty(); + mark_repaint_needed(); inputter.function_set_status(true); } else { inputter.function_set_status(false); @@ -3514,6 +3487,9 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat clear_pager(); } update_buff_pos(el); + command_line_changed(el); + super_highlight_me_plenty(); + mark_repaint_needed(); } else { flash(); } @@ -3554,16 +3530,8 @@ maybe_t reader_data_t::readline(int nchars_or_0) { event_fire_generic(parser(), L"fish_prompt"); exec_prompt(); - /// A helper that kicks off syntax highlighting, autosuggestion computing, and repaints. - auto color_suggest_repaint_now = [this] { - this->update_autosuggestion(); - this->super_highlight_me_plenty(); - if (this->is_repaint_needed()) this->layout_and_repaint(L"toplevel"); - this->force_exec_prompt_and_repaint = false; - }; - - // Start out as initially dirty. - force_exec_prompt_and_repaint = true; + super_highlight_me_plenty(); + repaint(); // 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(); @@ -3586,9 +3554,6 @@ maybe_t reader_data_t::readline(int nchars_or_0) { // Perhaps update the termsize. This is cheap if it has not changed. update_termsize(); - // Repaint as needed. - color_suggest_repaint_now(); - if (rls.nchars <= command_line.size()) { // We've already hit the specified character limit. rls.finished = true; @@ -3607,6 +3572,8 @@ maybe_t reader_data_t::readline(int nchars_or_0) { } if (!event_needing_handling || event_needing_handling->is_check_exit()) { + rls.coalescing_repaints = false; + repaint_if_needed(); continue; } else if (event_needing_handling->is_eof()) { reader_sighup(); @@ -3621,6 +3588,9 @@ maybe_t reader_data_t::readline(int nchars_or_0) { 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; + if (readline_cmd == rl::cancel && is_navigating_pager_contents()) { clear_transient_edit(); } @@ -3672,6 +3642,8 @@ maybe_t reader_data_t::readline(int nchars_or_0) { } rls.last_cmd = none(); } + + repaint_if_needed(); } // Emit a newline so that the output is on the line after the command. @@ -3767,12 +3739,15 @@ int reader_reading_interrupted() { return res; } -void reader_schedule_prompt_repaint() { - ASSERT_IS_MAIN_THREAD(); - reader_data_t *data = current_data_or_null(); - if (data && !data->force_exec_prompt_and_repaint) { - data->force_exec_prompt_and_repaint = true; - data->inputter.queue_ch(readline_cmd_t::repaint); +void reader_repaint_needed() { + if (reader_data_t *data = current_data_or_null()) { + data->mark_repaint_needed(); + } +} + +void reader_repaint_if_needed() { + if (reader_data_t *data = current_data_or_null()) { + data->repaint_if_needed(); } } @@ -3782,6 +3757,17 @@ void reader_queue_ch(const char_event_t &ch) { } } +void reader_react_to_color_change() { + reader_data_t *data = current_data_or_null(); + if (!data) return; + + if (!data->repaint_needed || !data->screen_reset_needed) { + data->repaint_needed = true; + data->screen_reset_needed = true; + data->inputter.queue_ch(readline_cmd_t::repaint); + } +} + const wchar_t *reader_get_buffer() { ASSERT_IS_MAIN_THREAD(); reader_data_t *data = current_data_or_null(); diff --git a/src/reader.h b/src/reader.h index f56a37121..8b0e2e213 100644 --- a/src/reader.h +++ b/src/reader.h @@ -138,9 +138,15 @@ void reader_change_history(const wcstring &name); /// \param reset_cursor_position If set, issue a \r so the line driver knows where we are void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor_position = true); -/// Tell the reader that it needs to re-exec the prompt and repaint. -/// This may be called in response to e.g. a color variable change. -void reader_schedule_prompt_repaint(); +/// Call this function to tell the reader that a repaint is needed, and should be performed when +/// possible. +void reader_repaint_needed(); + +/// Call this function to tell the reader that some color has changed. +void reader_react_to_color_change(); + +/// Repaint immediately if needed. +void reader_repaint_if_needed(); /// Enqueue an event to the back of the reader's input queue. class char_event_t;