summaryrefslogtreecommitdiff
path: root/Libraries
diff options
context:
space:
mode:
authorRok Povsic <rok.povsic@gmail.com>2021-01-02 11:59:55 +0100
committerAndreas Kling <kling@serenityos.org>2021-01-05 00:00:36 +0100
commitb4a783d9231d1c0d8dcc24d5659c34443e2976ab (patch)
tree499228084fc4e36187a5df0e7e0e391456311929 /Libraries
parent1c17ecdeb72cf62f651385fa302d401a19541345 (diff)
downloadserenity-b4a783d9231d1c0d8dcc24d5659c34443e2976ab.zip
TextEditor+EditingEngine: Add support for the basics of Vim emulation
Diffstat (limited to 'Libraries')
-rw-r--r--Libraries/LibGUI/CMakeLists.txt3
-rw-r--r--Libraries/LibGUI/EditingEngine.cpp453
-rw-r--r--Libraries/LibGUI/EditingEngine.h85
-rw-r--r--Libraries/LibGUI/Forward.h1
-rw-r--r--Libraries/LibGUI/RegularEditingEngine.cpp91
-rw-r--r--Libraries/LibGUI/RegularEditingEngine.h44
-rw-r--r--Libraries/LibGUI/TextEditor.cpp548
-rw-r--r--Libraries/LibGUI/TextEditor.h42
-rw-r--r--Libraries/LibGUI/VimEditingEngine.cpp213
-rw-r--r--Libraries/LibGUI/VimEditingEngine.h58
10 files changed, 1107 insertions, 431 deletions
diff --git a/Libraries/LibGUI/CMakeLists.txt b/Libraries/LibGUI/CMakeLists.txt
index 0a1a2a4b08..f5b8bc2f66 100644
--- a/Libraries/LibGUI/CMakeLists.txt
+++ b/Libraries/LibGUI/CMakeLists.txt
@@ -27,6 +27,7 @@ set(SOURCES
Dialog.cpp
DisplayLink.cpp
DragOperation.cpp
+ EditingEngine.cpp
EmojiInputDialog.cpp
Event.cpp
FileIconProvider.cpp
@@ -69,6 +70,7 @@ set(SOURCES
ProcessChooser.cpp
ProgressBar.cpp
RadioButton.cpp
+ RegularEditingEngine.cpp
ResizeCorner.cpp
RunningProcessesModel.cpp
ScrollBar.cpp
@@ -93,6 +95,7 @@ set(SOURCES
TreeView.cpp
UndoStack.cpp
Variant.cpp
+ VimEditingEngine.cpp
Widget.cpp
Window.cpp
WindowServerConnection.cpp
diff --git a/Libraries/LibGUI/EditingEngine.cpp b/Libraries/LibGUI/EditingEngine.cpp
new file mode 100644
index 0000000000..4cd0251a13
--- /dev/null
+++ b/Libraries/LibGUI/EditingEngine.cpp
@@ -0,0 +1,453 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibGUI/EditingEngine.h>
+#include <LibGUI/Event.h>
+#include <LibGUI/TextEditor.h>
+
+namespace GUI {
+
+EditingEngine::~EditingEngine()
+{
+}
+
+void EditingEngine::attach(TextEditor& editor)
+{
+ ASSERT(!m_editor);
+ m_editor = editor;
+}
+
+void EditingEngine::detach()
+{
+ ASSERT(m_editor);
+ m_editor = nullptr;
+}
+
+bool EditingEngine::on_key(const KeyEvent& event)
+{
+ if (event.key() == KeyCode::Key_Left) {
+ if (!event.shift() && m_editor->selection()->is_valid()) {
+ m_editor->set_cursor(m_editor->selection()->normalized().start());
+ m_editor->selection()->clear();
+ m_editor->did_update_selection();
+ if (!event.ctrl()) {
+ m_editor->update();
+ return true;
+ }
+ }
+ if (event.ctrl()) {
+ move_to_previous_span(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ return true;
+ }
+ move_one_left(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ return true;
+ }
+
+ if (event.key() == KeyCode::Key_Right) {
+ if (!event.shift() && m_editor->selection()->is_valid()) {
+ m_editor->set_cursor(m_editor->selection()->normalized().end());
+ m_editor->selection()->clear();
+ m_editor->did_update_selection();
+ if (!event.ctrl()) {
+ m_editor->update();
+ return true;
+ }
+ }
+ if (event.ctrl()) {
+ move_to_next_span(event);
+ return true;
+ }
+ move_one_right(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ return true;
+ }
+
+ if (event.key() == KeyCode::Key_Up) {
+ move_one_up(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ return true;
+ }
+
+ if (event.key() == KeyCode::Key_Down) {
+ move_one_down(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ return true;
+ }
+
+ if (event.key() == KeyCode::Key_Home) {
+ if (event.ctrl()) {
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ move_to_first_line();
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ } else {
+ move_to_line_beginning(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ }
+ return true;
+ }
+
+ if (event.key() == KeyCode::Key_End) {
+ if (event.ctrl()) {
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ move_to_last_line();
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ } else {
+ move_to_line_end(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ }
+ return true;
+ }
+
+ if (event.key() == KeyCode::Key_PageUp) {
+ move_page_up(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ return true;
+ }
+
+ if (event.key() == KeyCode::Key_PageDown) {
+ move_page_down(event);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+ return true;
+ }
+
+ return false;
+}
+
+void EditingEngine::move_one_left(const KeyEvent& event)
+{
+ if (m_editor->cursor().column() > 0) {
+ int new_column = m_editor->cursor().column() - 1;
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(m_editor->cursor().line(), new_column);
+ } else if (m_editor->cursor().line() > 0) {
+ int new_line = m_editor->cursor().line() - 1;
+ int new_column = m_editor->lines()[new_line].length();
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_line, new_column);
+ }
+}
+
+void EditingEngine::move_one_right(const KeyEvent& event)
+{
+ int new_line = m_editor->cursor().line();
+ int new_column = m_editor->cursor().column();
+ if (m_editor->cursor().column() < m_editor->current_line().length()) {
+ new_line = m_editor->cursor().line();
+ new_column = m_editor->cursor().column() + 1;
+ } else if (m_editor->cursor().line() != m_editor->line_count() - 1) {
+ new_line = m_editor->cursor().line() + 1;
+ new_column = 0;
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_line, new_column);
+}
+
+void EditingEngine::move_to_previous_span(const KeyEvent& event)
+{
+ TextPosition new_cursor;
+ if (m_editor->document().has_spans()) {
+ auto span = m_editor->document().first_non_skippable_span_before(m_editor->cursor());
+ if (span.has_value()) {
+ new_cursor = span.value().range.start();
+ } else {
+ // No remaining spans, just use word break calculation
+ new_cursor = m_editor->document().first_word_break_before(m_editor->cursor(), true);
+ }
+ } else {
+ new_cursor = m_editor->document().first_word_break_before(m_editor->cursor(), true);
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_cursor);
+}
+
+void EditingEngine::move_to_next_span(const KeyEvent& event)
+{
+ TextPosition new_cursor;
+ if (m_editor->document().has_spans()) {
+ auto span = m_editor->document().first_non_skippable_span_after(m_editor->cursor());
+ if (span.has_value()) {
+ new_cursor = span.value().range.start();
+ } else {
+ // No remaining spans, just use word break calculation
+ new_cursor = m_editor->document().first_word_break_after(m_editor->cursor());
+ }
+ } else {
+ new_cursor = m_editor->document().first_word_break_after(m_editor->cursor());
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_cursor);
+ if (event.shift() && m_editor->selection()->start().is_valid()) {
+ m_editor->selection()->set_end(m_editor->cursor());
+ m_editor->did_update_selection();
+ }
+}
+
+void EditingEngine::move_to_line_beginning(const KeyEvent& event)
+{
+ TextPosition new_cursor;
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ if (m_editor->is_line_wrapping_enabled()) {
+ // FIXME: Replicate the first_nonspace_column behavior in wrapping mode.
+ auto home_position = m_editor->cursor_content_rect().location().translated(-m_editor->width(), 0);
+ new_cursor = m_editor->text_position_at_content_position(home_position);
+ } else {
+ size_t first_nonspace_column = m_editor->current_line().first_non_whitespace_column();
+ if (m_editor->cursor().column() == first_nonspace_column) {
+ new_cursor = { m_editor->cursor().line(), 0 };
+ } else {
+ new_cursor = { m_editor->cursor().line(), first_nonspace_column };
+ }
+ }
+ m_editor->set_cursor(new_cursor);
+}
+
+void EditingEngine::move_to_line_end(const KeyEvent& event)
+{
+ TextPosition new_cursor;
+ if (m_editor->is_line_wrapping_enabled()) {
+ auto end_position = m_editor->cursor_content_rect().location().translated(m_editor->width(), 0);
+ new_cursor = m_editor->text_position_at_content_position(end_position);
+ } else {
+ new_cursor = { m_editor->cursor().line(), m_editor->current_line().length() };
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_cursor);
+}
+
+void EditingEngine::move_one_up(const KeyEvent& event)
+{
+ if (m_editor->cursor().line() > 0 || m_editor->is_line_wrapping_enabled()) {
+ if (event.ctrl() && event.shift()) {
+ move_selected_lines_up();
+ return;
+ }
+ TextPosition new_cursor;
+ if (m_editor->is_line_wrapping_enabled()) {
+ auto position_above = m_editor->cursor_content_rect().location().translated(0, -m_editor->line_height());
+ new_cursor = m_editor->text_position_at_content_position(position_above);
+ } else {
+ size_t new_line = m_editor->cursor().line() - 1;
+ size_t new_column = min(m_editor->cursor().column(), m_editor->line(new_line).length());
+ new_cursor = { new_line, new_column };
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_cursor);
+ }
+};
+
+void EditingEngine::move_one_down(const KeyEvent& event)
+{
+ if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_line_wrapping_enabled()) {
+ if (event.ctrl() && event.shift()) {
+ move_selected_lines_down();
+ return;
+ }
+ TextPosition new_cursor;
+ if (m_editor->is_line_wrapping_enabled()) {
+ new_cursor = m_editor->text_position_at_content_position(m_editor->cursor_content_rect().location().translated(0, m_editor->line_height()));
+ auto position_below = m_editor->cursor_content_rect().location().translated(0, m_editor->line_height());
+ new_cursor = m_editor->text_position_at_content_position(position_below);
+ } else {
+ size_t new_line = m_editor->cursor().line() + 1;
+ size_t new_column = min(m_editor->cursor().column(), m_editor->line(new_line).length());
+ new_cursor = { new_line, new_column };
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_cursor);
+ }
+};
+
+void EditingEngine::move_up(const KeyEvent& event, double page_height_factor)
+{
+ if (m_editor->cursor().line() > 0 || m_editor->is_line_wrapping_enabled()) {
+ int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor);
+
+ TextPosition new_cursor;
+ if (m_editor->is_line_wrapping_enabled()) {
+ auto position_above = m_editor->cursor_content_rect().location().translated(0, -pixels);
+ new_cursor = m_editor->text_position_at_content_position(position_above);
+ } else {
+ size_t page_step = (size_t)pixels / (size_t)m_editor->line_height();
+ size_t new_line = m_editor->cursor().line() < page_step ? 0 : m_editor->cursor().line() - page_step;
+ size_t new_column = min(m_editor->cursor().column(), m_editor->line(new_line).length());
+ new_cursor = { new_line, new_column };
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_cursor);
+ }
+};
+
+void EditingEngine::move_down(const KeyEvent& event, double page_height_factor)
+{
+ if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_line_wrapping_enabled()) {
+ int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor);
+ TextPosition new_cursor;
+ if (m_editor->is_line_wrapping_enabled()) {
+ auto position_below = m_editor->cursor_content_rect().location().translated(0, pixels);
+ new_cursor = m_editor->text_position_at_content_position(position_below);
+ } else {
+ size_t new_line = min(m_editor->line_count() - 1, m_editor->cursor().line() + pixels / m_editor->line_height());
+ size_t new_column = min(m_editor->cursor().column(), m_editor->lines()[new_line].length());
+ new_cursor = { new_line, new_column };
+ }
+ m_editor->toggle_selection_if_needed_for_event(event.shift());
+ m_editor->set_cursor(new_cursor);
+ };
+}
+
+void EditingEngine::move_page_up(const KeyEvent& event)
+{
+ move_up(event, 1);
+};
+
+void EditingEngine::move_page_down(const KeyEvent& event)
+{
+ move_down(event, 1);
+};
+
+void EditingEngine::move_to_first_line()
+{
+ m_editor->set_cursor(0, 0);
+};
+
+void EditingEngine::move_to_last_line()
+{
+ m_editor->set_cursor(m_editor->line_count() - 1, m_editor->lines()[m_editor->line_count() - 1].length());
+};
+
+void EditingEngine::get_selection_line_boundaries(size_t& first_line, size_t& last_line)
+{
+ auto selection = m_editor->normalized_selection();
+ if (!selection.is_valid()) {
+ first_line = m_editor->cursor().line();
+ last_line = m_editor->cursor().line();
+ return;
+ }
+ first_line = selection.start().line();
+ last_line = selection.end().line();
+ if (first_line != last_line && selection.end().column() == 0)
+ last_line -= 1;
+}
+
+void EditingEngine::move_selected_lines_up()
+{
+ if (!m_editor->is_editable())
+ return;
+ size_t first_line;
+ size_t last_line;
+ get_selection_line_boundaries(first_line, last_line);
+
+ if (first_line == 0)
+ return;
+
+ auto& lines = m_editor->document().lines();
+ lines.insert((int)last_line, lines.take((int)first_line - 1));
+ m_editor->set_cursor({ first_line - 1, 0 });
+
+ if (m_editor->has_selection()) {
+ m_editor->selection()->set_start({ first_line - 1, 0 });
+ m_editor->selection()->set_end({ last_line - 1, m_editor->line(last_line - 1).length() });
+ }
+
+ m_editor->did_change();
+ m_editor->update();
+}
+
+void EditingEngine::move_selected_lines_down()
+{
+ if (!m_editor->is_editable())
+ return;
+ size_t first_line;
+ size_t last_line;
+ get_selection_line_boundaries(first_line, last_line);
+
+ auto& lines = m_editor->document().lines();
+ ASSERT(lines.size() != 0);
+ if (last_line >= lines.size() - 1)
+ return;
+
+ lines.insert((int)first_line, lines.take((int)last_line + 1));
+ m_editor->set_cursor({ first_line + 1, 0 });
+
+ if (m_editor->has_selection()) {
+ m_editor->selection()->set_start({ first_line + 1, 0 });
+ m_editor->selection()->set_end({ last_line + 1, m_editor->line(last_line + 1).length() });
+ }
+
+ m_editor->did_change();
+ m_editor->update();
+}
+
+void EditingEngine::delete_char()
+{
+ if (!m_editor->is_editable())
+ return;
+ m_editor->do_delete();
+};
+
+void EditingEngine::delete_line()
+{
+ if (!m_editor->is_editable())
+ return;
+ m_editor->delete_current_line();
+};
+
+}
diff --git a/Libraries/LibGUI/EditingEngine.h b/Libraries/LibGUI/EditingEngine.h
new file mode 100644
index 0000000000..37c25410ce
--- /dev/null
+++ b/Libraries/LibGUI/EditingEngine.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Noncopyable.h>
+#include <LibGUI/Event.h>
+#include <LibGUI/TextDocument.h>
+
+namespace GUI {
+
+enum CursorWidth {
+ NARROW,
+ WIDE
+};
+
+class EditingEngine {
+ AK_MAKE_NONCOPYABLE(EditingEngine);
+ AK_MAKE_NONMOVABLE(EditingEngine);
+
+public:
+ virtual ~EditingEngine();
+
+ virtual CursorWidth cursor_width() const { return NARROW; }
+
+ void attach(TextEditor& editor);
+ void detach();
+
+ virtual bool on_key(const KeyEvent& event);
+
+protected:
+ EditingEngine() { }
+
+ WeakPtr<TextEditor> m_editor;
+
+ void move_one_left(const KeyEvent& event);
+ void move_one_right(const KeyEvent& event);
+ void move_one_up(const KeyEvent& event);
+ void move_one_down(const KeyEvent& event);
+ void move_to_previous_span(const KeyEvent& event);
+ void move_to_next_span(const KeyEvent& event);
+ void move_to_line_beginning(const KeyEvent& event);
+ void move_to_line_end(const KeyEvent& event);
+ void move_page_up(const KeyEvent& event);
+ void move_page_down(const KeyEvent& event);
+ void move_to_first_line();
+ void move_to_last_line();
+
+ void move_up(const KeyEvent& event, double page_height_factor);
+ void move_down(const KeyEvent& event, double page_height_factor);
+
+ void get_selection_line_boundaries(size_t& first_line, size_t& last_line);
+
+ void delete_line();
+ void delete_char();
+
+private:
+ void move_selected_lines_up();
+ void move_selected_lines_down();
+};
+
+}
diff --git a/Libraries/LibGUI/Forward.h b/Libraries/LibGUI/Forward.h
index 2631a7e658..3f7d19c408 100644
--- a/Libraries/LibGUI/Forward.h
+++ b/Libraries/LibGUI/Forward.h
@@ -42,6 +42,7 @@ class CheckBox;
class Command;
class DragEvent;
class DropEvent;
+class EditingEngine;
class FileSystemModel;
class Frame;
class GroupBox;
diff --git a/Libraries/LibGUI/RegularEditingEngine.cpp b/Libraries/LibGUI/RegularEditingEngine.cpp
new file mode 100644
index 0000000000..ffcbe941cd
--- /dev/null
+++ b/Libraries/LibGUI/RegularEditingEngine.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/QuickSort.h>
+#include <LibGUI/RegularEditingEngine.h>
+#include <LibGUI/TextEditor.h>
+
+namespace GUI {
+
+CursorWidth RegularEditingEngine::cursor_width() const
+{
+ return CursorWidth::NARROW;
+}
+
+bool RegularEditingEngine::on_key(const KeyEvent& event)
+{
+ if (EditingEngine::on_key(event))
+ return true;
+
+ if (event.key() == KeyCode::Key_Escape) {
+ if (m_editor->on_escape_pressed)
+ m_editor->on_escape_pressed();
+ return true;
+ }
+
+ if (event.alt() && event.shift() && event.key() == KeyCode::Key_S) {
+ sort_selected_lines();
+ return true;
+ }
+
+ return false;
+}
+
+static int strcmp_utf32(const u32* s1, const u32* s2, size_t n)
+{
+ while (n-- > 0) {
+ if (*s1++ != *s2++)
+ return s1[-1] < s2[-1] ? -1 : 1;
+ }
+ return 0;
+}
+
+void RegularEditingEngine::sort_selected_lines()
+{
+ if (!m_editor->is_editable())
+ return;
+
+ if (!m_editor->has_selection())
+ return;
+
+ size_t first_line;
+ size_t last_line;
+ get_selection_line_boundaries(first_line, last_line);
+
+ auto& lines = m_editor->document().lines();
+
+ auto start = lines.begin() + (int)first_line;
+ auto end = lines.begin() + (int)last_line + 1;
+
+ quick_sort(start, end, [](auto& a, auto& b) {
+ return strcmp_utf32(a.code_points(), b.code_points(), min(a.length(), b.length())) < 0;
+ });
+
+ m_editor->did_change();
+ m_editor->update();
+}
+
+}
diff --git a/Libraries/LibGUI/RegularEditingEngine.h b/Libraries/LibGUI/RegularEditingEngine.h
new file mode 100644
index 0000000000..87e51b08d8
--- /dev/null
+++ b/Libraries/LibGUI/RegularEditingEngine.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/EditingEngine.h>
+
+namespace GUI {
+
+class RegularEditingEngine final : public EditingEngine {
+
+public:
+ virtual CursorWidth cursor_width() const override;
+
+ virtual bool on_key(const KeyEvent& event) override;
+
+private:
+ void sort_selected_lines();
+};
+
+}
diff --git a/Libraries/LibGUI/TextEditor.cpp b/Libraries/LibGUI/TextEditor.cpp
index 49e177d455..19458a19a6 100644
--- a/Libraries/LibGUI/TextEditor.cpp
+++ b/Libraries/LibGUI/TextEditor.cpp
@@ -24,7 +24,6 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include <AK/QuickSort.h>
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <AK/TemporaryChange.h>
@@ -32,9 +31,11 @@
#include <LibGUI/Action.h>
#include <LibGUI/AutocompleteProvider.h>
#include <LibGUI/Clipboard.h>
+#include <LibGUI/EditingEngine.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
+#include <LibGUI/RegularEditingEngine.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/SyntaxHighlighter.h>
#include <LibGUI/TextEditor.h>
@@ -82,6 +83,7 @@ TextEditor::TextEditor(Type type)
});
m_automatic_selection_scroll_timer->stop();
create_actions();
+ set_editing_engine(make<RegularEditingEngine>());
}
TextEditor::~TextEditor()
@@ -599,22 +601,6 @@ void TextEditor::paint_event(PaintEvent& event)
painter.fill_rect(cursor_content_rect(), palette().text_cursor());
}
-void TextEditor::toggle_selection_if_needed_for_event(const KeyEvent& event)
-{
- if (event.shift() && !m_selection.is_valid()) {
- m_selection.set(m_cursor, {});
- did_update_selection();
- update();
- return;
- }
- if (!event.shift() && m_selection.is_valid()) {
- m_selection.clear();
- did_update_selection();
- update();
- return;
- }
-}
-
void TextEditor::select_all()
{
TextPosition start_of_document { 0, 0 };
@@ -625,99 +611,6 @@ void TextEditor::select_all()
update();
}
-void TextEditor::get_selection_line_boundaries(size_t& first_line, size_t& last_line)
-{
- auto selection = normalized_selection();
- if (!selection.is_valid()) {
- first_line = m_cursor.line();
- last_line = m_cursor.line();
- return;
- }
- first_line = selection.start().line();
- last_line = selection.end().line();
- if (first_line != last_line && selection.end().column() == 0)
- last_line -= 1;
-}
-
-void TextEditor::move_selected_lines_up()
-{
- size_t first_line;
- size_t last_line;
- get_selection_line_boundaries(first_line, last_line);
-
- if (first_line == 0)
- return;
-
- auto& lines = document().lines();
- lines.insert((int)last_line, lines.take((int)first_line - 1));
- m_cursor = { first_line - 1, 0 };
-
- if (has_selection()) {
- m_selection.set_start({ first_line - 1, 0 });
- m_selection.set_end({ last_line - 1, line(last_line - 1).length() });
- }
-
- did_change();
- update();
-}
-
-void TextEditor::move_selected_lines_down()
-{
- size_t first_line;
- size_t last_line;
- get_selection_line_boundaries(first_line, last_line);
-
- auto& lines = document().lines();
- ASSERT(lines.size() != 0);
- if (last_line >= lines.size() - 1)
- return;
-
- lines.insert((int)first_line, lines.take((int)last_line + 1));
- m_cursor = { first_line + 1, 0 };
-
- if (has_selection()) {
- m_selection.set_start({ first_line + 1, 0 });
- m_selection.set_end({ last_line + 1, line(last_line + 1).length() });
- }
-
- did_change();
- update();
-}
-
-static int strcmp_utf32(const u32* s1, const u32* s2, size_t n)
-{
- while (n-- > 0) {
- if (*s1++ != *s2++)
- return s1[-1] < s2[-1] ? -1 : 1;
- }
- return 0;
-}
-
-void TextEditor::sort_selected_lines()
-{
- if (!is_editable())
- return;
-
- if (!has_selection())
- return;
-
- size_t first_line;
- size_t last_line;
- get_selection_line_boundaries(first_line, last_line);
-
- auto& lines = document().lines();
-
- auto start = lines.begin() + (int)first_line;
- auto end = lines.begin() + (int)last_line + 1;
-
- quick_sort(start, end, [](auto& a, auto& b) {
- return strcmp_utf32(a.code_points(), b.code_points(), min(a.length(), b.length())) < 0;
- });
-
- did_change();
- update();
-}
-
void TextEditor::keydown_event(KeyEvent& event)
{
TemporaryChange change { m_should_keep_autocomplete_box, true };
@@ -742,293 +635,81 @@ void TextEditor::keydown_event(KeyEvent& event)
return;
}
- if (is_single_line() && event.key() == KeyCode::Key_Tab)
- return ScrollableWidget::keydown_event(event);
-
- if (is_single_line() && event.key() == KeyCode::Key_Return) {
- if (on_return_pressed)
- on_return_pressed();
- return;
- }
+ if (is_single_line()) {
+ if (event.key() == KeyCode::Key_Tab)
+ return ScrollableWidget::keydown_event(event);
- ArmedScopeGuard update_autocomplete { [&] {
- if (m_autocomplete_box && m_autocomplete_box->is_visible()) {
- m_autocomplete_provider->provide_completions([&](auto completions) {
- m_autocomplete_box->update_suggestions(move(completions));
- });
+ if (event.key() == KeyCode::Key_Return) {
+ if (on_return_pressed)
+ on_return_pressed();
+ return;
}
- } };
- if (event.key() == KeyCode::Key_Escape) {
- if (on_escape_pressed)
- on_escape_pressed();
- return;
- }
- if (is_multi_line() && event.key() == KeyCode::Key_Up) {
- if (m_cursor.line() > 0 || m_line_wrapping_enabled) {
- if (event.ctrl() && event.shift()) {
- move_selected_lines_up();
- return;
- }
- TextPosition new_cursor;
- if (m_line_wrapping_enabled) {
- auto position_above = cursor_content_rect().location().translated(0, -line_height());
- new_cursor = text_position_at_content_position(position_above);
- } else {
- size_t new_line = m_cursor.line() - 1;
- size_t new_column = min(m_cursor.column(), line(new_line).length());
- new_cursor = { new_line, new_column };
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
- }
- return;
- } else if (event.key() == KeyCode::Key_Up) {
- if (on_up_pressed)
- on_up_pressed();
- return;
- }
- if (is_multi_line() && event.key() == KeyCode::Key_Down) {
- if (m_cursor.line() < (line_count() - 1) || m_line_wrapping_enabled) {
- if (event.ctrl() && event.shift()) {
- move_selected_lines_down();
- return;
- }
- TextPosition new_cursor;
- if (m_line_wrapping_enabled) {
- new_cursor = text_position_at_content_position(cursor_content_rect().location().translated(0, line_height()));
- auto position_below = cursor_content_rect().location().translated(0, line_height());
- new_cursor = text_position_at_content_position(position_below);
- } else {
- size_t new_line = m_cursor.line() + 1;
- size_t new_column = min(m_cursor.column(), line(new_line).length());
- new_cursor = { new_line, new_column };
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
- }
- return;
- } else if (event.key() == KeyCode::Key_Down) {
- if (on_down_pressed)
- on_down_pressed();
- return;
- }
- if (is_multi_line() && event.key() == KeyCode::Key_PageUp) {
- if (m_cursor.line() > 0 || m_line_wrapping_enabled) {
- TextPosition new_cursor;
- if (m_line_wrapping_enabled) {
- auto position_above = cursor_content_rect().location().translated(0, -visible_content_rect().height());
- new_cursor = text_position_at_content_position(position_above);
- } else {
- size_t page_step = (size_t)visible_content_rect().height() / (size_t)line_height();
- size_t new_line = m_cursor.line() < page_step ? 0 : m_cursor.line() - page_step;
- size_t new_column = min(m_cursor.column(), line(new_line).length());
- new_cursor = { new_line, new_column };
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
+ if (event.key() == KeyCode::Key_Up) {
+ if (on_up_pressed)
+ on_up_pressed();
+ return;
}
- return;
- } else if (event.key() == KeyCode::Key_PageUp) {
- if (on_pageup_pressed)
- on_pageup_pressed();
- return;
- }
- if (is_multi_line() && event.key() == KeyCode::Key_PageDown) {
- if (m_cursor.line() < (line_count() - 1) || m_line_wrapping_enabled) {
- TextPosition new_cursor;
- if (m_line_wrapping_enabled) {
- auto position_below = cursor_content_rect().location().translated(0, visible_content_rect().height());
- new_cursor = text_position_at_content_position(position_below);
- } else {
- size_t new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height());
- size_t new_column = min(m_cursor.column(), lines()[new_line].length());
- new_cursor = { new_line, new_column };
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
+
+ if (event.key() == KeyCode::Key_Down) {
+ if (on_down_pressed)
+ on_down_pressed();
+ return;
}
- return;
- } else if (event.key() == KeyCode::Key_PageDown) {
- if (on_pagedown_pressed)
- on_pagedown_pressed();
- return;
- }
- if (event.key() == KeyCode::Key_Left) {
- if (!event.shift() && m_selection.is_valid()) {
- set_cursor(m_selection.normalized().start());
- m_selection.clear();
- did_update_selection();
- if (!event.ctrl()) {
- update();
- return;
- }
+
+ if (event.key() == KeyCode::Key_PageUp) {
+ if (on_pageup_pressed)
+ on_pageup_pressed();
+ return;
}
- if (event.ctrl()) {
- TextPosition new_cursor;
- if (document().has_spans()) {
- auto span = document().first_non_skippable_span_before(m_cursor);
- if (span.has_value()) {
- new_cursor = span.value().range.start();
- } else {
- // No remaining spans, just use word break calculation
- new_cursor = document().first_word_break_before(m_cursor, true);
- }
- } else {
- new_cursor = document().first_word_break_before(m_cursor, true);
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
+
+ if (event.key() == KeyCode::Key_PageDown) {
+ if (on_pagedown_pressed)
+ on_pagedown_pressed();
return;
}
- if (m_cursor.column() > 0) {
- int new_column = m_cursor.column() - 1;
- toggle_selection_if_needed_for_event(event);
- set_cursor(m_cursor.line(), new_column);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
- } else if (m_cursor.line() > 0) {
- int new_line = m_cursor.line() - 1;
- int new_column = lines()[new_line].length();
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_line, new_column);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
+
+ } else if (is_multi_line()) {
+ ArmedScopeGuard update_autocomplete { [&] {
+ if (m_autocomplete_box && m_autocomplete_box->is_visible()) {
+ m_autocomplete_provider->provide_completions([&](auto completions) {
+ m_autocomplete_box->update_suggestions(move(completions));
+ });
}
- }
- return;
- }
- if (event.key() == KeyCode::Key_Right) {
- if (!event.shift() && m_selection.is_valid()) {
- set_cursor(m_selection.normalized().end());
- m_selection.clear();
- did_update_selection();
- if (!event.ctrl()) {
- update();
+ } };
+
+ if (!event.shift() && !event.alt() && event.ctrl() && event.key() == KeyCode::Key_Space) {
+ if (m_autocomplete_provider) {
+ try_show_autocomplete();
+ update_autocomplete.disarm();
return;
}
}
- if (event.ctrl()) {
- TextPosition new_cursor;
- if (document().has_spans()) {
- auto span = document().first_non_skippable_span_after(m_cursor);
- if (span.has_value()) {
- new_cursor = span.value().range.start();
- } else {
- // No remaining spans, just use word break calculation
- new_cursor = document().first_word_break_after(m_cursor);
- }
- } else {
- new_cursor = document().first_word_break_after(m_cursor);
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
- return;
- }
- int new_line = m_cursor.line();
- int new_column = m_cursor.column();
- if (m_cursor.column() < current_line().length()) {
- new_line = m_cursor.line();
- new_column = m_cursor.column() + 1;
- } else if (m_cursor.line() != line_count() - 1) {
- new_line = m_cursor.line() + 1;
- new_column = 0;
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_line, new_column);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
- return;
- }
- if (!event.ctrl() && event.key() == KeyCode::Key_Home) {
- TextPosition new_cursor;
- toggle_selection_if_needed_for_event(event);
- if (m_line_wrapping_enabled) {
- // FIXME: Replicate the first_nonspace_column behavior in wrapping mode.
- auto home_position = cursor_content_rect().location().translated(-width(), 0);
- new_cursor = text_position_at_content_position(home_position);
- } else {
- size_t first_nonspace_column = current_line().first_non_whitespace_column();
- if (m_cursor.column() == first_nonspace_column) {
- new_cursor = { m_cursor.line(), 0 };
- } else {
- new_cursor = { m_cursor.line(), first_nonspace_column };
- }
- }
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
- return;
+ } else {
+ ASSERT_NOT_REACHED();
}
- if (!event.ctrl() && event.key() == KeyCode::Key_End) {
- TextPosition new_cursor;
- if (m_line_wrapping_enabled) {
- auto end_position = cursor_content_rect().location().translated(width(), 0);
- new_cursor = text_position_at_content_position(end_position);
- } else {
- new_cursor = { m_cursor.line(), current_line().length() };
- }
- toggle_selection_if_needed_for_event(event);
- set_cursor(new_cursor);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
+
+ if (m_editing_engine->on_key(event))
return;
- }
- if (event.ctrl() && event.key() == KeyCode::Key_Home) {
- toggle_selection_if_needed_for_event(event);
- set_cursor(0, 0);
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
+
+ if (event.key() == KeyCode::Key_Escape) {
+ if (on_escape_pressed)
+ on_escape_pressed();
return;
}
- if (event.ctrl() && event.key() == KeyCode::Key_End) {
- toggle_selection_if_needed_for_event(event);
- set_cursor(line_count() - 1, lines()[line_count() - 1].length());
- if (event.shift() && m_selection.start().is_valid()) {
- m_selection.set_end(m_cursor);
- did_update_selection();
- }
+
+ if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) {
+ if (m_autocomplete_box)
+ m_autocomplete_box->close();
return;
}
- if (event.alt() && event.shift() && event.key() == KeyCode::Key_S) {
- sort_selected_lines();
+
+ if (event.key() == KeyCode::Key_Delete) {
+ if (m_autocomplete_box)
+ m_autocomplete_box->close();
return;
}
+
if (event.key() == KeyCode::Key_Backspace) {
if (!is_editable())
return;
@@ -1069,43 +750,8 @@ void TextEditor::keydown_event(KeyEvent& event)
return;
}
- if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) {
- if (!is_editable())
- return;
- if (m_autocomplete_box)
- m_autocomplete_box->close();
- delete_current_line();
- return;
- }
-
- if (event.key() == KeyCode::Key_Delete) {
- if (!is_editable())
- return;
- if (m_autocomplete_box)
- m_autocomplete_box->close();
- do_delete();
- return;
- }
-
- if (!event.shift() && !event.alt() && event.ctrl() && event.key() == KeyCode::Key_Space) {
- if (m_autocomplete_provider) {
- try_show_autocomplete();
- update_autocomplete.disarm();
- return;
- }
- }
-
- if (is_editable() && !event.ctrl() && !event.alt() && event.code_point() != 0) {
- StringBuilder sb;
- sb.append_code_point(event.code_point());
-
- if (should_autocomplete_automatically()) {
- if (sb.string_view().is_whitespace())
- m_autocomplete_timer->stop();
- else
- m_autocomplete_timer->start();
- }
- insert_at_cursor_or_replace_selection(sb.to_string());
+ if (!event.ctrl() && !event.alt() && event.code_point() != 0) {
+ add_code_point(event.code_point());
return;
}
@@ -1156,6 +802,53 @@ void TextEditor::do_delete()
}
}
+void TextEditor::add_code_point(u32 code_point)
+{
+ if (!is_editable())
+ return;
+
+ StringBuilder sb;
+ sb.append_code_point(code_point);
+
+ if (should_autocomplete_automatically()) {
+ if (sb.string_view().is_whitespace())
+ m_autocomplete_timer->stop();
+ else
+ m_autocomplete_timer->start();
+ }
+ insert_at_cursor_or_replace_selection(sb.to_string());
+};
+
+void TextEditor::reset_cursor_blink()
+{
+ m_cursor_state = true;
+ update_cursor();
+ stop_timer();
+ start_timer(500);
+}
+
+void TextEditor::toggle_selection_if_needed_for_event(bool is_selecting)
+{
+ if (is_selecting && !selection()->is_valid()) {
+ selection()->set(cursor(), {});
+ did_update_selection();
+ update();
+ return;
+ }
+ if (!is_selecting && selection()->is_valid()) {
+ selection()->clear();
+ did_update_selection();
+ update();
+ return;
+ }
+ if (is_selecting && selection()->start().is_valid()) {
+ selection()->set_end(cursor());
+ did_update_selection();
+ update();
+ return;
+ }
+}
+
int TextEditor::content_x_for_position(const TextPosition& position) const
{
auto& line = this->line(position.line());
@@ -1208,7 +901,7 @@ Gfx::IntRect TextEditor::content_rect_for_position(const TextPosition& position)
rect = {
visual_line_rect.x() + x - (m_horizontal_content_padding),
visual_line_rect.y(),
- 1,
+ m_editing_engine->cursor_width() == CursorWidth::WIDE ? 7 : 1,
line_height()
};
return IterationDecision::Break;
@@ -1335,6 +1028,7 @@ void TextEditor::focusin_event(FocusEvent& event)
select_all();
m_cursor_state = true;
update_cursor();
+ stop_timer();
start_timer(500);
if (on_focusin)
on_focusin();
@@ -1903,6 +1597,26 @@ void TextEditor::set_autocomplete_provider(OwnPtr<AutocompleteProvider>&& provid
m_autocomplete_box->close();
}
+const EditingEngine* TextEditor::editing_engine() const
+{
+ return m_editing_engine.ptr();
+}
+
+void TextEditor::set_editing_engine(OwnPtr<EditingEngine> editing_engine)
+{
+ if (m_editing_engine)
+ m_editing_engine->detach();
+ m_editing_engine = move(editing_engine);
+
+ ASSERT(m_editing_engine);
+ m_editing_engine->attach(*this);
+
+ m_cursor_state = true;
+ update_cursor();
+ stop_timer();
+ start_timer(500);
+}
+
int TextEditor::line_height() const
{
return font().glyph_height() + m_line_spacing;
@@ -1944,5 +1658,9 @@ void TextEditor::set_should_autocomplete_automatically(bool value)
remove_child(*m_autocomplete_timer);
m_autocomplete_timer = nullptr;
}
+int TextEditor::number_of_visible_lines() const
+{
+ return visible_content_rect().height() / line_height();
+}
}
diff --git a/Libraries/LibGUI/TextEditor.h b/Libraries/LibGUI/TextEditor.h
index bfc146fac8..1ffeb36555 100644
--- a/Libraries/LibGUI/TextEditor.h
+++ b/Libraries/LibGUI/TextEditor.h
@@ -43,6 +43,7 @@ class TextEditor
: public ScrollableWidget
, public TextDocument::Client {
C_OBJECT(TextEditor)
+
public:
enum Type {
MultiLine,
@@ -65,6 +66,9 @@ public:
const String& placeholder() const { return m_placeholder; }
void set_placeholder(const StringView& placeholder) { m_placeholder = placeholder; }
+ TextDocumentLine& current_line() { return line(m_cursor.line()); }
+ const TextDocumentLine& current_line() const { return line(m_cursor.line()); }
+
void set_visualize_trailing_whitespace(bool);
bool visualize_trailing_whitespace() const { return m_visualize_trailing_whitespace; }
@@ -107,6 +111,10 @@ public:
void scroll_cursor_into_view();
void scroll_position_into_view(const TextPosition&);
size_t line_count() const { return document().line_count(); }
+ TextDocumentLine& line(size_t index) { return document().line(index); }
+ const TextDocumentLine& line(size_t index) const { return document().line(index); }
+ NonnullOwnPtrVector<TextDocumentLine>& lines() { return document().lines(); }
+ const NonnullOwnPtrVector<TextDocumentLine>& lines() const { return document().lines(); }
int line_spacing() const { return m_line_spacing; }
int line_height() const;
TextPosition cursor() const { return m_cursor; }
@@ -164,11 +172,27 @@ public:
const AutocompleteProvider* autocomplete_provider() const;
void set_autocomplete_provider(OwnPtr<AutocompleteProvider>&&);
+ const EditingEngine* editing_engine() const;
+ void set_editing_engine(OwnPtr<EditingEngine>);
+
bool should_autocomplete_automatically() const { return m_autocomplete_timer; }
void set_should_autocomplete_automatically(bool);
bool is_in_drag_select() const { return m_in_drag_select; }
+ TextRange* selection() { return &m_selection; };
+ void did_update_selection();
+ void did_change();
+ void update_cursor();
+
+ void add_code_point(u32 code_point);
+ void reset_cursor_blink();
+ void toggle_selection_if_needed_for_event(bool is_selecting);
+
+ int number_of_visible_lines() const;
+ Gfx::IntRect cursor_content_rect() const;
+ TextPosition text_position_at_content_position(const Gfx::IntPoint&) const;
+
protected:
explicit TextEditor(Type = Type::MultiLine);
@@ -191,7 +215,6 @@ protected:
Gfx::IntRect ruler_content_rect(size_t line) const;
TextPosition text_position_at(const Gfx::IntPoint&) const;
- TextPosition text_position_at_content_position(const Gfx::IntPoint&) const;
bool ruler_visible() const { return m_ruler_visible; }
Gfx::IntRect content_rect_for_position(const TextPosition&) const;
int ruler_width() const;
@@ -211,7 +234,6 @@ private:
void create_actions();
void paint_ruler(Painter&);
void update_content_size();
- void did_change();
int fixed_glyph_width() const;
void defer_reflow();
@@ -240,27 +262,13 @@ private:
Gfx::IntRect line_content_rect(size_t item_index) const;
Gfx::IntRect line_widget_rect(size_t line_index) const;
- Gfx::IntRect cursor_content_rect() const;
- void update_cursor();
- const NonnullOwnPtrVector<TextDocumentLine>& lines() const { return document().lines(); }
- NonnullOwnPtrVector<TextDocumentLine>& lines() { return document().lines(); }
- TextDocumentLine& line(size_t index) { return document().line(index); }
- const TextDocumentLine& line(size_t index) const { return document().line(index); }
- TextDocumentLine& current_line() { return line(m_cursor.line()); }
- const TextDocumentLine& current_line() const { return line(m_cursor.line()); }
- void toggle_selection_if_needed_for_event(const KeyEvent&);
void delete_selection();
- void did_update_selection();
int content_x_for_position(const TextPosition&) const;
Gfx::IntRect ruler_rect_in_inner_coordinates() const;
Gfx::IntRect visible_text_rect_in_inner_coordinates() const;
void recompute_all_visual_lines();
void ensure_cursor_is_valid();
void flush_pending_change_notification_if_needed();
- void get_selection_line_boundaries(size_t& first_line, size_t& last_line);
- void move_selected_lines_up();
- void move_selected_lines_down();
- void sort_selected_lines();
size_t visual_line_containing(size_t line_index, size_t column) const;
void recompute_visual_lines(size_t line_index);
@@ -336,6 +344,8 @@ private:
RefPtr<Core::Timer> m_automatic_selection_scroll_timer;
RefPtr<Core::Timer> m_autocomplete_timer;
+ OwnPtr<EditingEngine> m_editing_engine;
+
Gfx::IntPoint m_last_mousemove_position;
RefPtr<Gfx::Bitmap> m_icon;
diff --git a/Libraries/LibGUI/VimEditingEngine.cpp b/Libraries/LibGUI/VimEditingEngine.cpp
new file mode 100644
index 0000000000..94b9631004
--- /dev/null
+++ b/Libraries/LibGUI/VimEditingEngine.cpp
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibGUI/Event.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/VimEditingEngine.h>
+
+namespace GUI {
+
+CursorWidth VimEditingEngine::cursor_width() const
+{
+ return m_vim_mode == VimMode::Normal ? CursorWidth::WIDE : CursorWidth::NARROW;
+}
+
+bool VimEditingEngine::on_key(const KeyEvent& event)
+{
+ if (EditingEngine::on_key(event))
+ return true;
+
+ switch (m_vim_mode) {
+ case (VimMode::Insert):
+ return on_key_in_insert_mode(event);
+ case (VimMode::Normal):
+ return on_key_in_normal_mode(event);
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ return false;
+}
+
+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)) {
+ switch_to_normal_mode();
+ return true;
+ }
+ return false;
+}
+
+bool VimEditingEngine::on_key_in_normal_mode(const KeyEvent& event)
+{
+ if (m_previous_key == KeyCode::Key_D) {
+ if (event.key() == KeyCode::Key_D) {
+ delete_line();
+ }
+ m_previous_key = {};
+ } else if (m_previous_key == KeyCode::Key_G) {
+ if (event.key() == KeyCode::Key_G) {
+ move_to_first_line();
+ }
+ m_previous_key = {};
+ } else {
+ // 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;
+ 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;
+ case (KeyCode::Key_O):
+ move_to_line_beginning(event);
+ m_editor->add_code_point(0x0A);
+ move_one_up(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);
+ break;
+ case (KeyCode::Key_R):
+ m_editor->redo();
+ break;
+ case (KeyCode::Key_U):
+ move_half_page_up(event);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // No modifier is pressed.
+ if (!event.ctrl() && !event.shift() && !event.alt()) {
+ switch (event.key()) {
+ case (KeyCode::Key_A):
+ move_one_right(event);
+ switch_to_insert_mode();
+ break;
+ case (KeyCode::Key_B):
+ move_to_previous_span(event); // FIXME: This probably isn't 100% correct.
+ break;
+ case (KeyCode::Key_Backspace):
+ case (KeyCode::Key_H):
+ case (KeyCode::Key_Left):
+ move_one_left(event);
+ break;
+ case (KeyCode::Key_D):
+ case (KeyCode::Key_G):
+ m_previous_key = event.key();
+ break;
+ case (KeyCode::Key_Down):
+ case (KeyCode::Key_J):
+ move_one_down(event);
+ break;
+ 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;
+ case (KeyCode::Key_O):
+ move_to_line_end(event);
+ m_editor->add_code_point(0x0A);
+ switch_to_insert_mode();
+ break;
+ case (KeyCode::Key_U):
+ m_editor->undo();
+ break;
+ case (KeyCode::Key_W):
+ move_to_next_span(event); // FIXME: This probably isn't 100% correct.
+ break;
+ case (KeyCode::Key_X):
+ delete_char();
+ break;
+ case (KeyCode::Key_0):
+ move_to_line_beginning(event);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return true;
+}
+
+void VimEditingEngine::switch_to_normal_mode()
+{
+ m_vim_mode = VimMode::Normal;
+ m_editor->reset_cursor_blink();
+};
+
+void VimEditingEngine::switch_to_insert_mode()
+{
+ m_vim_mode = VimMode::Insert;
+ m_editor->reset_cursor_blink();
+};
+
+void VimEditingEngine::move_half_page_up(const KeyEvent& event)
+{
+ move_up(event, 0.5);
+};
+
+void VimEditingEngine::move_half_page_down(const KeyEvent& event)
+{
+ move_down(event, 0.5);
+};
+
+}
diff --git a/Libraries/LibGUI/VimEditingEngine.h b/Libraries/LibGUI/VimEditingEngine.h
new file mode 100644
index 0000000000..5b399008b1
--- /dev/null
+++ b/Libraries/LibGUI/VimEditingEngine.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/EditingEngine.h>
+
+namespace GUI {
+
+class VimEditingEngine final : public EditingEngine {
+
+public:
+ virtual CursorWidth cursor_width() const override;
+
+ virtual bool on_key(const KeyEvent& event) override;
+
+private:
+ enum VimMode {
+ Normal,
+ Insert,
+ };
+
+ VimMode m_vim_mode { VimMode::Normal };
+
+ KeyCode m_previous_key {};
+ void switch_to_normal_mode();
+ void switch_to_insert_mode();
+ void move_half_page_up(const KeyEvent& event);
+ void move_half_page_down(const KeyEvent& event);
+
+ bool on_key_in_insert_mode(const KeyEvent& event);
+ bool on_key_in_normal_mode(const KeyEvent& event);
+};
+
+}