Add the input function undo which is bound to `\c_` (control + / on
some terminals). Redoing the most recent chain of undos is supported,
redo is bound to `\e/` for now.

Closes #1367.
This approach should not have the issues discussed in #5897.

Every single modification to the commandline can be undone individually,
except for adjacent single-character inserts, which are coalesced,
so they can be reverted with a single undo. Coalescing is not done for
space characters, so each word can be undone separately.

When moving between history search entries, only the current history
search entry is reachable via the undo history. This allows to go back
to the original search string with a single undo, or by pressing the
escape key.
Similarly, when moving between pager entries, only the most recent
selection in the pager can be undone.
This commit is contained in:
Johannes Altmanninger 2020-02-04 12:47:44 +01:00
parent f7edfba5d7
commit 8a033b9f3d
9 changed files with 414 additions and 102 deletions

View file

@ -9,6 +9,7 @@
### Interactive improvements
#### New or improved bindings
- New readline commands `undo` (Ctrl+_) and `redo` (Alt-/) can be used to revert changes to the command line or the pager search field (#6570).
#### Improved prompts
- The default and example prompts print the correct exit status for commands prefixed with `not` (#6566).

View file

@ -69,6 +69,8 @@ function fish_default_key_bindings -d "Default (Emacs-like) key bindings for fis
bind --preset $argv \cf forward-char
bind --preset $argv \cb backward-char
bind --preset $argv \ct transpose-chars
bind --preset $argv \c_ undo
bind --preset $argv \e/ redo
bind --preset $argv \et transpose-words
bind --preset $argv \eu upcase-word

View file

@ -172,6 +172,8 @@ The following special input functions are available:
- ``up-line``, move up one line
- ``undo`` and ``redo``, revert or redo the most recent edits on the command line
- ``upcase-word``, make the current word uppercase
- ``yank``, insert the latest entry of the killring into the buffer

View file

@ -1440,6 +1440,10 @@ Emacs mode commands
- :kbd:`Alt+t` transposes the last two words
- :kbd:`Control+_` (:kbd:`Control+/` on some terminals) undoes the most recent edit of the line
- :kbd:`Alt+/` reverts the most recent undo
You can change these key bindings using the :ref:`bind <cmd-bind>` builtin.

View file

@ -2000,46 +2000,55 @@ static void test_abbreviations() {
if (*mresult != L"bar") err(L"Wrong abbreviation result for foo");
maybe_t<wcstring> result;
result = reader_expand_abbreviation_in_command(L"just a command", 3, vars);
auto expand_abbreviation_in_command = [](const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars) -> maybe_t<wcstring> {
if (auto edit = reader_expand_abbreviation_in_command(cmdline, cursor_pos, vars)) {
wcstring cmdline_expanded = cmdline;
apply_edit(&cmdline_expanded, *edit);
return cmdline_expanded;
}
return none_t();
};
result = expand_abbreviation_in_command(L"just a command", 3, vars);
if (result) err(L"Command wrongly expanded on line %ld", (long)__LINE__);
result = reader_expand_abbreviation_in_command(L"gc somebranch", 0, vars);
result = expand_abbreviation_in_command(L"gc somebranch", 0, vars);
if (!result) err(L"Command not expanded on line %ld", (long)__LINE__);
result = reader_expand_abbreviation_in_command(L"gc somebranch", std::wcslen(L"gc"), vars);
result = expand_abbreviation_in_command(L"gc somebranch", std::wcslen(L"gc"), vars);
if (!result) err(L"gc not expanded");
if (result != L"git checkout somebranch")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
// Space separation.
result = reader_expand_abbreviation_in_command(L"gx somebranch", std::wcslen(L"gc"), vars);
result = expand_abbreviation_in_command(L"gx somebranch", std::wcslen(L"gc"), vars);
if (!result) err(L"gx not expanded");
if (result != L"git checkout somebranch")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
result = reader_expand_abbreviation_in_command(L"echo hi ; gc somebranch",
std::wcslen(L"echo hi ; g"), vars);
result = expand_abbreviation_in_command(L"echo hi ; gc somebranch", std::wcslen(L"echo hi ; g"),
vars);
if (!result) err(L"gc not expanded on line %ld", (long)__LINE__);
if (result != L"echo hi ; git checkout somebranch")
err(L"gc incorrectly expanded on line %ld", (long)__LINE__);
result = reader_expand_abbreviation_in_command(
L"echo (echo (echo (echo (gc ", std::wcslen(L"echo (echo (echo (echo (gc"), vars);
result = expand_abbreviation_in_command(L"echo (echo (echo (echo (gc ",
std::wcslen(L"echo (echo (echo (echo (gc"), vars);
if (!result) err(L"gc not expanded on line %ld", (long)__LINE__);
if (result != L"echo (echo (echo (echo (git checkout ")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
// If commands should be expanded.
result = reader_expand_abbreviation_in_command(L"if gc", std::wcslen(L"if gc"), vars);
result = expand_abbreviation_in_command(L"if gc", std::wcslen(L"if gc"), vars);
if (!result) err(L"gc not expanded on line %ld", (long)__LINE__);
if (result != L"if git checkout")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
// Others should not be.
result = reader_expand_abbreviation_in_command(L"of gc", std::wcslen(L"of gc"), vars);
result = expand_abbreviation_in_command(L"of gc", std::wcslen(L"of gc"), vars);
if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__);
// Others should not be.
result = reader_expand_abbreviation_in_command(L"command gc", std::wcslen(L"command gc"), vars);
result = expand_abbreviation_in_command(L"command gc", std::wcslen(L"command gc"), vars);
if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__);
vars.pop();
@ -3261,6 +3270,64 @@ static void test_line_iterator() {
do_test((lines2 == wcstring_list_t{L"", L"", L"Alpha", L"Beta", L"Gamma", L"", L"Delta"}));
}
static void test_undo() {
say(L"Testing undo/redo setting and restoring text and cursor position.");
editable_line_t line;
do_test(!line.undo()); // nothing to undo
do_test(line.text() == L"");
do_test(line.position() == 0);
line.push_edit(edit_t(0, 0, L"a b c"));
do_test(line.text() == L"a b c");
do_test(line.position() == 5);
line.set_position(2);
line.replace_substring(2, 1, L"B"); // replacement right of cursor
do_test(line.text() == L"a B c");
line.undo();
do_test(line.text() == L"a b c");
do_test(line.position() == 2);
line.redo();
do_test(line.text() == L"a B c");
do_test(line.position() == 3);
do_test(!line.redo()); // nothing to redo
line.erase_substring(0, 2); // deletion left of cursor
do_test(line.text() == L"B c");
do_test(line.position() == 1);
line.undo();
do_test(line.text() == L"a B c");
do_test(line.position() == 3);
line.redo();
do_test(line.text() == L"B c");
do_test(line.position() == 1);
line.replace_substring(0, line.size(), L"a b c"); // replacement left and right of cursor
do_test(line.text() == L"a b c");
do_test(line.position() == 5);
say(L"Testing undoing coalesced edits.");
line.clear();
line.insert_string(L"a");
line.insert_string(L"b");
line.insert_string(L"c");
do_test(line.undo_history.edits.size() == 1);
line.insert_string(L" ");
do_test(line.undo_history.edits.size() == 2);
line.undo();
line.undo();
line.redo();
do_test(line.text() == L"abc");
do_test(line.undo_history.edits.size() == 2);
// This removes the space insertion from the history, bu tdoes not coalesce with the first edit.
line.insert_string(L"d");
do_test(line.undo_history.edits.size() == 2);
line.insert_string(L"e");
do_test(line.text() == L"abcde");
line.undo();
do_test(line.text() == L"abc");
}
#define UVARS_PER_THREAD 8
#define UVARS_TEST_PATH L"test/fish_uvars_test/varsfile.txt"
@ -5580,6 +5647,7 @@ int main(int argc, char **argv) {
if (should_test_function("input")) test_input();
if (should_test_function("io")) test_fd_set();
if (should_test_function("line_iterator")) test_line_iterator();
if (should_test_function("undo")) test_undo();
if (should_test_function("universal")) test_universal();
if (should_test_function("universal")) test_universal_output();
if (should_test_function("universal")) test_universal_parsing();

View file

@ -145,7 +145,10 @@ static const input_function_metadata_t input_function_metadata[] = {
{readline_cmd_t::func_and, L"and"},
{readline_cmd_t::expand_abbr, L"expand-abbr"},
{readline_cmd_t::delete_or_exit, L"delete-or-exit"},
{readline_cmd_t::cancel, L"cancel"}};
{readline_cmd_t::cancel, L"cancel"},
{readline_cmd_t::undo, L"undo"},
{readline_cmd_t::redo, L"redo"},
};
static_assert(sizeof(input_function_metadata) / sizeof(input_function_metadata[0]) ==
input_function_count,

View file

@ -69,6 +69,8 @@ enum class readline_cmd_t {
expand_abbr,
delete_or_exit,
cancel,
undo,
redo,
repeat_jump,
// NOTE: This one has to be last.
reverse_repeat_jump

View file

@ -144,14 +144,122 @@ operation_context_t get_bg_context(const std::shared_ptr<environment_t> &env,
return operation_context_t{nullptr, *env, std::move(cancel_checker)};
}
bool edit_t::operator==(const edit_t &other) const {
return cursor_position_before_edit == other.cursor_position_before_edit &&
offset == other.offset && length == other.length && old == other.old &&
replacement == other.replacement;
}
void undo_history_t::clear() {
edits.clear();
edits_applied = 0;
may_coalesce = false;
}
void apply_edit(wcstring *target, const edit_t &edit) {
target->replace(edit.offset, edit.length, edit.replacement);
}
/// Returns the number of characters left of the cursor that are removed by the
/// deletion in the given edit.
static size_t chars_deleted_left_of_cursor(const edit_t &edit) {
if (edit.cursor_position_before_edit > edit.offset) {
return std::min(edit.length, edit.cursor_position_before_edit - edit.offset);
}
return 0;
}
/// Compute the position of the cursor after the given edit.
static size_t cursor_position_after_edit(const edit_t &edit) {
size_t cursor = edit.cursor_position_before_edit + edit.replacement.size();
size_t removed = chars_deleted_left_of_cursor(edit);
return cursor > removed ? cursor - removed : 0;
}
/// Whether we want to append this string to the previous edit.
static bool want_to_coalesce_insertion_of(const editable_line_t &el, const wcstring &str) {
// The previous edit must support coalescing.
if (!el.undo_history.may_coalesce) return false;
// Only consolidate single character inserts.
if (str.size() != 1) return false;
// Make an undo group after every space.
if (str.at(0) == L' ') return false;
assert(!el.undo_history.edits.empty());
const edit_t &last_edit = el.undo_history.edits.back();
// Don't add to the last edit if it deleted something.
if (last_edit.length != 0) return false;
// Must not have moved the cursor!
if (cursor_position_after_edit(last_edit) != el.position()) return false;
return true;
}
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 = std::min(start, string_length); //!OCLINT(parameter reassignment)
len = std::min(len, string_length - start); //!OCLINT(parameter reassignment)
this->text().insert(this->position(), str, start, len);
this->position() += len;
if (want_to_coalesce_insertion_of(*this, str)) {
edit_t &edit = undo_history.edits.back();
edit.replacement.append(str);
apply_edit(&text_, edit_t(position(), 0, str));
set_position(position() + len);
} else {
push_edit(edit_t(position(), 0, str.substr(start, len)));
}
undo_history.may_coalesce = (str.size() == 1);
}
void editable_line_t::erase_substring(size_t offset, size_t length) {
push_edit(edit_t(offset, length, L""));
undo_history.may_coalesce = false;
}
void editable_line_t::replace_substring(size_t offset, size_t length, wcstring &&replacement) {
push_edit(edit_t(offset, length, replacement));
undo_history.may_coalesce = false;
}
bool editable_line_t::undo() {
if (undo_history.edits_applied == 0) return false; // nothing to undo
const edit_t &edit = undo_history.edits.at(undo_history.edits_applied - 1);
undo_history.edits_applied--;
edit_t inverse = edit_t(edit.offset, edit.replacement.size(), L"");
inverse.replacement = edit.old;
size_t old_position = edit.cursor_position_before_edit;
apply_edit(&text_, std::move(inverse));
set_position(old_position);
undo_history.may_coalesce = false;
return true;
}
void editable_line_t::push_edit(edit_t &&edit) {
bool edit_does_nothing = edit.length == 0 && edit.replacement == L"";
if (edit_does_nothing) return;
if (undo_history.edits_applied != undo_history.edits.size()) {
// After undoing some edits, the user is making a new edit;
// we are about to create a new edit branch.
// Discard all edits that were undone because we only support
// linear undo/redo, they will be unreachable.
undo_history.edits.erase(undo_history.edits.begin() + undo_history.edits_applied,
undo_history.edits.end());
}
edit.cursor_position_before_edit = position();
edit.old = text_.substr(edit.offset, edit.length);
apply_edit(&text_, edit);
set_position(cursor_position_after_edit(edit));
assert(undo_history.edits_applied == undo_history.edits.size());
undo_history.edits_applied++;
undo_history.edits.emplace_back(edit);
}
bool editable_line_t::redo() {
if (undo_history.edits_applied >= undo_history.edits.size()) return false; // nothing to redo
const edit_t &edit = undo_history.edits.at(undo_history.edits_applied);
undo_history.edits_applied++;
apply_edit(&text_, edit);
set_position(cursor_position_after_edit(edit));
undo_history.may_coalesce = false; // Make a new undo group here.
return true;
}
namespace {
@ -342,6 +450,10 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
std::shared_ptr<parser_t> parser_ref;
/// String containing the whole current commandline.
editable_line_t command_line;
/// Whether the most recent modification to the command line was done by either history search
/// 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;
/// String containing the autosuggestion.
wcstring autosuggestion;
/// Current pager.
@ -448,7 +560,7 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
reader_data_t(std::shared_ptr<parser_t> parser, history_t *hist)
: parser_ref(std::move(parser)), inputter(*parser_ref), history(hist) {}
void update_buff_pos(editable_line_t *el, size_t buff_pos);
void update_buff_pos(editable_line_t *el, maybe_t<size_t> new_pos = none_t());
void repaint();
void kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv);
bool insert_string(editable_line_t *el, const wcstring &str);
@ -490,11 +602,12 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
bool cont_after_prefix_insertion);
void sanity_check() const;
void set_command_line_and_position(editable_line_t *el, const wcstring &new_str, size_t pos);
void replace_current_token(const wcstring &new_token);
void set_command_line_and_position(editable_line_t *el, wcstring &&new_str, size_t pos);
void clear_transient_edit();
void replace_current_token(wcstring &&new_token);
void update_command_line_from_history_search();
void set_buffer_maintaining_pager(const wcstring &b, size_t pos);
void remove_backward();
void set_buffer_maintaining_pager(const wcstring &b, size_t pos, bool transient = false);
void delete_char(bool backward = true);
};
/// This variable is set to a signal by the signal handler when ^C is pressed.
@ -597,8 +710,11 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline,
}
/// Update the cursor position.
void reader_data_t::update_buff_pos(editable_line_t *el, size_t buff_pos) {
el->position() = buff_pos;
void reader_data_t::update_buff_pos(editable_line_t *el, maybe_t<size_t> new_pos) {
if (new_pos) {
el->set_position(*new_pos);
}
size_t buff_pos = el->position();
if (el == &command_line && sel_active) {
if (sel_begin_pos <= buff_pos) {
sel_start_pos = sel_begin_pos;
@ -677,17 +793,9 @@ void reader_data_t::kill(editable_line_t *el, size_t begin_idx, size_t length, i
kill_replace(old, kill_item);
}
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 = std::min(el->position() - begin_idx, length);
update_buff_pos(el, el->position() - backtrack);
}
el->text().erase(begin_idx, length);
el->erase_substring(begin_idx, length);
update_buff_pos(el);
command_line_changed(el);
super_highlight_me_plenty();
repaint();
}
@ -735,15 +843,19 @@ void reader_data_t::pager_selection_changed() {
completion_apply_to_command_line(completion->completion, completion->flags,
this->cycle_command_line, &cursor_pos, false);
}
set_buffer_maintaining_pager(new_cmd_line, cursor_pos);
// Only update if something changed, to avoid useless edits in the undo history.
if (new_cmd_line != command_line.text()) {
set_buffer_maintaining_pager(new_cmd_line, cursor_pos, true /* transient */);
}
// Trigger repaint (see issue #765).
reader_repaint_needed();
}
/// Expand abbreviations at the given cursor position. Does NOT inspect 'data'.
maybe_t<wcstring> reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars) {
maybe_t<edit_t> reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars) {
// See if we are at "command position". Get the surrounding command substitution, and get the
// extent of the first token.
const wchar_t *const buff = cmdline.c_str();
@ -791,16 +903,14 @@ maybe_t<wcstring> reader_expand_abbreviation_in_command(const wcstring &cmdline,
}
// Now if we found a command node, expand it.
maybe_t<wcstring> result{};
maybe_t<edit_t> result{};
if (matching_cmd_node) {
const wcstring token = matching_cmd_node.get_source(subcmd);
if (auto abbreviation = expand_abbreviation(token, vars)) {
// There was an abbreviation! Replace the token in the full command. Maintain the
// relative position of the cursor.
wcstring output = cmdline;
source_range_t r = *matching_cmd_node.source_range();
output.replace(subcmd_offset + r.start, r.length, *abbreviation);
result = std::move(output);
result = edit_t(subcmd_offset + r.start, r.length, std::move(*abbreviation));
}
}
return result;
@ -817,14 +927,10 @@ bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) {
// Try expanding abbreviations.
size_t cursor_pos = el->position() - std::min(el->position(), cursor_backtrack);
if (auto new_cmdline =
reader_expand_abbreviation_in_command(el->text(), cursor_pos, vars())) {
// We expanded an abbreviation! The cursor moves by the difference in the command line
// lengths.
size_t new_buff_pos = el->position() + new_cmdline->size() - el->text().size();
el->text() = std::move(*new_cmdline);
update_buff_pos(el, new_buff_pos);
if (auto edit = reader_expand_abbreviation_in_command(el->text(), cursor_pos, vars())) {
el->push_edit(std::move(*edit));
update_buff_pos(el);
el->undo_history.may_coalesce = false;
command_line_changed(el);
result = true;
}
@ -1067,6 +1173,8 @@ static bool command_ends_paging(readline_cmd_t c, bool focused_on_search_field)
case rl::capitalize_word:
case rl::beginning_of_buffer:
case rl::end_of_buffer:
case rl::undo:
case rl::redo:
// These commands operate on the search field if that's where the focus is.
return !focused_on_search_field;
default:
@ -1093,19 +1201,26 @@ static bool command_ends_history_search(readline_cmd_t c) {
/// Remove the previous character in the character buffer and on the screen using syntax
/// highlighting, etc.
void reader_data_t::remove_backward() {
void reader_data_t::delete_char(bool backward) {
editable_line_t *el = active_edit_line();
if (el->position() <= 0) return;
size_t pos = el->position();
if (!backward) {
pos++;
}
size_t pos_end = pos;
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 {
update_buff_pos(el, el->position() - 1);
width = fish_wcwidth(el->text().at(el->position()));
el->text().erase(el->position(), 1);
} while (width == 0 && el->position() > 0);
pos--;
width = fish_wcwidth(el->text().at(pos));
} while (width == 0 && pos > 0);
el->erase_substring(pos, pos_end - pos);
update_buff_pos(el);
command_line_changed(el);
suppress_autosuggestion = true;
@ -1120,7 +1235,7 @@ 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, el->position());
update_buff_pos(el);
command_line_changed(el);
if (el == &command_line) {
@ -1383,17 +1498,20 @@ void reader_data_t::accept_autosuggestion(bool full, move_word_style_t style) {
// Accept the autosuggestion.
if (full) {
// Just take the whole thing.
command_line.text() = autosuggestion;
command_line.replace_substring(0, command_line.size(), std::move(autosuggestion));
} else {
// Accept characters according to the specified style.
move_word_state_machine_t state(style);
for (size_t idx = command_line.size(); idx < autosuggestion.size(); idx++) {
wchar_t wc = autosuggestion.at(idx);
size_t want;
for (want = command_line.size(); want < autosuggestion.size(); want++) {
wchar_t wc = autosuggestion.at(want);
if (!state.consume_char(wc)) break;
command_line.text().push_back(wc);
}
size_t have = command_line.size();
command_line.replace_substring(command_line.size(), 0,
autosuggestion.substr(have, want - have));
}
update_buff_pos(&command_line, command_line.size());
update_buff_pos(&command_line);
command_line_changed(&command_line);
super_highlight_me_plenty();
repaint();
@ -1832,18 +1950,30 @@ void reader_data_t::sanity_check() const {
}
/// Set the specified string as the current buffer.
void reader_data_t::set_command_line_and_position(editable_line_t *el, const wcstring &new_str,
void reader_data_t::set_command_line_and_position(editable_line_t *el, wcstring &&new_str,
size_t pos) {
el->text() = new_str;
el->push_edit(edit_t(0, el->size(), std::move(new_str)));
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();
}
void reader_data_t::replace_current_token(const wcstring &new_token) {
/// Undo the transient edit und update commandline accordingly.
void reader_data_t::clear_transient_edit() {
assert(command_line_has_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;
}
void reader_data_t::replace_current_token(wcstring &&new_token) {
const wchar_t *begin, *end;
size_t new_pos;
// Find current token.
editable_line_t *el = active_edit_line();
@ -1852,24 +1982,32 @@ void reader_data_t::replace_current_token(const wcstring &new_token) {
if (!begin || !end) return;
// Make new string.
wcstring new_buff(buff, begin - buff);
new_buff.append(new_token);
new_buff.append(end);
new_pos = (begin - buff) + new_token.size();
set_command_line_and_position(el, new_buff, new_pos);
size_t offset = begin - buff;
size_t length = end - buff;
el->replace_substring(offset, length, std::move(new_token));
}
/// Apply the history search to the command line.
void reader_data_t::update_command_line_from_history_search() {
wcstring new_text = history_search.is_at_end() ? history_search.search_string()
: history_search.current_result();
if (history_search.by_token()) {
replace_current_token(new_text);
} else if (history_search.by_line() || history_search.by_prefix()) {
set_command_line_and_position(&command_line, new_text, new_text.size());
editable_line_t *el = active_edit_line();
if (command_line_has_transient_edit) {
el->undo();
}
if (history_search.by_token()) {
replace_current_token(std::move(new_text));
} else {
assert((history_search.by_line() || history_search.by_prefix()) &&
"Unknown history search type");
el->replace_substring(0, el->size(), std::move(new_text));
}
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 };
@ -1922,11 +2060,17 @@ void reader_data_t::move_word(editable_line_t *el, bool move_right, bool erase,
}
/// Sets the command line contents, without clearing the pager.
void reader_data_t::set_buffer_maintaining_pager(const wcstring &b, size_t pos) {
void reader_data_t::set_buffer_maintaining_pager(const wcstring &b, size_t pos, bool transient) {
// 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();
command_line.text() = b;
if (transient) {
if (command_line_has_transient_edit) {
command_line.undo();
}
command_line_has_transient_edit = true;
}
command_line.replace_substring(0, command_line.size(), wcstring(b));
command_line_changed(&command_line);
// Don't set a position past the command line length.
@ -2298,7 +2442,7 @@ static int read_i(parser_t &parser) {
} else if (tmp) {
const wcstring command = tmp.acquire();
data->update_buff_pos(&data->command_line, 0);
data->command_line.text().clear();
data->command_line.clear();
data->command_line_changed(&data->command_line);
wcstring_list_t argv(1, command);
event_fire_generic(parser, L"fish_preexec", &argv);
@ -2540,7 +2684,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
// Remove a trailing backslash. This may trigger an extra repaint, but this is
// rare.
if (is_backslashed(el->text(), el->position())) {
remove_backward();
delete_char();
}
// Get the string; we have to do this after removing any trailing backslash.
@ -2685,16 +2829,22 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
}
case rl::yank_pop: {
if (rls.yank_len) {
for (size_t i = 0; i < rls.yank_len; i++) remove_backward();
editable_line_t *el = active_edit_line();
wcstring yank_str = kill_yank_rotate();
insert_string(active_edit_line(), yank_str);
rls.yank_len = yank_str.size();
size_t new_yank_len = yank_str.size();
el->replace_substring(el->position() - rls.yank_len, rls.yank_len,
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;
}
case rl::backward_delete_char: {
remove_backward();
delete_char();
break;
}
case rl::delete_or_exit:
@ -2703,8 +2853,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
// syntax highlighting, etc.
editable_line_t *el = active_edit_line();
if (el->position() < el->size()) {
update_buff_pos(el, el->position() + 1);
remove_backward();
delete_char(false /* backward */);
} else if (c == rl::delete_or_exit && el->empty()) {
reader_set_end_loop(true);
}
@ -3059,7 +3208,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
new_buff.append(prev);
new_buff.append(trail);
// Put cursor right after the second token.
set_command_line_and_position(el, new_buff, tok_end - buff);
set_command_line_and_position(el, std::move(new_buff), tok_end - buff);
}
break;
}
@ -3072,7 +3221,9 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
// We apply the operation from the current location to the end of the word.
size_t pos = el->position();
size_t init_pos = pos;
move_word(el, MOVE_DIR_RIGHT, false, move_word_style_punctuation, false);
wcstring replacement;
for (; pos < el->position(); pos++) {
wchar_t chr = el->text().at(pos);
@ -3090,9 +3241,11 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
else
chr = towlower(chr);
command_line.text().at(pos) = chr;
replacement.push_back(chr);
capitalized_first = capitalized_first || make_uppercase;
}
el->replace_substring(init_pos, pos - init_pos, std::move(replacement));
update_buff_pos(el);
command_line_changed(el);
super_highlight_me_plenty();
reader_repaint_needed();
@ -3191,6 +3344,23 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
inputter.function_set_status(false);
}
break;
}
case rl::undo:
case rl::redo: {
editable_line_t *el = active_edit_line();
bool ok = (c == rl::undo) ? el->undo() : el->redo();
if (ok) {
if (el == &command_line) {
clear_pager();
}
update_buff_pos(el);
command_line_changed(el);
super_highlight_me_plenty();
mark_repaint_needed();
} else {
flash();
}
break;
}
// Some commands should have been handled internally by input_readch().
case rl::self_insert: {
@ -3286,7 +3456,7 @@ maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
if (readline_cmd != rl::repaint) rls.coalescing_repaints = false;
if (readline_cmd == rl::cancel && is_navigating_pager_contents()) {
set_command_line_and_position(&command_line, cycle_command_line, cycle_cursor_pos);
clear_transient_edit();
}
// Clear the pager if necessary.
@ -3301,8 +3471,8 @@ maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
// "cancel" means to abort the whole thing, other ending commands mean to finish the
// search.
if (readline_cmd == rl::cancel) {
history_search.go_to_end();
update_command_line_from_history_search();
// Go back to the search string by simply undoing the history-search edit.
clear_transient_edit();
}
history_search.reset();
}
@ -3320,6 +3490,7 @@ maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
// End paging upon inserting into the normal command line.
if (el == &command_line) {
clear_pager();
command_line_has_transient_edit = false;
}
} else {
// This can happen if the user presses a control char we don't recognize. No

View file

@ -20,39 +20,98 @@ class io_chain_t;
class operation_context_t;
class parser_t;
/// An edit action that can be undone.
struct edit_t {
/// When undoing the edit we use this to restore the previous cursor position.
size_t cursor_position_before_edit = 0;
/// The span of text that is replaced by this edit.
size_t offset, length;
/// The strings that are removed and added by this edit, respectively.
wcstring old, replacement;
explicit edit_t(size_t offset, size_t length, wcstring replacement)
: offset(offset), length(length), replacement(std::move(replacement)) {}
/// Used for testing.
bool operator==(const edit_t &other) const;
};
/// Modify a string according to the given edit.
/// Currently exposed for testing only.
void apply_edit(wcstring *target, const edit_t &edit);
/// The history of all edits to some command line.
struct undo_history_t {
/// The stack of edits that can be undone or redone atomically.
std::vector<edit_t> edits;
/// The position in the undo stack that corresponds to the current
/// state of the input line.
/// Invariants:
/// edits_applied - 1 is the index of the next edit to undo.
/// edits_applied is the index of the next edit to redo.
///
/// For example, if nothing was undone, edits_applied is edits.size().
/// If every single edit was undone, edits_applied is 0.
size_t edits_applied = 0;
/// Whether we allow the next edit to be grouped together with the
/// last one.
bool may_coalesce = false;
/// Empty the history.
void clear();
};
/// Helper class for storing a command line.
class editable_line_t {
/// The command line.
wcstring text_;
/// The current position of the cursor in the command line.
size_t position_;
size_t position_ = 0;
public:
undo_history_t undo_history;
const wcstring &text() const { return text_; }
/// Set the text directly without maintaining undo invariants. Use with caution.
void set_text_bypassing_undo_history(wcstring &&text) { text_ = text; }
size_t position() const { return position_; }
void set_position(size_t position) { position_ = position; }
// TODO remove these overloads!
wcstring &text() { return text_; }
size_t &position() { return position_; }
void set_position(size_t position) { position_ = position; }
// Gets the length of the text.
size_t size() const { return text().size(); }
bool empty() const { return text().empty(); }
wchar_t at(size_t idx) { return text().at(idx); }
void clear() {
text_.clear();
undo_history.clear();
if (empty()) return;
set_text_bypassing_undo_history(L"");
set_position(0);
}
wchar_t at(size_t idx) { return text().at(idx); }
editable_line_t() : text_(), position_(0) {}
/// Modify the commandline according to @edit. Most modifications to the
/// text should pass through this function. You can use one of the wrappers below.
void push_edit(edit_t &&edit);
/// Erase @length characters starting at @offset.
void erase_substring(size_t offset, size_t length);
/// Replace the text of length @length at @offset by @replacement.
void replace_substring(size_t offset, size_t length, wcstring &&replacement);
/// Inserts a substring of str given by start, len at the cursor position.
void insert_string(const wcstring &str, size_t start = 0, size_t len = wcstring::npos);
/// Undo the most recent edit that was not yet undone. Returns true on success.
bool undo();
/// Redo the most recent undo. Returns true on success.
bool redo();
};
/// Read commands from \c fd until encountering EOF.
@ -216,8 +275,8 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline,
/// Expand abbreviations at the given cursor position. Exposed for testing purposes only.
/// \return none if no abbreviations were expanded, otherwise the new command line.
maybe_t<wcstring> reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars);
maybe_t<edit_t> reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars);
/// Apply a completion string. Exposed for testing only.
wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags,