summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZac <zacary.gillerat@connect.qut.edu.au>2021-01-25 04:24:56 +1000
committerGitHub <noreply@github.com>2021-01-24 19:24:56 +0100
commit330ab52ddb8befa74a18a86ee53c033e1267631a (patch)
treea7ae9730b65bd02d0c11045935aa284350c18552
parent0678dab7dca0bba07e9370140637cbac64625a42 (diff)
downloadserenity-330ab52ddb8befa74a18a86ee53c033e1267631a.zip
Vim: More correct word jumping (#5090)
Implemented move_to_beginning_of_next(), move_to_end_of_next(), move_to_beginning_of_previous() and move_to_end_of_previous() functions for more correct word jumping than the move_to_xxx_span() methods that were previously used.
-rw-r--r--Userland/Libraries/LibGUI/EditingEngine.cpp251
-rw-r--r--Userland/Libraries/LibGUI/EditingEngine.h4
-rw-r--r--Userland/Libraries/LibGUI/VimEditingEngine.cpp22
3 files changed, 273 insertions, 4 deletions
diff --git a/Userland/Libraries/LibGUI/EditingEngine.cpp b/Userland/Libraries/LibGUI/EditingEngine.cpp
index 4cd0251a13..b49b3031c0 100644
--- a/Userland/Libraries/LibGUI/EditingEngine.cpp
+++ b/Userland/Libraries/LibGUI/EditingEngine.cpp
@@ -387,6 +387,257 @@ void EditingEngine::get_selection_line_boundaries(size_t& first_line, size_t& la
last_line -= 1;
}
+void EditingEngine::move_to_beginning_of_next_word()
+{
+ /* The rules that have been coded in:
+ * Jump to the next punct or alnum after any whitespace
+ * Jump to the next non-consecutive punct regardless of whitespace
+ * Jump to the next alnum if started on punct regardless of whitespace
+ * If the end of the input is reached, jump there
+ */
+
+ auto vim_isalnum = [](int c) {
+ return c == '_' || isalnum(c);
+ };
+
+ auto vim_ispunct = [](int c) {
+ return c != '_' && ispunct(c);
+ };
+
+ bool started_on_punct = vim_ispunct(m_editor->current_line().to_utf8().characters()[m_editor->cursor().column()]);
+ bool has_seen_whitespace = false;
+ bool is_first_line = true;
+ auto& lines = m_editor->lines();
+ auto cursor = m_editor->cursor();
+ for (size_t line_index = cursor.line(); line_index < lines.size(); line_index++) {
+ auto& line = lines.at(line_index);
+
+ if (line.is_empty() && !is_first_line) {
+ return m_editor->set_cursor({ line_index, 0 });
+ } else if (line.is_empty()) {
+ has_seen_whitespace = true;
+ }
+
+ is_first_line = false;
+
+ for (size_t column_index = 0; column_index < lines.at(line_index).length(); column_index++) {
+ if (line_index == cursor.line() && column_index < cursor.column())
+ continue;
+ const u32* line_chars = line.view().code_points();
+ 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 });
+ }
+
+ if (vim_ispunct(current_char) && !started_on_punct) {
+ return m_editor->set_cursor({ 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;
+ }
+
+ if (line_index == lines.size() - 1 && column_index == line.length() - 1) {
+ m_editor->set_cursor({ line_index, column_index });
+ return;
+ }
+
+ // Implicit newline
+ if (column_index == line.length() - 1)
+ has_seen_whitespace = true;
+ }
+ }
+}
+
+void EditingEngine::move_to_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
+ * If the end of the input is reached, jump there
+ */
+
+ auto vim_isalnum = [](int c) {
+ return c == '_' || isalnum(c);
+ };
+
+ auto vim_ispunct = [](int c) {
+ return c != '_' && ispunct(c);
+ };
+
+ bool is_first_line = true;
+ bool is_first_iteration = true;
+ auto& lines = m_editor->lines();
+ auto cursor = m_editor->cursor();
+ for (size_t line_index = cursor.line(); line_index < lines.size(); line_index++) {
+ auto& line = lines.at(line_index);
+
+ if (line.is_empty() && !is_first_line) {
+ return m_editor->set_cursor({ line_index, 0 });
+ }
+
+ is_first_line = false;
+
+ for (size_t column_index = 0; column_index < lines.at(line_index).length(); column_index++) {
+ if (line_index == cursor.line() && column_index < cursor.column())
+ continue;
+
+ const u32* line_chars = line.view().code_points();
+ 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 });
+ else if (column_index == lines.at(line_index).length() - 1) {
+ is_first_iteration = false;
+ continue;
+ }
+
+ 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 });
+
+ if (!is_first_iteration && vim_ispunct(current_char) && (isspace(next_char) || vim_isalnum(next_char)))
+ return m_editor->set_cursor({ line_index, column_index });
+
+ if (line_index == lines.size() - 1 && column_index == line.length() - 1) {
+ return m_editor->set_cursor({ line_index, column_index });
+ }
+
+ is_first_iteration = false;
+ }
+ }
+}
+
+void EditingEngine::move_to_end_of_previous_word()
+{
+ auto vim_isalnum = [](int c) {
+ return c == '_' || isalnum(c);
+ };
+
+ auto vim_ispunct = [](int c) {
+ return c != '_' && ispunct(c);
+ };
+
+ bool started_on_punct = vim_ispunct(m_editor->current_line().to_utf8().characters()[m_editor->cursor().column()]);
+ bool is_first_line = true;
+ bool has_seen_whitespace = false;
+ auto& lines = m_editor->lines();
+ auto cursor = m_editor->cursor();
+ for (size_t line_index = cursor.line(); (int)line_index >= 0; line_index--) {
+ auto& line = lines.at(line_index);
+
+ if (line.is_empty() && !is_first_line) {
+ return m_editor->set_cursor({ line_index, 0 });
+ } else if (line.is_empty()) {
+ has_seen_whitespace = true;
+ }
+
+ is_first_line = false;
+
+ size_t line_length = lines.at(line_index).length();
+ for (size_t column_index = line_length - 1; (int)column_index >= 0; column_index--) {
+ if (line_index == cursor.line() && column_index > cursor.column())
+ continue;
+
+ const u32* line_chars = line.view().code_points();
+ 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 });
+ }
+
+ if (vim_ispunct(current_char) && !started_on_punct) {
+ return m_editor->set_cursor({ 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;
+ }
+
+ if (line_index == lines.size() - 1 && column_index == line.length() - 1) {
+ m_editor->set_cursor({ line_index, column_index });
+ return;
+ }
+
+ // Implicit newline when wrapping back up to the end of the previous line.
+ if (column_index == 0)
+ has_seen_whitespace = true;
+ }
+ }
+}
+
+void EditingEngine::move_to_beginning_of_previous_word()
+{
+ auto vim_isalnum = [](int c) {
+ return c == '_' || isalnum(c);
+ };
+
+ auto vim_ispunct = [](int c) {
+ return c != '_' && ispunct(c);
+ };
+
+ bool is_first_iterated_line = true;
+ bool is_first_iteration = true;
+ auto& lines = m_editor->lines();
+ auto cursor = m_editor->cursor();
+ for (size_t line_index = cursor.line(); (int)line_index >= 0; line_index--) {
+ auto& line = lines.at(line_index);
+
+ if (line.is_empty() && !is_first_iterated_line) {
+ return m_editor->set_cursor({ line_index, 0 });
+ }
+
+ is_first_iterated_line = false;
+
+ size_t line_length = lines.at(line_index).length();
+ for (size_t column_index = line_length; (int)column_index >= 0; column_index--) {
+ if (line_index == cursor.line() && column_index > cursor.column())
+ continue;
+
+ if (column_index == line_length) {
+ is_first_iteration = false;
+ continue;
+ }
+
+ const u32* line_chars = line.view().code_points();
+ 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 });
+ else if (column_index == 0) {
+ is_first_iteration = false;
+ continue;
+ }
+
+ 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 });
+
+ if (!is_first_iteration && vim_ispunct(current_char) && (isspace(next_char) || vim_isalnum(next_char)))
+ return m_editor->set_cursor({ line_index, column_index });
+
+ if (line_index == lines.size() - 1 && column_index == line.length() - 1) {
+ return m_editor->set_cursor({ line_index, column_index });
+ }
+
+ is_first_iteration = false;
+ }
+ }
+}
+
void EditingEngine::move_selected_lines_up()
{
if (!m_editor->is_editable())
diff --git a/Userland/Libraries/LibGUI/EditingEngine.h b/Userland/Libraries/LibGUI/EditingEngine.h
index 37c25410ce..b8f65b4113 100644
--- a/Userland/Libraries/LibGUI/EditingEngine.h
+++ b/Userland/Libraries/LibGUI/EditingEngine.h
@@ -68,6 +68,10 @@ protected:
void move_page_down(const KeyEvent& event);
void move_to_first_line();
void move_to_last_line();
+ void move_to_beginning_of_next_word();
+ void move_to_end_of_next_word();
+ void move_to_end_of_previous_word();
+ void move_to_beginning_of_previous_word();
void move_up(const KeyEvent& event, double page_height_factor);
void move_down(const KeyEvent& event, double page_height_factor);
diff --git a/Userland/Libraries/LibGUI/VimEditingEngine.cpp b/Userland/Libraries/LibGUI/VimEditingEngine.cpp
index 3cd80ad5fe..6a49a20d1a 100644
--- a/Userland/Libraries/LibGUI/VimEditingEngine.cpp
+++ b/Userland/Libraries/LibGUI/VimEditingEngine.cpp
@@ -73,6 +73,8 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
} else if (m_previous_key == KeyCode::Key_G) {
if (event.key() == KeyCode::Key_G) {
move_to_first_line();
+ } else if (event.key() == KeyCode::Key_E) {
+ move_to_end_of_previous_word();
}
m_previous_key = {};
} else {
@@ -139,7 +141,7 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
switch_to_insert_mode();
break;
case (KeyCode::Key_B):
- move_to_previous_span(event); // FIXME: This probably isn't 100% correct.
+ move_to_beginning_of_previous_word();
break;
case (KeyCode::Key_Backspace):
case (KeyCode::Key_H):
@@ -147,6 +149,11 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
move_one_left(event);
break;
case (KeyCode::Key_D):
+ m_previous_key = event.key();
+ break;
+ case (KeyCode::Key_E):
+ move_to_end_of_next_word();
+ break;
case (KeyCode::Key_G):
m_previous_key = event.key();
break;
@@ -174,7 +181,7 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
m_editor->undo();
break;
case (KeyCode::Key_W):
- move_to_next_span(event); // FIXME: This probably isn't 100% correct.
+ move_to_beginning_of_next_word();
break;
case (KeyCode::Key_X):
delete_char();
@@ -203,6 +210,9 @@ bool VimEditingEngine::on_key_in_visual_mode(const KeyEvent& event)
if (event.key() == KeyCode::Key_G) {
move_to_first_line();
update_selection_on_cursor_move();
+ } else if (event.key() == KeyCode::Key_E) {
+ move_to_end_of_previous_word();
+ update_selection_on_cursor_move();
}
m_previous_key = {};
} else {
@@ -260,7 +270,7 @@ bool VimEditingEngine::on_key_in_visual_mode(const KeyEvent& event)
if (!event.ctrl() && !event.shift() && !event.alt()) {
switch (event.key()) {
case (KeyCode::Key_B):
- move_to_previous_span(event); // FIXME: This probably isn't 100% correct.
+ move_to_beginning_of_previous_word();
update_selection_on_cursor_move();
break;
case (KeyCode::Key_Backspace):
@@ -273,6 +283,10 @@ bool VimEditingEngine::on_key_in_visual_mode(const KeyEvent& event)
// TODO: Yank selected text
m_editor->do_delete();
break;
+ case (KeyCode::Key_E):
+ move_to_end_of_next_word();
+ update_selection_on_cursor_move();
+ break;
case (KeyCode::Key_G):
m_previous_key = event.key();
break;
@@ -295,7 +309,7 @@ bool VimEditingEngine::on_key_in_visual_mode(const KeyEvent& event)
// FIXME: Set selection to uppercase.
break;
case (KeyCode::Key_W):
- move_to_next_span(event); // FIXME: This probably isn't 100% correct.
+ move_to_beginning_of_next_word();
update_selection_on_cursor_move();
break;
case (KeyCode::Key_X):