From dd4ba2ee88747616f7125024b192cec75cd7e58c Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sat, 6 Aug 2022 21:55:39 +0200 Subject: [PATCH] Prepare to record selection changes as perceived by the user To be able to undo selection changes, we want to record selections from all commands that modify selections. Each such command will get its own private copy of the selections object. This copy will live until the command is finished executing. All child commands that are run while the command is executing, will also use the same copy, because to the user it's all just one selection change anyway. Add an RAII object in all places where we might modify selections. The next commit will use this to create the private selections copy in the constructor (if there is none) and remove redundant history items in the destructor. We could avoid the RAII object in some places but that seems worse. For lifetimes that don't correspond to a lexical scope, we use a std::unique_ptr. For lambdas that require conversion to std::function, we use std::shared_ptr because we need something that's copyable. --- src/commands.cc | 2 ++ src/context.hh | 14 +++++++++++ src/input_handler.cc | 31 +++++++++++++++-------- src/normal.cc | 59 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 91 insertions(+), 15 deletions(-) diff --git a/src/commands.cc b/src/commands.cc index 6715fd05..f7d328e3 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -2009,6 +2009,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")) { @@ -2523,6 +2524,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.hh b/src/context.hh index 6a7480cc..a855d243 100644 --- a/src/context.hh +++ b/src/context.hh @@ -180,5 +180,19 @@ private: SafePtr m_buffer; }; +struct ScopedSelectionEdition +{ + ScopedSelectionEdition(Context& context) + : m_context{context}, + m_buffer{context.has_buffer() ? &context.buffer() : nullptr} {} + ScopedSelectionEdition(ScopedSelectionEdition&& other) : m_context{other.m_context}, m_buffer{other.m_buffer} + { other.m_buffer = nullptr; } + + ~ScopedSelectionEdition() {} +private: + Context& m_context; + SafePtr m_buffer; +}; + } #endif // context_hh_INCLUDED diff --git a/src/input_handler.cc b/src/input_handler.cc index 956c32d7..73ad276d 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -93,59 +93,67 @@ 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: { + 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: { + 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 +161,7 @@ struct MouseHandler } private: - bool m_dragging = false; + std::unique_ptr m_dragging; BufferCoord m_anchor; }; @@ -1199,6 +1207,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 +1558,7 @@ private: } ScopedEdition m_edition; + ScopedSelectionEdition m_selection_edition; InsertCompleter m_completer; const bool m_restore_cursor; bool m_auto_complete; @@ -1806,6 +1816,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 0a559b47..4873703a 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) { @@ -143,6 +144,7 @@ template 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) @@ -422,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); @@ -442,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) { @@ -555,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; @@ -637,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(); } @@ -683,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(); @@ -719,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); @@ -751,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; @@ -827,7 +836,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()) @@ -947,6 +957,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 { @@ -1048,6 +1059,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; @@ -1074,6 +1086,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) @@ -1089,6 +1102,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; @@ -1112,6 +1126,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(); @@ -1129,7 +1144,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) @@ -1166,7 +1182,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; @@ -1212,6 +1229,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) @@ -1236,6 +1254,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); @@ -1416,6 +1435,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(); @@ -1469,6 +1489,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(); @@ -1486,6 +1507,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(); ) @@ -1570,6 +1592,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) @@ -1587,6 +1610,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; @@ -1601,6 +1625,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(); @@ -1654,6 +1679,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) @@ -1694,6 +1720,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()), @@ -1718,6 +1745,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()), @@ -1751,6 +1779,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; @@ -1874,6 +1903,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) @@ -1927,7 +1957,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()); } @@ -1938,6 +1971,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); @@ -2021,6 +2056,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", @@ -2033,6 +2069,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) @@ -2051,6 +2088,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: @@ -2061,6 +2099,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); } @@ -2070,6 +2109,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) { @@ -2083,11 +2123,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()) @@ -2099,6 +2141,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()) @@ -2112,12 +2155,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(); @@ -2129,6 +2174,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(); @@ -2140,18 +2186,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;