Skip to content

Commit

Permalink
Merge remote-tracking branch 'occivink/undo-history-as-plain-list'
Browse files Browse the repository at this point in the history
  • Loading branch information
mawww committed Apr 24, 2023
2 parents 7efcb94 + e0d33f5 commit a4918f9
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 186 deletions.
2 changes: 0 additions & 2 deletions README.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -540,9 +540,7 @@ Changes
* `<a-!>`: append command output after each selection

* `u`: undo last change
* `<a-u>`: move backward in history
* `U`: redo last change
* `<a-U>`: move forward in history

* `&`: align selections, align the cursor of selections by inserting
spaces before the first character of the selection
Expand Down
16 changes: 7 additions & 9 deletions doc/pages/expansions.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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_ +
Expand Down
6 changes: 0 additions & 6 deletions doc/pages/keys.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,9 @@ Yanking (copying) and pasting use the *"* register by default (See <<registers#,
*u*::
undo last change

*<a-u>*::
move backward in history

*U*::
redo last change

*<a-U>*::
move forward in history

*<c-h>*::
undo last selection change

Expand Down
181 changes: 64 additions & 117 deletions src/buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<BufferLines&>(m_lines) = std::move(lines);
Expand Down Expand Up @@ -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<UndoGroup>();
}
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)
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -646,7 +594,7 @@ void Buffer::run_hook_in_own_context(Hook hook, StringView param, String client_

Optional<BufferCoord> 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;
}
Expand Down Expand Up @@ -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");

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");

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");

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());

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.history().size() == 9);

auto check_content = [&](const Vector<StringView>& lines) {
kak_assert(buffer.line_count() == lines.size());
for (size_t i = 0; i < lines.size(); ++i)
kak_assert(buffer[i] == lines[i] + "\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));

kak_assert(buffer.redo(1000));
check_content({ "allo ?", "mais que fais la police", "foo" , });

kak_assert(buffer.undo(1000));
check_content({ "allo ?", "mais que fais la police", " hein ?", " youpi" , });
}};

}
23 changes: 8 additions & 15 deletions src/buffer.hh
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,6 @@ public:
};
friend constexpr bool with_bit_ops(Meta::Type<Flags>) { 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,
Expand All @@ -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;
Expand Down Expand Up @@ -245,14 +241,14 @@ public:

struct HistoryNode : UseMemoryDomain<MemoryDomain::BufferMeta>
{
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<HistoryNode>& history() const { return m_history; }
const UndoGroup& current_undo_group() const { return m_current_undo_group; }

Expand All @@ -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
{
Expand All @@ -289,14 +284,12 @@ private:
Flags m_flags;

Vector<HistoryNode> 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<Change, MemoryDomain::BufferMeta> m_changes;

Expand Down
Loading

0 comments on commit a4918f9

Please sign in to comment.