From e0d33f51b36c9f0be7ae2467dab455d211bbf561 Mon Sep 17 00:00:00 2001 From: Olivier Perret Date: Mon, 17 Apr 2023 10:25:51 +0200 Subject: [PATCH] Switch undo storage from a tree to a plain list Whenever a new history node is committed after some undo steps, instead of creating a new branch in the undo graph, we first append the inverse modifications starting from the end of the undo list up to the current position before adding the new node. For example let's assume that the undo history is A-B-C, that a single undo has been done (bringing us to state B) and that a new change D is committed. Instead of creating a new branch starting at B, we add the inverse of C (noted ^C) at the end, and D afterwards. This results in the undo history A-B-C-^C-D. Since C-^C collapses to a null change, this is equivalent to A-B-D but without having lost the C branch of the history. If a new change is committed while no undo has been done, the new history node is simply appended to the list, as was the case previously. This results in a simplification of the user interaction, as two bindings are now sufficient to walk the entire undo history, as opposed to needing extra bindings to switch branches whenever they occur. The and bindings are now free. It also simplifies the implementation, as the graph traversal and branching code are not needed anymore. The parent and child of a node are now respectively the previous and the next elements in the list, so there is no need to store their ID as part of the node. Only the committing of an undo group is slightly more complex, as inverse history nodes need to be added depending on the current position in the undo list. The following article was the initial motivation for this change: https://github.com/zaboople/klonk/blob/master/TheGURQ.md --- README.asciidoc | 2 - doc/pages/expansions.asciidoc | 16 +-- doc/pages/keys.asciidoc | 6 - src/buffer.cc | 173 ++++++++---------------- src/buffer.hh | 23 ++-- src/buffer_utils.cc | 10 -- src/normal.cc | 25 ---- test/compose/history/kak_quoted_history | 2 +- test/compose/history/rc | 2 +- 9 files changed, 77 insertions(+), 182 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 561be22a..93b6d93e 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -540,9 +540,7 @@ Changes * ``: append command output after each selection * `u`: undo last change - * ``: move backward in history * `U`: redo last change - * ``: move forward in history * `&`: align selections, align the cursor of selections by inserting spaces before the first character of the selection diff --git a/doc/pages/expansions.asciidoc b/doc/pages/expansions.asciidoc index 81ccd7b7..191e588b 100644 --- a/doc/pages/expansions.asciidoc +++ b/doc/pages/expansions.asciidoc @@ -285,15 +285,13 @@ The following expansions are supported (with required context _in italics_): *%val{history}*:: _in buffer, window scope_ + - the full change history of the buffer, including undo forks, in terms - of `parent committed redo_child modification0 modification1 ...` - entries, where _parent_ is the index of the entry's predecessor (entry - 0, which is the root of the history tree, will always have `-` here), - _committed_ is a count in seconds from Kakoune's steady clock's epoch - of the creation of the history entry, _redo_child_ is the index of the - child which will be visited for `U` (always `-` at the leaves of the - history), and each _modification_ is presented as in - `%val{uncommitted_modifications}`. + the full change history of the buffer, as a list of + `committed modification0 modification1 ...` entries. + _committed_ is a count in seconds from Kakoune's steady clock's epoch of + the creation of the history entry, and each _modification_ is presented + as in `%val{uncommitted_modifications}`. + Boundaries in the list can be identified knowing that _committed_ is a + plain integer, and each _modification_ starts with `+` or `-`. *%val{history_id}*:: _in buffer, window scope_ + diff --git a/doc/pages/keys.asciidoc b/doc/pages/keys.asciidoc index 060eebd7..2b2645aa 100644 --- a/doc/pages/keys.asciidoc +++ b/doc/pages/keys.asciidoc @@ -305,15 +305,9 @@ Yanking (copying) and pasting use the *"* register by default (See <*:: - move backward in history - *U*:: redo last change -**:: - move forward in history - **:: undo last selection change diff --git a/src/buffer.cc b/src/buffer.cc index c968690f..8baa7423 100644 --- a/src/buffer.cc +++ b/src/buffer.cc @@ -20,8 +20,8 @@ namespace Kakoune { -Buffer::HistoryNode::HistoryNode(HistoryId parent) - : parent{parent}, committed{Clock::now()} +Buffer::HistoryNode::HistoryNode() + : committed{Clock::now()} {} Buffer::Buffer(String name, Flags flags, BufferLines lines, @@ -31,9 +31,9 @@ Buffer::Buffer(String name, Flags flags, BufferLines lines, m_name{(flags & Flags::File) ? real_path(parse_filename(name)) : std::move(name)}, m_display_name{(flags & Flags::File) ? compact_path(m_name) : m_name}, m_flags{flags | Flags::NoUndo}, - m_history{{HistoryId::Invalid}}, - m_history_id{HistoryId::First}, - m_last_save_history_id{HistoryId::First}, + m_history{{HistoryNode{}}}, + m_history_id{0}, + m_last_save_history_id{0}, m_fs_status{fs_status} { #ifdef KAK_DEBUG @@ -114,7 +114,7 @@ bool Buffer::set_name(String name) if (m_flags & Buffer::Flags::File and not file_exists(m_name)) { m_flags |= Buffer::Flags::New; - m_last_save_history_id = HistoryId::Invalid; + m_last_save_history_id = InvalidHistoryId; } } else @@ -201,9 +201,9 @@ void Buffer::reload(BufferLines lines, ByteOrderMark bom, EolFormat eolformat, F if (not record_undo) { // Erase history about to be invalidated history - m_history_id = HistoryId::First; - m_last_save_history_id = HistoryId::First; - m_history = {HistoryNode{HistoryId::Invalid}}; + m_history_id = 0; + m_last_save_history_id = 0; + m_history = {HistoryNode{}}; m_changes.push_back({ Change::Erase, {0,0}, line_count() }); static_cast(m_lines) = std::move(lines); @@ -273,12 +273,20 @@ void Buffer::commit_undo_group() if (m_current_undo_group.empty()) return; - const HistoryId id = next_history_id(); - m_history.push_back({m_history_id}); + // walk back to current position in history, and append reverse events + for (size_t current = m_history.size() - 1; current != m_history_id ; --current) + { + m_history.emplace_back(); + m_history.back().undo_group = m_history[current].undo_group + | reverse() + | transform([](auto& modif) { return modif.inverse(); }) + | gather(); + } + m_history.emplace_back(); m_history.back().undo_group = std::move(m_current_undo_group); + m_current_undo_group.clear(); - current_history_node().redo_child = id; - m_history_id = id; + m_history_id = m_history.size() - 1; } bool Buffer::undo(size_t count) @@ -287,15 +295,15 @@ bool Buffer::undo(size_t count) commit_undo_group(); - if (current_history_node().parent == HistoryId::Invalid) + if ((m_history_id - 1) == InvalidHistoryId) return false; - while (count-- != 0 and current_history_node().parent != HistoryId::Invalid) + while (count-- != 0 and (m_history_id - 1) != InvalidHistoryId) { for (const Modification& modification : current_history_node().undo_group | reverse()) apply_modification(modification.inverse()); - m_history_id = current_history_node().parent; + m_history_id--; } return true; @@ -305,79 +313,19 @@ bool Buffer::redo(size_t count) { throw_if_read_only(); - if (current_history_node().redo_child == HistoryId::Invalid) + if (m_history_id == (m_history.size() - 1)) return false; kak_assert(m_current_undo_group.empty()); - while (count-- != 0 and current_history_node().redo_child != HistoryId::Invalid) + while (count-- != 0 and m_history_id != (m_history.size() - 1)) { - m_history_id = current_history_node().redo_child; + m_history_id++; for (const Modification& modification : current_history_node().undo_group) apply_modification(modification); } - return true; -} -bool Buffer::move_to(HistoryId id) -{ - if (id >= next_history_id()) - return false; - - throw_if_read_only(); - - commit_undo_group(); - - auto find_lowest_common_parent = [this](HistoryId a, HistoryId b) { - auto depth_of = [this](HistoryId id) { - size_t depth = 0; - for (; history_node(id).parent != HistoryId::Invalid; id = history_node(id).parent) - ++depth; - return depth; - }; - auto depthA = depth_of(a), depthB = depth_of(b); - - for (; depthA > depthB; --depthA) - a = history_node(a).parent; - for (; depthB > depthA; --depthB) - b = history_node(b).parent; - - while (a != b) - { - a = history_node(a).parent; - b = history_node(b).parent; - } - - kak_assert(a == b and a != HistoryId::Invalid); - return a; - }; - - auto parent = find_lowest_common_parent(m_history_id, id); - - // undo up to common parent - for (auto id = m_history_id; id != parent; id = history_node(id).parent) - { - for (const Modification& modification : history_node(id).undo_group | reverse()) - apply_modification(modification.inverse()); - } - - static void (*apply_from_parent)(Buffer&, HistoryId, HistoryId) = - [](Buffer& buffer, HistoryId parent, HistoryId id) { - if (id == parent) - return; - - auto& node = buffer.history_node(id); - apply_from_parent(buffer, parent, node.parent); - - buffer.history_node(node.parent).redo_child = id; - - for (const Modification& modification : node.undo_group) - buffer.apply_modification(modification); - }; - - apply_from_parent(*this, parent, id); - m_history_id = id; return true; } @@ -646,7 +594,7 @@ void Buffer::run_hook_in_own_context(Hook hook, StringView param, String client_ Optional Buffer::last_modification_coord() const { - if (m_history_id == HistoryId::First) + if (m_history_id == 0) return {}; return current_history_node().undo_group.back().coord; } @@ -734,50 +682,49 @@ UnitTest test_undo{[]() buffer.insert(2_line, "tchou\n"); // change 3 buffer.commit_undo_group(); buffer.undo(); - buffer.insert(2_line, "mutch\n"); // change 4 + buffer.insert(2_line, "mutch\n"); // change 4 (inverse of 3) and 5 buffer.commit_undo_group(); - buffer.erase({2, 1}, {2, 5}); // change 5 + buffer.erase({2, 1}, {2, 5}); // change 6 buffer.commit_undo_group(); buffer.undo(2); buffer.redo(2); buffer.undo(); - buffer.replace(2_line, buffer.end_coord(), "foo"); // change 6 + buffer.replace(2_line, buffer.end_coord(), "foo"); // change 7 (inverse of 6) and 8 buffer.commit_undo_group(); - kak_assert((int)buffer.line_count() == 3); - kak_assert(buffer[0_line] == "allo ?\n"); - kak_assert(buffer[1_line] == "mais que fais la police\n"); - kak_assert(buffer[2_line] == "foo\n"); + kak_assert(buffer.history().size() == 9); - buffer.move_to((Buffer::HistoryId)3); - kak_assert((int)buffer.line_count() == 5); - kak_assert(buffer[0_line] == "allo ?\n"); - kak_assert(buffer[1_line] == "mais que fais la police\n"); - kak_assert(buffer[2_line] == "tchou\n"); - kak_assert(buffer[3_line] == " hein ?\n"); - kak_assert(buffer[4_line] == " youpi\n"); + auto check_content = [&](const Vector& lines) { + kak_assert(buffer.line_count() == lines.size()); + for (size_t i = 0; i < lines.size(); ++i) + kak_assert(buffer[i] == lines[i] + "\n"); + }; - buffer.move_to((Buffer::HistoryId)4); - kak_assert((int)buffer.line_count() == 5); - kak_assert(buffer[0_line] == "allo ?\n"); - kak_assert(buffer[1_line] == "mais que fais la police\n"); - kak_assert(buffer[2_line] == "mutch\n"); - kak_assert(buffer[3_line] == " hein ?\n"); - kak_assert(buffer[4_line] == " youpi\n"); + kak_assert(not buffer.redo(1)); + check_content({ "allo ?", "mais que fais la police", "foo" , }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", "mutch" , " hein ?", " youpi", }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", "m" , " hein ?", " youpi", }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", "mutch" , " hein ?", " youpi", }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", " hein ?", " youpi" , }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", "tchou" , " hein ?", " youpi", }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", " hein ?", " youpi" , }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", " hein ?", " youpi" , "kanaky", }); + kak_assert(buffer.undo(1)); + check_content({ "allo ?", "mais que fais la police", " hein ?", " youpi" , }); + kak_assert(not buffer.undo(1)); - buffer.move_to(Buffer::HistoryId::First); - kak_assert((int)buffer.line_count() == 4); - kak_assert(buffer[0_line] == "allo ?\n"); - kak_assert(buffer[1_line] == "mais que fais la police\n"); - kak_assert(buffer[2_line] == " hein ?\n"); - kak_assert(buffer[3_line] == " youpi\n"); - kak_assert(not buffer.undo()); + kak_assert(buffer.redo(1000)); + check_content({ "allo ?", "mais que fais la police", "foo" , }); - buffer.move_to((Buffer::HistoryId)5); - kak_assert(not buffer.redo()); - - buffer.move_to((Buffer::HistoryId)6); - kak_assert(not buffer.redo()); + kak_assert(buffer.undo(1000)); + check_content({ "allo ?", "mais que fais la police", " hein ?", " youpi" , }); }}; } diff --git a/src/buffer.hh b/src/buffer.hh index 7b671c0e..415d32da 100644 --- a/src/buffer.hh +++ b/src/buffer.hh @@ -123,8 +123,6 @@ public: }; friend constexpr bool with_bit_ops(Meta::Type) { return true; } - enum class HistoryId : size_t { First = 0, Invalid = (size_t)-1 }; - Buffer(String name, Flags flags, BufferLines lines, ByteOrderMark bom = ByteOrderMark::None, EolFormat eolformat = EolFormat::Lf, @@ -150,9 +148,7 @@ public: void commit_undo_group(); bool undo(size_t count = 1); bool redo(size_t count = 1); - bool move_to(HistoryId id); - HistoryId current_history_id() const noexcept { return m_history_id; } - HistoryId next_history_id() const noexcept { return (HistoryId)m_history.size(); } + size_t current_history_id() const noexcept { return m_history_id; } String string(BufferCoord begin, BufferCoord end) const; StringView substr(BufferCoord begin, BufferCoord end) const; @@ -245,14 +241,14 @@ public: struct HistoryNode : UseMemoryDomain { - HistoryNode(HistoryId parent); + HistoryNode(); - HistoryId parent; - HistoryId redo_child = HistoryId::Invalid; TimePoint committed; UndoGroup undo_group; }; + static constexpr size_t InvalidHistoryId = (size_t)-1; + const Vector& history() const { return m_history; } const UndoGroup& current_undo_group() const { return m_current_undo_group; } @@ -263,7 +259,6 @@ private: BufferCoord do_erase(BufferCoord begin, BufferCoord end); void apply_modification(const Modification& modification); - void revert_modification(const Modification& modification); struct LineList : BufferLines { @@ -289,14 +284,12 @@ private: Flags m_flags; Vector m_history; - HistoryId m_history_id = HistoryId::Invalid; - HistoryId m_last_save_history_id = HistoryId::Invalid; + size_t m_history_id = InvalidHistoryId; + size_t m_last_save_history_id = InvalidHistoryId; UndoGroup m_current_undo_group; - HistoryNode& history_node(HistoryId id) { return m_history[(size_t)id]; } - const HistoryNode& history_node(HistoryId id) const { return m_history[(size_t)id]; } - HistoryNode& current_history_node() { return m_history[(size_t)m_history_id]; } - const HistoryNode& current_history_node() const { return m_history[(size_t)m_history_id]; } + HistoryNode& current_history_node() { return m_history[m_history_id]; } + const HistoryNode& current_history_node() const { return m_history[m_history_id]; } Vector m_changes; diff --git a/src/buffer_utils.cc b/src/buffer_utils.cc index 47f9a88b..9ff1f4ce 100644 --- a/src/buffer_utils.cc +++ b/src/buffer_utils.cc @@ -301,14 +301,6 @@ void write_to_debug_buffer(StringView str) } -auto to_string(Buffer::HistoryId id) -{ - using Result = decltype(to_string(size_t{})); - if (id == Buffer::HistoryId::Invalid) - return Result{1, "-"}; - return to_string(static_cast(id)); -} - static String modification_as_string(const Buffer::Modification& modification) { return format("{}{}.{}|{}", @@ -323,9 +315,7 @@ Vector history_as_strings(const Vector& history) for (auto& node : history) { auto seconds = std::chrono::duration_cast(node.committed.time_since_epoch()); - res.push_back(to_string(node.parent)); res.push_back(to_string(seconds.count())); - res.push_back(to_string(node.redo_child)); for (auto& modification : node.undo_group) res.push_back(modification_as_string(modification)); }; diff --git a/src/normal.cc b/src/normal.cc index 02cea4ef..e3220521 100644 --- a/src/normal.cc +++ b/src/normal.cc @@ -2009,29 +2009,6 @@ void redo(Context& context, NormalParams params) throw runtime_error("nothing left to redo"); } -template -void move_in_history(Context& context, NormalParams params) -{ - Buffer& buffer = context.buffer(); - size_t timestamp = buffer.timestamp(); - const int count = std::max(1, params.count); - const int history_id = (size_t)buffer.current_history_id() + direction * count; - const int max_history_id = (int)buffer.next_history_id() - 1; - if (buffer.move_to((Buffer::HistoryId)history_id)) - { - auto ranges = compute_modified_ranges(buffer, timestamp); - if (not ranges.empty()) - context.selections_write_only() = std::move(ranges); - - context.print_status({ format("moved to change #{} ({})", - history_id, max_history_id), - context.faces()["Information"] }); - } - else - throw runtime_error(format("no such change: #{} ({})", - history_id, max_history_id)); -} - template void undo_selection_change(Context& context, NormalParams params) { @@ -2362,8 +2339,6 @@ static constexpr HashMap { {'u'}, {"undo", undo} }, { {'U'}, {"redo", redo} }, - { {alt('u')}, {"move backward in history", move_in_history} }, - { {alt('U')}, {"move forward in history", move_in_history} }, { {ctrl('h')}, {"undo selection change", undo_selection_change} }, { {ctrl('k')}, {"redo selection change", undo_selection_change} }, diff --git a/test/compose/history/kak_quoted_history b/test/compose/history/kak_quoted_history index 5265a633..2e78a548 100644 --- a/test/compose/history/kak_quoted_history +++ b/test/compose/history/kak_quoted_history @@ -1 +1 @@ -'-' '$timestamp' '1' '0' '$timestamp' '-' '+0.5|m' '+0.6|i' '+0.7|d' '+0.8|d' '+0.9|l' '+0.10|e' '-0.8|dle' +'$timestamp' '$timestamp' '+0.5|m' '+0.6|i' '+0.7|d' '+0.8|d' '+0.9|l' '+0.10|e' '-0.8|dle' diff --git a/test/compose/history/rc b/test/compose/history/rc index da4c3cd7..f8a37fda 100644 --- a/test/compose/history/rc +++ b/test/compose/history/rc @@ -1,6 +1,6 @@ # Make our expansion have a predictable timestamp hook global ClientClose .* %{ nop %sh{ - kak -f 'ghflwc$timestamp3flwc$timestamp' kak_quoted_history + kak -f 'ghlwc$timestamp1flwc$timestamp' kak_quoted_history } }