Merge remote-tracking branch 'krobelus/undo-selection-change'

This commit is contained in:
Maxime Coste 2022-11-10 08:56:42 +11:00
commit 91d45a100a
8 changed files with 366 additions and 57 deletions

View File

@ -314,6 +314,12 @@ Yanking (copying) and pasting use the *"* register by default (See <<registers#,
*<a-U>*:: *<a-U>*::
move forward in history 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 align selections, align the cursor of each selection by inserting spaces
before the first character of each selection before the first character of each selection

View File

@ -169,7 +169,7 @@ DisplayLine Client::generate_mode_line() const
return modeline; 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) if (m_buffer_reload_dialog_opened)
close_buffer_reload_dialog(); close_buffer_reload_dialog();
@ -180,12 +180,20 @@ void Client::change_buffer(Buffer& buffer)
m_window->options().unregister_watcher(*this); m_window->options().unregister_watcher(*this);
m_window->set_client(nullptr); m_window->set_client(nullptr);
client_manager.add_free_window(std::move(m_window), client_manager.add_free_window(std::move(m_window),
std::move(context().selections())); context().selections());
m_window = std::move(ws.window); m_window = std::move(ws.window);
m_window->set_client(this); m_window->set_client(this);
m_window->options().register_watcher(*this); m_window->options().register_watcher(*this);
if (set_selections)
(*set_selections)();
else
{
ScopedSelectionEdition selection_edition{context()};
context().selections_write_only() = std::move(ws.selections); context().selections_write_only() = std::move(ws.selections);
}
context().set_window(*m_window); context().set_window(*m_window);
m_window->set_dimensions(m_ui->dimensions()); m_window->set_dimensions(m_ui->dimensions());

View File

@ -65,7 +65,7 @@ public:
InputHandler& input_handler() { return m_input_handler; } InputHandler& input_handler() { return m_input_handler; }
const InputHandler& input_handler() const { 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; StringView get_env_var(StringView name) const;

View File

@ -2020,6 +2020,7 @@ void context_wrap(const ParametersParser& parser, Context& context, StringView d
ScopedSetBool disable_history(c.history_disabled()); ScopedSetBool disable_history(c.history_disabled());
ScopedEdition edition{c}; ScopedEdition edition{c};
ScopedSelectionEdition selection_edition{c};
if (parser.get_switch("itersel")) if (parser.get_switch("itersel"))
{ {
@ -2534,6 +2535,7 @@ const CommandDesc select_cmd = {
else if (parser.get_switch("display-column")) else if (parser.get_switch("display-column"))
column_type = ColumnType::DisplayColumn; column_type = ColumnType::DisplayColumn;
ColumnCount tabstop = context.options()["tabstop"].get<int>(); ColumnCount tabstop = context.options()["tabstop"].get<int>();
ScopedSelectionEdition selection_edition{context};
context.selections_write_only() = selection_list_from_strings(buffer, column_type, parser.positionals_from(0), timestamp, 0, tabstop); context.selections_write_only() = selection_list_from_strings(buffer, column_type, parser.positionals_from(0), timestamp, 0, tabstop);
} }
}; };

View File

@ -16,17 +16,17 @@ Context::Context(InputHandler& input_handler, SelectionList selections,
Flags flags, String name) Flags flags, String name)
: m_flags(flags), : m_flags(flags),
m_input_handler{&input_handler}, m_input_handler{&input_handler},
m_selections{std::move(selections)}, m_selection_history{*this, std::move(selections)},
m_name(std::move(name)) m_name(std::move(name))
{} {}
Context::Context(EmptyContextFlag) {} Context::Context(EmptyContextFlag) : m_selection_history{*this} {}
Buffer& Context::buffer() const Buffer& Context::buffer() const
{ {
if (not has_buffer()) if (not has_buffer())
throw runtime_error("no buffer in context"); throw runtime_error("no buffer in context");
return const_cast<Buffer&>((*m_selections).buffer()); return const_cast<Buffer&>(selections(false).buffer());
} }
Window& Context::window() const 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<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()) if (has_buffer() and &buffer == &this->buffer())
return; return;
@ -176,12 +316,18 @@ void Context::change_buffer(Buffer& buffer)
{ {
client().info_hide(); client().info_hide();
client().menu_hide(); client().menu_hide();
client().change_buffer(buffer); client().change_buffer(buffer, std::move(set_selections));
} }
else else
{ {
m_window.reset(); 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()) if (has_input_handler())
@ -192,9 +338,8 @@ void Context::forget_buffer(Buffer& buffer)
{ {
m_jump_list.forget_buffer(buffer); m_jump_list.forget_buffer(buffer);
if (&this->buffer() != &buffer) if (&this->buffer() == &buffer)
return; {
if (is_editing() && has_input_handler()) if (is_editing() && has_input_handler())
input_handler().reset_normal_mode(); input_handler().reset_normal_mode();
@ -202,6 +347,9 @@ void Context::forget_buffer(Buffer& buffer)
change_buffer(last_buffer ? *last_buffer : BufferManager::instance().get_first_buffer()); change_buffer(last_buffer ? *last_buffer : BufferManager::instance().get_first_buffer());
} }
m_selection_history.forget_buffer(buffer);
}
Buffer* Context::last_buffer() const Buffer* Context::last_buffer() const
{ {
const auto jump_list = m_jump_list.get_as_list(); const auto jump_list = m_jump_list.get_as_list();
@ -223,24 +371,29 @@ Buffer* Context::last_buffer() const
return previous_buffer != jump_list.rend() ? &previous_buffer->buffer() : nullptr; return previous_buffer != jump_list.rend() ? &previous_buffer->buffer() : nullptr;
} }
SelectionList& Context::selections() SelectionList& Context::selections(bool update)
{ {
if (not m_selections) return m_selection_history.selections(update);
throw runtime_error("no selections in context"); }
(*m_selections).update();
return *m_selections; void Context::undo_selection_change()
{
m_selection_history.undo();
}
void Context::redo_selection_change()
{
m_selection_history.redo();
} }
SelectionList& Context::selections_write_only() SelectionList& Context::selections_write_only()
{ {
if (not m_selections) return selections(false);
throw runtime_error("no selections in context");
return *m_selections;
} }
const SelectionList& Context::selections() const const SelectionList& Context::selections(bool update) const
{ {
return const_cast<Context&>(*this).selections(); return const_cast<Context&>(*this).selections(update);
} }
Vector<String> Context::selections_content() const Vector<String> Context::selections_content() const
@ -277,7 +430,7 @@ void Context::end_edition()
StringView Context::main_sel_register_value(StringView reg) const 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); return RegisterManager::instance()[reg].get_main(*this, index);
} }

View File

@ -72,7 +72,7 @@ public:
Context& operator=(const Context&) = delete; Context& operator=(const Context&) = delete;
Buffer& buffer() const; Buffer& buffer() const;
bool has_buffer() const { return (bool)m_selections; } bool has_buffer() const { return not m_selection_history.empty(); }
Window& window() const; Window& window() const;
bool has_window() const { return (bool)m_window; } bool has_window() const { return (bool)m_window; }
@ -83,14 +83,18 @@ public:
InputHandler& input_handler() const; InputHandler& input_handler() const;
bool has_input_handler() const { return (bool)m_input_handler; } bool has_input_handler() const { return (bool)m_input_handler; }
SelectionList& selections(); SelectionList& selections(bool update = true);
const SelectionList& selections() const; const SelectionList& selections(bool update = true) const;
Vector<String> selections_content() const; Vector<String> selections_content() const;
// Return potentially out of date selections // Return potentially out of date selections
SelectionList& selections_write_only(); 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 forget_buffer(Buffer& buffer);
void set_client(Client& client); void set_client(Client& client);
@ -113,6 +117,7 @@ public:
bool is_editing() const { return m_edition_level!= 0; } bool is_editing() const { return m_edition_level!= 0; }
void disable_undo_handling() { m_edition_level = -1; } 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; } NestedBool& hooks_disabled() { return m_hooks_disabled; }
const NestedBool& hooks_disabled() const { return m_hooks_disabled; } const NestedBool& hooks_disabled() const { return m_hooks_disabled; }
@ -145,6 +150,7 @@ private:
size_t m_edition_timestamp = 0; size_t m_edition_timestamp = 0;
friend struct ScopedEdition; friend struct ScopedEdition;
friend struct ScopedSelectionEdition;
Flags m_flags = Flags::None; Flags m_flags = Flags::None;
@ -152,7 +158,45 @@ private:
SafePtr<Window> m_window; SafePtr<Window> m_window;
SafePtr<Client> m_client; 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; String m_name;
@ -180,5 +224,20 @@ private:
SafePtr<Buffer> m_buffer; SafePtr<Buffer> 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<Buffer> m_buffer;
};
} }
#endif // context_hh_INCLUDED #endif // context_hh_INCLUDED

View File

@ -93,59 +93,69 @@ struct MouseHandler
Buffer& buffer = context.buffer(); Buffer& buffer = context.buffer();
BufferCoord cursor; BufferCoord cursor;
auto& selections = context.selections();
constexpr auto modifiers = Key::Modifiers::Control | Key::Modifiers::Alt | Key::Modifiers::Shift | Key::Modifiers::MouseButtonMask; constexpr auto modifiers = Key::Modifiers::Control | Key::Modifiers::Alt | Key::Modifiers::Shift | Key::Modifiers::MouseButtonMask;
switch ((key.modifiers & ~modifiers).value) switch ((key.modifiers & ~modifiers).value)
{ {
case Key::Modifiers::MousePress: case Key::Modifiers::MousePress:
switch (key.mouse_button()) switch (key.mouse_button())
{ {
case Key::MouseButton::Right: case Key::MouseButton::Right: {
m_dragging = false; kak_assert(not context.is_editing_selection());
m_dragging.reset();
cursor = context.window().buffer_coord(key.coord()); cursor = context.window().buffer_coord(key.coord());
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections();
if (key.modifiers & Key::Modifiers::Control) if (key.modifiers & Key::Modifiers::Control)
selections = {{selections.begin()->anchor(), cursor}}; selections = {{selections.begin()->anchor(), cursor}};
else else
selections.main() = {selections.main().anchor(), cursor}; selections.main() = {selections.main().anchor(), cursor};
selections.sort_and_merge_overlapping(); selections.sort_and_merge_overlapping();
return true; return true;
}
case Key::MouseButton::Left: case Key::MouseButton::Left: {
m_dragging = true; kak_assert(not context.is_editing_selection());
m_dragging.reset(new ScopedSelectionEdition{context});
m_anchor = context.window().buffer_coord(key.coord()); m_anchor = context.window().buffer_coord(key.coord());
if (not (key.modifiers & Key::Modifiers::Control)) if (not (key.modifiers & Key::Modifiers::Control))
context.selections_write_only() = { buffer, m_anchor}; context.selections_write_only() = { buffer, m_anchor};
else else
{ {
auto& selections = context.selections();
size_t main = selections.size(); size_t main = selections.size();
selections.push_back({m_anchor}); selections.push_back({m_anchor});
selections.set_main_index(main); selections.set_main_index(main);
selections.sort_and_merge_overlapping(); selections.sort_and_merge_overlapping();
} }
return true; return true;
}
default: return true; default: return true;
} }
case Key::Modifiers::MouseRelease: case Key::Modifiers::MouseRelease: {
if (not m_dragging) if (not m_dragging)
return true; return true;
m_dragging = false; auto& selections = context.selections();
cursor = context.window().buffer_coord(key.coord()); cursor = context.window().buffer_coord(key.coord());
selections.main() = {buffer.clamp(m_anchor), cursor}; selections.main() = {buffer.clamp(m_anchor), cursor};
selections.sort_and_merge_overlapping(); selections.sort_and_merge_overlapping();
m_dragging.reset();
return true; return true;
}
case Key::Modifiers::MousePos: case Key::Modifiers::MousePos: {
if (not m_dragging) if (not m_dragging)
return true; return true;
cursor = context.window().buffer_coord(key.coord()); cursor = context.window().buffer_coord(key.coord());
auto& selections = context.selections();
selections.main() = {buffer.clamp(m_anchor), cursor}; selections.main() = {buffer.clamp(m_anchor), cursor};
selections.sort_and_merge_overlapping(); selections.sort_and_merge_overlapping();
return true; return true;
}
case Key::Modifiers::Scroll: case Key::Modifiers::Scroll:
scroll_window(context, static_cast<int32_t>(key.key), m_dragging); scroll_window(context, static_cast<int32_t>(key.key), (bool)m_dragging);
return true; return true;
default: return false; default: return false;
@ -153,7 +163,7 @@ struct MouseHandler
} }
private: private:
bool m_dragging = false; std::unique_ptr<ScopedSelectionEdition> m_dragging;
BufferCoord m_anchor; BufferCoord m_anchor;
}; };
@ -1199,6 +1209,7 @@ public:
Insert(InputHandler& input_handler, InsertMode mode, int count) Insert(InputHandler& input_handler, InsertMode mode, int count)
: InputMode(input_handler), : InputMode(input_handler),
m_edition(context()), m_edition(context()),
m_selection_edition(context()),
m_completer(context()), m_completer(context()),
m_restore_cursor(mode == InsertMode::Append), m_restore_cursor(mode == InsertMode::Append),
m_auto_complete{context().options()["autocomplete"].get<AutoComplete>() & AutoComplete::Insert}, m_auto_complete{context().options()["autocomplete"].get<AutoComplete>() & AutoComplete::Insert},
@ -1549,6 +1560,7 @@ private:
} }
ScopedEdition m_edition; ScopedEdition m_edition;
ScopedSelectionEdition m_selection_edition;
InsertCompleter m_completer; InsertCompleter m_completer;
const bool m_restore_cursor; const bool m_restore_cursor;
bool m_auto_complete; 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); win_pos.line = clamp(win_pos.line + offset, 0_line, line_count-1);
ScopedSelectionEdition selection_edition{context};
SelectionList& selections = context.selections(); SelectionList& selections = context.selections();
Selection& main_selection = selections.main(); Selection& main_selection = selections.main();
const BufferCoord anchor = main_selection.anchor(); const BufferCoord anchor = main_selection.anchor();

View File

@ -74,6 +74,7 @@ UnitTest test_merge_selection{[] {
template<SelectMode mode, typename T> template<SelectMode mode, typename T>
void select(Context& context, T func) void select(Context& context, T func)
{ {
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
if (mode == SelectMode::Append) if (mode == SelectMode::Append)
{ {
@ -140,8 +141,11 @@ void select_and_set_last(Context& context, Func&& func)
} }
template<SelectMode mode = SelectMode::Replace> template<SelectMode mode = SelectMode::Replace>
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); coord = buffer.clamp(coord);
if (mode == SelectMode::Replace) if (mode == SelectMode::Replace)
selections = SelectionList{ buffer, coord }; selections = SelectionList{ buffer, coord };
@ -212,7 +216,7 @@ void goto_commands(Context& context, NormalParams params)
if (params.count != 0) if (params.count != 0)
{ {
context.push_jump(); context.push_jump();
select_coord<mode>(context.buffer(), LineCount{params.count - 1}, context.selections()); select_coord<mode>(context, LineCount{params.count - 1});
if (context.has_window()) if (context.has_window())
context.window().center_line(LineCount{params.count-1}); context.window().center_line(LineCount{params.count-1});
} }
@ -229,7 +233,7 @@ void goto_commands(Context& context, NormalParams params)
case 'g': case 'g':
case 'k': case 'k':
context.push_jump(); context.push_jump();
select_coord<mode>(buffer, BufferCoord{0,0}, context.selections()); select_coord<mode>(context, BufferCoord{0,0});
break; break;
case 'l': case 'l':
select<mode, select_to_line_end<true>>(context, {}); select<mode, select_to_line_end<true>>(context, {});
@ -242,17 +246,17 @@ void goto_commands(Context& context, NormalParams params)
break; break;
case 'j': case 'j':
context.push_jump(); context.push_jump();
select_coord<mode>(buffer, buffer.line_count() - 1, context.selections()); select_coord<mode>(context, buffer.line_count() - 1);
break; break;
case 'e': case 'e':
context.push_jump(); context.push_jump();
select_coord<mode>(buffer, buffer.back_coord(), context.selections()); select_coord<mode>(context, buffer.back_coord());
break; break;
case 't': case 't':
if (context.has_window()) if (context.has_window())
{ {
auto line = context.window().position().line; auto line = context.window().position().line;
select_coord<mode>(buffer, line, context.selections()); select_coord<mode>(context, line);
} }
break; break;
case 'b': case 'b':
@ -260,7 +264,7 @@ void goto_commands(Context& context, NormalParams params)
{ {
auto& window = context.window(); auto& window = context.window();
auto line = window.position().line + window.dimensions().line - 1; auto line = window.position().line + window.dimensions().line - 1;
select_coord<mode>(buffer, line, context.selections()); select_coord<mode>(context, line);
} }
break; break;
case 'c': case 'c':
@ -268,7 +272,7 @@ void goto_commands(Context& context, NormalParams params)
{ {
auto& window = context.window(); auto& window = context.window();
auto line = window.position().line + window.dimensions().line / 2; auto line = window.position().line + window.dimensions().line / 2;
select_coord<mode>(buffer, line, context.selections()); select_coord<mode>(context, line);
} }
break; break;
case 'a': case 'a':
@ -328,7 +332,7 @@ void goto_commands(Context& context, NormalParams params)
throw runtime_error("no last modification position"); throw runtime_error("no last modification position");
if (*pos >= buffer.back_coord()) if (*pos >= buffer.back_coord())
pos = buffer.back_coord(); pos = buffer.back_coord();
select_coord<mode>(buffer, *pos, context.selections()); select_coord<mode>(context, *pos);
break; break;
} }
default: default:
@ -420,6 +424,7 @@ void replace_with_char(Context& context, NormalParams)
if (not cp or key == Key::Escape) if (not cp or key == Key::Escape)
return; return;
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
Buffer& buffer = context.buffer(); Buffer& buffer = context.buffer();
context.selections().for_each([&](size_t index, Selection& sel) { context.selections().for_each([&](size_t index, Selection& sel) {
CharCount count = char_length(buffer, sel); CharCount count = char_length(buffer, sel);
@ -440,6 +445,7 @@ void for_each_codepoint(Context& context, NormalParams)
using Utf8It = utf8::iterator<BufferIterator>; using Utf8It = utf8::iterator<BufferIterator>;
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
Buffer& buffer = context.buffer(); Buffer& buffer = context.buffer();
context.selections().for_each([&](size_t index, Selection& sel) { 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"], prompt, {}, default_command, context.faces()["Prompt"],
PromptFlags::DropHistoryEntriesWithBlankPrefix, '|', PromptFlags::DropHistoryEntriesWithBlankPrefix, '|',
shell_complete, shell_complete,
[default_command](StringView cmdline, PromptEvent event, Context& context) [default_command, selection_edition=std::make_shared<ScopedSelectionEdition>(context)]
(StringView cmdline, PromptEvent event, Context& context)
{ {
if (event != PromptEvent::Validate) if (event != PromptEvent::Validate)
return; return;
@ -635,6 +642,7 @@ void erase_selections(Context& context, NormalParams params)
RegisterManager::instance()[reg].set(context, context.selections_content()); RegisterManager::instance()[reg].set(context, context.selections_content());
} }
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
context.selections().erase(); context.selections().erase();
} }
@ -681,6 +689,7 @@ void paste(Context& context, NormalParams params)
auto& buffer = context.buffer(); auto& buffer = context.buffer();
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
context.selections().for_each([&](size_t index, Selection& sel) { context.selections().for_each([&](size_t index, Selection& sel) {
auto& str = strings[std::min(strings.size()-1, index)]; auto& str = strings[std::min(strings.size()-1, index)];
auto& min = sel.min(); auto& min = sel.min();
@ -717,6 +726,7 @@ void paste_all(Context& context, NormalParams params)
Buffer& buffer = context.buffer(); Buffer& buffer = context.buffer();
Vector<Selection> result; Vector<Selection> result;
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
{ {
ScopedEdition edition(context); ScopedEdition edition(context);
@ -749,7 +759,8 @@ void insert_output(Context& context, NormalParams params)
prompt, {}, default_command, context.faces()["Prompt"], prompt, {}, default_command, context.faces()["Prompt"],
PromptFlags::DropHistoryEntriesWithBlankPrefix, '|', PromptFlags::DropHistoryEntriesWithBlankPrefix, '|',
shell_complete, shell_complete,
[default_command](StringView cmdline, PromptEvent event, Context& context) [default_command, selection_edition=std::make_shared<ScopedSelectionEdition>(context)]
(StringView cmdline, PromptEvent event, Context& context)
{ {
if (event != PromptEvent::Validate) if (event != PromptEvent::Validate)
return; 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; }); [&](auto&& m) { candidates.push_back(m.candidate().str()); return true; });
return {(int)(word.begin() - regex.begin()), pos, std::move(candidates) }; 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<ScopedSelectionEdition>(context)]
(StringView str, PromptEvent event, Context& context) mutable {
try try
{ {
if (event != PromptEvent::Change and context.has_client()) if (event != PromptEvent::Change and context.has_client())
@ -941,6 +953,7 @@ void search_next(Context& context, NormalParams params)
if (not str.empty()) if (not str.empty())
{ {
Regex regex{str, direction_flags(regex_mode)}; Regex regex{str, direction_flags(regex_mode)};
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
bool main_wrapped = false; bool main_wrapped = false;
do { do {
@ -1042,6 +1055,7 @@ void split_regex(Context& context, NormalParams params)
void split_lines(Context& context, NormalParams params) void split_lines(Context& context, NormalParams params)
{ {
const LineCount count{params.count == 0 ? 1 : params.count}; const LineCount count{params.count == 0 ? 1 : params.count};
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
auto& buffer = context.buffer(); auto& buffer = context.buffer();
Vector<Selection> res; Vector<Selection> res;
@ -1068,6 +1082,7 @@ void split_lines(Context& context, NormalParams params)
void select_boundaries(Context& context, NormalParams) void select_boundaries(Context& context, NormalParams)
{ {
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
Vector<Selection> res; Vector<Selection> res;
for (auto& sel : selections) for (auto& sel : selections)
@ -1083,6 +1098,7 @@ void join_lines_select_spaces(Context& context, NormalParams)
{ {
auto& buffer = context.buffer(); auto& buffer = context.buffer();
Vector<Selection> selections; Vector<Selection> selections;
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
{ {
const LineCount min_line = sel.min().line; 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) void join_lines(Context& context, NormalParams params)
{ {
ScopedSelectionEdition selection_edition{context};
SelectionList sels{context.selections()}; SelectionList sels{context.selections()};
auto restore_sels = on_scope_end([&]{ auto restore_sels = on_scope_end([&]{
sels.update(); sels.update();
@ -1123,7 +1140,8 @@ void keep(Context& context, NormalParams params)
const char reg = to_lower(params.reg ? params.reg : '/'); const char reg = to_lower(params.reg ? params.reg : '/');
regex_prompt(context, prompt.str(), 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<ScopedSelectionEdition>(context)]
(const Regex& regex, PromptEvent event, Context& context) { (const Regex& regex, PromptEvent event, Context& context) {
RegisterManager::instance()[reg].restore(context, saved_reg); RegisterManager::instance()[reg].restore(context, saved_reg);
if (event == PromptEvent::Abort) if (event == PromptEvent::Abort)
@ -1160,7 +1178,8 @@ void keep_pipe(Context& context, NormalParams params)
context.input_handler().prompt( context.input_handler().prompt(
"keep pipe:", {}, default_command, context.faces()["Prompt"], "keep pipe:", {}, default_command, context.faces()["Prompt"],
PromptFlags::DropHistoryEntriesWithBlankPrefix, '|', shell_complete, PromptFlags::DropHistoryEntriesWithBlankPrefix, '|', shell_complete,
[default_command](StringView cmdline, PromptEvent event, Context& context) { [default_command, selection_edition=std::make_shared<ScopedSelectionEdition>(context)]
(StringView cmdline, PromptEvent event, Context& context) {
if (event != PromptEvent::Validate) if (event != PromptEvent::Validate)
return; return;
@ -1206,6 +1225,7 @@ void indent(Context& context, NormalParams params)
ScopedEdition edition(context); ScopedEdition edition(context);
auto& buffer = context.buffer(); auto& buffer = context.buffer();
LineCount last_line = 0; LineCount last_line = 0;
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
{ {
for (auto line = std::max(last_line, sel.min().line); line < sel.max().line+1; ++line) 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(); auto& buffer = context.buffer();
LineCount last_line = 0; LineCount last_line = 0;
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
{ {
for (auto line = std::max(sel.min().line, last_line); for (auto line = std::max(sel.min().line, last_line);
@ -1410,6 +1431,7 @@ void scroll(Context& context, NormalParams params)
template<Direction direction> template<Direction direction>
void copy_selections_on_next_lines(Context& context, NormalParams params) void copy_selections_on_next_lines(Context& context, NormalParams params)
{ {
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
auto& buffer = context.buffer(); auto& buffer = context.buffer();
const ColumnCount tabstop = context.options()["tabstop"].get<int>(); const ColumnCount tabstop = context.options()["tabstop"].get<int>();
@ -1463,6 +1485,7 @@ template<Direction direction>
void rotate_selections(Context& context, NormalParams params) void rotate_selections(Context& context, NormalParams params)
{ {
const int count = params.count ? params.count : 1; const int count = params.count ? params.count : 1;
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
const int index = selections.main_index(); const int index = selections.main_index();
const int num = selections.size(); 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()) if (group == 0 or group > (int)strings.size())
group = (int)strings.size(); group = (int)strings.size();
count = count % group; count = count % group;
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
auto main = strings.begin() + selections.main_index(); auto main = strings.begin() + selections.main_index();
for (auto it = strings.begin(); it != strings.end(); ) 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]); auto keys = parse_keys(reg_val[0]);
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
do do
{ {
for (auto& key : keys) for (auto& key : keys)
@ -1581,6 +1606,7 @@ void jump(Context& context, NormalParams params)
Buffer* oldbuf = &context.buffer(); Buffer* oldbuf = &context.buffer();
Buffer& buffer = const_cast<Buffer&>(jump.buffer()); Buffer& buffer = const_cast<Buffer&>(jump.buffer());
ScopedSelectionEdition selection_edition{context};
if (&buffer != oldbuf) if (&buffer != oldbuf)
context.change_buffer(buffer); context.change_buffer(buffer);
context.selections_write_only() = jump; context.selections_write_only() = jump;
@ -1595,6 +1621,7 @@ void push_selections(Context& context, NormalParams)
void align(Context& context, NormalParams) void align(Context& context, NormalParams)
{ {
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
auto& buffer = context.buffer(); auto& buffer = context.buffer();
const ColumnCount tabstop = context.options()["tabstop"].get<int>(); const ColumnCount tabstop = context.options()["tabstop"].get<int>();
@ -1648,6 +1675,7 @@ void copy_indent(Context& context, NormalParams params)
{ {
int selection = params.count; int selection = params.count;
auto& buffer = context.buffer(); auto& buffer = context.buffer();
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
Vector<LineCount> lines; Vector<LineCount> lines;
for (auto sel : selections) 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; const ColumnCount tabstop = params.count == 0 ? opt_tabstop : params.count;
Vector<Selection> tabs; Vector<Selection> tabs;
Vector<String> spaces; Vector<String> spaces;
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
{ {
for (auto it = buffer.iterator_at(sel.min()), 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<int>(); const ColumnCount opt_tabstop = context.options()["tabstop"].get<int>();
const ColumnCount tabstop = params.count == 0 ? opt_tabstop : params.count; const ColumnCount tabstop = params.count == 0 ? opt_tabstop : params.count;
Vector<Selection> spaces; Vector<Selection> spaces;
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
{ {
for (auto it = buffer.iterator_at(sel.min()), 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) void trim_selections(Context& context, NormalParams)
{ {
auto& buffer = context.buffer(); auto& buffer = context.buffer();
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
Vector<int> to_remove; Vector<int> to_remove;
@ -1773,7 +1804,7 @@ void trim_selections(Context& context, NormalParams)
selections.remove(i); 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 != '^') if (not is_basic_alpha(reg) and reg != '^')
throw runtime_error("selections can only be saved to the '^' and alphabetic registers"); 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; return;
const auto op = key_to_combine_op(key); const auto op = key_to_combine_op(key);
ScopedSelectionEdition selection_edition{context};
auto& sels = context.selections(); auto& sels = context.selections();
list.update(); list.update();
if (op == CombineOp::Append) if (op == CombineOp::Append)
@ -1921,7 +1953,10 @@ void save_selections(Context& context, NormalParams params)
}; };
if (combine and not empty) 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"); combine_selections(context, read_selections_from_register(reg, context), save_to_reg, "combine selections to register");
}
else else
save_to_reg(context, context.selections()); 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 : '^'); const char reg = to_lower(params.reg ? params.reg : '^');
auto selections = read_selections_from_register(reg, context); auto selections = read_selections_from_register(reg, context);
ScopedSelectionEdition selection_edition{context};
auto set_selections = [reg](Context& context, SelectionList sels) { auto set_selections = [reg](Context& context, SelectionList sels) {
auto size = sels.size(); auto size = sels.size();
context.selections_write_only() = std::move(sels); context.selections_write_only() = std::move(sels);
@ -2001,6 +2038,20 @@ void move_in_history(Context& context, NormalParams params)
history_id, max_history_id)); 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) void exec_user_mappings(Context& context, NormalParams params)
{ {
on_next_key_with_autoinfo(context, "user-mapping", KeymapMode::None, 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}; InputHandler::ScopedForceNormal force_normal{context.input_handler(), params};
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
for (auto& key : mapping.keys) for (auto& key : mapping.keys)
context.input_handler().handle_key(key); context.input_handler().handle_key(key);
}, "user mapping", }, "user mapping",
@ -2027,6 +2079,7 @@ void add_empty_line(Context& context, NormalParams params)
int count = std::max(params.count, 1); int count = std::max(params.count, 1);
String new_lines{'\n', CharCount{count}}; String new_lines{'\n', CharCount{count}};
auto& buffer = context.buffer(); auto& buffer = context.buffer();
ScopedSelectionEdition selection_edition{context};
auto& sels = context.selections(); auto& sels = context.selections();
ScopedEdition edition{context}; ScopedEdition edition{context};
for (int i = 0; i < sels.size(); ++i) for (int i = 0; i < sels.size(); ++i)
@ -2045,6 +2098,7 @@ public:
void operator() (Context& context, NormalParams params) void operator() (Context& context, NormalParams params)
{ {
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
do { m_func(context, {0, params.reg}); } while(--params.count > 0); do { m_func(context, {0, params.reg}); } while(--params.count > 0);
} }
private: private:
@ -2055,6 +2109,7 @@ template<void (*func)(Context&, NormalParams)>
void repeated(Context& context, NormalParams params) void repeated(Context& context, NormalParams params)
{ {
ScopedEdition edition(context); ScopedEdition edition(context);
ScopedSelectionEdition selection_edition{context};
do { func(context, {0, params.reg}); } while(--params.count > 0); 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); kak_assert(mode == SelectMode::Replace or mode == SelectMode::Extend);
const Type offset{direction * std::max(params.count,1)}; const Type offset{direction * std::max(params.count,1)};
const ColumnCount tabstop = context.options()["tabstop"].get<int>(); const ColumnCount tabstop = context.options()["tabstop"].get<int>();
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
for (auto& sel : selections) for (auto& sel : selections)
{ {
@ -2077,11 +2133,13 @@ void move_cursor(Context& context, NormalParams params)
void select_whole_buffer(Context& context, NormalParams) void select_whole_buffer(Context& context, NormalParams)
{ {
auto& buffer = context.buffer(); auto& buffer = context.buffer();
ScopedSelectionEdition selection_edition{context};
context.selections_write_only() = SelectionList{buffer, {{0,0}, {buffer.back_coord(), max_column}}}; context.selections_write_only() = SelectionList{buffer, {{0,0}, {buffer.back_coord(), max_column}}};
} }
void keep_selection(Context& context, NormalParams p) void keep_selection(Context& context, NormalParams p)
{ {
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
const int index = p.count ? p.count-1 : selections.main_index(); const int index = p.count ? p.count-1 : selections.main_index();
if (index >= selections.size()) if (index >= selections.size())
@ -2093,6 +2151,7 @@ void keep_selection(Context& context, NormalParams p)
void remove_selection(Context& context, NormalParams p) void remove_selection(Context& context, NormalParams p)
{ {
ScopedSelectionEdition selection_edition{context};
auto& selections = context.selections(); auto& selections = context.selections();
const int index = p.count ? p.count-1 : selections.main_index(); const int index = p.count ? p.count-1 : selections.main_index();
if (index >= selections.size()) if (index >= selections.size())
@ -2106,12 +2165,14 @@ void remove_selection(Context& context, NormalParams p)
void clear_selections(Context& context, NormalParams) void clear_selections(Context& context, NormalParams)
{ {
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
sel.anchor() = sel.cursor(); sel.anchor() = sel.cursor();
} }
void flip_selections(Context& context, NormalParams) void flip_selections(Context& context, NormalParams)
{ {
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
{ {
const BufferCoord tmp = sel.anchor(); const BufferCoord tmp = sel.anchor();
@ -2123,6 +2184,7 @@ void flip_selections(Context& context, NormalParams)
void ensure_forward(Context& context, NormalParams) void ensure_forward(Context& context, NormalParams)
{ {
ScopedSelectionEdition selection_edition{context};
for (auto& sel : context.selections()) for (auto& sel : context.selections())
{ {
const BufferCoord min = sel.min(), max = sel.max(); 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) void merge_consecutive(Context& context, NormalParams params)
{ {
ScopedSelectionEdition selection_edition{context};
ensure_forward(context, params); ensure_forward(context, params);
context.selections().merge_consecutive(); context.selections().merge_consecutive();
} }
void merge_overlapping(Context& context, NormalParams params) void merge_overlapping(Context& context, NormalParams params)
{ {
ScopedSelectionEdition selection_edition{context};
ensure_forward(context, params); ensure_forward(context, params);
context.selections().merge_overlapping(); context.selections().merge_overlapping();
} }
void duplicate_selections(Context& context, NormalParams params) void duplicate_selections(Context& context, NormalParams params)
{ {
ScopedSelectionEdition selection_edition{context};
SelectionList& sels = context.selections(); SelectionList& sels = context.selections();
Vector<Selection> new_sels; Vector<Selection> new_sels;
const int count = params.count ? params.count : 2; const int count = params.count ? params.count : 2;
@ -2312,6 +2377,9 @@ static constexpr HashMap<Key, NormalCmd, MemoryDomain::Undefined, KeymapBackend>
{ {alt('u')}, {"move backward in history", move_in_history<Direction::Backward>} }, { {alt('u')}, {"move backward in history", move_in_history<Direction::Backward>} },
{ {alt('U')}, {"move forward in history", move_in_history<Direction::Forward>} }, { {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('i')}, {"select inner object", select_object<ObjectFlags::ToBegin | ObjectFlags::ToEnd | ObjectFlags::Inner>} },
{ {alt('a')}, {"select whole object", select_object<ObjectFlags::ToBegin | ObjectFlags::ToEnd>} }, { {alt('a')}, {"select whole object", select_object<ObjectFlags::ToBegin | ObjectFlags::ToEnd>} },
{ {'['}, {"select to object start", select_object<ObjectFlags::ToBegin>} }, { {'['}, {"select to object start", select_object<ObjectFlags::ToBegin>} },