LibGUI: Add visual line mode to VimEditingEngine

Applications using the Vim emulation engine now support line-wise text
selection.

We already have support for character-wise text selection, by pressing
`v` from normal mode.

However now can also trigger line-wise text selection by pressing
`shift+v` from normal mode, and then using vertical motion commands
(e.g. `j` or `k`) to expand the selection. This is a standard vim
feature.

In visual line mode the following operations are supported:

  * `escape`: back to normal mode
  * `u`: convert to lowercase
  * `U`: convert to uppercase
  * `~`: toggle case
  * `ctrl+d`: move down by 50% of page height
  * `ctrl+u`: move up by 50% of page height
  * `d` or `x`: delete selection
  * `c`: change selection
  * `y`: copy selection
  * `page up`: move up by 100% of page height
  * `page down`: move down by 100% of page height

Notably I didn't implement pressing `v` to go to regular
(character-wise) visual mode straight from visual line mode. This is
tricky to implement in the current code base, and there's an
alternative, which is to take a detour via normal mode.
This commit is contained in:
Robbie Vanbrabant 2022-08-25 15:27:49 +01:00 committed by Andreas Kling
parent f8e82da4b4
commit 26a3b42a15
2 changed files with 146 additions and 1 deletions

View file

@ -780,6 +780,8 @@ bool VimEditingEngine::on_key(KeyEvent const& event)
return on_key_in_insert_mode(event);
case (VimMode::Visual):
return on_key_in_visual_mode(event);
case (VimMode::VisualLine):
return on_key_in_visual_line_mode(event);
case (VimMode::Normal):
return on_key_in_normal_mode(event);
default:
@ -1003,6 +1005,9 @@ bool VimEditingEngine::on_key_in_normal_mode(KeyEvent const& event)
case (KeyCode::Key_P):
put_before();
break;
case (KeyCode::Key_V):
switch_to_visual_line_mode();
return true;
default:
break;
}
@ -1234,6 +1239,127 @@ bool VimEditingEngine::on_key_in_visual_mode(KeyEvent const& event)
return true;
}
bool VimEditingEngine::on_key_in_visual_line_mode(KeyEvent const& event)
{
// If the motion state machine requires the next character, feed it.
if (m_motion.should_consume_next_character()) {
m_motion.add_key_code(event.key(), event.ctrl(), event.shift(), event.alt());
if (m_motion.is_complete()) {
if (!m_motion.is_cancelled()) {
auto maybe_new_position = m_motion.get_position(*this, true);
if (maybe_new_position.has_value()) {
auto new_position = maybe_new_position.value();
m_editor->set_cursor(new_position);
update_selection_on_cursor_move();
}
}
m_motion.reset();
}
return true;
}
// Handle first any key codes that are to be applied regardless of modifiers.
switch (event.key()) {
case (KeyCode::Key_Escape):
switch_to_normal_mode();
if (m_editor->on_escape_pressed)
m_editor->on_escape_pressed();
return true;
default:
break;
}
// SHIFT is pressed.
if (event.shift() && !event.ctrl() && !event.alt()) {
switch (event.key()) {
case (KeyCode::Key_U):
casefold_selection(Casing::Uppercase);
switch_to_normal_mode();
return true;
case (KeyCode::Key_Tilde):
casefold_selection(Casing::Invertcase);
switch_to_normal_mode();
return true;
default:
break;
}
}
// CTRL is pressed.
if (event.ctrl() && !event.shift() && !event.alt()) {
switch (event.key()) {
case (KeyCode::Key_D):
move_half_page_down();
update_selection_on_cursor_move();
return true;
case (KeyCode::Key_U):
move_half_page_up();
update_selection_on_cursor_move();
return true;
default:
break;
}
}
// No modifier is pressed.
if (!event.ctrl() && !event.shift() && !event.alt()) {
switch (event.key()) {
case (KeyCode::Key_D):
yank(m_editor->selection(), Line);
m_editor->do_delete();
switch_to_normal_mode();
return true;
case (KeyCode::Key_X):
yank(m_editor->selection(), Line);
m_editor->do_delete();
switch_to_normal_mode();
return true;
case (KeyCode::Key_C):
yank(m_editor->selection(), Line);
m_editor->do_delete();
switch_to_insert_mode();
return true;
case (KeyCode::Key_Y):
yank(m_editor->selection(), Line);
switch_to_normal_mode();
return true;
case (KeyCode::Key_U):
casefold_selection(Casing::Lowercase);
switch_to_normal_mode();
return true;
case (KeyCode::Key_PageUp):
move_page_up();
update_selection_on_cursor_move();
return true;
case (KeyCode::Key_PageDown):
move_page_down();
update_selection_on_cursor_move();
return true;
default:
break;
}
}
// By default, we feed the motion state machine.
m_motion.add_key_code(event.key(), event.ctrl(), event.shift(), event.alt());
if (m_motion.is_complete()) {
if (!m_motion.is_cancelled()) {
auto maybe_new_position = m_motion.get_position(*this, true);
if (maybe_new_position.has_value()) {
auto new_position = maybe_new_position.value();
m_editor->set_cursor(new_position);
update_selection_on_cursor_move();
}
}
m_motion.reset();
}
return true;
}
void VimEditingEngine::switch_to_normal_mode()
{
m_vim_mode = VimMode::Normal;
@ -1263,6 +1389,17 @@ void VimEditingEngine::switch_to_visual_mode()
m_motion.reset();
}
void VimEditingEngine::switch_to_visual_line_mode()
{
m_vim_mode = VimMode::VisualLine;
m_editor->reset_cursor_blink();
m_previous_key = {};
m_selection_start_position = TextPosition { m_editor->cursor().line(), 0 };
m_editor->selection().set(m_selection_start_position, { m_editor->cursor().line(), m_editor->current_line().length() });
m_editor->did_update_selection();
m_motion.reset();
}
void VimEditingEngine::update_selection_on_cursor_move()
{
auto cursor = m_editor->cursor();
@ -1276,6 +1413,11 @@ void VimEditingEngine::update_selection_on_cursor_move()
end.set_column(end.column() + 1);
}
if (m_vim_mode == VimMode::VisualLine) {
start = TextPosition { start.line(), 0 };
end = TextPosition { end.line(), m_editor->line(end.line()).length() };
}
m_editor->selection().set(start, end);
m_editor->did_update_selection();
}

View file

@ -152,7 +152,8 @@ private:
enum VimMode {
Normal,
Insert,
Visual
Visual,
VisualLine
};
enum YankType {
@ -185,6 +186,7 @@ private:
void switch_to_normal_mode();
void switch_to_insert_mode();
void switch_to_visual_mode();
void switch_to_visual_line_mode();
void move_half_page_up();
void move_half_page_down();
void move_to_previous_empty_lines_block();
@ -193,6 +195,7 @@ private:
bool on_key_in_insert_mode(KeyEvent const& event);
bool on_key_in_normal_mode(KeyEvent const& event);
bool on_key_in_visual_mode(KeyEvent const& event);
bool on_key_in_visual_line_mode(KeyEvent const& event);
void casefold_selection(Casing);