diff options
author | Rok Povsic <rok.povsic@gmail.com> | 2021-01-02 11:59:55 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-01-05 00:00:36 +0100 |
commit | b4a783d9231d1c0d8dcc24d5659c34443e2976ab (patch) | |
tree | 499228084fc4e36187a5df0e7e0e391456311929 /Libraries | |
parent | 1c17ecdeb72cf62f651385fa302d401a19541345 (diff) | |
download | serenity-b4a783d9231d1c0d8dcc24d5659c34443e2976ab.zip |
TextEditor+EditingEngine: Add support for the basics of Vim emulation
Diffstat (limited to 'Libraries')
-rw-r--r-- | Libraries/LibGUI/CMakeLists.txt | 3 | ||||
-rw-r--r-- | Libraries/LibGUI/EditingEngine.cpp | 453 | ||||
-rw-r--r-- | Libraries/LibGUI/EditingEngine.h | 85 | ||||
-rw-r--r-- | Libraries/LibGUI/Forward.h | 1 | ||||
-rw-r--r-- | Libraries/LibGUI/RegularEditingEngine.cpp | 91 | ||||
-rw-r--r-- | Libraries/LibGUI/RegularEditingEngine.h | 44 | ||||
-rw-r--r-- | Libraries/LibGUI/TextEditor.cpp | 548 | ||||
-rw-r--r-- | Libraries/LibGUI/TextEditor.h | 42 | ||||
-rw-r--r-- | Libraries/LibGUI/VimEditingEngine.cpp | 213 | ||||
-rw-r--r-- | Libraries/LibGUI/VimEditingEngine.h | 58 |
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); +}; + +} |