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 <*:: move forward in history +**:: + undo last selection change + +**:: + 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> 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> 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 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> 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> 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 m_window; SafePtr m_client; - Optional 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 m_history; + HistoryId m_history_id = HistoryId::Invalid; + Optional 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 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 { {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", redo_selection_change} }, + { {alt('i')}, {"select inner object", select_object} }, { {alt('a')}, {"select whole object", select_object} }, { {'['}, {"select to object start", select_object} },