Vim: Add change word and delete word functionality

Add the functionality of key sequences 'cw', 'ce', 'cb', 'dw', 'de' and 'db'.
This commit is contained in:
Zac 2021-01-27 21:57:39 +10:00 committed by Andreas Kling
parent e2d7945e0c
commit aaf691c4ef
Notes: sideshowbarker 2024-07-18 22:48:02 +09:00
5 changed files with 129 additions and 28 deletions

View file

@ -387,7 +387,7 @@ void EditingEngine::get_selection_line_boundaries(size_t& first_line, size_t& la
last_line -= 1;
}
void EditingEngine::move_to_beginning_of_next_word()
TextPosition EditingEngine::find_beginning_of_next_word()
{
/* The rules that have been coded in:
* Jump to the next punct or alnum after any whitespace
@ -413,7 +413,7 @@ void EditingEngine::move_to_beginning_of_next_word()
auto& line = lines.at(line_index);
if (line.is_empty() && !is_first_line) {
return m_editor->set_cursor({ line_index, 0 });
return { line_index, 0 };
} else if (line.is_empty()) {
has_seen_whitespace = true;
}
@ -427,24 +427,22 @@ void EditingEngine::move_to_beginning_of_next_word()
const u32 current_char = line_chars[column_index];
if (started_on_punct && vim_isalnum(current_char)) {
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
}
if (vim_ispunct(current_char) && !started_on_punct) {
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
}
if (isspace(current_char))
has_seen_whitespace = true;
if (has_seen_whitespace && (vim_isalnum(current_char) || vim_ispunct(current_char))) {
m_editor->set_cursor({ line_index, column_index });
return;
return { line_index, column_index };
}
if (line_index == lines.size() - 1 && column_index == line.length() - 1) {
m_editor->set_cursor({ line_index, column_index });
return;
return { line_index, column_index };
}
// Implicit newline
@ -452,11 +450,16 @@ void EditingEngine::move_to_beginning_of_next_word()
has_seen_whitespace = true;
}
}
ASSERT_NOT_REACHED();
}
void EditingEngine::move_to_end_of_next_word()
void EditingEngine::move_to_beginning_of_next_word()
{
m_editor->set_cursor(find_beginning_of_next_word());
}
TextPosition EditingEngine::find_end_of_next_word()
{
/* The rules that have been coded in:
* If the current_char is alnum and the next is whitespace or punct
* If the current_char is punct and the next is whitespace or alnum
@ -479,7 +482,7 @@ void EditingEngine::move_to_end_of_next_word()
auto& line = lines.at(line_index);
if (line.is_empty() && !is_first_line) {
return m_editor->set_cursor({ line_index, 0 });
return { line_index, 0 };
}
is_first_line = false;
@ -492,7 +495,7 @@ void EditingEngine::move_to_end_of_next_word()
const u32 current_char = line_chars[column_index];
if (column_index == lines.at(line_index).length() - 1 && !is_first_iteration && (vim_isalnum(current_char) || vim_ispunct(current_char)))
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
else if (column_index == lines.at(line_index).length() - 1) {
is_first_iteration = false;
continue;
@ -501,21 +504,28 @@ void EditingEngine::move_to_end_of_next_word()
const u32 next_char = line_chars[column_index + 1];
if (!is_first_iteration && vim_isalnum(current_char) && (isspace(next_char) || vim_ispunct(next_char)))
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
if (!is_first_iteration && vim_ispunct(current_char) && (isspace(next_char) || vim_isalnum(next_char)))
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
if (line_index == lines.size() - 1 && column_index == line.length() - 1) {
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
}
is_first_iteration = false;
}
}
ASSERT_NOT_REACHED();
}
void EditingEngine::move_to_end_of_previous_word()
void EditingEngine::move_to_end_of_next_word()
{
m_editor->set_cursor(find_end_of_next_word());
}
TextPosition EditingEngine::find_end_of_previous_word()
{
auto vim_isalnum = [](int c) {
return c == '_' || isalnum(c);
@ -534,7 +544,7 @@ void EditingEngine::move_to_end_of_previous_word()
auto& line = lines.at(line_index);
if (line.is_empty() && !is_first_line) {
return m_editor->set_cursor({ line_index, 0 });
return { line_index, 0 };
} else if (line.is_empty()) {
has_seen_whitespace = true;
}
@ -550,11 +560,11 @@ void EditingEngine::move_to_end_of_previous_word()
const u32 current_char = line_chars[column_index];
if (started_on_punct && vim_isalnum(current_char)) {
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
}
if (vim_ispunct(current_char) && !started_on_punct) {
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
}
if (isspace(current_char)) {
@ -562,13 +572,11 @@ void EditingEngine::move_to_end_of_previous_word()
}
if (has_seen_whitespace && (vim_isalnum(current_char) || vim_ispunct(current_char))) {
m_editor->set_cursor({ line_index, column_index });
return;
return { line_index, column_index };
}
if (line_index == 0 && column_index == 0) {
m_editor->set_cursor({ line_index, column_index });
return;
return { line_index, column_index };
}
// Implicit newline when wrapping back up to the end of the previous line.
@ -576,9 +584,15 @@ void EditingEngine::move_to_end_of_previous_word()
has_seen_whitespace = true;
}
}
ASSERT_NOT_REACHED();
}
void EditingEngine::move_to_beginning_of_previous_word()
void EditingEngine::move_to_end_of_previous_word()
{
m_editor->set_cursor(find_end_of_previous_word());
}
TextPosition EditingEngine::find_beginning_of_previous_word()
{
auto vim_isalnum = [](int c) {
return c == '_' || isalnum(c);
@ -596,7 +610,7 @@ void EditingEngine::move_to_beginning_of_previous_word()
auto& line = lines.at(line_index);
if (line.is_empty() && !is_first_iterated_line) {
return m_editor->set_cursor({ line_index, 0 });
return { line_index, 0 };
}
is_first_iterated_line = false;
@ -615,9 +629,9 @@ void EditingEngine::move_to_beginning_of_previous_word()
const u32 current_char = line_chars[column_index];
if (column_index == 0 && !is_first_iteration && (vim_isalnum(current_char) || vim_ispunct(current_char))) {
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
} else if (line_index == 0 && column_index == 0) {
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
} else if (column_index == 0 && is_first_iteration) {
is_first_iteration = false;
continue;
@ -626,14 +640,20 @@ void EditingEngine::move_to_beginning_of_previous_word()
const u32 next_char = line_chars[column_index - 1];
if (!is_first_iteration && vim_isalnum(current_char) && (isspace(next_char) || vim_ispunct(next_char)))
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
if (!is_first_iteration && vim_ispunct(current_char) && (isspace(next_char) || vim_isalnum(next_char)))
return m_editor->set_cursor({ line_index, column_index });
return { line_index, column_index };
is_first_iteration = false;
}
}
ASSERT_NOT_REACHED();
}
void EditingEngine::move_to_beginning_of_previous_word()
{
m_editor->set_cursor(find_beginning_of_previous_word());
}
void EditingEngine::move_selected_lines_up()

View file

@ -68,9 +68,13 @@ protected:
void move_page_down(const KeyEvent& event);
void move_to_first_line();
void move_to_last_line();
TextPosition find_beginning_of_next_word();
void move_to_beginning_of_next_word();
TextPosition find_end_of_next_word();
void move_to_end_of_next_word();
TextPosition find_end_of_previous_word();
void move_to_end_of_previous_word();
TextPosition find_beginning_of_previous_word();
void move_to_beginning_of_previous_word();
void move_up(const KeyEvent& event, double page_height_factor);

View file

@ -1130,6 +1130,15 @@ void TextEditor::delete_selection()
update();
}
void TextEditor::delete_text_range(TextRange range)
{
auto normalized_range = range.normalized();
execute<RemoveTextCommand>(document().text_in_range(normalized_range), normalized_range);
did_change();
set_cursor(normalized_range.start());
update();
}
void TextEditor::insert_at_cursor_or_replace_selection(const StringView& text)
{
ReflowDeferrer defer(*this);

View file

@ -193,6 +193,8 @@ public:
Gfx::IntRect cursor_content_rect() const;
TextPosition text_position_at_content_position(const Gfx::IntPoint&) const;
void delete_text_range(TextRange);
protected:
explicit TextEditor(Type = Type::MultiLine);

View file

@ -65,10 +65,41 @@ bool VimEditingEngine::on_key_in_insert_mode(const KeyEvent& event)
bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
{
// FIXME: changing or deleting word methods don't correctly support 1 letter words.
// For example, in the line 'return 0;' with the cursor on the '0',
// keys 'cw' will move to the delete '0;' rather than just the '0'.
// FIXME: Changing or deleting the last word on a line will bring the next line
// up to the cursor.
if (m_previous_key == KeyCode::Key_D) {
if (event.key() == KeyCode::Key_D) {
yank(Line);
delete_line();
} else if (event.key() == KeyCode::Key_W) {
// If the current char is an alnum or punct, delete from said char, to the
// beginning of the next word including any whitespace in between the two words.
// If the current char is whitespace, delete from the cursor to the beginning of the next world.
u32 current_char = m_editor->current_line().view().code_points()[m_editor->cursor().column()];
TextPosition delete_to;
if (isspace(current_char)) {
delete_to = find_beginning_of_next_word();
} else {
delete_to = find_end_of_next_word();
delete_to.set_column(delete_to.column() + 1);
}
m_editor->delete_text_range(TextRange(m_editor->cursor(), delete_to).normalized());
} else if (event.key() == KeyCode::Key_B) {
// Will delete from the current char to the beginning of the previous word regardless of whitespace.
// Does not delete the starting char, see note below.
TextPosition delete_to = find_beginning_of_previous_word();
// NOTE: Intentionally don't adjust m_editor->cursor() for the wide cursor's column
// because in the original vim... they don't.
m_editor->delete_text_range(TextRange(delete_to, m_editor->cursor()).normalized());
} else if (event.key() == KeyCode::Key_E) {
// Delete from the current char to the end of the next word regardless of whitespace.
TextPosition delete_to = find_end_of_next_word();
delete_to.set_column(delete_to.column() + 1);
m_editor->delete_text_range(TextRange(m_editor->cursor(), delete_to).normalized());
}
m_previous_key = {};
} else if (m_previous_key == KeyCode::Key_G) {
@ -102,6 +133,41 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
m_editor->add_code_point(0x0A);
}
switch_to_insert_mode();
} else if (event.key() == KeyCode::Key_W) {
// Delete to the end of the next word, if in the middle of a word, this will delete
// from the cursor to the said of said word. If the cursor is on whitespace,
// any whitespace between the cursor and the beginning of the next word will be deleted.
u32 current_char = m_editor->current_line().view().code_points()[m_editor->cursor().column()];
TextPosition delete_to;
if (isspace(current_char)) {
delete_to = find_beginning_of_next_word();
} else {
delete_to = find_end_of_next_word();
delete_to.set_column(delete_to.column() + 1);
}
m_editor->delete_text_range(TextRange(m_editor->cursor(), delete_to).normalized());
switch_to_insert_mode();
} else if (event.key() == KeyCode::Key_B) {
// Delete to the beginning of the previous word, if in the middle of a word, this will delete
// from the cursor to the beginning of said word. If the cursor is on whitespace
// it, and the previous word will be deleted.
u32 current_char = m_editor->current_line().view().code_points()[m_editor->cursor().column()];
TextPosition delete_to = find_beginning_of_previous_word();
TextPosition adjusted_cursor = m_editor->cursor();
// Adjust cursor for the column the wide cursor is covering
if (isalnum(current_char) || ispunct(current_char))
adjusted_cursor.set_column(adjusted_cursor.column() + 1);
m_editor->delete_text_range(TextRange(delete_to, adjusted_cursor).normalized());
switch_to_insert_mode();
} else if (event.key() == KeyCode::Key_E) {
// Delete to the end of the next word, if in the middle of a word, this will delete
// from the cursor to the end of said word. If the cursor is on whitespace
// it, and the next word will be deleted.
TextPosition delete_to = find_end_of_next_word();
TextPosition adjusted_cursor = m_editor->cursor();
delete_to.set_column(delete_to.column() + 1);
m_editor->delete_text_range(TextRange(adjusted_cursor, delete_to).normalized());
switch_to_insert_mode();
}
m_previous_key = {};
} else {