diff --git a/doc/pages/keys.asciidoc b/doc/pages/keys.asciidoc index 2574a8e2..35724419 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 27f0fbaa..cbdfa233 100644 --- a/src/client.cc +++ b/src/client.cc @@ -169,7 +169,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(); @@ -180,12 +180,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/commands.cc b/src/commands.cc index 001ba0f9..123736b0 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -2020,6 +2020,7 @@ void context_wrap(const ParametersParser& parser, Context& context, StringView d ScopedSetBool disable_history(c.history_disabled()); ScopedEdition edition{c}; + ScopedSelectionEdition selection_edition{c}; if (parser.get_switch("itersel")) { @@ -2534,6 +2535,7 @@ const CommandDesc select_cmd = { else if (parser.get_switch("display-column")) column_type = ColumnType::DisplayColumn; ColumnCount tabstop = context.options()["tabstop"].get(); + ScopedSelectionEdition selection_edition{context}; context.selections_write_only() = selection_list_from_strings(buffer, column_type, parser.positionals_from(0), timestamp, 0, tabstop); } }; diff --git a/src/context.cc b/src/context.cc index efa91938..fe9bd346 100644 --- a/src/context.cc +++ b/src/context.cc @@ -16,17 +16,17 @@ 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 { if (not has_buffer()) throw runtime_error("no buffer in context"); - return const_cast((*m_selections).buffer()); + return const_cast(selections(false).buffer()); } Window& Context::window() 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 @@ -223,24 +371,29 @@ Buffer* Context::last_buffer() const return previous_buffer != jump_list.rend() ? &previous_buffer->buffer() : nullptr; } -SelectionList& Context::selections() +SelectionList& Context::selections(bool update) { - if (not m_selections) - throw runtime_error("no selections in context"); - (*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() { - if (not m_selections) - throw runtime_error("no selections in context"); - return *m_selections; + return selections(false); } -const SelectionList& Context::selections() const +const SelectionList& Context::selections(bool update) const { - return const_cast(*this).selections(); + return const_cast(*this).selections(update); } Vector Context::selections_content() const @@ -277,7 +430,7 @@ void Context::end_edition() StringView Context::main_sel_register_value(StringView reg) const { - size_t index = m_selections ? (*m_selections).main_index() : 0; + size_t index = has_buffer() ? selections(false).main_index() : 0; return RegisterManager::instance()[reg].get_main(*this, index); } diff --git a/src/context.hh b/src/context.hh index fd29b897..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; } @@ -83,14 +83,18 @@ public: InputHandler& input_handler() const; bool has_input_handler() const { return (bool)m_input_handler; } - SelectionList& selections(); - const SelectionList& selections() const; + SelectionList& selections(bool update = true); + const SelectionList& selections(bool update = true) const; Vector selections_content() const; // 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; @@ -180,5 +224,20 @@ private: SafePtr m_buffer; }; +struct ScopedSelectionEdition +{ + ScopedSelectionEdition(Context& context) + : m_context{context}, + 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() { if (m_buffer) m_context.m_selection_history.end_edition(); } +private: + Context& m_context; + SafePtr m_buffer; +}; + } #endif // context_hh_INCLUDED diff --git a/src/input_handler.cc b/src/input_handler.cc index bf2e5a4b..61a07a40 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -93,59 +93,69 @@ struct MouseHandler Buffer& buffer = context.buffer(); BufferCoord cursor; - auto& selections = context.selections(); constexpr auto modifiers = Key::Modifiers::Control | Key::Modifiers::Alt | Key::Modifiers::Shift | Key::Modifiers::MouseButtonMask; switch ((key.modifiers & ~modifiers).value) { case Key::Modifiers::MousePress: switch (key.mouse_button()) { - case Key::MouseButton::Right: - m_dragging = false; + 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}; + auto& selections = context.selections(); if (key.modifiers & Key::Modifiers::Control) selections = {{selections.begin()->anchor(), cursor}}; else selections.main() = {selections.main().anchor(), cursor}; selections.sort_and_merge_overlapping(); return true; + } - case Key::MouseButton::Left: - m_dragging = true; + 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)) context.selections_write_only() = { buffer, m_anchor}; else { + auto& selections = context.selections(); size_t main = selections.size(); selections.push_back({m_anchor}); selections.set_main_index(main); selections.sort_and_merge_overlapping(); } return true; + } default: return true; } - case Key::Modifiers::MouseRelease: + case Key::Modifiers::MouseRelease: { if (not m_dragging) return true; - m_dragging = false; + auto& selections = context.selections(); cursor = context.window().buffer_coord(key.coord()); selections.main() = {buffer.clamp(m_anchor), cursor}; selections.sort_and_merge_overlapping(); + m_dragging.reset(); return true; + } - case Key::Modifiers::MousePos: + case Key::Modifiers::MousePos: { if (not m_dragging) return true; cursor = context.window().buffer_coord(key.coord()); + auto& selections = context.selections(); selections.main() = {buffer.clamp(m_anchor), cursor}; selections.sort_and_merge_overlapping(); return true; + } case Key::Modifiers::Scroll: - scroll_window(context, static_cast(key.key), m_dragging); + scroll_window(context, static_cast(key.key), (bool)m_dragging); return true; default: return false; @@ -153,7 +163,7 @@ struct MouseHandler } private: - bool m_dragging = false; + std::unique_ptr m_dragging; BufferCoord m_anchor; }; @@ -1199,6 +1209,7 @@ public: Insert(InputHandler& input_handler, InsertMode mode, int count) : InputMode(input_handler), m_edition(context()), + m_selection_edition(context()), m_completer(context()), m_restore_cursor(mode == InsertMode::Append), m_auto_complete{context().options()["autocomplete"].get() & AutoComplete::Insert}, @@ -1549,6 +1560,7 @@ private: } ScopedEdition m_edition; + ScopedSelectionEdition m_selection_edition; InsertCompleter m_completer; const bool m_restore_cursor; bool m_auto_complete; @@ -1806,6 +1818,7 @@ void scroll_window(Context& context, LineCount offset, bool mouse_dragging) win_pos.line = clamp(win_pos.line + offset, 0_line, line_count-1); + ScopedSelectionEdition selection_edition{context}; SelectionList& selections = context.selections(); Selection& main_selection = selections.main(); const BufferCoord anchor = main_selection.anchor(); diff --git a/src/normal.cc b/src/normal.cc index 5f0512d7..a6cabec1 100644 --- a/src/normal.cc +++ b/src/normal.cc @@ -74,6 +74,7 @@ UnitTest test_merge_selection{[] { template void select(Context& context, T func) { + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); if (mode == SelectMode::Append) { @@ -140,8 +141,11 @@ void select_and_set_last(Context& context, Func&& func) } template -void select_coord(Buffer& buffer, BufferCoord coord, SelectionList& selections) +void select_coord(Context& context, BufferCoord coord) { + Buffer& buffer = context.buffer(); + ScopedSelectionEdition selection_edition{context}; + SelectionList& selections = context.selections(); coord = buffer.clamp(coord); if (mode == SelectMode::Replace) selections = SelectionList{ buffer, coord }; @@ -212,7 +216,7 @@ void goto_commands(Context& context, NormalParams params) if (params.count != 0) { context.push_jump(); - select_coord(context.buffer(), LineCount{params.count - 1}, context.selections()); + select_coord(context, LineCount{params.count - 1}); if (context.has_window()) context.window().center_line(LineCount{params.count-1}); } @@ -229,7 +233,7 @@ void goto_commands(Context& context, NormalParams params) case 'g': case 'k': context.push_jump(); - select_coord(buffer, BufferCoord{0,0}, context.selections()); + select_coord(context, BufferCoord{0,0}); break; case 'l': select>(context, {}); @@ -242,17 +246,17 @@ void goto_commands(Context& context, NormalParams params) break; case 'j': context.push_jump(); - select_coord(buffer, buffer.line_count() - 1, context.selections()); + select_coord(context, buffer.line_count() - 1); break; case 'e': context.push_jump(); - select_coord(buffer, buffer.back_coord(), context.selections()); + select_coord(context, buffer.back_coord()); break; case 't': if (context.has_window()) { auto line = context.window().position().line; - select_coord(buffer, line, context.selections()); + select_coord(context, line); } break; case 'b': @@ -260,7 +264,7 @@ void goto_commands(Context& context, NormalParams params) { auto& window = context.window(); auto line = window.position().line + window.dimensions().line - 1; - select_coord(buffer, line, context.selections()); + select_coord(context, line); } break; case 'c': @@ -268,7 +272,7 @@ void goto_commands(Context& context, NormalParams params) { auto& window = context.window(); auto line = window.position().line + window.dimensions().line / 2; - select_coord(buffer, line, context.selections()); + select_coord(context, line); } break; case 'a': @@ -328,7 +332,7 @@ void goto_commands(Context& context, NormalParams params) throw runtime_error("no last modification position"); if (*pos >= buffer.back_coord()) pos = buffer.back_coord(); - select_coord(buffer, *pos, context.selections()); + select_coord(context, *pos); break; } default: @@ -420,6 +424,7 @@ void replace_with_char(Context& context, NormalParams) if (not cp or key == Key::Escape) return; ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; Buffer& buffer = context.buffer(); context.selections().for_each([&](size_t index, Selection& sel) { CharCount count = char_length(buffer, sel); @@ -440,6 +445,7 @@ void for_each_codepoint(Context& context, NormalParams) using Utf8It = utf8::iterator; ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; Buffer& buffer = context.buffer(); context.selections().for_each([&](size_t index, Selection& sel) { @@ -553,7 +559,8 @@ void pipe(Context& context, NormalParams params) prompt, {}, default_command, context.faces()["Prompt"], PromptFlags::DropHistoryEntriesWithBlankPrefix, '|', shell_complete, - [default_command](StringView cmdline, PromptEvent event, Context& context) + [default_command, selection_edition=std::make_shared(context)] + (StringView cmdline, PromptEvent event, Context& context) { if (event != PromptEvent::Validate) return; @@ -635,6 +642,7 @@ void erase_selections(Context& context, NormalParams params) RegisterManager::instance()[reg].set(context, context.selections_content()); } ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; context.selections().erase(); } @@ -681,6 +689,7 @@ void paste(Context& context, NormalParams params) auto& buffer = context.buffer(); ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; context.selections().for_each([&](size_t index, Selection& sel) { auto& str = strings[std::min(strings.size()-1, index)]; auto& min = sel.min(); @@ -717,6 +726,7 @@ void paste_all(Context& context, NormalParams params) Buffer& buffer = context.buffer(); Vector result; + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); { ScopedEdition edition(context); @@ -749,7 +759,8 @@ void insert_output(Context& context, NormalParams params) prompt, {}, default_command, context.faces()["Prompt"], PromptFlags::DropHistoryEntriesWithBlankPrefix, '|', shell_complete, - [default_command](StringView cmdline, PromptEvent event, Context& context) + [default_command, selection_edition=std::make_shared(context)] + (StringView cmdline, PromptEvent event, Context& context) { if (event != PromptEvent::Validate) return; @@ -821,7 +832,8 @@ void regex_prompt(Context& context, String prompt, char reg, T func) [&](auto&& m) { candidates.push_back(m.candidate().str()); return true; }); return {(int)(word.begin() - regex.begin()), pos, std::move(candidates) }; }, - [=, func=T(std::move(func))](StringView str, PromptEvent event, Context& context) mutable { + [=, func=T(std::move(func)), selection_edition=std::make_shared(context)] + (StringView str, PromptEvent event, Context& context) mutable { try { if (event != PromptEvent::Change and context.has_client()) @@ -941,6 +953,7 @@ void search_next(Context& context, NormalParams params) if (not str.empty()) { Regex regex{str, direction_flags(regex_mode)}; + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); bool main_wrapped = false; do { @@ -1042,6 +1055,7 @@ void split_regex(Context& context, NormalParams params) void split_lines(Context& context, NormalParams params) { const LineCount count{params.count == 0 ? 1 : params.count}; + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); auto& buffer = context.buffer(); Vector res; @@ -1068,6 +1082,7 @@ void split_lines(Context& context, NormalParams params) void select_boundaries(Context& context, NormalParams) { + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); Vector res; for (auto& sel : selections) @@ -1083,6 +1098,7 @@ void join_lines_select_spaces(Context& context, NormalParams) { auto& buffer = context.buffer(); Vector selections; + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) { const LineCount min_line = sel.min().line; @@ -1106,6 +1122,7 @@ void join_lines_select_spaces(Context& context, NormalParams) void join_lines(Context& context, NormalParams params) { + ScopedSelectionEdition selection_edition{context}; SelectionList sels{context.selections()}; auto restore_sels = on_scope_end([&]{ sels.update(); @@ -1123,7 +1140,8 @@ void keep(Context& context, NormalParams params) const char reg = to_lower(params.reg ? params.reg : '/'); regex_prompt(context, prompt.str(), reg, - [reg, saved_reg = RegisterManager::instance()[reg].save(context)] + [reg, saved_reg = RegisterManager::instance()[reg].save(context), + selection_edition=std::make_shared(context)] (const Regex& regex, PromptEvent event, Context& context) { RegisterManager::instance()[reg].restore(context, saved_reg); if (event == PromptEvent::Abort) @@ -1160,7 +1178,8 @@ void keep_pipe(Context& context, NormalParams params) context.input_handler().prompt( "keep pipe:", {}, default_command, context.faces()["Prompt"], PromptFlags::DropHistoryEntriesWithBlankPrefix, '|', shell_complete, - [default_command](StringView cmdline, PromptEvent event, Context& context) { + [default_command, selection_edition=std::make_shared(context)] + (StringView cmdline, PromptEvent event, Context& context) { if (event != PromptEvent::Validate) return; @@ -1206,6 +1225,7 @@ void indent(Context& context, NormalParams params) ScopedEdition edition(context); auto& buffer = context.buffer(); LineCount last_line = 0; + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) { for (auto line = std::max(last_line, sel.min().line); line < sel.max().line+1; ++line) @@ -1230,6 +1250,7 @@ void deindent(Context& context, NormalParams params) auto& buffer = context.buffer(); LineCount last_line = 0; + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) { for (auto line = std::max(sel.min().line, last_line); @@ -1410,6 +1431,7 @@ void scroll(Context& context, NormalParams params) template void copy_selections_on_next_lines(Context& context, NormalParams params) { + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); auto& buffer = context.buffer(); const ColumnCount tabstop = context.options()["tabstop"].get(); @@ -1463,6 +1485,7 @@ template void rotate_selections(Context& context, NormalParams params) { const int count = params.count ? params.count : 1; + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); const int index = selections.main_index(); const int num = selections.size(); @@ -1480,6 +1503,7 @@ void rotate_selections_content(Context& context, NormalParams params) if (group == 0 or group > (int)strings.size()) group = (int)strings.size(); count = count % group; + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); auto main = strings.begin() + selections.main_index(); for (auto it = strings.begin(); it != strings.end(); ) @@ -1564,6 +1588,7 @@ void replay_macro(Context& context, NormalParams params) auto keys = parse_keys(reg_val[0]); ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; do { for (auto& key : keys) @@ -1581,6 +1606,7 @@ void jump(Context& context, NormalParams params) Buffer* oldbuf = &context.buffer(); Buffer& buffer = const_cast(jump.buffer()); + ScopedSelectionEdition selection_edition{context}; if (&buffer != oldbuf) context.change_buffer(buffer); context.selections_write_only() = jump; @@ -1595,6 +1621,7 @@ void push_selections(Context& context, NormalParams) void align(Context& context, NormalParams) { + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); auto& buffer = context.buffer(); const ColumnCount tabstop = context.options()["tabstop"].get(); @@ -1648,6 +1675,7 @@ void copy_indent(Context& context, NormalParams params) { int selection = params.count; auto& buffer = context.buffer(); + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); Vector lines; for (auto sel : selections) @@ -1688,6 +1716,7 @@ void tabs_to_spaces(Context& context, NormalParams params) const ColumnCount tabstop = params.count == 0 ? opt_tabstop : params.count; Vector tabs; Vector spaces; + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) { for (auto it = buffer.iterator_at(sel.min()), @@ -1712,6 +1741,7 @@ void spaces_to_tabs(Context& context, NormalParams params) const ColumnCount opt_tabstop = context.options()["tabstop"].get(); const ColumnCount tabstop = params.count == 0 ? opt_tabstop : params.count; Vector spaces; + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) { for (auto it = buffer.iterator_at(sel.min()), @@ -1745,6 +1775,7 @@ void spaces_to_tabs(Context& context, NormalParams params) void trim_selections(Context& context, NormalParams) { auto& buffer = context.buffer(); + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); Vector to_remove; @@ -1773,7 +1804,7 @@ void trim_selections(Context& context, NormalParams) selections.remove(i); } -SelectionList read_selections_from_register(char reg, Context& context) +SelectionList read_selections_from_register(char reg, const Context& context) { if (not is_basic_alpha(reg) and reg != '^') throw runtime_error("selections can only be saved to the '^' and alphabetic registers"); @@ -1868,6 +1899,7 @@ void combine_selections(Context& context, SelectionList list, Func func, StringV return; const auto op = key_to_combine_op(key); + ScopedSelectionEdition selection_edition{context}; auto& sels = context.selections(); list.update(); if (op == CombineOp::Append) @@ -1921,7 +1953,10 @@ void save_selections(Context& context, NormalParams params) }; if (combine and not empty) + { + ScopedSelectionEdition selection_edition{context}; combine_selections(context, read_selections_from_register(reg, context), save_to_reg, "combine selections to register"); + } else save_to_reg(context, context.selections()); } @@ -1932,6 +1967,8 @@ void restore_selections(Context& context, NormalParams params) const char reg = to_lower(params.reg ? params.reg : '^'); auto selections = read_selections_from_register(reg, context); + ScopedSelectionEdition selection_edition{context}; + auto set_selections = [reg](Context& context, SelectionList sels) { auto size = sels.size(); context.selections_write_only() = std::move(sels); @@ -2001,6 +2038,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, @@ -2015,6 +2066,7 @@ void exec_user_mappings(Context& context, NormalParams params) InputHandler::ScopedForceNormal force_normal{context.input_handler(), params}; ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; for (auto& key : mapping.keys) context.input_handler().handle_key(key); }, "user mapping", @@ -2027,6 +2079,7 @@ void add_empty_line(Context& context, NormalParams params) int count = std::max(params.count, 1); String new_lines{'\n', CharCount{count}}; auto& buffer = context.buffer(); + ScopedSelectionEdition selection_edition{context}; auto& sels = context.selections(); ScopedEdition edition{context}; for (int i = 0; i < sels.size(); ++i) @@ -2045,6 +2098,7 @@ public: void operator() (Context& context, NormalParams params) { ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; do { m_func(context, {0, params.reg}); } while(--params.count > 0); } private: @@ -2055,6 +2109,7 @@ template void repeated(Context& context, NormalParams params) { ScopedEdition edition(context); + ScopedSelectionEdition selection_edition{context}; do { func(context, {0, params.reg}); } while(--params.count > 0); } @@ -2064,6 +2119,7 @@ void move_cursor(Context& context, NormalParams params) kak_assert(mode == SelectMode::Replace or mode == SelectMode::Extend); const Type offset{direction * std::max(params.count,1)}; const ColumnCount tabstop = context.options()["tabstop"].get(); + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); for (auto& sel : selections) { @@ -2077,11 +2133,13 @@ void move_cursor(Context& context, NormalParams params) void select_whole_buffer(Context& context, NormalParams) { auto& buffer = context.buffer(); + ScopedSelectionEdition selection_edition{context}; context.selections_write_only() = SelectionList{buffer, {{0,0}, {buffer.back_coord(), max_column}}}; } void keep_selection(Context& context, NormalParams p) { + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); const int index = p.count ? p.count-1 : selections.main_index(); if (index >= selections.size()) @@ -2093,6 +2151,7 @@ void keep_selection(Context& context, NormalParams p) void remove_selection(Context& context, NormalParams p) { + ScopedSelectionEdition selection_edition{context}; auto& selections = context.selections(); const int index = p.count ? p.count-1 : selections.main_index(); if (index >= selections.size()) @@ -2106,12 +2165,14 @@ void remove_selection(Context& context, NormalParams p) void clear_selections(Context& context, NormalParams) { + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) sel.anchor() = sel.cursor(); } void flip_selections(Context& context, NormalParams) { + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) { const BufferCoord tmp = sel.anchor(); @@ -2123,6 +2184,7 @@ void flip_selections(Context& context, NormalParams) void ensure_forward(Context& context, NormalParams) { + ScopedSelectionEdition selection_edition{context}; for (auto& sel : context.selections()) { const BufferCoord min = sel.min(), max = sel.max(); @@ -2134,18 +2196,21 @@ void ensure_forward(Context& context, NormalParams) void merge_consecutive(Context& context, NormalParams params) { + ScopedSelectionEdition selection_edition{context}; ensure_forward(context, params); context.selections().merge_consecutive(); } void merge_overlapping(Context& context, NormalParams params) { + ScopedSelectionEdition selection_edition{context}; ensure_forward(context, params); context.selections().merge_overlapping(); } void duplicate_selections(Context& context, NormalParams params) { + ScopedSelectionEdition selection_edition{context}; SelectionList& sels = context.selections(); Vector new_sels; const int count = params.count ? params.count : 2; @@ -2312,6 +2377,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} },