Skip to content

Commit

Permalink
Logic for undoing selection changes
Browse files Browse the repository at this point in the history
From the issue:

> It often happens to me that I carefully craft a selection with multiple
> cursors, ready to make changes elegantly, only to completely mess it
> up by pressing a wrong key (by merging the cursors for example). Being
> able to undo the last selection change (even if only until the previous
> buffer change) would make this much less painful.

Fix this by always keeping the last few selections in memory and
allowing simple linear undo/redo of selections.
The perliminary key bindings are  <c-h> and <c-k>. I find them more
convenient to type than any of X Y <backspace> <minus>.

When a sequence of n selection-undo-operations is followed by another
selection change, we drop the redo information of the n selections
that were undone. This reduces noise in the undo history, because it
only drops selections that have already been undone.

Currently there's no special interaction with undo of buffer changes;
the two histories are completely independent.
It's possible to restore selections that predate a buffer modification.
(When trying to apply an old buffer's selection to the new buffer,
Kakoune computes a diff of the buffers and updates the selection
accordingly. This works quite well for simple examples.)

In future we might want to synchronize buffer/selection undo histories
so we can implement Sublime Text's "Soft undo" feature.

Closes mawww#898
  • Loading branch information
krobelus committed Aug 7, 2022
1 parent e4d8df2 commit 84ef303
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 8 deletions.
6 changes: 6 additions & 0 deletions doc/pages/keys.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ Yanking (copying) and pasting use the *"* register by default (See <<registers#,
*<a-U>*::
move forward in history

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

*<c-k>*::
redo last selection change

*&*::
align selections, align the cursor of each selection by inserting spaces
before the first character of each selection
Expand Down
2 changes: 1 addition & 1 deletion src/client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ void Client::change_buffer(Buffer& buffer)
m_window = std::move(ws.window);
m_window->set_client(this);
m_window->options().register_watcher(*this);
context().selections_write_only() = std::move(ws.selections);
context().reset_selections(std::move(ws.selections));
context().set_window(*m_window);

m_window->set_dimensions(m_ui->dimensions());
Expand Down
92 changes: 87 additions & 5 deletions src/context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Context::Context(InputHandler& input_handler, SelectionList selections,
Flags flags, String name)
: m_flags(flags),
m_input_handler{&input_handler},
m_selections{std::move(selections)},
m_selections_history{std::move(selections)},
m_name(std::move(name))
{}

Expand Down Expand Up @@ -181,7 +181,7 @@ void Context::change_buffer(Buffer& buffer)
else
{
m_window.reset();
m_selections = SelectionList{buffer, Selection{}};
reset_selections(SelectionList{buffer, Selection{}});
}

if (has_input_handler())
Expand Down Expand Up @@ -225,11 +225,44 @@ Buffer* Context::last_buffer() const

SelectionList& Context::selections(bool update)
{
if (not m_selections)
if (m_selections_history.empty())
throw runtime_error("no selections in context");
if (update)
(*m_selections).update();
return *m_selections;
m_selections_history.at(m_current_selections).update();
return m_selections_history.at(m_current_selections);
}

void Context::reset_selections(SelectionList new_selections) {
m_selections_history.assign({std::move(new_selections)});
m_current_selections = 0;
}

void Context::undo_selection_change()
{
if (m_in_selection_edition)
throw runtime_error("selection undo is only allowed at top-level");
kak_assert(not m_selections_history.empty());
SelectionList old = selections();
do
{
if (m_current_selections == 0)
throw runtime_error("no selection change to undo");
--m_current_selections;
} while (selections() == old);
}

void Context::redo_selection_change()
{
if (m_in_selection_edition)
throw runtime_error("selection redo is only allowed at top-level");
kak_assert(not m_selections_history.empty());
SelectionList old = selections();
do
{
if (m_current_selections == m_selections_history.size() - 1)
throw runtime_error("no selection change to redo");
++m_current_selections;
} while (selections() == old);
}

SelectionList& Context::selections_write_only()
Expand Down Expand Up @@ -280,4 +313,53 @@ StringView Context::main_sel_register_value(StringView reg) const
return RegisterManager::instance()[reg].get_main(*this, index);
}

void Context::push_selections_history()
{
bool already_active = m_in_selection_edition;
m_in_selection_edition.set();
if (already_active)
return;

m_selections_history.push_back(selections());

m_previous_selections = m_current_selections;
constexpr int max_size = 50; // arbitrary
if (m_selections_history.size() == max_size + 1)
{
m_selections_history.pop_front();
m_previous_selections--;
}
m_current_selections = m_selections_history.size() - 1;
}

void Context::pop_selections_history()
{
m_in_selection_edition.unset();

if (m_in_selection_edition) // still active
return;

if (m_current_selections == 0)
return;
kak_assert(m_current_selections == m_selections_history.size() - 1);
auto& previous = m_selections_history.at(m_previous_selections);
auto& current = m_selections_history.at(m_current_selections);
if (previous == current and (true or previous.timestamp() == current.timestamp()))
{
// No selection change, so this history entry is redundant.
// Let's kick out the older one, because the newer one might have
// a higher timestamp.
std::swap(previous, current);
m_selections_history.pop_back(); // Drop previous selections.
m_current_selections = m_previous_selections; // Point to current selections.
}
else
{
// Drop redo history.
m_selections_history.erase(m_selections_history.begin() + m_previous_selections + 1,
m_selections_history.end() - 1);
m_current_selections = m_selections_history.size() - 1;
}
}

}
32 changes: 30 additions & 2 deletions src/context.hh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "utils.hh"

#include <functional>
#include <deque>

namespace Kakoune
{
Expand Down Expand Up @@ -72,7 +73,7 @@ public:
Context& operator=(const Context&) = delete;

Buffer& buffer() const;
bool has_buffer() const { return (bool)m_selections; }
bool has_buffer() const { return not m_selections_history.empty(); }

Window& window() const;
bool has_window() const { return (bool)m_window; }
Expand All @@ -90,6 +91,10 @@ public:
// Return potentially out of date selections
SelectionList& selections_write_only();

void reset_selections(SelectionList new_selections);
void undo_selection_change();
void redo_selection_change();

void change_buffer(Buffer& buffer);
void forget_buffer(Buffer& buffer);

Expand Down Expand Up @@ -146,13 +151,21 @@ private:

friend struct ScopedEdition;

void push_selections_history();
void pop_selections_history();
NestedBool m_in_selection_edition;
size_t m_current_selections = 0;
size_t m_previous_selections = -1;

friend struct ScopedSelection;

Flags m_flags = Flags::None;

SafePtr<InputHandler> m_input_handler;
SafePtr<Window> m_window;
SafePtr<Client> m_client;

Optional<SelectionList> m_selections;
std::deque<SelectionList> m_selections_history;

String m_name;

Expand Down Expand Up @@ -180,5 +193,20 @@ private:
SafePtr<Buffer> m_buffer;
};

struct ScopedSelection
{
ScopedSelection(Context& context)
: m_context{context},
m_buffer{context.has_buffer() ? &context.buffer() : nullptr}
{ if (m_buffer) m_context.push_selections_history(); };
ScopedSelection(ScopedSelection&& other) : m_context{other.m_context}, m_buffer{other.m_buffer}
{ other.m_buffer = nullptr; };

~ScopedSelection() { if (m_buffer) m_context.pop_selections_history(); };
private:
Context& m_context;
SafePtr<Buffer> m_buffer;
};

}
#endif // context_hh_INCLUDED
17 changes: 17 additions & 0 deletions src/normal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,20 @@ void move_in_history(Context& context, NormalParams params)
history_id, max_history_id));
}

void undo_selection_change(Context& context, NormalParams params)
{
int count = std::max(1, params.count);
while (count--)
context.undo_selection_change();
}

void redo_selection_change(Context& context, NormalParams params)
{
int count = std::max(1, params.count);
while (count--)
context.redo_selection_change();
}

void exec_user_mappings(Context& context, NormalParams params)
{
on_next_key_with_autoinfo(context, "user-mapping", KeymapMode::None,
Expand Down Expand Up @@ -2318,6 +2332,9 @@ static constexpr HashMap<Key, NormalCmd, MemoryDomain::Undefined, KeymapBackend>
{ {alt('u')}, {"move backward in history", move_in_history<Direction::Backward>} },
{ {alt('U')}, {"move forward in history", move_in_history<Direction::Forward>} },

{ {ctrl('h')}, {"undo selection change", undo_selection_change} },
{ {ctrl('k')}, {"redo selection change", redo_selection_change} },

{ {alt('i')}, {"select inner object", select_object<ObjectFlags::ToBegin | ObjectFlags::ToEnd | ObjectFlags::Inner>} },
{ {alt('a')}, {"select whole object", select_object<ObjectFlags::ToBegin | ObjectFlags::ToEnd>} },
{ {'['}, {"select to object start", select_object<ObjectFlags::ToBegin>} },
Expand Down

0 comments on commit 84ef303

Please sign in to comment.