summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/pages/keys.asciidoc6
-rw-r--r--src/client.cc14
-rw-r--r--src/client.hh2
-rw-r--r--src/context.cc186
-rw-r--r--src/context.hh55
-rw-r--r--src/input_handler.cc2
-rw-r--r--src/normal.cc17
7 files changed, 257 insertions, 25 deletions
diff --git a/doc/pages/keys.asciidoc b/doc/pages/keys.asciidoc
index 73f0e432..bce3b070 100644
--- a/doc/pages/keys.asciidoc
+++ b/doc/pages/keys.asciidoc
@@ -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
diff --git a/src/client.cc b/src/client.cc
index 09a39288..877160a9 100644
--- a/src/client.cc
+++ b/src/client.cc
@@ -170,7 +170,7 @@ DisplayLine Client::generate_mode_line() const
return modeline;
}
-void Client::change_buffer(Buffer& buffer)
+void Client::change_buffer(Buffer& buffer, Optional<FunctionRef<void()>> set_selections)
{
if (m_buffer_reload_dialog_opened)
close_buffer_reload_dialog();
@@ -181,12 +181,20 @@ void Client::change_buffer(Buffer& buffer)
m_window->options().unregister_watcher(*this);
m_window->set_client(nullptr);
client_manager.add_free_window(std::move(m_window),
- std::move(context().selections()));
+ context().selections());
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);
+
+ if (set_selections)
+ (*set_selections)();
+ else
+ {
+ ScopedSelectionEdition selection_edition{context()};
+ context().selections_write_only() = std::move(ws.selections);
+ }
+
context().set_window(*m_window);
m_window->set_dimensions(m_ui->dimensions());
diff --git a/src/client.hh b/src/client.hh
index 3fa27627..e379e1e7 100644
--- a/src/client.hh
+++ b/src/client.hh
@@ -65,7 +65,7 @@ public:
InputHandler& input_handler() { return m_input_handler; }
const InputHandler& input_handler() const { return m_input_handler; }
- void change_buffer(Buffer& buffer);
+ void change_buffer(Buffer& buffer, Optional<FunctionRef<void()>> set_selection);
StringView get_env_var(StringView name) const;
diff --git a/src/context.cc b/src/context.cc
index 3970a6ca..fe9bd346 100644
--- a/src/context.cc
+++ b/src/context.cc
@@ -16,11 +16,11 @@ 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_selection_history{*this, std::move(selections)},
m_name(std::move(name))
{}
-Context::Context(EmptyContextFlag) {}
+Context::Context(EmptyContextFlag) : m_selection_history{*this} {}
Buffer& Context::buffer() const
{
@@ -164,7 +164,147 @@ void JumpList::forget_buffer(Buffer& buffer)
}
}
-void Context::change_buffer(Buffer& buffer)
+Context::SelectionHistory::SelectionHistory(Context& context) : m_context(context) {}
+
+Context::SelectionHistory::SelectionHistory(Context& context, SelectionList selections)
+ : m_context(context),
+ m_history{HistoryNode{std::move(selections), HistoryId::Invalid}},
+ m_history_id(HistoryId::First) {}
+
+void Context::SelectionHistory::initialize(SelectionList selections)
+{
+ kak_assert(empty());
+ m_history = {HistoryNode{std::move(selections), HistoryId::Invalid}};
+ m_history_id = HistoryId::First;
+}
+
+SelectionList& Context::SelectionHistory::selections(bool update)
+{
+ if (empty())
+ throw runtime_error("no selections in context");
+ auto& sels = m_staging ? m_staging->selections : current_history_node().selections;
+ if (update)
+ sels.update();
+ return sels;
+}
+
+void Context::SelectionHistory::begin_edition()
+{
+ if (not in_edition())
+ m_staging = HistoryNode{selections(), m_history_id};
+ m_in_edition.set();
+}
+
+void Context::SelectionHistory::end_edition()
+{
+ kak_assert(in_edition());
+ m_in_edition.unset();
+ if (in_edition())
+ return;
+
+ if (m_history_id != HistoryId::Invalid and current_history_node().selections == m_staging->selections)
+ {
+ auto& sels = m_history[(size_t)m_history_id].selections;
+ sels.force_timestamp(m_staging->selections.timestamp());
+ sels.set_main_index(m_staging->selections.main_index());
+ }
+ else
+ {
+ m_history_id = next_history_id();
+ m_history.push_back(std::move(*m_staging));
+ }
+ m_staging.reset();
+}
+
+void Context::SelectionHistory::undo()
+{
+ if (in_edition())
+ throw runtime_error("selection undo is only supported at top-level");
+ kak_assert(not empty());
+ begin_edition();
+ auto end = on_scope_end([&] {
+ kak_assert(current_history_node().selections == m_staging->selections);
+ end_edition();
+ });
+ HistoryId parent = current_history_node().parent;
+ if (parent == HistoryId::Invalid)
+ throw runtime_error("no selection change to undo");
+ auto select_parent = [&, parent] {
+ HistoryId before_undo = m_history_id;
+ m_history_id = parent;
+ current_history_node().redo_child = before_undo;
+ m_staging = current_history_node();
+ };
+ if (&history_node(parent).selections.buffer() == &m_context.buffer())
+ select_parent();
+ else
+ m_context.change_buffer(history_node(parent).selections.buffer(), { std::move(select_parent) });
+ // });
+}
+
+void Context::SelectionHistory::redo()
+{
+ if (in_edition())
+ throw runtime_error("selection redo is only supported at top-level");
+ kak_assert(not empty());
+ begin_edition();
+ auto end = on_scope_end([&] {
+ kak_assert(current_history_node().selections == m_staging->selections);
+ end_edition();
+ });
+ HistoryId child = current_history_node().redo_child;
+ if (child == HistoryId::Invalid)
+ throw runtime_error("no selection change to redo");
+ auto select_child = [&, child] {
+ m_history_id = child;
+ m_staging = current_history_node();
+ };
+ if (&history_node(child).selections.buffer() == &m_context.buffer())
+ select_child();
+ else
+ m_context.change_buffer(history_node(child).selections.buffer(), { std::move(select_child) });
+}
+
+void Context::SelectionHistory::forget_buffer(Buffer& buffer)
+{
+ Vector<HistoryId, MemoryDomain::Selections> new_ids;
+ size_t bias = 0;
+ for (size_t i = 0; i < m_history.size(); ++i)
+ {
+ auto& node = history_node((HistoryId)i);
+ HistoryId id;
+ if (&node.selections.buffer() == &buffer)
+ {
+ id = HistoryId::Invalid;
+ ++bias;
+ }
+ else
+ id = (HistoryId)(i - bias);
+ new_ids.push_back(id);
+ }
+ auto new_id = [&new_ids](HistoryId old_id) -> HistoryId {
+ return old_id == HistoryId::Invalid ? HistoryId::Invalid : new_ids[(size_t)old_id];
+ };
+
+ m_history.erase(remove_if(m_history, [&buffer](const auto& node) {
+ return &node.selections.buffer() == &buffer;
+ }), m_history.end());
+
+ for (auto& node : m_history)
+ {
+ node.parent = new_id(node.parent);
+ node.redo_child = new_id(node.redo_child);
+ }
+ m_history_id = new_id(m_history_id);
+ if (m_staging)
+ {
+ m_staging->parent = new_id(m_staging->parent);
+ kak_assert(m_staging->redo_child == HistoryId::Invalid);
+ }
+ kak_assert(m_history_id != HistoryId::Invalid or m_staging);
+}
+
+void Context::change_buffer(Buffer& buffer, Optional<FunctionRef<void()>> set_selections)
{
if (has_buffer() and &buffer == &this->buffer())
return;
@@ -176,12 +316,18 @@ void Context::change_buffer(Buffer& buffer)
{
client().info_hide();
client().menu_hide();
- client().change_buffer(buffer);
+ client().change_buffer(buffer, std::move(set_selections));
}
else
{
m_window.reset();
- m_selections = SelectionList{buffer, Selection{}};
+ if (m_selection_history.empty())
+ m_selection_history.initialize(SelectionList{buffer, Selection{}});
+ else
+ {
+ ScopedSelectionEdition selection_edition{*this};
+ selections_write_only() = SelectionList{buffer, Selection{}};
+ }
}
if (has_input_handler())
@@ -192,14 +338,16 @@ void Context::forget_buffer(Buffer& buffer)
{
m_jump_list.forget_buffer(buffer);
- if (&this->buffer() != &buffer)
- return;
+ if (&this->buffer() == &buffer)
+ {
+ if (is_editing() && has_input_handler())
+ input_handler().reset_normal_mode();
- if (is_editing() && has_input_handler())
- input_handler().reset_normal_mode();
+ auto last_buffer = this->last_buffer();
+ change_buffer(last_buffer ? *last_buffer : BufferManager::instance().get_first_buffer());
+ }
- auto last_buffer = this->last_buffer();
- change_buffer(last_buffer ? *last_buffer : BufferManager::instance().get_first_buffer());
+ m_selection_history.forget_buffer(buffer);
}
Buffer* Context::last_buffer() const
@@ -225,11 +373,17 @@ Buffer* Context::last_buffer() const
SelectionList& Context::selections(bool update)
{
- if (not m_selections)
- throw runtime_error("no selections in context");
- if (update)
- (*m_selections).update();
- return *m_selections;
+ return m_selection_history.selections(update);
+}
+
+void Context::undo_selection_change()
+{
+ m_selection_history.undo();
+}
+
+void Context::redo_selection_change()
+{
+ m_selection_history.redo();
}
SelectionList& Context::selections_write_only()
diff --git a/src/context.hh b/src/context.hh
index a855d243..3f8fbc02 100644
--- a/src/context.hh
+++ b/src/context.hh
@@ -72,7 +72,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_selection_history.empty(); }
Window& window() const;
bool has_window() const { return (bool)m_window; }
@@ -90,7 +90,11 @@ public:
// Return potentially out of date selections
SelectionList& selections_write_only();
- void change_buffer(Buffer& buffer);
+ void end_selection_edition() { m_selection_history.end_edition(); }
+ void undo_selection_change();
+ void redo_selection_change();
+
+ void change_buffer(Buffer& buffer, Optional<FunctionRef<void()>> set_selection = {});
void forget_buffer(Buffer& buffer);
void set_client(Client& client);
@@ -113,6 +117,7 @@ public:
bool is_editing() const { return m_edition_level!= 0; }
void disable_undo_handling() { m_edition_level = -1; }
+ bool is_editing_selection() const { return m_selection_history.in_edition(); }
NestedBool& hooks_disabled() { return m_hooks_disabled; }
const NestedBool& hooks_disabled() const { return m_hooks_disabled; }
@@ -145,6 +150,7 @@ private:
size_t m_edition_timestamp = 0;
friend struct ScopedEdition;
+ friend struct ScopedSelectionEdition;
Flags m_flags = Flags::None;
@@ -152,7 +158,45 @@ private:
SafePtr<Window> m_window;
SafePtr<Client> m_client;
- Optional<SelectionList> m_selections;
+ class SelectionHistory {
+ public:
+ SelectionHistory(Context& context);
+ SelectionHistory(Context& context, SelectionList selections);
+ void initialize(SelectionList selections);
+ bool empty() const { return m_history.empty() and not m_staging; }
+ SelectionList& selections(bool update = true);
+
+ void begin_edition();
+ void end_edition();
+ bool in_edition() const { return m_in_edition; }
+
+ void undo();
+ void redo();
+ void forget_buffer(Buffer& buffer);
+ private:
+ enum class HistoryId : size_t { First = 0, Invalid = (size_t)-1 };
+
+ struct HistoryNode
+ {
+ HistoryNode(SelectionList selections, HistoryId parent) : selections(selections), parent(parent) {}
+
+ SelectionList selections;
+ HistoryId parent;
+ HistoryId redo_child = HistoryId::Invalid;
+ };
+
+ HistoryId next_history_id() const noexcept { return (HistoryId)m_history.size(); }
+ 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() { kak_assert((size_t)m_history_id < m_history.size()); return m_history[(size_t)m_history_id]; }
+
+ Context& m_context;
+ Vector<HistoryNode> m_history;
+ HistoryId m_history_id = HistoryId::Invalid;
+ Optional<HistoryNode> m_staging;
+ NestedBool m_in_edition;
+ };
+ SelectionHistory m_selection_history;
String m_name;
@@ -184,11 +228,12 @@ struct ScopedSelectionEdition
{
ScopedSelectionEdition(Context& context)
: m_context{context},
- m_buffer{context.has_buffer() ? &context.buffer() : nullptr} {}
+ m_buffer{context.has_buffer() ? &context.buffer() : nullptr}
+ { if (m_buffer) m_context.m_selection_history.begin_edition(); }
ScopedSelectionEdition(ScopedSelectionEdition&& other) : m_context{other.m_context}, m_buffer{other.m_buffer}
{ other.m_buffer = nullptr; }
- ~ScopedSelectionEdition() {}
+ ~ScopedSelectionEdition() { if (m_buffer) m_context.m_selection_history.end_edition(); }
private:
Context& m_context;
SafePtr<Buffer> m_buffer;
diff --git a/src/input_handler.cc b/src/input_handler.cc
index 73ad276d..25c450c8 100644
--- a/src/input_handler.cc
+++ b/src/input_handler.cc
@@ -100,6 +100,7 @@ struct MouseHandler
switch (key.mouse_button())
{
case Key::MouseButton::Right: {
+ kak_assert(not context.is_editing_selection());
m_dragging.reset();
cursor = context.window().buffer_coord(key.coord());
ScopedSelectionEdition selection_edition{context};
@@ -113,6 +114,7 @@ struct MouseHandler
}
case Key::MouseButton::Left: {
+ kak_assert(not context.is_editing_selection());
m_dragging.reset(new ScopedSelectionEdition{context});
m_anchor = context.window().buffer_coord(key.coord());
if (not (key.modifiers & Key::Modifiers::Control))
diff --git a/src/normal.cc b/src/normal.cc
index 4873703a..531b6a08 100644
--- a/src/normal.cc
+++ b/src/normal.cc
@@ -2042,6 +2042,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,
@@ -2367,6 +2381,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>} },