Merge remote-tracking branch 'krobelus/undo-selection-change'
This commit is contained in:
commit
91d45a100a
|
@ -314,6 +314,12 @@ Yanking (copying) and pasting use the *"* register by default (See <<registers#,
|
|||
*<a-U>*::
|
||||
move forward in history
|
||||
|
||||
*<c-h>*::
|
||||
undo last selection change
|
||||
|
||||
*<c-k>*::
|
||||
redo last selection change
|
||||
|
||||
*&*::
|
||||
align selections, align the cursor of each selection by inserting spaces
|
||||
before the first character of each selection
|
||||
|
|
|
@ -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<FunctionRef<void()>> 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());
|
||||
|
|
|
@ -65,7 +65,7 @@ public:
|
|||
InputHandler& input_handler() { return m_input_handler; }
|
||||
const InputHandler& input_handler() const { return m_input_handler; }
|
||||
|
||||
void change_buffer(Buffer& buffer);
|
||||
void change_buffer(Buffer& buffer, Optional<FunctionRef<void()>> set_selection);
|
||||
|
||||
StringView get_env_var(StringView name) const;
|
||||
|
||||
|
|
|
@ -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<int>();
|
||||
ScopedSelectionEdition selection_edition{context};
|
||||
context.selections_write_only() = selection_list_from_strings(buffer, column_type, parser.positionals_from(0), timestamp, 0, tabstop);
|
||||
}
|
||||
};
|
||||
|
|
199
src/context.cc
199
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<Buffer&>((*m_selections).buffer());
|
||||
return const_cast<Buffer&>(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<HistoryId, MemoryDomain::Selections> new_ids;
|
||||
size_t bias = 0;
|
||||
for (size_t i = 0; i < m_history.size(); ++i)
|
||||
{
|
||||
auto& node = history_node((HistoryId)i);
|
||||
HistoryId id;
|
||||
if (&node.selections.buffer() == &buffer)
|
||||
{
|
||||
id = HistoryId::Invalid;
|
||||
++bias;
|
||||
}
|
||||
else
|
||||
id = (HistoryId)(i - bias);
|
||||
new_ids.push_back(id);
|
||||
}
|
||||
auto new_id = [&new_ids](HistoryId old_id) -> HistoryId {
|
||||
return old_id == HistoryId::Invalid ? HistoryId::Invalid : new_ids[(size_t)old_id];
|
||||
};
|
||||
|
||||
m_history.erase(remove_if(m_history, [&buffer](const auto& node) {
|
||||
return &node.selections.buffer() == &buffer;
|
||||
}), m_history.end());
|
||||
|
||||
for (auto& node : m_history)
|
||||
{
|
||||
node.parent = new_id(node.parent);
|
||||
node.redo_child = new_id(node.redo_child);
|
||||
}
|
||||
m_history_id = new_id(m_history_id);
|
||||
if (m_staging)
|
||||
{
|
||||
m_staging->parent = new_id(m_staging->parent);
|
||||
kak_assert(m_staging->redo_child == HistoryId::Invalid);
|
||||
}
|
||||
kak_assert(m_history_id != HistoryId::Invalid or m_staging);
|
||||
}
|
||||
|
||||
void Context::change_buffer(Buffer& buffer, Optional<FunctionRef<void()>> set_selections)
|
||||
{
|
||||
if (has_buffer() and &buffer == &this->buffer())
|
||||
return;
|
||||
|
@ -176,12 +316,18 @@ void Context::change_buffer(Buffer& buffer)
|
|||
{
|
||||
client().info_hide();
|
||||
client().menu_hide();
|
||||
client().change_buffer(buffer);
|
||||
client().change_buffer(buffer, std::move(set_selections));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_window.reset();
|
||||
m_selections = SelectionList{buffer, Selection{}};
|
||||
if (m_selection_history.empty())
|
||||
m_selection_history.initialize(SelectionList{buffer, Selection{}});
|
||||
else
|
||||
{
|
||||
ScopedSelectionEdition selection_edition{*this};
|
||||
selections_write_only() = SelectionList{buffer, Selection{}};
|
||||
}
|
||||
}
|
||||
|
||||
if (has_input_handler())
|
||||
|
@ -192,14 +338,16 @@ void Context::forget_buffer(Buffer& buffer)
|
|||
{
|
||||
m_jump_list.forget_buffer(buffer);
|
||||
|
||||
if (&this->buffer() != &buffer)
|
||||
return;
|
||||
if (&this->buffer() == &buffer)
|
||||
{
|
||||
if (is_editing() && has_input_handler())
|
||||
input_handler().reset_normal_mode();
|
||||
|
||||
if (is_editing() && has_input_handler())
|
||||
input_handler().reset_normal_mode();
|
||||
auto last_buffer = this->last_buffer();
|
||||
change_buffer(last_buffer ? *last_buffer : BufferManager::instance().get_first_buffer());
|
||||
}
|
||||
|
||||
auto last_buffer = this->last_buffer();
|
||||
change_buffer(last_buffer ? *last_buffer : BufferManager::instance().get_first_buffer());
|
||||
m_selection_history.forget_buffer(buffer);
|
||||
}
|
||||
|
||||
Buffer* Context::last_buffer() const
|
||||
|
@ -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<Context&>(*this).selections();
|
||||
return const_cast<Context&>(*this).selections(update);
|
||||
}
|
||||
|
||||
Vector<String> 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String> 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<FunctionRef<void()>> set_selection = {});
|
||||
void forget_buffer(Buffer& buffer);
|
||||
|
||||
void set_client(Client& client);
|
||||
|
@ -113,6 +117,7 @@ public:
|
|||
|
||||
bool is_editing() const { return m_edition_level!= 0; }
|
||||
void disable_undo_handling() { m_edition_level = -1; }
|
||||
bool is_editing_selection() const { return m_selection_history.in_edition(); }
|
||||
|
||||
NestedBool& hooks_disabled() { return m_hooks_disabled; }
|
||||
const NestedBool& hooks_disabled() const { return m_hooks_disabled; }
|
||||
|
@ -145,6 +150,7 @@ private:
|
|||
size_t m_edition_timestamp = 0;
|
||||
|
||||
friend struct ScopedEdition;
|
||||
friend struct ScopedSelectionEdition;
|
||||
|
||||
Flags m_flags = Flags::None;
|
||||
|
||||
|
@ -152,7 +158,45 @@ private:
|
|||
SafePtr<Window> m_window;
|
||||
SafePtr<Client> m_client;
|
||||
|
||||
Optional<SelectionList> m_selections;
|
||||
class SelectionHistory {
|
||||
public:
|
||||
SelectionHistory(Context& context);
|
||||
SelectionHistory(Context& context, SelectionList selections);
|
||||
void initialize(SelectionList selections);
|
||||
bool empty() const { return m_history.empty() and not m_staging; }
|
||||
SelectionList& selections(bool update = true);
|
||||
|
||||
void begin_edition();
|
||||
void end_edition();
|
||||
bool in_edition() const { return m_in_edition; }
|
||||
|
||||
void undo();
|
||||
void redo();
|
||||
void forget_buffer(Buffer& buffer);
|
||||
private:
|
||||
enum class HistoryId : size_t { First = 0, Invalid = (size_t)-1 };
|
||||
|
||||
struct HistoryNode
|
||||
{
|
||||
HistoryNode(SelectionList selections, HistoryId parent) : selections(selections), parent(parent) {}
|
||||
|
||||
SelectionList selections;
|
||||
HistoryId parent;
|
||||
HistoryId redo_child = HistoryId::Invalid;
|
||||
};
|
||||
|
||||
HistoryId next_history_id() const noexcept { return (HistoryId)m_history.size(); }
|
||||
HistoryNode& history_node(HistoryId id) { return m_history[(size_t)id]; }
|
||||
const HistoryNode& history_node(HistoryId id) const { return m_history[(size_t)id]; }
|
||||
HistoryNode& current_history_node() { kak_assert((size_t)m_history_id < m_history.size()); return m_history[(size_t)m_history_id]; }
|
||||
|
||||
Context& m_context;
|
||||
Vector<HistoryNode> m_history;
|
||||
HistoryId m_history_id = HistoryId::Invalid;
|
||||
Optional<HistoryNode> m_staging;
|
||||
NestedBool m_in_edition;
|
||||
};
|
||||
SelectionHistory m_selection_history;
|
||||
|
||||
String m_name;
|
||||
|
||||
|
@ -180,5 +224,20 @@ private:
|
|||
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
|
||||
|
|
|
@ -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<int32_t>(key.key), m_dragging);
|
||||
scroll_window(context, static_cast<int32_t>(key.key), (bool)m_dragging);
|
||||
return true;
|
||||
|
||||
default: return false;
|
||||
|
@ -153,7 +163,7 @@ struct MouseHandler
|
|||
}
|
||||
|
||||
private:
|
||||
bool m_dragging = false;
|
||||
std::unique_ptr<ScopedSelectionEdition> 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>() & 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();
|
||||
|
|
|
@ -74,6 +74,7 @@ UnitTest test_merge_selection{[] {
|
|||
template<SelectMode mode, typename T>
|
||||
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<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);
|
||||
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<mode>(context.buffer(), LineCount{params.count - 1}, context.selections());
|
||||
select_coord<mode>(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<mode>(buffer, BufferCoord{0,0}, context.selections());
|
||||
select_coord<mode>(context, BufferCoord{0,0});
|
||||
break;
|
||||
case 'l':
|
||||
select<mode, select_to_line_end<true>>(context, {});
|
||||
|
@ -242,17 +246,17 @@ void goto_commands(Context& context, NormalParams params)
|
|||
break;
|
||||
case 'j':
|
||||
context.push_jump();
|
||||
select_coord<mode>(buffer, buffer.line_count() - 1, context.selections());
|
||||
select_coord<mode>(context, buffer.line_count() - 1);
|
||||
break;
|
||||
case 'e':
|
||||
context.push_jump();
|
||||
select_coord<mode>(buffer, buffer.back_coord(), context.selections());
|
||||
select_coord<mode>(context, buffer.back_coord());
|
||||
break;
|
||||
case 't':
|
||||
if (context.has_window())
|
||||
{
|
||||
auto line = context.window().position().line;
|
||||
select_coord<mode>(buffer, line, context.selections());
|
||||
select_coord<mode>(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<mode>(buffer, line, context.selections());
|
||||
select_coord<mode>(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<mode>(buffer, line, context.selections());
|
||||
select_coord<mode>(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<mode>(buffer, *pos, context.selections());
|
||||
select_coord<mode>(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<BufferIterator>;
|
||||
|
||||
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<ScopedSelectionEdition>(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<Selection> 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<ScopedSelectionEdition>(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<ScopedSelectionEdition>(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<Selection> 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<Selection> res;
|
||||
for (auto& sel : selections)
|
||||
|
@ -1083,6 +1098,7 @@ void join_lines_select_spaces(Context& context, NormalParams)
|
|||
{
|
||||
auto& buffer = context.buffer();
|
||||
Vector<Selection> 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<ScopedSelectionEdition>(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<ScopedSelectionEdition>(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<Direction direction>
|
||||
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<int>();
|
||||
|
@ -1463,6 +1485,7 @@ template<Direction direction>
|
|||
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<Buffer&>(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<int>();
|
||||
|
@ -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<LineCount> 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<Selection> tabs;
|
||||
Vector<String> 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<int>();
|
||||
const ColumnCount tabstop = params.count == 0 ? opt_tabstop : params.count;
|
||||
Vector<Selection> 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<int> 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 (*func)(Context&, NormalParams)>
|
|||
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<int>();
|
||||
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<Selection> new_sels;
|
||||
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 forward in history", move_in_history<Direction::Forward>} },
|
||||
|
||||
{ {ctrl('h')}, {"undo selection change", undo_selection_change} },
|
||||
{ {ctrl('k')}, {"redo selection change", redo_selection_change} },
|
||||
|
||||
{ {alt('i')}, {"select inner object", select_object<ObjectFlags::ToBegin | ObjectFlags::ToEnd | ObjectFlags::Inner>} },
|
||||
{ {alt('a')}, {"select whole object", select_object<ObjectFlags::ToBegin | ObjectFlags::ToEnd>} },
|
||||
{ {'['}, {"select to object start", select_object<ObjectFlags::ToBegin>} },
|
||||
|
|
Loading…
Reference in New Issue
Block a user