diff options
Diffstat (limited to 'reader.cpp')
-rw-r--r-- | reader.cpp | 1694 |
1 files changed, 976 insertions, 718 deletions
@@ -45,6 +45,8 @@ commence. #if HAVE_NCURSES_H #include <ncurses.h> +#elif HAVE_NCURSES_CURSES_H +#include <ncurses/curses.h> #else #include <curses.h> #endif @@ -99,6 +101,8 @@ commence. #include "path.h" #include "parse_util.h" #include "parser_keywords.h" +#include "parse_tree.h" +#include "pager.h" /** Maximum length of prefix string when printing completion @@ -179,10 +183,17 @@ static volatile unsigned int s_generation_count; /* This pthreads generation count is set when an autosuggestion background thread starts up, so it can easily check if the work it is doing is no longer useful. */ static pthread_key_t generation_count_key; -/* A color is an int */ -typedef int color_t; +static void set_command_line_and_position(editable_line_t *el, const wcstring &new_str, size_t pos); -static void set_command_line_and_position(const wcstring &new_str, size_t pos); +void editable_line_t::insert_string(const wcstring &str, size_t start, size_t len) +{ + // Clamp the range to something valid + size_t string_length = str.size(); + start = mini(start, string_length); + len = mini(len, string_length - start); + this->text.insert(this->position, str, start, len); + this->position += len; +} /** A struct describing the state of the interactive reader. These @@ -194,11 +205,17 @@ class reader_data_t public: /** String containing the whole current commandline */ - wcstring command_line; + editable_line_t command_line; /** String containing the autosuggestion */ wcstring autosuggestion; + /** Current pager */ + pager_t pager; + + /** Current page rendering */ + page_rendering_t current_page_rendering; + /** Whether autosuggesting is allowed at all */ bool allow_autosuggestion; @@ -240,20 +257,44 @@ public: /** The current position in search_prev */ size_t search_pos; - /** Length of the command */ - size_t command_length() const + bool is_navigating_pager_contents() const { - return command_line.size(); + return this->pager.is_navigating_contents(); + } + + /* The line that is currently being edited. Typically the command line, but may be the search field */ + editable_line_t *active_edit_line() + { + if (this->is_navigating_pager_contents() && this->pager.is_search_field_shown()) + { + return &this->pager.search_field_line; + } + else + { + return &this->command_line; + } } /** Do what we need to do whenever our command line changes */ - void command_line_changed(void); + void command_line_changed(const editable_line_t *el); + + /** Do what we need to do whenever our pager selection */ + void pager_selection_changed(); /** Expand abbreviations at the current cursor position, minus backtrack_amt. */ bool expand_abbreviation_as_necessary(size_t cursor_backtrack); - /** The current position of the cursor in buff. */ - size_t buff_pos; + /** Indicates whether a selection is currently active */ + bool sel_active; + + /** The position of the cursor, when selection was initiated. */ + size_t sel_begin_pos; + + /** The start position of the current selection, if one. */ + size_t sel_start_pos; + + /** The stop position of the current selection, if one. */ + size_t sel_stop_pos; /** Name of the current application */ wcstring app_name; @@ -268,12 +309,16 @@ public: /** The output of the last evaluation of the right prompt command */ wcstring right_prompt_buff; + /* Completion support */ + wcstring cycle_command_line; + size_t cycle_cursor_pos; + /** 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<color_t> colors; + std::vector<highlight_spec_t> colors; /** An array defining the block level at each character. */ std::vector<int> indents; @@ -338,7 +383,11 @@ public: history(0), token_history_pos(0), search_pos(0), - buff_pos(0), + sel_active(0), + sel_begin_pos(0), + sel_start_pos(0), + sel_stop_pos(0), + cycle_cursor_pos(0), complete_func(0), highlight_function(0), test_func(0), @@ -353,6 +402,12 @@ public: } }; +/* Sets the command line contents, without clearing the pager */ +static void reader_set_buffer_maintaining_pager(const wcstring &b, size_t pos); + +/* Clears the pager */ +static void clear_pager(); + /** The current interactive reading context */ @@ -399,7 +454,7 @@ static struct termios terminal_mode_on_startup; static struct termios terminal_mode_for_executing_programs; -static void reader_super_highlight_me_plenty(size_t pos); +static void reader_super_highlight_me_plenty(int highlight_pos_adjust = 0, bool no_io = false); /** Variable to keep track of forced exits - see \c reader_exit_forced(); @@ -432,6 +487,29 @@ static void term_donate() } + +/** + Update the cursor position +*/ +static void update_buff_pos(editable_line_t *el, size_t buff_pos) +{ + el->position = buff_pos; + if (el == &data->command_line && data->sel_active) + { + if (data->sel_begin_pos <= buff_pos) + { + data->sel_start_pos = data->sel_begin_pos; + data->sel_stop_pos = buff_pos; + } + else + { + data->sel_start_pos = buff_pos; + data->sel_stop_pos = data->sel_begin_pos; + } + } +} + + /** Grab control of terminal */ @@ -514,50 +592,62 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline, const wcstr commandline, write the prompt, perform syntax highlighting, write the commandline and move the cursor. */ - static void reader_repaint() { + editable_line_t *cmd_line = &data->command_line; // Update the indentation - parser_t::principal_parser().test(data->command_line.c_str(), &data->indents[0]); + data->indents = parse_util_compute_indents(cmd_line->text); // Combine the command and autosuggestion into one string - wcstring full_line = combine_command_and_autosuggestion(data->command_line, data->autosuggestion); + wcstring full_line = combine_command_and_autosuggestion(cmd_line->text, data->autosuggestion); size_t len = full_line.size(); if (len < 1) len = 1; - std::vector<color_t> colors = data->colors; - colors.resize(len, HIGHLIGHT_AUTOSUGGESTION); + std::vector<highlight_spec_t> colors = data->colors; + colors.resize(len, highlight_spec_autosuggestion); + + if (data->sel_active) + { + highlight_spec_t selection_color = highlight_make_background(highlight_spec_selection); + for (size_t i = data->sel_start_pos; i <= std::min(len - 1, data->sel_stop_pos); i++) + { + colors[i] = selection_color; + } + } std::vector<int> indents = data->indents; indents.resize(len); + // Re-render our completions page if necessary + // We set the term size to 1 less than the true term height. This means we will always show the (bottom) line of the prompt. + data->pager.set_term_size(maxi(1, common_get_width()), maxi(1, common_get_height() - 1)); + data->pager.update_rendering(&data->current_page_rendering); + + bool focused_on_pager = data->active_edit_line() == &data->pager.search_field_line; + size_t cursor_position = focused_on_pager ? data->pager.cursor_position() : cmd_line->position; + s_write(&data->screen, data->left_prompt_buff, data->right_prompt_buff, full_line, - data->command_length(), + cmd_line->size(), &colors[0], &indents[0], - data->buff_pos); + cursor_position, + data->sel_start_pos, + data->sel_stop_pos, + data->current_page_rendering, + focused_on_pager); data->repaint_needed = false; } -static void reader_repaint_without_autosuggestion() -{ - // Swap in an empty autosuggestion, repaint, then swap it out - wcstring saved_autosuggestion; - data->autosuggestion.swap(saved_autosuggestion); - reader_repaint(); - data->autosuggestion.swap(saved_autosuggestion); -} - /** Internal helper function for handling killing parts of text. */ -static void reader_kill(size_t begin_idx, size_t length, int mode, int newv) +static void reader_kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv) { - const wchar_t *begin = data->command_line.c_str() + begin_idx; + const wchar_t *begin = el->text.c_str() + begin_idx; if (newv) { data->kill_item = wcstring(begin, length); @@ -580,19 +670,18 @@ static void reader_kill(size_t begin_idx, size_t length, int mode, int newv) kill_replace(old, data->kill_item); } - if (data->buff_pos > begin_idx) + if (el->position > begin_idx) { /* Move the buff position back by the number of characters we deleted, but don't go past buff_pos */ - size_t backtrack = mini(data->buff_pos - begin_idx, length); - data->buff_pos -= backtrack; + size_t backtrack = mini(el->position - begin_idx, length); + update_buff_pos(el, el->position - backtrack); } - data->command_line.erase(begin_idx, length); - data->command_line_changed(); + el->text.erase(begin_idx, length); + data->command_line_changed(el); - reader_super_highlight_me_plenty(data->buff_pos); + reader_super_highlight_me_plenty(); reader_repaint(); - } @@ -630,19 +719,54 @@ void reader_pop_current_filename() /** Make sure buffers are large enough to hold the current string length */ -void reader_data_t::command_line_changed() +void reader_data_t::command_line_changed(const editable_line_t *el) { ASSERT_IS_MAIN_THREAD(); - size_t len = command_length(); + 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. */ - color_t last_color = colors.empty() ? color_t() : colors.back(); - colors.resize(len, last_color); + /* 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); - indents.resize(len); + indents.resize(len); - /* Update the gen count */ - s_generation_count++; + /* Update the gen count */ + s_generation_count++; + } + else if (el == &this->pager.search_field_line) + { + this->pager.refilter_completions(); + this->pager_selection_changed(); + } +} + +void reader_data_t::pager_selection_changed() +{ + ASSERT_IS_MAIN_THREAD(); + + const completion_t *completion = this->pager.selected_completion(this->current_page_rendering); + + /* Update the cursor and command line */ + size_t cursor_pos = this->cycle_cursor_pos; + wcstring new_cmd_line; + + if (completion == NULL) + { + new_cmd_line = this->cycle_command_line; + } + else + { + new_cmd_line = completion_apply_to_command_line(completion->completion, completion->flags, this->cycle_command_line, &cursor_pos, false); + } + reader_set_buffer_maintaining_pager(new_cmd_line, cursor_pos); + + /* Since we just inserted a completion, don't immediately do a new autosuggestion */ + this->suppress_autosuggestion = true; + + /* Trigger repaint (see #765) */ + reader_repaint_needed(); } /* Expand abbreviations at the given cursor position. Does NOT inspect 'data'. */ @@ -659,117 +783,55 @@ bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t curso const size_t subcmd_offset = cmdsub_begin - buff; const wcstring subcmd = wcstring(cmdsub_begin, cmdsub_end - cmdsub_begin); - const wchar_t *subcmd_cstr = subcmd.c_str(); + const size_t subcmd_cursor_pos = cursor_pos - subcmd_offset; - /* Get the token containing the cursor */ - const wchar_t *subcmd_tok_begin = NULL, *subcmd_tok_end = NULL; - assert(cursor_pos >= subcmd_offset); - size_t subcmd_cursor_pos = cursor_pos - subcmd_offset; - parse_util_token_extent(subcmd_cstr, subcmd_cursor_pos, &subcmd_tok_begin, &subcmd_tok_end, NULL, NULL); + /* Parse this subcmd */ + parse_node_tree_t parse_tree; + parse_tree_from_string(subcmd, parse_flag_continue_after_error | parse_flag_accept_incomplete_tokens, &parse_tree, NULL); - /* Compute the offset of the token before the cursor within the subcmd */ - assert(subcmd_tok_begin >= subcmd_cstr); - assert(subcmd_tok_end >= subcmd_tok_begin); - const size_t subcmd_tok_begin_offset = subcmd_tok_begin - subcmd_cstr; - const size_t subcmd_tok_length = subcmd_tok_end - subcmd_tok_begin; - - /* Now parse the subcmd, looking for commands */ - bool had_cmd = false, previous_token_is_cmd = false; - tokenizer_t tok(subcmd_cstr, TOK_ACCEPT_UNFINISHED | TOK_SQUASH_ERRORS); - for (; tok_has_next(&tok); tok_next(&tok)) + /* Look for plain statements where the cursor is at the end of the command */ + const parse_node_t *matching_cmd_node = NULL; + const size_t len = parse_tree.size(); + for (size_t i=0; i < len; i++) { - size_t tok_pos = static_cast<size_t>(tok_get_pos(&tok)); - if (tok_pos > subcmd_tok_begin_offset) - { - /* We've passed the token we're interested in */ - break; - } + const parse_node_t &node = parse_tree.at(i); - int last_type = tok_last_type(&tok); + /* Only interested in plain statements with source */ + if (node.type != symbol_plain_statement || ! node.has_source()) + continue; - switch (last_type) - { - case TOK_STRING: - { - if (had_cmd) - { - /* Parameter to the command. */ - } - else - { - const wcstring potential_cmd = tok_last(&tok); - if (parser_keywords_is_subcommand(potential_cmd)) - { - if (potential_cmd == L"command" || potential_cmd == L"builtin") - { - /* 'command' and 'builtin' defeat abbreviation expansion. Skip this command. */ - had_cmd = true; - } - else - { - /* Other subcommand. Pretend it doesn't exist so that we can expand the following command */ - had_cmd = false; - } - } - else - { - /* It's a normal command */ - had_cmd = true; - if (tok_pos == subcmd_tok_begin_offset) - { - /* This is the token we care about! */ - previous_token_is_cmd = true; - } - } - } - break; - } + /* Skip decorated statements */ + if (parse_tree.decoration_for_plain_statement(node) != parse_statement_decoration_none) + continue; - case TOK_REDIRECT_NOCLOB: - case TOK_REDIRECT_OUT: - case TOK_REDIRECT_IN: - case TOK_REDIRECT_APPEND: - case TOK_REDIRECT_FD: - { - if (!had_cmd) - { - break; - } - tok_next(&tok); - break; - } + /* Get the command node. Skip it if we can't or it has no source */ + const parse_node_t *cmd_node = parse_tree.get_child(node, 0, parse_token_type_string); + if (cmd_node == NULL || ! cmd_node->has_source()) + continue; - case TOK_PIPE: - case TOK_BACKGROUND: - case TOK_END: - { - had_cmd = false; - break; - } - - case TOK_COMMENT: - case TOK_ERROR: - default: - { - break; - } + /* Now see if its source range contains our cursor, including at the end */ + if (subcmd_cursor_pos >= cmd_node->source_start && subcmd_cursor_pos <= cmd_node->source_start + cmd_node->source_length) + { + /* Success! */ + matching_cmd_node = cmd_node; + break; } } + /* Now if we found a command node, expand it */ bool result = false; - if (previous_token_is_cmd) + if (matching_cmd_node != NULL) { - /* The token is a command. Try expanding it as an abbreviation. */ - const wcstring token = wcstring(subcmd, subcmd_tok_begin_offset, subcmd_tok_length); + assert(matching_cmd_node->type == parse_token_type_string); + const wcstring token = matching_cmd_node->get_source(subcmd); wcstring abbreviation; if (expand_abbreviation(token, &abbreviation)) { /* There was an abbreviation! Replace the token in the full command. Maintain the relative position of the cursor. */ if (output != NULL) { - size_t cmd_tok_begin_offset = subcmd_tok_begin_offset + subcmd_offset; output->assign(cmdline); - output->replace(cmd_tok_begin_offset, subcmd_tok_length, abbreviation); + output->replace(subcmd_offset + matching_cmd_node->source_start, matching_cmd_node->source_length, abbreviation); } result = true; } @@ -781,19 +843,21 @@ bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t curso bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) { bool result = false; - if (this->expand_abbreviations) + editable_line_t *el = data->active_edit_line(); + if (this->expand_abbreviations && el == &data->command_line) { /* Try expanding abbreviations */ wcstring new_cmdline; - size_t cursor_pos = this->buff_pos - mini(this->buff_pos, cursor_backtrack); - if (reader_expand_abbreviation_in_command(this->command_line, cursor_pos, &new_cmdline)) + size_t cursor_pos = el->position - mini(el->position, cursor_backtrack); + if (reader_expand_abbreviation_in_command(el->text, cursor_pos, &new_cmdline)) { /* We expanded an abbreviation! The cursor moves by the difference in the command line lengths. */ - size_t new_buff_pos = this->buff_pos + new_cmdline.size() - this->command_line.size(); + size_t new_buff_pos = el->position + new_cmdline.size() - el->text.size(); - this->command_line.swap(new_cmdline); - data->buff_pos = new_buff_pos; - data->command_line_changed(); + + el->text.swap(new_cmdline); + update_buff_pos(el, new_buff_pos); + data->command_line_changed(el); result = true; } } @@ -843,9 +907,8 @@ bool reader_thread_job_is_stale() return (void*)(uintptr_t) s_generation_count != pthread_getspecific(generation_count_key); } -void reader_write_title() +void reader_write_title(const wcstring &cmd) { - const wchar_t *title; const env_var_t term_str = env_get_string(L"TERM"); /* @@ -875,27 +938,33 @@ void reader_write_title() { char *n = ttyname(STDIN_FILENO); - if (contains(term, L"linux")) { return; } - if (strstr(n, "tty") || strstr(n, "/vc/")) + if (contains(term, L"dumb")) return; - + if (strstr(n, "tty") || strstr(n, "/vc/")) + return; } - title = function_exists(L"fish_title")?L"fish_title":DEFAULT_TITLE; - - if (wcslen(title) ==0) - return; + wcstring fish_title_command = DEFAULT_TITLE; + if (function_exists(L"fish_title")) + { + fish_title_command = L"fish_title"; + if (! cmd.empty()) + { + fish_title_command.append(L" "); + fish_title_command.append(parse_util_escape_string_with_quote(cmd, L'\0')); + } + } wcstring_list_t lst; proc_push_interactive(0); - if (exec_subshell(title, lst, false /* do not apply exit status */) != -1) + if (exec_subshell(fish_title_command, lst, false /* do not apply exit status */) != -1) { if (! lst.empty()) { @@ -956,7 +1025,7 @@ static void exec_prompt() } /* Write the screen title */ - reader_write_title(); + reader_write_title(L""); } void reader_init() @@ -984,19 +1053,27 @@ void reader_init() // PCA disable VDSUSP (typically control-Y), which is a funny job control // function available only on OS X and BSD systems // This lets us use control-Y for yank instead - #ifdef VDSUSP +#ifdef VDSUSP shell_modes.c_cc[VDSUSP] = _POSIX_VDISABLE; - #endif +#endif #endif } void reader_destroy() { - tcsetattr(0, TCSANOW, &terminal_mode_on_startup); pthread_key_delete(generation_count_key); } +void restore_term_mode() +{ + // Restore the term mode if we own the terminal + // It's important we do this before restore_foreground_process_group, otherwise we won't think we own the terminal + if (getpid() == tcgetpgrp(STDIN_FILENO)) + { + tcsetattr(STDIN_FILENO, TCSANOW, &terminal_mode_on_startup); + } +} void reader_exit(int do_exit, int forced) { @@ -1057,29 +1134,92 @@ void reader_react_to_color_change() } +/* Indicates if the given command char ends paging */ +static bool command_ends_paging(wchar_t c, bool focused_on_search_field) +{ + switch (c) + { + /* These commands always end paging */ + case R_HISTORY_SEARCH_BACKWARD: + case R_HISTORY_SEARCH_FORWARD: + case R_BEGINNING_OF_HISTORY: + case R_END_OF_HISTORY: + case R_HISTORY_TOKEN_SEARCH_BACKWARD: + case R_HISTORY_TOKEN_SEARCH_FORWARD: + case R_ACCEPT_AUTOSUGGESTION: + case R_CANCEL: + return true; + + /* These commands never do */ + 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: + default: + return false; + + /* R_EXECUTE does end paging, but only executes if it was not paging. So it's handled specially */ + case R_EXECUTE: + return false; + + /* These commands operate on the search field if that's where the focus is */ + case R_BEGINNING_OF_LINE: + case R_END_OF_LINE: + case R_FORWARD_WORD: + case R_BACKWARD_WORD: + 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_BACKWARD_KILL_WORD: + case R_BACKWARD_KILL_PATH_COMPONENT: + 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: + return ! focused_on_search_field; + } +} + /** Remove the previous character in the character buffer and on the screen using syntax highlighting, etc. */ static void remove_backward() { + editable_line_t *el = data->active_edit_line(); - if (data->buff_pos <= 0) + if (el->position <= 0) return; /* Fake composed character sequences by continuing to delete until we delete a character of width at least 1. */ int width; do { - data->buff_pos -= 1; - width = fish_wcwidth(data->command_line.at(data->buff_pos)); - data->command_line.erase(data->buff_pos, 1); + update_buff_pos(el, el->position - 1); + width = fish_wcwidth(el->text.at(el->position)); + el->text.erase(el->position, 1); } - while (width == 0 && data->buff_pos > 0); - data->command_line_changed(); + while (width == 0 && el->position > 0); + data->command_line_changed(el); data->suppress_autosuggestion = true; - reader_super_highlight_me_plenty(data->buff_pos); + reader_super_highlight_me_plenty(); reader_repaint(); @@ -1089,26 +1229,51 @@ static void remove_backward() /** Insert the characters of the string into the command line buffer and print them to the screen using syntax highlighting, etc. - Optionally also expand abbreviations. + Optionally also expand abbreviations, after space characters. Returns true if the string changed. */ -static bool insert_string(const wcstring &str, bool should_expand_abbreviations = false) +static bool insert_string(editable_line_t *el, const wcstring &str, bool allow_expand_abbreviations = false) { size_t len = str.size(); if (len == 0) return false; - - data->command_line.insert(data->buff_pos, str); - data->buff_pos += len; - data->command_line_changed(); - data->suppress_autosuggestion = false; - - if (should_expand_abbreviations) - data->expand_abbreviation_as_necessary(1); - - /* Syntax highlight. Note we must have that buff_pos > 0 because we just added something nonzero to its length */ - assert(data->buff_pos > 0); - reader_super_highlight_me_plenty(data->buff_pos-1); + + /* Start inserting. If we are expanding abbreviations, we have to do this after every space (see #1434), so look for spaces. We try to do this efficiently (rather than the simpler character at a time) to avoid expensive work in command_line_changed() */ + size_t cursor = 0; + while (cursor < len) + { + /* Determine the position of the next expansion-triggering char (possibly none), and the end of the range we wish to insert */ + const wchar_t *expansion_triggering_chars = L" ;|&^><"; + size_t char_triggering_expansion_pos = allow_expand_abbreviations ? str.find_first_of(expansion_triggering_chars, cursor) : wcstring::npos; + bool has_expansion_triggering_char = (char_triggering_expansion_pos != wcstring::npos); + size_t range_end = (has_expansion_triggering_char ? char_triggering_expansion_pos + 1 : len); + + /* Insert from the cursor up to but not including the range end */ + assert(range_end > cursor); + el->insert_string(str, cursor, range_end - cursor); + + update_buff_pos(el, el->position); + data->command_line_changed(el); + + /* If we got an expansion trigger, then the last character we inserted was it (i.e. was a space). Expand abbreviations. */ + if (has_expansion_triggering_char && allow_expand_abbreviations) + { + assert(range_end > 0); + assert(wcschr(expansion_triggering_chars, str.at(range_end - 1))); + data->expand_abbreviation_as_necessary(1); + } + cursor = range_end; + } + + if (el == &data->command_line) + { + data->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); + reader_super_highlight_me_plenty(-1); + } + reader_repaint(); return true; @@ -1118,9 +1283,9 @@ static bool insert_string(const wcstring &str, bool should_expand_abbreviations Insert the character into the command line buffer and print it to the screen using syntax highlighting, etc. */ -static bool insert_char(wchar_t c, bool should_expand_abbreviations = false) +static bool insert_char(editable_line_t *el, wchar_t c, bool allow_expand_abbreviations = false) { - return insert_string(wcstring(1, c), should_expand_abbreviations); + return insert_string(el, wcstring(1, c), allow_expand_abbreviations); } @@ -1149,7 +1314,6 @@ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flag { size_t move_cursor; const wchar_t *begin, *end; - wchar_t *escaped; const wchar_t *buff = command_line.c_str(); parse_util_token_extent(buff, cursor_pos, &begin, 0, 0, 0); @@ -1161,10 +1325,9 @@ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flag { /* Respect COMPLETE_DONT_ESCAPE_TILDES */ bool no_tilde = !!(flags & COMPLETE_DONT_ESCAPE_TILDES); - escaped = escape(val, ESCAPE_ALL | ESCAPE_NO_QUOTED | (no_tilde ? ESCAPE_NO_TILDE : 0)); + wcstring escaped = escape(val, ESCAPE_ALL | ESCAPE_NO_QUOTED | (no_tilde ? ESCAPE_NO_TILDE : 0)); sb.append(escaped); - move_cursor = wcslen(escaped); - free(escaped); + move_cursor = escaped.size(); } else { @@ -1249,150 +1412,15 @@ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flag */ static void completion_insert(const wchar_t *val, complete_flags_t flags) { - size_t cursor = data->buff_pos; - wcstring new_command_line = completion_apply_to_command_line(val, flags, data->command_line, &cursor, false /* not append only */); - reader_set_buffer(new_command_line, cursor); + editable_line_t *el = data->active_edit_line(); + size_t cursor = el->position; + wcstring new_command_line = completion_apply_to_command_line(val, flags, el->text, &cursor, false /* not append only */); + reader_set_buffer_maintaining_pager(new_command_line, cursor); /* Since we just inserted a completion, don't immediately do a new autosuggestion */ data->suppress_autosuggestion = true; } -/* Return an escaped path to fish_pager */ -static wcstring escaped_fish_pager_path(void) -{ - wcstring result; - const env_var_t bin_dir = env_get_string(L"__fish_bin_dir"); - if (bin_dir.missing_or_empty()) - { - /* This isn't good, hope our normal command stuff can find it */ - result = L"fish_pager"; - } - else - { - result = escape_string(bin_dir + L"/fish_pager", ESCAPE_ALL); - } - return result; -} - -/** - Run the fish_pager command to display the completion list. If the - fish_pager outputs any text, it is inserted into the input - backbuffer. - - \param prefix the string to display before every completion. - \param is_quoted should be set if the argument is quoted. This will change the display style. - \param comp the list of completions to display -*/ -static void run_pager(const wcstring &prefix, int is_quoted, const std::vector<completion_t> &comp) -{ - wcstring msg; - wcstring prefix_esc; - char *foo; - - shared_ptr<io_buffer_t> in_buff(io_buffer_t::create(true, 3)); - shared_ptr<io_buffer_t> out_buff(io_buffer_t::create(false, 4)); - - // The above may fail e.g. if we have too many open fds - if (in_buff.get() == NULL || out_buff.get() == NULL) - return; - - wchar_t *escaped_separator; - - if (prefix.empty()) - { - prefix_esc = L"\"\""; - } - else - { - prefix_esc = escape_string(prefix, 1); - } - - - const wcstring pager_path = escaped_fish_pager_path(); - const wcstring cmd = format_string(L"%ls -c 3 -r 4 %ls -p %ls", - // L"valgrind --track-fds=yes --log-file=pager.txt --leak-check=full ./%ls %d %ls", - pager_path.c_str(), - is_quoted?L"-q":L"", - prefix_esc.c_str()); - - escaped_separator = escape(COMPLETE_SEP_STR, 1); - - for (size_t i=0; i< comp.size(); i++) - { - long base_len=-1; - const completion_t &el = comp.at(i); - - wcstring completion_text; - wcstring description_text; - - // Note that an empty completion is perfectly sensible here, e.g. tab-completing 'foo' with a file called 'foo' and another called 'foobar' - if ((el.flags & COMPLETE_REPLACES_TOKEN) && match_type_shares_prefix(el.match.type)) - { - // Compute base_len if we have not yet - if (base_len == -1) - { - const wchar_t *begin, *buff = data->command_line.c_str(); - - parse_util_token_extent(buff, data->buff_pos, &begin, 0, 0, 0); - base_len = data->buff_pos - (begin-buff); - } - - completion_text = escape_string(el.completion.c_str() + base_len, ESCAPE_ALL | ESCAPE_NO_QUOTED); - } - else - { - completion_text = escape_string(el.completion, ESCAPE_ALL | ESCAPE_NO_QUOTED); - } - - - if (! el.description.empty()) - { - description_text = escape_string(el.description, true); - } - - /* It's possible (even common) to have an empty completion with no description. An example would be completing 'foo' with extant files 'foo' and 'foobar'. But fish_pager ignores blank lines. So if our completion text is empty, always include a description, even if it's empty. - */ - msg.reserve(msg.size() + completion_text.size() + description_text.size() + 2); - msg.append(completion_text); - if (! description_text.empty() || completion_text.empty()) - { - msg.append(escaped_separator); - msg.append(description_text); - } - msg.push_back(L'\n'); - } - - free(escaped_separator); - - foo = wcs2str(msg.c_str()); - in_buff->out_buffer_append(foo, strlen(foo)); - free(foo); - - term_donate(); - parser_t &parser = parser_t::principal_parser(); - io_chain_t io_chain; - io_chain.push_back(out_buff); - io_chain.push_back(in_buff); - parser.eval(cmd, io_chain, TOP); - term_steal(); - - out_buff->read(); - - const char zero = 0; - out_buff->out_buffer_append(&zero, 1); - - const char *out_data = out_buff->out_buffer_ptr(); - if (out_data) - { - const wcstring str = str2wcstring(out_data); - size_t idx = str.size(); - while (idx--) - { - input_unreadch(str.at(idx)); - } - } -} - struct autosuggestion_context_t { wcstring search_string; @@ -1402,21 +1430,16 @@ struct autosuggestion_context_t file_detection_context_t detector; const wcstring working_directory; const env_vars_snapshot_t vars; - wcstring_list_t commands_to_load; const unsigned int generation_count; - // don't reload more than once - bool has_tried_reloading; - autosuggestion_context_t(history_t *history, const wcstring &term, size_t pos) : search_string(term), cursor_pos(pos), searcher(*history, term, HISTORY_SEARCH_TYPE_PREFIX), - detector(history, term), + detector(history), working_directory(env_get_pwd_slash()), vars(env_vars_snapshot_t::highlighting_keys), - generation_count(s_generation_count), - has_tried_reloading(false) + generation_count(s_generation_count) { } @@ -1486,12 +1509,12 @@ struct autosuggestion_context_t /* Try normal completions */ std::vector<completion_t> completions; - complete(search_string, completions, COMPLETION_REQUEST_AUTOSUGGESTION, &this->commands_to_load); + complete(search_string, completions, COMPLETION_REQUEST_AUTOSUGGESTION); if (! completions.empty()) { const completion_t &comp = completions.at(0); size_t cursor = this->cursor_pos; - this->autosuggestion = completion_apply_to_command_line(comp.completion.c_str(), comp.flags, this->search_string, &cursor, true /* append only */); + this->autosuggestion = completion_apply_to_command_line(comp.completion, comp.flags, this->search_string, &cursor, true /* append only */); return 1; } @@ -1507,34 +1530,19 @@ static int threaded_autosuggest(autosuggestion_context_t *ctx) static bool can_autosuggest(void) { /* We autosuggest if suppress_autosuggestion is not set, if we're not doing a history search, and our command line contains a non-whitespace character. */ + const editable_line_t *el = data->active_edit_line(); const wchar_t *whitespace = L" \t\r\n\v"; return ! data->suppress_autosuggestion && data->history_search.is_at_end() && - data->command_line.find_first_not_of(whitespace) != wcstring::npos; + el == &data->command_line && + el->text.find_first_not_of(whitespace) != wcstring::npos; } static void autosuggest_completed(autosuggestion_context_t *ctx, int result) { - - /* Extract the commands to load */ - wcstring_list_t commands_to_load; - ctx->commands_to_load.swap(commands_to_load); - - /* If we have autosuggestions to load, load them and try again */ - if (! result && ! commands_to_load.empty() && ! ctx->has_tried_reloading) - { - ctx->has_tried_reloading = true; - for (wcstring_list_t::const_iterator iter = commands_to_load.begin(); iter != commands_to_load.end(); ++iter) - { - complete_load(*iter, false); - } - iothread_perform(threaded_autosuggest, autosuggest_completed, ctx); - return; - } - if (result && can_autosuggest() && - ctx->search_string == data->command_line && + ctx->search_string == data->command_line.text && string_prefixes_string_case_insensitive(ctx->search_string, ctx->autosuggestion)) { /* Autosuggestion is active and the search term has not changed, so we're good to go */ @@ -1552,7 +1560,8 @@ static void update_autosuggestion(void) data->autosuggestion.clear(); if (data->allow_autosuggestion && ! data->suppress_autosuggestion && ! data->command_line.empty() && data->history_search.is_at_end()) { - autosuggestion_context_t *ctx = new autosuggestion_context_t(data->history, data->command_line, data->buff_pos); + const editable_line_t *el = data->active_edit_line(); + autosuggestion_context_t *ctx = new autosuggestion_context_t(data->history, el->text, el->position); iothread_perform(threaded_autosuggest, autosuggest_completed, ctx); } } @@ -1562,11 +1571,14 @@ static void accept_autosuggestion(bool full) { if (! data->autosuggestion.empty()) { + /* Accepting an autosuggestion clears the pager */ + clear_pager(); + /* Accept the autosuggestion */ if (full) { /* Just take the whole thing */ - data->command_line = data->autosuggestion; + data->command_line.text = data->autosuggestion; } else { @@ -1577,16 +1589,37 @@ static void accept_autosuggestion(bool full) wchar_t wc = data->autosuggestion.at(idx); if (! state.consume_char(wc)) break; - data->command_line.push_back(wc); + data->command_line.text.push_back(wc); } } - data->buff_pos = data->command_line.size(); - data->command_line_changed(); - reader_super_highlight_me_plenty(data->buff_pos); + update_buff_pos(&data->command_line, data->command_line.size()); + data->command_line_changed(&data->command_line); + reader_super_highlight_me_plenty(); reader_repaint(); } } +/* Ensure we have no pager contents */ +static void clear_pager() +{ + if (data) + { + data->pager.clear(); + data->current_page_rendering = page_rendering_t(); + reader_repaint_needed(); + } +} + +static void select_completion_in_direction(enum selection_direction_t dir) +{ + assert(data != NULL); + bool selection_changed = data->pager.select_next_completion_in_direction(dir, data->current_page_rendering); + if (selection_changed) + { + data->pager_selection_changed(); + } +} + /** Flash the screen. This function only changed the color of the current line, since the flash_screen sequnce is rather painful to @@ -1596,9 +1629,10 @@ static void reader_flash() { struct timespec pollint; - for (size_t i=0; i<data->buff_pos; i++) + editable_line_t *el = &data->command_line; + for (size_t i=0; i<el->position; i++) { - data->colors.at(i) = HIGHLIGHT_SEARCH_MATCH<<16; + data->colors.at(i) = highlight_spec_search_match<<16; } reader_repaint(); @@ -1607,7 +1641,7 @@ static void reader_flash() pollint.tv_nsec = 100 * 1000000; nanosleep(&pollint, NULL); - reader_super_highlight_me_plenty(data->buff_pos); + reader_super_highlight_me_plenty(); reader_repaint(); } @@ -1705,40 +1739,6 @@ static void prioritize_completions(std::vector<completion_t> &comp) sort(comp.begin(), comp.end(), compare_completions_by_match_type); } -/* Given a list of completions, get the completion at an index past *inout_idx, and then increment it. inout_idx should be initialized to (size_t)(-1) for the first call. */ -static const completion_t *cycle_competions(const std::vector<completion_t> &comp, const wcstring &command_line, size_t *inout_idx) -{ - const size_t size = comp.size(); - if (size == 0) - return NULL; - - // note start_idx will be set to -1 initially, so that when it gets incremented we start at 0 - const size_t start_idx = *inout_idx; - size_t idx = start_idx; - - const completion_t *result = NULL; - size_t remaining = comp.size(); - while (remaining--) - { - /* Bump the index */ - idx = (idx + 1) % size; - - /* Get the completion */ - const completion_t &c = comp.at(idx); - - /* Try this completion */ - if (!(c.flags & COMPLETE_REPLACES_TOKEN) || reader_can_replace(command_line, c.flags)) - { - /* Success */ - result = &c; - break; - } - } - - *inout_idx = idx; - return result; -} - /** Handle the list of completions. This means the following: @@ -1755,18 +1755,20 @@ static const completion_t *cycle_competions(const std::vector<completion_t> &com completions. \param comp the list of completion strings + \param continue_after_prefix_insertion If we have a shared prefix, whether to print the list of completions after inserting it. Return true if we inserted text into the command line, false if we did not. */ -static bool handle_completions(const std::vector<completion_t> &comp) +static bool handle_completions(const std::vector<completion_t> &comp, bool continue_after_prefix_insertion) { bool done = false; bool success = false; - const wchar_t *begin, *end, *buff = data->command_line.c_str(); + const editable_line_t *el = &data->command_line; + const wchar_t *begin, *end, *buff = el->text.c_str(); - parse_util_token_extent(buff, data->buff_pos, &begin, 0, 0, 0); - end = buff+data->buff_pos; + parse_util_token_extent(buff, el->position, &begin, 0, 0, 0); + end = buff+el->position; const wcstring tok(begin, end - begin); @@ -1886,8 +1888,12 @@ static bool handle_completions(const std::vector<completion_t> &comp) break; } } + + /* Determine if we use the prefix. We use it if it's non-empty and it will actually make the command line longer. It may make the command line longer by virtue of not using REPLACE_TOKEN (so it always appends to the command line), or by virtue of replacing the token but being longer than it. */ + bool use_prefix = common_prefix.size() > (will_replace_token ? tok.size() : 0); + assert(! use_prefix || ! common_prefix.empty()); - if (! common_prefix.empty()) + if (use_prefix) { /* We got something. If more than one completion contributed, then it means we have a prefix; don't insert a space after it */ if (prefix_is_partial_completion) @@ -1895,15 +1901,16 @@ static bool handle_completions(const std::vector<completion_t> &comp) completion_insert(common_prefix.c_str(), flags); success = true; } - else + + if (continue_after_prefix_insertion || ! use_prefix) { - /* We didn't get a common prefix. Print the list. */ + /* We didn't get a common prefix, or we want to print the list anyways. */ size_t len, prefix_start = 0; wcstring prefix; - parse_util_get_parameter_info(data->command_line, data->buff_pos, NULL, &prefix_start, NULL); + parse_util_get_parameter_info(el->text, el->position, NULL, &prefix_start, NULL); - assert(data->buff_pos >= prefix_start); - len = data->buff_pos - prefix_start; + assert(el->position >= prefix_start); + len = el->position - prefix_start; if (match_type_requires_full_replacement(best_match_type)) { @@ -1912,32 +1919,30 @@ static bool handle_completions(const std::vector<completion_t> &comp) } else if (len <= PREFIX_MAX_LEN) { - prefix.append(data->command_line, prefix_start, len); + prefix.append(el->text, prefix_start, len); } else { // append just the end of the string prefix = wcstring(&ellipsis_char, 1); - prefix.append(data->command_line, prefix_start + len - PREFIX_MAX_LEN, PREFIX_MAX_LEN); + prefix.append(el->text, prefix_start + len - PREFIX_MAX_LEN, PREFIX_MAX_LEN); } - { - int is_quoted; + wchar_t quote; + parse_util_get_parameter_info(el->text, el->position, "e, NULL, NULL); - wchar_t quote; - parse_util_get_parameter_info(data->command_line, data->buff_pos, "e, NULL, NULL); - is_quoted = (quote != L'\0'); + /* Update the pager data */ + data->pager.set_prefix(prefix); + data->pager.set_completions(surviving_completions); - /* Clear the autosuggestion from the old commandline before abandoning it (see #561) */ - if (! data->autosuggestion.empty()) - reader_repaint_without_autosuggestion(); + /* Invalidate our rendering */ + data->current_page_rendering = page_rendering_t(); - write_loop(1, "\n", 1); + /* Modify the command line to reflect the new pager */ + data->pager_selection_changed(); + + reader_repaint_needed(); - run_pager(prefix, is_quoted, surviving_completions); - } - s_reset(&data->screen, screen_reset_abandon_line); - reader_repaint(); success = false; } } @@ -2143,18 +2148,16 @@ static void reader_interactive_destroy() void reader_sanity_check() { - if (get_is_interactive()) + /* Note: 'data' is non-null if we are interactive, except in the testing environment */ + if (get_is_interactive() && data != NULL) { - if (!data) - sanity_lose(); - - if (!(data->buff_pos <= data->command_length())) + if (data->command_line.position > data->command_line.size()) sanity_lose(); - if (data->colors.size() != data->command_length()) + if (data->colors.size() != data->command_line.size()) sanity_lose(); - if (data->indents.size() != data->command_length()) + if (data->indents.size() != data->command_line.size()) sanity_lose(); } @@ -2163,13 +2166,13 @@ void reader_sanity_check() /** Set the specified string as the current buffer. */ -static void set_command_line_and_position(const wcstring &new_str, size_t pos) +static void set_command_line_and_position(editable_line_t *el, const wcstring &new_str, size_t pos) { - data->command_line = new_str; - data->command_line_changed(); - data->buff_pos = pos; - reader_super_highlight_me_plenty(data->buff_pos); - reader_repaint(); + el->text = new_str; + update_buff_pos(el, pos); + data->command_line_changed(el); + reader_super_highlight_me_plenty(); + reader_repaint_needed(); } static void reader_replace_current_token(const wchar_t *new_token) @@ -2179,8 +2182,9 @@ static void reader_replace_current_token(const wchar_t *new_token) size_t new_pos; /* Find current token */ - const wchar_t *buff = data->command_line.c_str(); - parse_util_token_extent(buff, data->buff_pos, &begin, &end, 0, 0); + editable_line_t *el = data->active_edit_line(); + const wchar_t *buff = el->text.c_str(); + parse_util_token_extent(buff, el->position, &begin, &end, 0, 0); if (!begin || !end) return; @@ -2191,7 +2195,7 @@ static void reader_replace_current_token(const wchar_t *new_token) new_buff.append(end); new_pos = (begin-buff) + wcslen(new_token); - set_command_line_and_position(new_buff, new_pos); + set_command_line_and_position(el, new_buff, new_pos); } @@ -2200,9 +2204,10 @@ static void reader_replace_current_token(const wchar_t *new_token) */ static void reset_token_history() { + const editable_line_t *el = data->active_edit_line(); const wchar_t *begin, *end; - const wchar_t *buff = data->command_line.c_str(); - parse_util_token_extent((wchar_t *)buff, data->buff_pos, &begin, &end, 0, 0); + const wchar_t *buff = el->text.c_str(); + parse_util_token_extent((wchar_t *)buff, el->position, &begin, &end, 0, 0); data->search_buff.clear(); if (begin) @@ -2263,7 +2268,7 @@ static void handle_token_history(int forward, int reset) } reader_replace_current_token(str); - reader_super_highlight_me_plenty(data->buff_pos); + reader_super_highlight_me_plenty(); reader_repaint(); } else @@ -2277,7 +2282,6 @@ static void handle_token_history(int forward, int reset) */ if (data->history_search.go_backwards()) { - wcstring item = data->history_search.current_string(); data->token_history_buff = data->history_search.current_string(); } current_pos = data->token_history_buff.size(); @@ -2332,7 +2336,7 @@ static void handle_token_history(int forward, int reset) } } break; - + default: { break; @@ -2345,7 +2349,7 @@ static void handle_token_history(int forward, int reset) if (str) { reader_replace_current_token(str); - reader_super_highlight_me_plenty(data->buff_pos); + reader_super_highlight_me_plenty(); reader_repaint(); data->search_pos = data->search_prev.size(); data->search_prev.push_back(str); @@ -2373,19 +2377,19 @@ enum move_word_dir_t MOVE_DIR_RIGHT }; -static void move_word(bool move_right, bool erase, enum move_word_style_t style, bool newv) +static void move_word(editable_line_t *el, bool move_right, bool erase, enum move_word_style_t style, bool newv) { /* Return if we are already at the edge */ - const size_t boundary = move_right ? data->command_length() : 0; - if (data->buff_pos == boundary) + const size_t boundary = move_right ? el->size() : 0; + if (el->position == boundary) return; /* When moving left, a value of 1 means the character at index 0. */ move_word_state_machine_t state(style); - const wchar_t * const command_line = data->command_line.c_str(); - const size_t start_buff_pos = data->buff_pos; + const wchar_t * const command_line = el->text.c_str(); + const size_t start_buff_pos = el->position; - size_t buff_pos = data->buff_pos; + size_t buff_pos = el->position; while (buff_pos != boundary) { size_t idx = (move_right ? buff_pos : buff_pos - 1); @@ -2399,24 +2403,27 @@ static void move_word(bool move_right, bool erase, enum move_word_style_t style, if (buff_pos == start_buff_pos) buff_pos = (move_right ? buff_pos + 1 : buff_pos - 1); - /* If we are moving left, buff_pos-1 is the index of the first character we do not delete (possibly -1). If we are moving right, then buff_pos is that index - possibly data->command_length(). */ + /* If we are moving left, buff_pos-1 is the index of the first character we do not delete (possibly -1). If we are moving right, then buff_pos is that index - possibly el->size(). */ if (erase) { /* Don't autosuggest after a kill */ - data->suppress_autosuggestion = true; + if (el == &data->command_line) + { + data->suppress_autosuggestion = true; + } if (move_right) { - reader_kill(start_buff_pos, buff_pos - start_buff_pos, KILL_APPEND, newv); + reader_kill(el, start_buff_pos, buff_pos - start_buff_pos, KILL_APPEND, newv); } else { - reader_kill(buff_pos, start_buff_pos - buff_pos, KILL_PREPEND, newv); + reader_kill(el, buff_pos, start_buff_pos - buff_pos, KILL_PREPEND, newv); } } else { - data->buff_pos = buff_pos; + update_buff_pos(el, buff_pos); reader_repaint(); } @@ -2425,7 +2432,7 @@ static void move_word(bool move_right, bool erase, enum move_word_style_t style, const wchar_t *reader_get_buffer(void) { ASSERT_IS_MAIN_THREAD(); - return data?data->command_line.c_str():NULL; + return data ? data->command_line.text.c_str() : NULL; } history_t *reader_get_history(void) @@ -2434,39 +2441,61 @@ history_t *reader_get_history(void) return data ? data->history : NULL; } -void reader_set_buffer(const wcstring &b, size_t pos) +/* Sets the command line contents, without clearing the pager */ +static void reader_set_buffer_maintaining_pager(const wcstring &b, size_t pos) { - if (!data) - return; - /* Callers like to pass us pointers into ourselves, so be careful! I don't know if we can use operator= with a pointer to our interior, so use an intermediate. */ size_t command_line_len = b.size(); - data->command_line = b; - data->command_line_changed(); + data->command_line.text = b; + data->command_line_changed(&data->command_line); /* Don't set a position past the command line length */ if (pos > command_line_len) pos = command_line_len; - data->buff_pos = pos; + update_buff_pos(&data->command_line, pos); + /* Clear history search and pager contents */ data->search_mode = NO_SEARCH; data->search_buff.clear(); data->history_search.go_to_end(); - reader_super_highlight_me_plenty(data->buff_pos); + reader_super_highlight_me_plenty(); reader_repaint_needed(); } +/* Sets the command line contents, clearing the pager */ +void reader_set_buffer(const wcstring &b, size_t pos) +{ + if (!data) + return; + + clear_pager(); + reader_set_buffer_maintaining_pager(b, pos); +} + size_t reader_get_cursor_pos() { if (!data) return (size_t)(-1); - return data->buff_pos; + return data->command_line.position; } +bool reader_get_selection(size_t *start, size_t *len) +{ + bool result = false; + if (data != NULL && data->sel_active) + { + *start = data->sel_start_pos; + *len = std::min(data->sel_stop_pos - data->sel_start_pos + 1, data->command_line.size()); + result = true; + } + return result; +} + + #define ENV_CMD_DURATION L"CMD_DURATION" void set_env_cmd_duration(struct timeval *after, struct timeval *before) @@ -2481,34 +2510,8 @@ void set_env_cmd_duration(struct timeval *after, struct timeval *before) secs -= 1; } - if (secs < 1) - { - env_remove(ENV_CMD_DURATION, 0); - } - else - { - if (secs < 10) // 10 secs - { - swprintf(buf, 16, L"%lu.%02us", secs, usecs / 10000); - } - else if (secs < 60) // 1 min - { - swprintf(buf, 16, L"%lu.%01us", secs, usecs / 100000); - } - else if (secs < 600) // 10 mins - { - swprintf(buf, 16, L"%lum %lu.%01us", secs / 60, secs % 60, usecs / 100000); - } - else if (secs < 5400) // 1.5 hours - { - swprintf(buf, 16, L"%lum %lus", secs / 60, secs % 60); - } - else - { - swprintf(buf, 16, L"%.1fh", secs / 3600.0); - } - env_set(ENV_CMD_DURATION, buf, ENV_EXPORT); - } + swprintf(buf, 16, L"%d", (secs * 1000) + (usecs / 1000)); + env_set(ENV_CMD_DURATION, buf, ENV_UNEXPORT); } void reader_run_command(parser_t &parser, const wcstring &cmd) @@ -2521,7 +2524,7 @@ void reader_run_command(parser_t &parser, const wcstring &cmd) if (! ft.empty()) env_set(L"_", ft.c_str(), ENV_GLOBAL); - reader_write_title(); + reader_write_title(cmd); term_donate(); @@ -2547,28 +2550,26 @@ void reader_run_command(parser_t &parser, const wcstring &cmd) int reader_shell_test(const wchar_t *b) { - int res = parser_t::principal_parser().test(b); + assert(b != NULL); + wcstring bstr = b; - if (res & PARSER_TEST_ERROR) - { - wcstring sb; + /* Append a newline, to act as a statement terminator */ + bstr.push_back(L'\n'); - const int tmp[1] = {0}; - const int tmp2[1] = {0}; - const wcstring empty; - - s_write(&data->screen, - empty, - empty, - empty, - 0, - tmp, - tmp2, - 0); + parse_error_list_t errors; + int res = parse_util_detect_errors(bstr, &errors, true /* do accept incomplete */); + if (res & PARSER_TEST_ERROR) + { + wcstring error_desc; + parser_t::principal_parser().get_backtrace(bstr, errors, &error_desc); - parser_t::principal_parser().test(b, NULL, &sb, L"fish"); - fwprintf(stderr, L"%ls", sb.c_str()); + // ensure we end with a newline. Also add an initial newline, because it's likely the user just hit enter and so there's junk on the current line + if (! string_suffixes_string(L"\n", error_desc)) + { + error_desc.push_back(L'\n'); + } + fwprintf(stderr, L"\n%ls", error_desc.c_str()); } return res; } @@ -2593,7 +2594,7 @@ void reader_push(const wchar_t *name) data=n; - data->command_line_changed(); + data->command_line_changed(&data->command_line); if (data->next == 0) { @@ -2701,7 +2702,7 @@ public: const wcstring string_to_highlight; /** Color buffer */ - std::vector<color_t> colors; + std::vector<highlight_spec_t> colors; /** The position to use for bracket matching */ const size_t match_highlight_pos; @@ -2729,7 +2730,7 @@ public: { } - int threaded_highlight() + int perform_highlight() { if (generation_count != s_generation_count) { @@ -2749,14 +2750,15 @@ static void highlight_search(void) { if (! data->search_buff.empty() && ! data->history_search.is_at_end()) { + const editable_line_t *el = &data->command_line; const wcstring &needle = data->search_buff; - size_t match_pos = data->command_line.find(needle); + 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++) { - data->colors.at(i) |= (HIGHLIGHT_SEARCH_MATCH<<16); + data->colors.at(i) |= (highlight_spec_search_match<<16); } } } @@ -2765,10 +2767,10 @@ static void highlight_search(void) static void highlight_complete(background_highlight_context_t *ctx, int result) { ASSERT_IS_MAIN_THREAD(); - if (ctx->string_to_highlight == data->command_line) + if (ctx->string_to_highlight == data->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(ctx->colors.size() == data->command_length()); + assert(ctx->colors.size() == data->command_line.size()); if (data->colors != ctx->colors) { data->colors.swap(ctx->colors); @@ -2784,7 +2786,7 @@ static void highlight_complete(background_highlight_context_t *ctx, int result) static int threaded_highlight(background_highlight_context_t *ctx) { - return ctx->threaded_highlight(); + return ctx->perform_highlight(); } @@ -2794,19 +2796,36 @@ static int threaded_highlight(background_highlight_context_t *ctx) to avoid repaint issues on terminals where e.g. syntax highlighting maykes characters under the sursor unreadable. - \param match_highlight_pos the position to use for bracket matching. This need not be the same as the surrent cursor position + \param match_highlight_pos_adjust the adjustment to the position to use for bracket matching. This is added to the current cursor position and may be negative. \param error if non-null, any possible errors in the buffer are further descibed by the strings inserted into the specified arraylist + \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. */ -static void reader_super_highlight_me_plenty(size_t match_highlight_pos) +static void reader_super_highlight_me_plenty(int match_highlight_pos_adjust, bool no_io) { + const editable_line_t *el = &data->command_line; + long match_highlight_pos = (long)el->position + match_highlight_pos_adjust; + assert(match_highlight_pos >= 0); + reader_sanity_check(); - background_highlight_context_t *ctx = new background_highlight_context_t(data->command_line, match_highlight_pos, data->highlight_function); - iothread_perform(threaded_highlight, highlight_complete, ctx); + highlight_function_t highlight_func = no_io ? highlight_shell_no_io : data->highlight_function; + background_highlight_context_t *ctx = new background_highlight_context_t(el->text, match_highlight_pos, highlight_func); + if (no_io) + { + // Highlighting without IO, we just do it + // Note that highlight_complete deletes ctx. + int result = ctx->perform_highlight(); + highlight_complete(ctx, result); + } + else + { + // Highlighting including I/O proceeds in the background + iothread_perform(threaded_highlight, highlight_complete, ctx); + } 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 = data->command_line, &suggest = data->autosuggestion; + const wcstring &cmd = el->text, &suggest = data->autosuggestion; if (can_autosuggest() && ! suggest.empty() && string_prefixes_string_case_insensitive(cmd, suggest)) { /* The autosuggestion is still reasonable, so do nothing */ @@ -2818,10 +2837,10 @@ static void reader_super_highlight_me_plenty(size_t match_highlight_pos) } -int exit_status() +bool shell_is_exiting() { if (get_is_interactive()) - return job_list_is_empty() && data->end_loop; + return job_list_is_empty() && data != NULL && data->end_loop; else return end_loop; } @@ -2837,14 +2856,11 @@ static void handle_end_loop() job_t *j; int stopped_jobs_count=0; int is_breakpoint=0; - block_t *b; - parser_t &parser = parser_t::principal_parser(); + const parser_t &parser = parser_t::principal_parser(); - for (b = parser.current_block; - b; - b = b->outer) + for (size_t i = 0; i < parser.block_count(); i++) { - if (b->type() == BREAKPOINT) + if (parser.block_at_index(i)->type() == BREAKPOINT) { is_breakpoint = 1; break; @@ -2893,6 +2909,21 @@ static void handle_end_loop() } } +static bool selection_is_at_top() +{ + const pager_t *pager = &data->pager; + size_t row = pager->get_selected_row(data->current_page_rendering); + if (row != 0 && row != PAGER_SELECTION_NONE) + return false; + + size_t col = pager->get_selected_column(data->current_page_rendering); + if (col != 0 && col != PAGER_SELECTION_NONE) + return false; + + return true; +} + + /** Read interactively. Read input from stdin while providing editing facilities. @@ -2931,7 +2962,7 @@ static int read_i(void) during evaluation. */ - const wchar_t *tmp = reader_readline(); + const wchar_t *tmp = reader_readline(0); if (data->end_loop) { @@ -2939,11 +2970,14 @@ static int read_i(void) } else if (tmp) { - wcstring command = tmp; - data->buff_pos=0; - data->command_line.clear(); - data->command_line_changed(); + const wcstring command = tmp; + update_buff_pos(&data->command_line, 0); + data->command_line.text.clear(); + data->command_line_changed(&data->command_line); + wcstring_list_t argv(1, command); + event_fire_generic(L"fish_preexec", &argv); reader_run_command(parser, command); + event_fire_generic(L"fish_postexec", &argv); if (data->end_loop) { handle_end_loop(); @@ -2977,10 +3011,13 @@ static int can_read(int fd) /** Test if the specified character is in the private use area that fish uses to store internal characters + + Note: Allow U+F8FF because that's the Apple symbol, which is in the + OS X US keyboard layout. */ static int wchar_private(wchar_t c) { - return ((c >= 0xe000) && (c <= 0xf8ff)); + return ((c >= 0xe000) && (c < 0xf8ff)); } /** @@ -3020,7 +3057,7 @@ static wchar_t unescaped_quote(const wcstring &str, size_t pos) } -const wchar_t *reader_readline(void) +const wchar_t *reader_readline(int nchars) { wint_t c; int last_char=0; @@ -3034,19 +3071,16 @@ const wchar_t *reader_readline(void) /* 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; - /* The cycle index in our completion list */ - size_t completion_cycle_idx = (size_t)(-1); - /* The command line before completion */ - wcstring cycle_command_line; - size_t cycle_cursor_pos = 0; + data->cycle_command_line.clear(); + data->cycle_cursor_pos = 0; data->search_buff.clear(); data->search_mode = NO_SEARCH; exec_prompt(); - reader_super_highlight_me_plenty(data->buff_pos); + reader_super_highlight_me_plenty(); s_reset(&data->screen, screen_reset_abandon_line); reader_repaint(); @@ -3063,6 +3097,13 @@ const wchar_t *reader_readline(void) while (!finished && !data->end_loop) { + if (0 < nchars && (size_t)nchars <= data->command_line.size()) + { + // we've already hit the specified character limit + finished = 1; + break; + } + /* Sometimes strange input sequences seem to generate a zero byte. I believe these simply mean a character was pressed @@ -3075,6 +3116,7 @@ const wchar_t *reader_readline(void) is_interactive_read = 1; c=input_readch(); is_interactive_read = was_interactive_read; + //fprintf(stderr, "C: %lx\n", (long)c); if (((!wchar_private(c))) && (c>31) && (c != 127)) { @@ -3082,12 +3124,14 @@ const wchar_t *reader_readline(void) { wchar_t arr[READAHEAD_MAX+1]; - int i; + size_t i; + size_t limit = 0 < nchars ? std::min((size_t)nchars - data->command_line.size(), (size_t)READAHEAD_MAX) + : READAHEAD_MAX; memset(arr, 0, sizeof(arr)); arr[0] = c; - for (i=1; i<READAHEAD_MAX; i++) + for (i = 1; i < limit; ++i) { if (!can_read(0)) @@ -3095,7 +3139,10 @@ const wchar_t *reader_readline(void) c = 0; break; } - c = input_readch(); + // 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 ((!wchar_private(c)) && (c>31) && (c != 127)) { arr[i]=c; @@ -3105,13 +3152,19 @@ const wchar_t *reader_readline(void) break; } - insert_string(arr); + insert_string(&data->command_line, arr, true); } } if (c != 0) break; + + if (0 < nchars && (size_t)nchars <= data->command_line.size()) + { + c = R_NULL; + break; + } } /* If we get something other than a repaint, then stop coalescing them */ @@ -3121,31 +3174,47 @@ const wchar_t *reader_readline(void) if (last_char != R_YANK && last_char != R_YANK_POP) yank_len=0; - const wchar_t *buff = data->command_line.c_str(); - switch (c) + /* Restore the text */ + if (c == R_CANCEL && data->is_navigating_pager_contents()) + { + set_command_line_and_position(&data->command_line, data->cycle_command_line, data->cycle_cursor_pos); + } + + + /* Clear the pager if necessary */ + bool focused_on_search_field = (data->active_edit_line() == &data->pager.search_field_line); + if (command_ends_paging(c, focused_on_search_field)) { + clear_pager(); + } + //fprintf(stderr, "\n\nchar: %ls\n\n", describe_char(c).c_str()); + + switch (c) + { /* go to beginning of line*/ case R_BEGINNING_OF_LINE: { - while ((data->buff_pos>0) && - (buff[data->buff_pos-1] != L'\n')) + editable_line_t *el = data->active_edit_line(); + while (el->position > 0 && el->text.at(el->position - 1) != L'\n') { - data->buff_pos--; + update_buff_pos(el, el->position - 1); } - reader_repaint(); + reader_repaint_needed(); break; } case R_END_OF_LINE: { - if (data->buff_pos < data->command_length()) + editable_line_t *el = data->active_edit_line(); + if (el->position < el->size()) { - while (buff[data->buff_pos] && - buff[data->buff_pos] != L'\n') + const wchar_t *buff = el->text.c_str(); + while (buff[el->position] && + buff[el->position] != L'\n') { - data->buff_pos++; + update_buff_pos(el, el->position + 1); } } else @@ -3153,34 +3222,39 @@ const wchar_t *reader_readline(void) accept_autosuggestion(true); } - reader_repaint(); + reader_repaint_needed(); break; } case R_BEGINNING_OF_BUFFER: { - data->buff_pos = 0; - - reader_repaint(); + update_buff_pos(&data->command_line, 0); + reader_repaint_needed(); break; } /* go to EOL*/ case R_END_OF_BUFFER: { - data->buff_pos = data->command_length(); + update_buff_pos(&data->command_line, data->command_line.size()); - reader_repaint(); + reader_repaint_needed(); break; } case R_NULL: { - reader_repaint_if_needed(); 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) @@ -3203,26 +3277,25 @@ const wchar_t *reader_readline(void) /* complete */ case R_COMPLETE: + case R_COMPLETE_AND_SEARCH: { if (!data->complete_func) break; - if (! comp_empty && last_char == R_COMPLETE) + /* Use the command line only; it doesn't make sense to complete in any other line */ + editable_line_t *el = &data->command_line; + if (data->is_navigating_pager_contents() || (! comp_empty && last_char == R_COMPLETE)) { - /* The user typed R_COMPLETE more than once in a row. Cycle through our available completions */ - const completion_t *next_comp = cycle_competions(comp, cycle_command_line, &completion_cycle_idx); - if (next_comp != NULL) + /* 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 (data->current_page_rendering.remaining_to_disclose > 0) { - size_t cursor_pos = cycle_cursor_pos; - const wcstring new_cmd_line = completion_apply_to_command_line(next_comp->completion, next_comp->flags, cycle_command_line, &cursor_pos, false); - reader_set_buffer(new_cmd_line, cursor_pos); - - /* Since we just inserted a completion, don't immediately do a new autosuggestion */ - data->suppress_autosuggestion = true; - - /* Trigger repaint (see #765) */ - reader_repaint_if_needed(); + data->pager.set_fully_disclosed(true); + reader_repaint_needed(); + } + else + { + select_completion_in_direction(c == R_COMPLETE ? direction_next : direction_prev); } } else @@ -3230,32 +3303,35 @@ const wchar_t *reader_readline(void) /* 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(data->command_line, data->buff_pos)) + 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 = data->command_line.c_str(); + 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, data->buff_pos, &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, data->buff_pos - (cmdsub_begin-buff), &token_begin, &token_end, 0, 0); + 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 (data->buff_pos != end_of_token_offset) + if (el->position != end_of_token_offset) { - data->buff_pos = end_of_token_offset; + update_buff_pos(el, end_of_token_offset); reader_repaint(); } @@ -3263,23 +3339,27 @@ const wchar_t *reader_readline(void) const wcstring buffcpy = wcstring(cmdsub_begin, token_end); //fprintf(stderr, "Complete (%ls)\n", buffcpy.c_str()); - data->complete_func(buffcpy, comp, COMPLETION_REQUEST_DEFAULT | COMPLETION_REQUEST_DESCRIPTIONS | COMPLETION_REQUEST_FUZZY_MATCH, NULL); + data->complete_func(buffcpy, comp, COMPLETION_REQUEST_DEFAULT | COMPLETION_REQUEST_DESCRIPTIONS | COMPLETION_REQUEST_FUZZY_MATCH); /* Munge our completions */ sort_and_make_unique(comp); prioritize_completions(comp); /* Record our cycle_command_line */ - cycle_command_line = data->command_line; - cycle_cursor_pos = data->buff_pos; + data->cycle_command_line = el->text; + data->cycle_cursor_pos = el->position; - comp_empty = handle_completions(comp); + bool continue_after_prefix_insertion = (c == R_COMPLETE_AND_SEARCH); + comp_empty = handle_completions(comp, continue_after_prefix_insertion); - /* Start the cycle at the beginning */ - completion_cycle_idx = (size_t)(-1); + /* Show the search field if requested and if we printed a list of completions */ + if (c == R_COMPLETE_AND_SEARCH && ! comp_empty && ! data->pager.empty()) + { + data->pager.set_search_field_shown(true); + select_completion_in_direction(direction_next); + reader_repaint_needed(); + } - /* Repaint */ - reader_repaint_if_needed(); } break; @@ -3288,8 +3368,9 @@ const wchar_t *reader_readline(void) /* kill */ case R_KILL_LINE: { - const wchar_t *buff = data->command_line.c_str(); - const wchar_t *begin = &buff[data->buff_pos]; + editable_line_t *el = data->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') @@ -3301,7 +3382,7 @@ const wchar_t *reader_readline(void) size_t len = end-begin; if (len) { - reader_kill(begin - buff, len, KILL_APPEND, last_char!=R_KILL_LINE); + reader_kill(el, begin - buff, len, KILL_APPEND, last_char!=R_KILL_LINE); } break; @@ -3309,10 +3390,11 @@ const wchar_t *reader_readline(void) case R_BACKWARD_KILL_LINE: { - if (data->buff_pos > 0) + editable_line_t *el = data->active_edit_line(); + if (el->position > 0) { - const wchar_t *buff = data->command_line.c_str(); - const wchar_t *end = &buff[data->buff_pos]; + const wchar_t *buff = el->text.c_str(); + const wchar_t *end = &buff[el->position]; const wchar_t *begin = end; /* Make sure we delete at least one character (see #580) */ @@ -3330,7 +3412,7 @@ const wchar_t *reader_readline(void) size_t len = maxi<size_t>(end-begin, 1); begin = end - len; - reader_kill(begin - buff, len, KILL_PREPEND, last_char!=R_BACKWARD_KILL_LINE); + reader_kill(el, begin - buff, len, KILL_PREPEND, last_char!=R_BACKWARD_KILL_LINE); } break; @@ -3338,33 +3420,33 @@ const wchar_t *reader_readline(void) case R_KILL_WHOLE_LINE: { - const wchar_t *buff = data->command_line.c_str(); - const wchar_t *end = &buff[data->buff_pos]; - const wchar_t *begin = end; - size_t len; + /* We match the emacs behavior here: "kills the entire line including the following newline" */ + editable_line_t *el = data->active_edit_line(); + const wchar_t *buff = el->text.c_str(); - while (begin > buff && *begin != L'\n') + /* 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--; - - if (*begin == L'\n') - begin++; - - len = maxi<size_t>(end-begin, 0); - begin = end - len; - - while (*end && *end != L'\n') - end++; - - if (begin == end && *end) + } + + /* 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); - len = end-begin; - - if (len) + if (end > begin) { - reader_kill(begin - buff, len, KILL_APPEND, last_char!=R_KILL_WHOLE_LINE); + reader_kill(el, begin, end - begin, KILL_APPEND, last_char!=R_KILL_WHOLE_LINE); } - break; } @@ -3372,7 +3454,7 @@ const wchar_t *reader_readline(void) case R_YANK: { yank_str = kill_yank(); - insert_string(yank_str); + insert_string(data->active_edit_line(), yank_str); yank_len = wcslen(yank_str); break; } @@ -3386,7 +3468,7 @@ const wchar_t *reader_readline(void) remove_backward(); yank_str = kill_yank_rotate(); - insert_string(yank_str); + insert_string(data->active_edit_line(), yank_str); yank_len = wcslen(yank_str); } break; @@ -3403,16 +3485,15 @@ const wchar_t *reader_readline(void) { //history_reset(); data->history_search.go_to_end(); - reader_set_buffer(data->search_buff.c_str(), data->search_buff.size()); + reader_set_buffer(data->search_buff, data->search_buff.size()); } else { reader_replace_current_token(data->search_buff.c_str()); } data->search_buff.clear(); - reader_super_highlight_me_plenty(data->buff_pos); - reader_repaint(); - + reader_super_highlight_me_plenty(); + reader_repaint_needed(); } break; @@ -3432,9 +3513,10 @@ const wchar_t *reader_readline(void) Remove the current character in the character buffer and on the screen using syntax highlighting, etc. */ - if (data->buff_pos < data->command_length()) + editable_line_t *el = data->active_edit_line(); + if (el->position < el->size()) { - data->buff_pos++; + update_buff_pos(el, el->position + 1); remove_backward(); } break; @@ -3450,18 +3532,31 @@ const wchar_t *reader_readline(void) /* Delete any autosuggestion */ data->autosuggestion.clear(); + /* If the user hits return while navigating the pager, it only clears the pager */ + if (data->is_navigating_pager_contents()) + { + clear_pager(); + break; + } + + /* 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 = &data->command_line; + /* Allow backslash-escaped newlines, but only if the following character is whitespace, or we're at the end of the text (see issue #163) */ - if (is_backslashed(data->command_line, data->buff_pos)) + if (is_backslashed(el->text, el->position)) { - if (data->buff_pos >= data->command_length() || iswspace(data->command_line.at(data->buff_pos))) + if (el->position >= el->size() || iswspace(el->text.at(el->position))) { - insert_char('\n'); + insert_char(el, '\n'); break; } } /* See if this command is valid */ - int command_test_result = data->test_func(data->command_line.c_str()); + int command_test_result = data->test_func(el->text.c_str()); 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. */ @@ -3469,8 +3564,11 @@ const wchar_t *reader_readline(void) if (abbreviation_expanded) { /* It's our reponsibility to rehighlight and repaint. But everything we do below triggers a repaint. */ - reader_super_highlight_me_plenty(data->buff_pos); - command_test_result = data->test_func(data->command_line.c_str()); + command_test_result = data->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); + reader_super_highlight_me_plenty(0, skip_io); } } @@ -3480,15 +3578,13 @@ const wchar_t *reader_readline(void) case 0: { /* Finished command, execute it. Don't add items that start with a leading space. */ - if (! data->command_line.empty() && data->command_line.at(0) != L' ') + const editable_line_t *el = &data->command_line; + if (data->history != NULL && ! el->empty() && el->text.at(0) != L' ') { - if (data->history != NULL) - { - data->history->add_with_file_detection(data->command_line); - } + data->history->add_with_file_detection(el->text); } finished=1; - data->buff_pos=data->command_length(); + update_buff_pos(&data->command_line, data->command_line.size()); reader_repaint(); break; } @@ -3498,7 +3594,7 @@ const wchar_t *reader_readline(void) */ case PARSER_TEST_INCOMPLETE: { - insert_char('\n'); + insert_char(el, '\n'); break; } @@ -3510,7 +3606,7 @@ const wchar_t *reader_readline(void) default: { s_reset(&data->screen, screen_reset_abandon_line); - reader_repaint(); + reader_repaint_needed(); break; } @@ -3526,7 +3622,6 @@ const wchar_t *reader_readline(void) case R_HISTORY_TOKEN_SEARCH_FORWARD: { int reset = 0; - if (data->search_mode == NO_SEARCH) { reset = 1; @@ -3540,7 +3635,8 @@ const wchar_t *reader_readline(void) data->search_mode = TOKEN_SEARCH; } - data->search_buff.append(data->command_line); + const editable_line_t *el = &data->command_line; + data->search_buff.append(el->text); data->history_search = history_search_t(*data->history, data->search_buff, HISTORY_SEARCH_TYPE_CONTAINS); /* Skip the autosuggestion as history unless it was truncated */ @@ -3553,7 +3649,6 @@ const wchar_t *reader_readline(void) switch (data->search_mode) { - case LINE_SEARCH: { if ((c == R_HISTORY_SEARCH_BACKWARD) || @@ -3579,7 +3674,7 @@ const wchar_t *reader_readline(void) { new_text = data->history_search.current_string(); } - set_command_line_and_position(new_text, new_text.size()); + set_command_line_and_position(&data->command_line, new_text, new_text.size()); break; } @@ -3607,10 +3702,15 @@ const wchar_t *reader_readline(void) /* Move left*/ case R_BACKWARD_CHAR: { - if (data->buff_pos > 0) + editable_line_t *el = data->active_edit_line(); + if (data->is_navigating_pager_contents()) { - data->buff_pos--; - reader_repaint(); + select_completion_in_direction(direction_west); + } + else if (el->position > 0) + { + update_buff_pos(el, el->position-1); + reader_repaint_needed(); } break; } @@ -3618,10 +3718,15 @@ const wchar_t *reader_readline(void) /* Move right*/ case R_FORWARD_CHAR: { - if (data->buff_pos < data->command_length()) + editable_line_t *el = data->active_edit_line(); + if (data->is_navigating_pager_contents()) { - data->buff_pos++; - reader_repaint(); + select_completion_in_direction(direction_east); + } + else if (el->position < el->size()) + { + update_buff_pos(el, el->position + 1); + reader_repaint_needed(); } else { @@ -3636,30 +3741,31 @@ const wchar_t *reader_readline(void) { move_word_style_t style = (c == R_BACKWARD_KILL_PATH_COMPONENT ? move_word_style_path_components : move_word_style_punctuation); bool newv = (last_char != R_BACKWARD_KILL_WORD && last_char != R_BACKWARD_KILL_PATH_COMPONENT); - move_word(MOVE_DIR_LEFT, true /* erase */, style, newv); + move_word(data->active_edit_line(), MOVE_DIR_LEFT, true /* erase */, style, newv); break; } /* kill one word right */ case R_KILL_WORD: { - move_word(MOVE_DIR_RIGHT, true /* erase */, move_word_style_punctuation, last_char!=R_KILL_WORD); + move_word(data->active_edit_line(), MOVE_DIR_RIGHT, true /* erase */, move_word_style_punctuation, last_char!=R_KILL_WORD); break; } /* move one word left*/ case R_BACKWARD_WORD: { - move_word(MOVE_DIR_LEFT, false /* do not erase */, move_word_style_punctuation, false); + move_word(data->active_edit_line(), MOVE_DIR_LEFT, false /* do not erase */, move_word_style_punctuation, false); break; } /* move one word right*/ case R_FORWARD_WORD: { - if (data->buff_pos < data->command_length()) + editable_line_t *el = data->active_edit_line(); + if (el->position < el->size()) { - move_word(MOVE_DIR_RIGHT, false /* do not erase */, move_word_style_punctuation, false); + move_word(el, MOVE_DIR_RIGHT, false /* do not erase */, move_word_style_punctuation, false); } else { @@ -3670,12 +3776,13 @@ const wchar_t *reader_readline(void) case R_BEGINNING_OF_HISTORY: { - data->history_search = history_search_t(*data->history, data->command_line, HISTORY_SEARCH_TYPE_PREFIX); + const editable_line_t *el = &data->command_line; + data->history_search = history_search_t(*data->history, el->text, HISTORY_SEARCH_TYPE_PREFIX); data->history_search.go_to_beginning(); if (! data->history_search.is_at_end()) { wcstring new_text = data->history_search.current_string(); - set_command_line_and_position(new_text, new_text.size()); + set_command_line_and_position(&data->command_line, new_text, new_text.size()); } break; @@ -3690,38 +3797,71 @@ const wchar_t *reader_readline(void) case R_UP_LINE: case R_DOWN_LINE: { - int line_old = parse_util_get_line_from_offset(data->command_line, data->buff_pos); - int line_new; + if (data->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; + } - if (c == R_UP_LINE) - line_new = line_old-1; + /* Now do the selection */ + select_completion_in_direction(direction); + } + else if (c == R_DOWN_LINE && ! data->pager.empty()) + { + /* We pressed down with a non-empty pager contents, begin navigation */ + select_completion_in_direction(direction_south); + } else - line_new = line_old+1; + { + /* Not navigating the pager contents */ + editable_line_t *el = data->active_edit_line(); + int line_old = parse_util_get_line_from_offset(el->text, el->position); + int line_new; - int line_count = parse_util_lineno(data->command_line.c_str(), data->command_length())-1; + if (c == R_UP_LINE) + line_new = line_old-1; + else + line_new = line_old+1; - if (line_new >= 0 && line_new <= line_count) - { - size_t base_pos_new; - size_t base_pos_old; + 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; + 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(data->command_line, line_new); + base_pos_new = parse_util_get_offset_from_line(el->text, line_new); - base_pos_old = parse_util_get_offset_from_line(data->command_line, line_old); + 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 = data->indents.at(base_pos_old); - indent_new = data->indents.at(base_pos_new); + assert(base_pos_new != (size_t)(-1) && base_pos_old != (size_t)(-1)); + indent_old = data->indents.at(base_pos_old); + indent_new = data->indents.at(base_pos_new); - line_offset_old = data->buff_pos - parse_util_get_offset_from_line(data->command_line, line_old); - total_offset_new = parse_util_get_offset(data->command_line, line_new, line_offset_old - 4*(indent_new-indent_old)); - data->buff_pos = total_offset_new; - reader_repaint(); + 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; @@ -3731,7 +3871,7 @@ const wchar_t *reader_readline(void) { data->suppress_autosuggestion = true; data->autosuggestion.clear(); - reader_repaint(); + reader_repaint_needed(); break; } @@ -3743,41 +3883,46 @@ const wchar_t *reader_readline(void) case R_TRANSPOSE_CHARS: { - if (data->command_length() < 2) + editable_line_t *el = data->active_edit_line(); + if (el->size() < 2) { break; } /* If the cursor is at the end, transpose the last two characters of the line */ - if (data->buff_pos == data->command_length()) + if (el->position == el->size()) { - data->buff_pos--; + 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 (data->buff_pos > 0) + if (el->position > 0) { - wcstring local_cmd = data->command_line; - std::swap(local_cmd.at(data->buff_pos), local_cmd.at(data->buff_pos-1)); - set_command_line_and_position(local_cmd, data->buff_pos + 1); + 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: { - size_t len = data->command_length(); - const wchar_t *buff = data->command_line.c_str(); + editable_line_t *el = data->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 */ - while (data->buff_pos != len && !iswalnum(buff[data->buff_pos])) - data->buff_pos++; + 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, data->buff_pos, &tok_begin, &tok_end, &prev_begin, &prev_end); + 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]) @@ -3802,57 +3947,143 @@ const wchar_t *reader_readline(void) new_buff.append(prev); new_buff.append(trail); /* Put cursor right after the second token */ - set_command_line_and_position(new_buff, tok_end - buff); + 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 = data->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 = data->buff_pos; - move_word(MOVE_DIR_RIGHT, false, move_word_style_punctuation, false); - for (; pos < data->buff_pos; pos++) + 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 = data->command_line.at(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); - - data->command_line.at(pos) = chr; + + data->command_line.text.at(pos) = chr; capitalized_first = capitalized_first || make_uppercase; } - data->command_line_changed(); - reader_super_highlight_me_plenty(data->buff_pos); - reader_repaint(); + data->command_line_changed(el); + reader_super_highlight_me_plenty(); + reader_repaint_needed(); break; } - + + case R_BEGIN_SELECTION: + { + data->sel_active = true; + data->sel_begin_pos = data->command_line.position; + data->sel_start_pos = data->command_line.position; + data->sel_stop_pos = data->command_line.position; + break; + } + + case R_END_SELECTION: + { + data->sel_active = false; + data->sel_start_pos = data->command_line.position; + data->sel_stop_pos = data->command_line.position; + break; + } + + case R_KILL_SELECTION: + { + bool newv = (last_char != R_KILL_SELECTION); + size_t start, len; + if (reader_get_selection(&start, &len)) + { + reader_kill(&data->command_line, start, len, KILL_APPEND, newv); + } + break; + } + + case R_FORWARD_JUMP: + { + editable_line_t *el = data->active_edit_line(); + wchar_t target = input_function_pop_arg(); + bool status = false; + + for (size_t i = el->position + 1; i < el->size(); i++) + { + if (el->at(i) == target) + { + update_buff_pos(el, i); + status = true; + break; + } + } + input_function_set_status(status); + reader_repaint_needed(); + break; + } + + case R_BACKWARD_JUMP: + { + editable_line_t *el = data->active_edit_line(); + wchar_t target = input_function_pop_arg(); + bool status = false; + + size_t tmp_pos = el->position; + while (tmp_pos--) + { + if (el->at(tmp_pos) == target) + { + update_buff_pos(el, tmp_pos); + status = true; + break; + } + } + input_function_set_status(status); + reader_repaint_needed(); + break; + } + /* Other, if a normal character, we add it to the command */ default: { if ((!wchar_private(c)) && (((c>31) || (c==L'\n'))&& (c != 127))) { - /* Expand abbreviations after space */ - bool should_expand_abbreviations = (c == L' '); + bool allow_expand_abbreviations = false; + if (data->is_navigating_pager_contents()) + { + data->pager.set_search_field_shown(true); + } + else + { + allow_expand_abbreviations = true; + } /* Regular character */ - insert_char(c, should_expand_abbreviations); + editable_line_t *el = data->active_edit_line(); + insert_char(data->active_edit_line(), c, allow_expand_abbreviations); + + /* End paging upon inserting into the normal command line */ + if (el == &data->command_line) + { + clear_pager(); + } + + } else { @@ -3881,10 +4112,20 @@ const wchar_t *reader_readline(void) } last_char = c; + + reader_repaint_if_needed(); } writestr(L"\n"); + /* Ensure we have no pager contents when we exit */ + if (! data->pager.empty()) + { + /* Clear to end of screen to erase the pager contents. TODO: this may fail if eos doesn't exist, in which case we should emit newlines */ + screen_force_clear_to_end(); + data->pager.clear(); + } + if (!reader_exit_forced()) { if (tcsetattr(0,TCSANOW,&old_modes)) /* return to previous mode */ @@ -3895,7 +4136,7 @@ const wchar_t *reader_readline(void) set_color(rgb_color_t::reset(), rgb_color_t::reset()); } - return finished ? data->command_line.c_str() : 0; + return finished ? data->command_line.text.c_str() : NULL; } int reader_search_mode() @@ -3908,6 +4149,15 @@ int reader_search_mode() return !!data->search_mode; } +int reader_has_pager_contents() +{ + if (!data) + { + return -1; + } + + return ! data->current_page_rendering.screen_data.empty(); +} /** Read non-interactively. Read input from stdin without displaying @@ -3964,7 +4214,7 @@ static int read_ni(int fd, const io_chain_t &io) acc.insert(acc.end(), buff, buff + c); } - const wcstring str = acc.empty() ? wcstring() : str2wcstring(&acc.at(0), acc.size()); + wcstring str = acc.empty() ? wcstring() : str2wcstring(&acc.at(0), acc.size()); acc.clear(); if (fclose(in_stream)) @@ -3975,13 +4225,21 @@ static int read_ni(int fd, const io_chain_t &io) res = 1; } - wcstring sb; - if (! parser.test(str.c_str(), 0, &sb, L"fish")) + /* Swallow a BOM (#1518) */ + if (! str.empty() && str.at(0) == UTF8_BOM_WCHAR) + { + str.erase(0, 1); + } + + parse_error_list_t errors; + if (! parse_util_detect_errors(str, &errors, false /* do not accept incomplete */)) { parser.eval(str, io, TOP); } else { + wcstring sb; + parser.get_backtrace(str, errors, &sb); fwprintf(stderr, L"%ls", sb.c_str()); res = 1; } |