فهرست منبع

Vim: Add change word and delete word functionality

Add the functionality of key sequences 'cw', 'ce', 'cb', 'dw', 'de' and 'db'.
Zac 4 سال پیش
والد
کامیت
aaf691c4ef

+ 48 - 28
Userland/Libraries/LibGUI/EditingEngine.cpp

@@ -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()

+ 4 - 0
Userland/Libraries/LibGUI/EditingEngine.h

@@ -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);

+ 9 - 0
Userland/Libraries/LibGUI/TextEditor.cpp

@@ -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);

+ 2 - 0
Userland/Libraries/LibGUI/TextEditor.h

@@ -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);
 

+ 66 - 0
Userland/Libraries/LibGUI/VimEditingEngine.cpp

@@ -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 {