summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Userland/Libraries/LibGUI/EditingEngine.h6
-rw-r--r--Userland/Libraries/LibGUI/VimEditingEngine.cpp1195
-rw-r--r--Userland/Libraries/LibGUI/VimEditingEngine.h132
3 files changed, 1073 insertions, 260 deletions
diff --git a/Userland/Libraries/LibGUI/EditingEngine.h b/Userland/Libraries/LibGUI/EditingEngine.h
index c718bfe6a1..83c89ca124 100644
--- a/Userland/Libraries/LibGUI/EditingEngine.h
+++ b/Userland/Libraries/LibGUI/EditingEngine.h
@@ -29,6 +29,12 @@ public:
void attach(TextEditor& editor);
void detach();
+ TextEditor& editor()
+ {
+ VERIFY(!m_editor.is_null());
+ return *m_editor.unsafe_ptr();
+ }
+
virtual bool on_key(const KeyEvent& event);
protected:
diff --git a/Userland/Libraries/LibGUI/VimEditingEngine.cpp b/Userland/Libraries/LibGUI/VimEditingEngine.cpp
index cef0a059b6..9e3734e264 100644
--- a/Userland/Libraries/LibGUI/VimEditingEngine.cpp
+++ b/Userland/Libraries/LibGUI/VimEditingEngine.cpp
@@ -1,15 +1,731 @@
/*
- * Copyright (c) 2020, the SerenityOS developers.
+ * Copyright (c) 2020-2021, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
+#include <AK/Assertions.h>
+#include <AK/String.h>
#include <LibGUI/Event.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/VimEditingEngine.h>
+#include <string.h>
namespace GUI {
+void VimCursor::move()
+{
+ if (m_forwards)
+ move_forwards();
+ else
+ move_backwards();
+}
+
+void VimCursor::move_reverse()
+{
+ if (m_forwards)
+ move_backwards();
+ else
+ move_forwards();
+}
+
+u32 VimCursor::peek()
+{
+ TextPosition saved_position = m_position;
+
+ move();
+ u32 peeked = current_char();
+
+ m_position = saved_position;
+ return peeked;
+}
+
+u32 VimCursor::peek_reverse()
+{
+ TextPosition saved_position = m_position;
+
+ move_reverse();
+ u32 peeked = current_char();
+
+ m_position = saved_position;
+ return peeked;
+}
+
+TextDocumentLine& VimCursor::current_line()
+{
+ return m_editor.line(m_position.line());
+}
+
+u32 VimCursor::current_char()
+{
+ if (on_empty_line()) {
+ // Fails all of isspace, ispunct, isalnum so should be good.
+ return 0;
+ } else {
+ return current_line().view().code_points()[m_position.column()];
+ }
+}
+
+bool VimCursor::on_empty_line()
+{
+ return current_line().length() == 0;
+}
+
+bool VimCursor::will_cross_line_boundary()
+{
+ if (on_empty_line())
+ return true;
+ else if (m_forwards && m_position.column() == current_line().length() - 1)
+ return true;
+ else if (!m_forwards && m_position.column() == 0)
+ return true;
+ else
+ return false;
+}
+
+void VimCursor::move_forwards()
+{
+ if (on_empty_line() || m_position.column() == current_line().length() - 1) {
+ if (m_position.line() == m_editor.line_count() - 1) {
+ // We have reached the end of the document, so any other
+ // forward movements are no-ops.
+ m_hit_edge = true;
+ } else {
+ m_position.set_column(0);
+ m_position.set_line(m_position.line() + 1);
+ m_crossed_line_boundary = true;
+ }
+ } else {
+ m_position.set_column(m_position.column() + 1);
+ m_crossed_line_boundary = false;
+ }
+}
+
+void VimCursor::move_backwards()
+{
+ if (m_position.column() == 0) {
+ if (m_position.line() == 0) {
+ // We have reached the start of the document, so any other
+ // backward movements are no-ops.
+ m_hit_edge = true;
+ } else {
+ m_position.set_line(m_position.line() - 1);
+ if (!on_empty_line())
+ m_position.set_column(current_line().length() - 1);
+ else
+ m_position.set_column(0);
+ m_crossed_line_boundary = true;
+ }
+ } else {
+ m_position.set_column(m_position.column() - 1);
+ m_crossed_line_boundary = false;
+ }
+}
+
+void VimMotion::add_key_code(KeyCode key, [[maybe_unused]] bool ctrl, bool shift, [[maybe_unused]] bool alt)
+{
+
+ if (is_complete())
+ return;
+
+ if (m_find_mode != FindMode::None) {
+ // We need to consume the next character because we are going to find
+ // until that character.
+
+ // HACK: there is no good way to obtain whether a character is alphanumeric
+ // from the keycode itself.
+ char const* keycode_str = key_code_to_string(key);
+
+ if (strlen(keycode_str) == 1 && (isalpha(keycode_str[0]) || isspace(keycode_str[0]))) {
+ m_next_character = tolower(keycode_str[0]);
+ m_unit = Unit::Find;
+ } else {
+ m_unit = Unit::Unknown;
+ }
+
+ m_is_complete = true;
+ m_should_consume_next_character = false;
+ return;
+ }
+
+ bool should_use_guirky = m_guirky_mode;
+
+ switch (key) {
+#define DIGIT(n) \
+ case KeyCode::Key_##n: \
+ m_amount = (m_amount * 10) + n; \
+ break
+
+ // Digits add digits to the amount.
+ DIGIT(1);
+ DIGIT(2);
+ DIGIT(3);
+ DIGIT(4);
+ DIGIT(5);
+ DIGIT(6);
+ DIGIT(7);
+ DIGIT(8);
+ DIGIT(9);
+
+#undef DIGIT
+
+ // If 0 appears while amount is 0, then it means beginning of line.
+ // Otherwise, it adds 0 to the amount.
+ case KeyCode::Key_0:
+ if (m_amount == 0) {
+ m_unit = Unit::Character;
+ m_amount = START_OF_LINE;
+ m_is_complete = true;
+ } else {
+ m_amount = m_amount * 10;
+ }
+ break;
+
+ // $ means end of line.
+ // TODO: d2$ in vim deletes to the end of the line and then the next line.
+ case KeyCode::Key_Dollar:
+ m_unit = Unit::Character;
+ m_amount = END_OF_LINE;
+ m_is_complete = true;
+ break;
+
+ // ^ means the first non-whitespace character for this line.
+ // It deletes backwards if you're in front of it, and forwards if you're behind.
+ case KeyCode::Key_Circumflex:
+ m_unit = Unit::Character;
+ m_amount = START_OF_NON_WHITESPACE;
+ m_is_complete = true;
+ break;
+
+ // j, up or + operates on this line and amount line(s) after.
+ case KeyCode::Key_J:
+ case KeyCode::Key_Up:
+ case KeyCode::Key_Plus:
+ m_unit = Unit::Line;
+
+ if (m_amount == 0)
+ m_amount = 1;
+
+ m_is_complete = true;
+ break;
+
+ // k, down or - operates on this line and amount line(s) before.
+ case KeyCode::Key_K:
+ case KeyCode::Key_Down:
+ case KeyCode::Key_Minus:
+ m_unit = Unit::Line;
+
+ if (m_amount == 0)
+ m_amount = -1;
+ else
+ m_amount = -m_amount;
+
+ m_is_complete = true;
+ break;
+
+ // BS, h or left operates on this character and amount character(s) before.
+ case KeyCode::Key_Backspace:
+ case KeyCode::Key_H:
+ case KeyCode::Key_Left:
+ m_unit = Unit::Character;
+
+ if (m_amount == 0)
+ m_amount = -1;
+ else
+ m_amount = -m_amount;
+
+ m_is_complete = true;
+ break;
+
+ // l or right operates on this character and amount character(s) after.
+ case KeyCode::Key_L:
+ case KeyCode::Key_Right:
+ m_unit = Unit::Character;
+
+ if (m_amount > 0)
+ m_amount--;
+
+ m_is_complete = true;
+ break;
+
+ // w operates on amount word(s) after.
+ // W operates on amount WORD(s) after.
+ case KeyCode::Key_W:
+ if (shift)
+ m_unit = Unit::WORD;
+ else
+ m_unit = Unit::Word;
+
+ if (m_amount == 0)
+ m_amount = 1;
+
+ m_is_complete = true;
+ break;
+
+ // b operates on amount word(s) before.
+ // B operates on amount WORD(s) before.
+ case KeyCode::Key_B:
+ if (shift)
+ m_unit = Unit::WORD;
+ else
+ m_unit = Unit::Word;
+
+ if (m_amount == 0)
+ m_amount = -1;
+ else
+ m_amount = -m_amount;
+
+ m_is_complete = true;
+ break;
+
+ // e operates on amount of word(s) after, till the end of the last word.
+ // E operates on amount of WORD(s) after, till the end of the last WORD.
+ // ge operates on amount of word(s) before, till the end of the last word.
+ // gE operates on amount of WORD(s) before, till the end of the last WORD.
+ case KeyCode::Key_E:
+ if (shift)
+ m_unit = Unit::EndOfWORD;
+ else
+ m_unit = Unit::EndOfWord;
+
+ if (m_guirky_mode) {
+ if (m_amount == 0)
+ m_amount = -1;
+ else
+ m_amount = -m_amount;
+
+ m_guirky_mode = false;
+ } else {
+ if (m_amount == 0)
+ m_amount = 1;
+ }
+
+ m_is_complete = true;
+ break;
+
+ // g enables guirky (g-prefix commands) mode.
+ // gg operates from the start of the document to the cursor.
+ // G operates from the cursor to the end of the document.
+ case KeyCode::Key_G:
+ if (m_guirky_mode) {
+ if (shift) {
+ // gG is not a valid command in vim.
+ m_guirky_mode = false;
+ m_unit = Unit::Unknown;
+ m_is_complete = true;
+ } else {
+ m_guirky_mode = false;
+ m_unit = Unit::Document;
+ m_amount = -1;
+ m_is_complete = true;
+ }
+ } else {
+ if (shift) {
+ m_unit = Unit::Document;
+ m_amount = 1;
+ m_is_complete = true;
+ } else {
+ m_guirky_mode = true;
+ }
+ }
+ break;
+
+ // t operates until the given character.
+ case KeyCode::Key_T:
+ m_find_mode = FindMode::To;
+ m_should_consume_next_character = true;
+
+ if (m_amount == 0)
+ m_amount = 1;
+ break;
+
+ // f operates through the given character.
+ case KeyCode::Key_F:
+ m_find_mode = FindMode::Find;
+ m_should_consume_next_character = true;
+
+ if (m_amount == 0)
+ m_amount = 1;
+ break;
+
+ default:
+ m_unit = Unit::Unknown;
+ m_is_complete = true;
+ break;
+ }
+
+ if (should_use_guirky && m_guirky_mode) {
+ // If we didn't use the g then we cancel the motion.
+ m_guirky_mode = false;
+ m_unit = Unit::Unknown;
+ m_is_complete = true;
+ }
+}
+
+Optional<TextRange> VimMotion::get_range(VimEditingEngine& engine, bool normalize_for_position)
+{
+ if (!is_complete() || is_cancelled())
+ return {};
+
+ TextEditor& editor = engine.editor();
+
+ auto position = editor.cursor();
+ int amount = abs(m_amount);
+ bool forwards = m_amount >= 0;
+ VimCursor cursor { editor, position, forwards };
+
+ m_start_line = m_end_line = position.line();
+ m_start_column = m_end_column = position.column();
+
+ switch (m_unit) {
+ case Unit::Unknown:
+ VERIFY_NOT_REACHED();
+ case Unit::Document: {
+ calculate_document_range(editor);
+ break;
+ }
+ case Unit::Line: {
+ calculate_line_range(editor, normalize_for_position);
+ break;
+ }
+ case Unit::EndOfWord:
+ case Unit::Word:
+ case Unit::EndOfWORD:
+ case Unit::WORD: {
+ calculate_word_range(cursor, amount, normalize_for_position);
+ break;
+ }
+ case Unit::Character: {
+ calculate_character_range(cursor, amount, normalize_for_position);
+ break;
+ }
+ case Unit::Find: {
+ calculate_find_range(cursor, amount);
+ break;
+ }
+ }
+
+ return { TextRange { { m_start_line, m_start_column }, { m_end_line, m_end_column } } };
+}
+
+void VimMotion::calculate_document_range(TextEditor& editor)
+{
+ if (m_amount >= 0) {
+ m_end_line = editor.line_count() - 1;
+ auto& last_line = editor.line(m_end_line);
+ m_end_column = last_line.length();
+ } else {
+ m_start_line = 0;
+ m_start_column = 0;
+ }
+}
+
+void VimMotion::calculate_line_range(TextEditor& editor, bool normalize_for_position)
+{
+ // Use this line +/- m_amount lines.
+ m_start_column = 0;
+ m_end_column = 0;
+
+ if (m_amount >= 0) {
+ m_end_line = min(m_end_line + !normalize_for_position + m_amount, editor.line_count());
+
+ // We can't delete to "last line + 1", so if we're on the last line,
+ // delete until the end.
+ if (m_end_line == editor.line_count()) {
+ m_end_line--;
+ m_end_column = editor.line(m_end_line).length();
+ }
+ } else {
+ // Can't write it as max(start_line + m_amount, 0) because of unsigned
+ // shenanigans.
+ if (m_start_line <= (unsigned)-m_amount)
+ m_start_line = 0;
+ else
+ m_start_line += m_amount;
+
+ if (m_end_line == editor.line_count() - 1)
+ m_end_column = editor.line(m_end_line).length();
+ else
+ m_end_line++;
+ }
+}
+
+void VimMotion::calculate_word_range(VimCursor& cursor, int amount, bool normalize_for_position)
+{
+ enum {
+ Whitespace,
+ Word,
+ Punctuation,
+ Unknown
+ };
+ // Word is defined as a-zA-Z0-9_.
+ auto part_of_word = [](u32 ch) { return ch == '_' || isalnum(ch); };
+ auto part_of_punctuation = [](u32 ch) { return ch != '_' && ispunct(ch); };
+ auto classify = [&](u32 ch) {
+ if (isspace(ch))
+ return Whitespace;
+ else if (part_of_word(ch))
+ return Word;
+ else if (part_of_punctuation(ch))
+ return Punctuation;
+ else
+ return Unknown;
+ };
+
+ // A small explanation for the code below: Because the direction of the
+ // movement for this motion determines what the "start" and "end" of a word
+ // is, the code below treats the motions like so:
+ // - Start of word: w/W/ge/gE
+ // - End of word: e/E/b/B
+
+ while (amount > 0) {
+ if (cursor.hit_edge())
+ break;
+
+ if ((!cursor.forwards() && (m_unit == Unit::Word || m_unit == Unit::WORD))
+ || (cursor.forwards() && (m_unit == Unit::EndOfWord || m_unit == Unit::EndOfWORD))) {
+ // End-of-word motions peek at the "next" character and if its class
+ // is not the same as ours, they move over one character (to end up
+ // at the new character class). This is required because we don't
+ // want to exit the word with end-of-word motions.
+
+ if (m_unit == Unit::Word || m_unit == Unit::EndOfWord) {
+ // Word-style peeking
+ int current_class = classify(cursor.current_char());
+ int peeked_class = classify(cursor.peek());
+ if (current_class != peeked_class) {
+ cursor.move();
+ }
+ } else {
+ // WORD-style peeking, much simpler
+ if (isspace(cursor.peek())) {
+ cursor.move();
+ }
+ }
+ } else {
+ // Start-of-word motions want to exit the word no matter which part
+ // of it we're in.
+ if (m_unit == Unit::Word || m_unit == Unit::EndOfWord) {
+ // Word-style consumption
+ if (part_of_word(cursor.current_char())) {
+ do {
+ cursor.move();
+ if (cursor.hit_edge() || cursor.crossed_line_boundary())
+ break;
+ } while (part_of_word(cursor.current_char()));
+ } else if (part_of_punctuation(cursor.current_char())) {
+ do {
+ cursor.move();
+ if (cursor.hit_edge() || cursor.crossed_line_boundary())
+ break;
+ } while (part_of_punctuation(cursor.current_char()));
+ } else if (cursor.on_empty_line()) {
+ cursor.move();
+ }
+ } else {
+ // WORD-style consumption
+ if (!isspace(cursor.current_char())) {
+ do {
+ cursor.move();
+ if (cursor.hit_edge() || cursor.crossed_line_boundary())
+ break;
+ } while (!isspace(cursor.current_char()));
+ } else if (cursor.on_empty_line()) {
+ cursor.move();
+ }
+ }
+ }
+
+ // Now consume any space if it exists.
+ if (isspace(cursor.current_char())) {
+ do {
+ cursor.move();
+ if (cursor.hit_edge())
+ break;
+ } while (isspace(cursor.current_char()));
+ }
+
+ if ((!cursor.forwards() && (m_unit == Unit::Word || m_unit == Unit::WORD))
+ || (cursor.forwards() && (m_unit == Unit::EndOfWord || m_unit == Unit::EndOfWORD))) {
+ // End-of-word motions consume until the class doesn't match.
+
+ if (m_unit == Unit::Word || m_unit == Unit::EndOfWord) {
+ // Word-style consumption
+ int current_class = classify(cursor.current_char());
+ while (classify(cursor.current_char()) == current_class) {
+ cursor.move();
+ if (cursor.hit_edge() || cursor.crossed_line_boundary())
+ break;
+ }
+ } else {
+ // WORD-style consumption
+ while (!isspace(cursor.current_char())) {
+ cursor.move();
+ if (cursor.hit_edge() || cursor.crossed_line_boundary())
+ break;
+ }
+ }
+ }
+
+ amount--;
+ }
+
+ // If we need to normalize for position then we do a move_reverse for
+ // end-of-word motions, because vim acts on end-of-word ranges through the
+ // character your cursor is placed on but acts on start-of-words *until* the
+ // character your cursor is placed on.
+ if (normalize_for_position) {
+ if ((!cursor.forwards() && (m_unit == Unit::Word || m_unit == Unit::WORD))
+ || (cursor.forwards() && (m_unit == Unit::EndOfWord || m_unit == Unit::EndOfWORD))) {
+ if (!cursor.hit_edge())
+ cursor.move_reverse();
+ }
+ }
+
+ if (cursor.forwards()) {
+ m_end_line = cursor.current_position().line();
+ m_end_column = cursor.current_position().column() + normalize_for_position;
+ } else {
+ m_start_line = cursor.current_position().line();
+ m_start_column = cursor.current_position().column();
+ }
+}
+
+void VimMotion::calculate_character_range(VimCursor& cursor, int amount, bool normalize_for_position)
+{
+ if (m_amount == START_OF_LINE) {
+ m_start_column = 0;
+ } else if (m_amount == END_OF_LINE) {
+ m_end_column = cursor.current_line().length();
+ } else if (m_amount == START_OF_NON_WHITESPACE) {
+ // Find the first non-whitespace character and set the range from current
+ // position to it.
+ TextPosition cursor_copy = cursor.current_position();
+ cursor.current_position().set_column(0);
+
+ while (isspace(cursor.current_char())) {
+ if (cursor.will_cross_line_boundary())
+ break;
+
+ cursor.move_forwards();
+ }
+
+ if (cursor_copy < cursor.current_position())
+ m_end_column = cursor.current_position().column() + 1;
+ else
+ m_start_column = cursor.current_position().column();
+ } else {
+ while (amount > 0) {
+ if (cursor.hit_edge() || cursor.will_cross_line_boundary())
+ break;
+
+ cursor.move();
+ amount--;
+ }
+
+ if (cursor.forwards()) {
+ m_end_column = cursor.current_position().column() + 1 + normalize_for_position;
+ } else {
+ m_start_column = cursor.current_position().column();
+ }
+ }
+}
+
+void VimMotion::calculate_find_range(VimCursor& cursor, int amount)
+{
+ // Find the searched character (case-insensitive).
+ while (amount > 0) {
+ cursor.move_forwards();
+
+ while ((unsigned)tolower(cursor.current_char()) != m_next_character) {
+ if (cursor.will_cross_line_boundary())
+ break;
+
+ cursor.move_forwards();
+ }
+
+ amount--;
+ }
+
+ // If we didn't find our character before reaching the end of the line, then
+ // we want the range to be invalid so no operation is performed.
+ if ((unsigned)tolower(cursor.current_char()) == m_next_character) {
+ // We found our character.
+ bool in_find_mode = m_find_mode == FindMode::Find;
+ m_end_column = cursor.current_position().column() + in_find_mode;
+ }
+
+ m_find_mode = FindMode::None;
+}
+
+Optional<TextPosition> VimMotion::get_position(VimEditingEngine& engine)
+{
+ auto range_optional = get_range(engine, true);
+ if (!range_optional.has_value())
+ return {};
+
+ auto range = range_optional.value();
+ if (!range.is_valid())
+ return {};
+
+ TextEditor& editor = engine.editor();
+ auto cursor_position = editor.cursor();
+
+ switch (m_unit) {
+ case Unit::Document: {
+ if (range.start().line() < cursor_position.line()) {
+ cursor_position.set_line(range.start().line());
+ } else {
+ cursor_position.set_line(range.end().line());
+ }
+ cursor_position.set_column(0);
+
+ return { cursor_position };
+ }
+ case Unit::Line: {
+ size_t line_number;
+ // Because we select lines from start to end, we can't use that
+ // to get the new position, so we do some correction here.
+ if (range.start().line() < cursor_position.line() || m_amount < 0) {
+ line_number = range.start().line();
+ } else {
+ line_number = range.end().line();
+ }
+
+ auto& line = editor.line(line_number);
+
+ cursor_position.set_line(line_number);
+ if (line.length() <= cursor_position.column()) {
+ cursor_position.set_column(line.length() - 1);
+ }
+
+ return { cursor_position };
+ }
+ default: {
+ if (range.start() < cursor_position) {
+ return { range.start() };
+ } else {
+ // Ranges are end-exclusive. The normalize_for_position argument we pass
+ // above in get_range normalizes some values which shouldn't be
+ // end-exclusive during normal operations.
+ bool is_at_start = range.end().column() == 0;
+ size_t column = is_at_start ? 0 : range.end().column() - 1;
+ // Need to not go beyond the last character, as standard in vim.
+ auto& line = editor.line(range.end().line());
+
+ return { TextPosition { range.end().line(), min(column, line.length() - 1) } };
+ }
+ }
+ }
+}
+
+void VimMotion::reset()
+{
+ m_unit = Unit::Unknown;
+ m_amount = 0;
+ m_is_complete = false;
+}
+
CursorWidth VimEditingEngine::cursor_width() const
{
return m_vim_mode == VimMode::Insert ? CursorWidth::NARROW : CursorWidth::WIDE;
@@ -37,6 +753,8 @@ bool VimEditingEngine::on_key(const KeyEvent& event)
bool VimEditingEngine::on_key_in_insert_mode(const KeyEvent& event)
{
if (event.key() == KeyCode::Key_Escape || (event.ctrl() && event.key() == KeyCode::Key_LeftBracket) || (event.ctrl() && event.key() == KeyCode::Key_C)) {
+ if (m_editor->cursor().column() > 0)
+ move_one_left(event);
switch_to_normal_mode();
return true;
}
@@ -45,57 +763,61 @@ 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'.
+ // Ignore auxiliary keypress events.
+ if (event.key() == KeyCode::Key_LeftShift
+ || event.key() == KeyCode::Key_RightShift
+ || event.key() == KeyCode::Key_Control
+ || event.key() == KeyCode::Key_Alt) {
+ return false;
+ }
- // 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) {
+ if (event.key() == KeyCode::Key_D && !m_motion.should_consume_next_character()) {
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_motion.reset();
+ m_previous_key = {};
+ } else {
+ m_motion.add_key_code(event.key(), event.ctrl(), event.shift(), event.alt());
+ if (m_motion.is_complete()) {
+ if (!m_motion.is_cancelled()) {
+ auto range = m_motion.get_range(*this);
+ VERIFY(range.has_value());
+
+ if (range->is_valid()) {
+ m_editor->delete_text_range(*range);
+ }
+ }
+
+ m_motion.reset();
+ m_previous_key = {};
}
- 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) {
- 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 if (m_previous_key == KeyCode::Key_Y) {
- if (event.key() == KeyCode::Key_Y) {
+ if (event.key() == KeyCode::Key_Y && !m_motion.should_consume_next_character()) {
yank(Line);
+ m_motion.reset();
+ m_previous_key = {};
+ } else {
+ m_motion.add_key_code(event.key(), event.ctrl(), event.shift(), event.alt());
+ if (m_motion.is_complete()) {
+ if (!m_motion.is_cancelled()) {
+ auto range = m_motion.get_range(*this);
+ VERIFY(range.has_value());
+
+ if (range->is_valid()) {
+ m_editor->set_selection(*range);
+ yank(Selection);
+ m_editor->clear_selection();
+ }
+ }
+
+ m_motion.reset();
+ m_previous_key = {};
+ }
}
- m_previous_key = {};
} else if (m_previous_key == KeyCode::Key_C) {
- if (event.key() == KeyCode::Key_C) {
+ if (event.key() == KeyCode::Key_C && !m_motion.should_consume_next_character()) {
// Needed because the code to replace the deleted line is called after delete_line() so
// what was the second last line before the delete, is now the last line.
bool was_second_last_line = m_editor->cursor().line() == m_editor->line_count() - 2;
@@ -113,53 +835,50 @@ 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);
+ } else {
+ m_motion.add_key_code(event.key(), event.ctrl(), event.shift(), event.alt());
+ if (m_motion.is_complete()) {
+ if (!m_motion.is_cancelled()) {
+ auto range = m_motion.get_range(*this);
+ VERIFY(range.has_value());
+
+ if (range->is_valid()) {
+ m_editor->set_selection(*range);
+ yank(Selection);
+ m_editor->delete_text_range(*range);
+ switch_to_insert_mode();
+ }
+ }
+
+ m_motion.reset();
+ m_previous_key = {};
}
- 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 {
+ if (m_motion.should_consume_next_character()) {
+ // We must consume the next character.
+ // FIXME: deduplicate with code below.
+ 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);
+ if (maybe_new_position.has_value()) {
+ auto new_position = maybe_new_position.value();
+ m_editor->set_cursor(new_position);
+ }
+ }
+
+ m_motion.reset();
+ }
+ return true;
+ }
+
// Handle first any key codes that are to be applied regardless of modifiers.
switch (event.key()) {
- case (KeyCode::Key_Dollar):
- move_to_line_end(event);
- break;
case (KeyCode::Key_Escape):
if (m_editor->on_escape_pressed)
m_editor->on_escape_pressed();
- break;
+ return true;
default:
break;
}
@@ -170,26 +889,24 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
case (KeyCode::Key_A):
move_to_line_end(event);
switch_to_insert_mode();
- break;
- case (KeyCode::Key_G):
- move_to_last_line();
- break;
+ return true;
case (KeyCode::Key_I):
move_to_line_beginning(event);
switch_to_insert_mode();
- break;
+ return true;
case (KeyCode::Key_O):
move_to_line_beginning(event);
m_editor->add_code_point(0x0A);
move_one_up(event);
switch_to_insert_mode();
- break;
+ return true;
+ // FIXME: Integrate these into vim motions too.
case (KeyCode::Key_LeftBrace):
move_to_previous_empty_lines_block();
- break;
+ return true;
case (KeyCode::Key_RightBrace):
move_to_next_empty_lines_block();
- break;
+ return true;
default:
break;
}
@@ -200,13 +917,13 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
switch (event.key()) {
case (KeyCode::Key_D):
move_half_page_down(event);
- break;
+ return true;
case (KeyCode::Key_R):
m_editor->redo();
- break;
+ return true;
case (KeyCode::Key_U):
move_half_page_up(event);
- break;
+ return true;
default:
break;
}
@@ -221,212 +938,172 @@ bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
case (KeyCode::Key_A):
move_one_right(event);
switch_to_insert_mode();
- break;
- case (KeyCode::Key_B):
- move_to_beginning_of_previous_word();
- break;
+ return true;
case (KeyCode::Key_C):
m_previous_key = event.key();
- break;
- case (KeyCode::Key_Backspace):
- case (KeyCode::Key_H):
- case (KeyCode::Key_Left):
- move_one_left(event);
- break;
+ return true;
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;
- case (KeyCode::Key_Down):
- case (KeyCode::Key_J):
- move_one_down(event);
- break;
+ return true;
case (KeyCode::Key_I):
switch_to_insert_mode();
- break;
- case (KeyCode::Key_K):
- case (KeyCode::Key_Up):
- move_one_up(event);
- break;
- case (KeyCode::Key_L):
- case (KeyCode::Key_Right):
- move_one_right(event);
- break;
+ return true;
case (KeyCode::Key_O):
move_to_line_end(event);
m_editor->add_code_point(0x0A);
switch_to_insert_mode();
- break;
+ return true;
case (KeyCode::Key_U):
m_editor->undo();
- break;
- case (KeyCode::Key_W):
- move_to_beginning_of_next_word();
- break;
+ return true;
case (KeyCode::Key_X):
yank({ m_editor->cursor(), { m_editor->cursor().line(), m_editor->cursor().column() + 1 } });
delete_char();
- break;
- case (KeyCode::Key_0):
- move_to_line_beginning(event);
- break;
+ return true;
case (KeyCode::Key_V):
switch_to_visual_mode();
- break;
+ return true;
case (KeyCode::Key_Y):
m_previous_key = event.key();
- break;
+ return true;
case (KeyCode::Key_P):
put(event);
- break;
+ return true;
default:
break;
}
}
+
+ // If nothing else handled the key, we'll be feeding the motion state
+ // machine instead.
+ 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);
+ if (maybe_new_position.has_value()) {
+ auto new_position = maybe_new_position.value();
+ m_editor->set_cursor(new_position);
+ }
+ }
+
+ m_motion.reset();
+ }
}
return true;
}
bool VimEditingEngine::on_key_in_visual_mode(const KeyEvent& event)
{
- if (m_previous_key == KeyCode::Key_G) {
- 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();
+ // 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);
+ 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();
}
- m_previous_key = {};
- } else {
- // Handle first any key codes that are to be applied regardless of modifiers.
+
+ 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_Dollar):
+ case (KeyCode::Key_A):
move_to_line_end(event);
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_Escape):
- switch_to_normal_mode();
- if (m_editor->on_escape_pressed)
- m_editor->on_escape_pressed();
- break;
+ switch_to_insert_mode();
+ return true;
+ case (KeyCode::Key_I):
+ move_to_line_beginning(event);
+ switch_to_insert_mode();
+ return true;
default:
break;
}
+ }
- // SHIFT is pressed.
- if (event.shift() && !event.ctrl() && !event.alt()) {
- switch (event.key()) {
- case (KeyCode::Key_A):
- move_to_line_end(event);
- switch_to_insert_mode();
- break;
- case (KeyCode::Key_G):
- move_to_last_line();
- break;
- case (KeyCode::Key_I):
- move_to_line_beginning(event);
- switch_to_insert_mode();
- break;
- default:
- break;
- }
+ // CTRL is pressed.
+ if (event.ctrl() && !event.shift() && !event.alt()) {
+ switch (event.key()) {
+ case (KeyCode::Key_D):
+ move_half_page_down(event);
+ update_selection_on_cursor_move();
+ return true;
+ case (KeyCode::Key_U):
+ move_half_page_up(event);
+ update_selection_on_cursor_move();
+ 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(event);
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_U):
- move_half_page_up(event);
- update_selection_on_cursor_move();
- break;
- default:
- break;
- }
+ // No modifier is pressed.
+ if (!event.ctrl() && !event.shift() && !event.alt()) {
+ switch (event.key()) {
+ case (KeyCode::Key_D):
+ yank(Selection);
+ m_editor->do_delete();
+ switch_to_normal_mode();
+ return true;
+ case (KeyCode::Key_U):
+ // FIXME: Set selection to uppercase.
+ return true;
+ case (KeyCode::Key_X):
+ yank(Selection);
+ m_editor->do_delete();
+ switch_to_normal_mode();
+ return true;
+ case (KeyCode::Key_V):
+ switch_to_normal_mode();
+ return true;
+ case (KeyCode::Key_C):
+ yank(Selection);
+ m_editor->do_delete();
+ switch_to_insert_mode();
+ return true;
+ case (KeyCode::Key_Y):
+ yank(Selection);
+ switch_to_normal_mode();
+ return true;
+ default:
+ break;
}
+ }
- // No modifier is pressed.
- if (!event.ctrl() && !event.shift() && !event.alt()) {
- switch (event.key()) {
- case (KeyCode::Key_B):
- move_to_beginning_of_previous_word();
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_Backspace):
- case (KeyCode::Key_H):
- case (KeyCode::Key_Left):
- move_one_left(event);
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_D):
- yank(Selection);
- m_editor->do_delete();
- switch_to_normal_mode();
- 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;
- case (KeyCode::Key_Down):
- case (KeyCode::Key_J):
- move_one_down(event);
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_K):
- case (KeyCode::Key_Up):
- move_one_up(event);
+ // 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);
+ if (maybe_new_position.has_value()) {
+ auto new_position = maybe_new_position.value();
+ m_editor->set_cursor(new_position);
update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_L):
- case (KeyCode::Key_Right):
- move_one_right(event);
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_U):
- // FIXME: Set selection to uppercase.
- break;
- case (KeyCode::Key_W):
- move_to_beginning_of_next_word();
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_X):
- yank(Selection);
- m_editor->do_delete();
- switch_to_normal_mode();
- break;
- case (KeyCode::Key_0):
- move_to_line_beginning(event);
- update_selection_on_cursor_move();
- break;
- case (KeyCode::Key_V):
- switch_to_normal_mode();
- break;
- case (KeyCode::Key_C):
- yank(Selection);
- m_editor->do_delete();
- switch_to_insert_mode();
- break;
- case (KeyCode::Key_Y):
- yank(Selection);
- switch_to_normal_mode();
- break;
- default:
- break;
}
}
+
+ m_motion.reset();
}
+
return true;
}
@@ -436,6 +1113,7 @@ void VimEditingEngine::switch_to_normal_mode()
m_editor->reset_cursor_blink();
m_previous_key = {};
clear_visual_mode_data();
+ m_motion.reset();
};
void VimEditingEngine::switch_to_insert_mode()
@@ -444,6 +1122,7 @@ void VimEditingEngine::switch_to_insert_mode()
m_editor->reset_cursor_blink();
m_previous_key = {};
clear_visual_mode_data();
+ m_motion.reset();
};
void VimEditingEngine::switch_to_visual_mode()
@@ -451,18 +1130,17 @@ void VimEditingEngine::switch_to_visual_mode()
m_vim_mode = VimMode::Visual;
m_editor->reset_cursor_blink();
m_previous_key = {};
- m_selection_start_position = m_editor->cursor();
m_editor->selection()->set(m_editor->cursor(), { m_editor->cursor().line(), m_editor->cursor().column() + 1 });
m_editor->did_update_selection();
+ m_motion.reset();
}
void VimEditingEngine::update_selection_on_cursor_move()
{
auto cursor = m_editor->cursor();
- auto start = m_selection_start_position < cursor ? m_selection_start_position : cursor;
- auto end = m_selection_start_position < cursor ? cursor : m_selection_start_position;
- end.set_column(end.column() + 1);
- m_editor->selection()->set(start, end);
+ auto& line = m_editor->current_line();
+ cursor.set_column(min(cursor.column() + 1, line.length()));
+ m_editor->selection()->set_end(cursor);
m_editor->did_update_selection();
}
@@ -472,7 +1150,6 @@ void VimEditingEngine::clear_visual_mode_data()
m_editor->selection()->clear();
m_editor->did_update_selection();
}
- m_selection_start_position = {};
}
void VimEditingEngine::move_half_page_up(const KeyEvent& event)
diff --git a/Userland/Libraries/LibGUI/VimEditingEngine.h b/Userland/Libraries/LibGUI/VimEditingEngine.h
index 7c57e9ebb5..1213359906 100644
--- a/Userland/Libraries/LibGUI/VimEditingEngine.h
+++ b/Userland/Libraries/LibGUI/VimEditingEngine.h
@@ -6,10 +6,140 @@
#pragma once
+#include <AK/Optional.h>
+#include <LibCore/Object.h>
#include <LibGUI/EditingEngine.h>
+#include <LibGUI/TextRange.h>
namespace GUI {
+// Wrapper over TextPosition that makes it easier to move it around as a cursor,
+// and to get the current line or character.
+class VimCursor {
+public:
+ VimCursor(TextEditor& editor, TextPosition initial_position, bool forwards)
+ : m_editor(editor)
+ , m_position(initial_position)
+ , m_forwards(forwards)
+ {
+ }
+
+ void move_forwards();
+ void move_backwards();
+
+ // Move a single character in the current direction.
+ void move();
+ // Move a single character in reverse.
+ void move_reverse();
+ // Peek a single character in the current direction.
+ u32 peek();
+ // Peek a single character in reverse.
+ u32 peek_reverse();
+ // Get the character the cursor is currently on.
+ u32 current_char();
+ // Get the line the cursor is currently on.
+ TextDocumentLine& current_line();
+ // Get the current position.
+ TextPosition& current_position() { return m_position; }
+
+ // Did we hit the edge of the document?
+ bool hit_edge() { return m_hit_edge; }
+ // Will the next move cross a line boundary?
+ bool will_cross_line_boundary();
+ // Did we cross a line boundary?
+ bool crossed_line_boundary() { return m_crossed_line_boundary; }
+ // Are we on an empty line?
+ bool on_empty_line();
+ // Are we going forwards?
+ bool forwards() { return m_forwards; }
+
+private:
+ TextEditor& m_editor;
+ TextPosition m_position;
+ bool m_forwards;
+
+ u32 m_cached_char { 0 };
+
+ bool m_hit_edge { false };
+ bool m_crossed_line_boundary { false };
+};
+
+class VimMotion {
+public:
+ enum class Unit {
+ // The motion isn't complete yet, or was invalid.
+ Unknown,
+ // Document. Anything non-negative is counted as G while anything else is gg.
+ Document,
+ // Lines.
+ Line,
+ // A sequence of letters, digits and underscores, or a sequence of other
+ // non-blank characters separated by whitespace.
+ Word,
+ // A sequence of non-blank characters separated by whitespace.
+ // This is how Vim separates w from W.
+ WORD,
+ // End of a word. This is basically the same as a word but it doesn't
+ // trim the spaces at the end.
+ EndOfWord,
+ // End of a WORD.
+ EndOfWORD,
+ // Characters (or Unicode codepoints based on how pedantic you want to
+ // get).
+ Character,
+ // Used for find-mode.
+ Find
+ };
+ enum class FindMode {
+ /// Find mode is not enabled.
+ None,
+ /// Finding until the given character.
+ To,
+ /// Finding through the given character.
+ Find
+ };
+
+ void add_key_code(KeyCode key, bool ctrl, bool shift, bool alt);
+ Optional<TextRange> get_range(class VimEditingEngine& engine, bool normalize_for_position = false);
+ Optional<TextPosition> get_position(VimEditingEngine& engine);
+ void reset();
+
+ /// Returns whether the motion should consume the next character no matter what.
+ /// Used for f and t motions.
+ bool should_consume_next_character() { return m_should_consume_next_character; }
+ bool is_complete() { return m_is_complete; }
+ bool is_cancelled() { return m_is_complete && m_unit == Unit::Unknown; }
+ Unit unit() { return m_unit; }
+ int amount() { return m_amount; }
+
+ // FIXME: come up with a better way to signal start/end of line than sentinels?
+ static constexpr int START_OF_LINE = NumericLimits<int>::min();
+ static constexpr int START_OF_NON_WHITESPACE = NumericLimits<int>::min() + 1;
+ static constexpr int END_OF_LINE = NumericLimits<int>::max();
+
+private:
+ void calculate_document_range(TextEditor&);
+ void calculate_line_range(TextEditor&, bool normalize_for_position);
+ void calculate_word_range(VimCursor&, int amount, bool normalize_for_position);
+ void calculate_WORD_range(VimCursor&, int amount, bool normalize_for_position);
+ void calculate_character_range(VimCursor&, int amount, bool normalize_for_position);
+ void calculate_find_range(VimCursor&, int amount);
+
+ Unit m_unit { Unit::Unknown };
+ int m_amount { 0 };
+ bool m_is_complete { false };
+ bool m_guirky_mode { false };
+ bool m_should_consume_next_character { false };
+
+ FindMode m_find_mode { FindMode::None };
+ u32 m_next_character { 0 };
+
+ size_t m_start_line { 0 };
+ size_t m_start_column { 0 };
+ size_t m_end_line { 0 };
+ size_t m_end_column { 0 };
+};
+
class VimEditingEngine final : public EditingEngine {
public:
@@ -30,6 +160,7 @@ private:
};
VimMode m_vim_mode { VimMode::Normal };
+ VimMotion m_motion;
YankType m_yank_type {};
String m_yank_buffer {};
@@ -37,7 +168,6 @@ private:
void yank(TextRange);
void put(const GUI::KeyEvent&);
- TextPosition m_selection_start_position {};
void update_selection_on_cursor_move();
void clear_visual_mode_data();