mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 12:48:10 +00:00
TextEditor+EditingEngine: Add support for the basics of Vim emulation
This commit is contained in:
parent
1c17ecdeb7
commit
b4a783d923
13 changed files with 1143 additions and 438 deletions
|
@ -47,6 +47,7 @@
|
|||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/MenuBar.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/RegularEditingEngine.h>
|
||||
#include <LibGUI/ShellSyntaxHighlighter.h>
|
||||
#include <LibGUI/Splitter.h>
|
||||
#include <LibGUI/StatusBar.h>
|
||||
|
@ -54,6 +55,7 @@
|
|||
#include <LibGUI/TextEditor.h>
|
||||
#include <LibGUI/ToolBar.h>
|
||||
#include <LibGUI/ToolBarContainer.h>
|
||||
#include <LibGUI/VimEditingEngine.h>
|
||||
#include <LibGfx/Font.h>
|
||||
#include <LibMarkdown/Document.h>
|
||||
#include <LibWeb/OutOfProcessWebView.h>
|
||||
|
@ -69,6 +71,7 @@ TextEditorWidget::TextEditorWidget()
|
|||
m_editor->set_ruler_visible(true);
|
||||
m_editor->set_automatic_indentation_enabled(true);
|
||||
m_editor->set_line_wrapping_enabled(true);
|
||||
m_editor->set_editing_engine(make<GUI::RegularEditingEngine>());
|
||||
|
||||
m_editor->on_change = [this] {
|
||||
update_preview();
|
||||
|
@ -271,6 +274,14 @@ TextEditorWidget::TextEditorWidget()
|
|||
m_editor->set_focus(true);
|
||||
};
|
||||
|
||||
m_vim_emulation_setting_action = GUI::Action::create_checkable("Vim emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [&](auto& action) {
|
||||
if (action.is_checked())
|
||||
m_editor->set_editing_engine(make<GUI::VimEditingEngine>());
|
||||
else
|
||||
m_editor->set_editing_engine(make<GUI::RegularEditingEngine>());
|
||||
});
|
||||
m_vim_emulation_setting_action->set_checked(false);
|
||||
|
||||
m_find_replace_action = GUI::Action::create("Find/Replace...", { Mod_Ctrl, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [this](auto&) {
|
||||
m_find_replace_widget->set_visible(true);
|
||||
m_find_widget->set_visible(true);
|
||||
|
@ -380,6 +391,8 @@ TextEditorWidget::TextEditorWidget()
|
|||
edit_menu.add_action(m_editor->paste_action());
|
||||
edit_menu.add_action(m_editor->delete_action());
|
||||
edit_menu.add_separator();
|
||||
edit_menu.add_action(*m_vim_emulation_setting_action);
|
||||
edit_menu.add_separator();
|
||||
edit_menu.add_action(*m_find_replace_action);
|
||||
edit_menu.add_action(*m_find_next_action);
|
||||
edit_menu.add_action(*m_find_regex_action);
|
||||
|
|
|
@ -76,6 +76,7 @@ private:
|
|||
RefPtr<GUI::Action> m_save_as_action;
|
||||
RefPtr<GUI::Action> m_find_replace_action;
|
||||
RefPtr<GUI::Action> m_line_wrapping_setting_action;
|
||||
RefPtr<GUI::Action> m_vim_emulation_setting_action;
|
||||
|
||||
RefPtr<GUI::Action> m_find_next_action;
|
||||
RefPtr<GUI::Action> m_find_regex_action;
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/EditingEngine.h>
|
||||
#include <LibGUI/FilePicker.h>
|
||||
#include <LibGUI/InputBox.h>
|
||||
#include <LibGUI/ItemListModel.h>
|
||||
|
@ -63,6 +64,7 @@
|
|||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/MenuBar.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/RegularEditingEngine.h>
|
||||
#include <LibGUI/Splitter.h>
|
||||
#include <LibGUI/StackWidget.h>
|
||||
#include <LibGUI/TabWidget.h>
|
||||
|
@ -72,6 +74,7 @@
|
|||
#include <LibGUI/ToolBar.h>
|
||||
#include <LibGUI/ToolBarContainer.h>
|
||||
#include <LibGUI/TreeView.h>
|
||||
#include <LibGUI/VimEditingEngine.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <LibGUI/Window.h>
|
||||
#include <LibGfx/FontDatabase.h>
|
||||
|
@ -234,6 +237,7 @@ void HackStudioWidget::open_file(const String& filename)
|
|||
current_editor().set_mode(GUI::TextEditor::Editable);
|
||||
current_editor().horizontal_scrollbar().set_value(new_project_file->horizontal_scroll_value());
|
||||
current_editor().vertical_scrollbar().set_value(new_project_file->vertical_scroll_value());
|
||||
current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
|
||||
|
||||
if (filename.ends_with(".frm")) {
|
||||
set_edit_mode(EditMode::Form);
|
||||
|
@ -854,6 +858,17 @@ void HackStudioWidget::create_edit_menubar(GUI::MenuBar& menubar)
|
|||
});
|
||||
line_wrapping_action->set_checked(current_editor().is_line_wrapping_enabled());
|
||||
edit_menu.add_action(line_wrapping_action);
|
||||
|
||||
edit_menu.add_separator();
|
||||
|
||||
auto vim_emulation_setting_action = GUI::Action::create_checkable("Vim emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [this](auto& action) {
|
||||
if (action.is_checked())
|
||||
current_editor().set_editing_engine(make<GUI::VimEditingEngine>());
|
||||
else
|
||||
current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
|
||||
});
|
||||
vim_emulation_setting_action->set_checked(false);
|
||||
edit_menu.add_action(vim_emulation_setting_action);
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_build_menubar(GUI::MenuBar& menubar)
|
||||
|
|
|
@ -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
|
||||
|
|
453
Libraries/LibGUI/EditingEngine.cpp
Normal file
453
Libraries/LibGUI/EditingEngine.cpp
Normal file
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
85
Libraries/LibGUI/EditingEngine.h
Normal file
85
Libraries/LibGUI/EditingEngine.h
Normal file
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
|
@ -42,6 +42,7 @@ class CheckBox;
|
|||
class Command;
|
||||
class DragEvent;
|
||||
class DropEvent;
|
||||
class EditingEngine;
|
||||
class FileSystemModel;
|
||||
class Frame;
|
||||
class GroupBox;
|
||||
|
|
91
Libraries/LibGUI/RegularEditingEngine.cpp
Normal file
91
Libraries/LibGUI/RegularEditingEngine.cpp
Normal file
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
44
Libraries/LibGUI/RegularEditingEngine.h
Normal file
44
Libraries/LibGUI/RegularEditingEngine.h
Normal file
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
|
@ -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,15 +635,41 @@ void TextEditor::keydown_event(KeyEvent& event)
|
|||
return;
|
||||
}
|
||||
|
||||
if (is_single_line() && event.key() == KeyCode::Key_Tab)
|
||||
if (is_single_line()) {
|
||||
if (event.key() == KeyCode::Key_Tab)
|
||||
return ScrollableWidget::keydown_event(event);
|
||||
|
||||
if (is_single_line() && event.key() == KeyCode::Key_Return) {
|
||||
if (event.key() == KeyCode::Key_Return) {
|
||||
if (on_return_pressed)
|
||||
on_return_pressed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key() == KeyCode::Key_Up) {
|
||||
if (on_up_pressed)
|
||||
on_up_pressed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key() == KeyCode::Key_Down) {
|
||||
if (on_down_pressed)
|
||||
on_down_pressed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key() == KeyCode::Key_PageUp) {
|
||||
if (on_pageup_pressed)
|
||||
on_pageup_pressed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key() == KeyCode::Key_PageDown) {
|
||||
if (on_pagedown_pressed)
|
||||
on_pagedown_pressed();
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (is_multi_line()) {
|
||||
ArmedScopeGuard update_autocomplete { [&] {
|
||||
if (m_autocomplete_box && m_autocomplete_box->is_visible()) {
|
||||
m_autocomplete_provider->provide_completions([&](auto completions) {
|
||||
|
@ -759,276 +678,38 @@ void TextEditor::keydown_event(KeyEvent& event)
|
|||
}
|
||||
} };
|
||||
|
||||
if (!event.shift() && !event.alt() && event.ctrl() && event.key() == KeyCode::Key_Space) {
|
||||
if (m_autocomplete_provider) {
|
||||
try_show_autocomplete();
|
||||
update_autocomplete.disarm();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
if (m_editing_engine->on_key(event))
|
||||
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();
|
||||
|
||||
if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) {
|
||||
if (m_autocomplete_box)
|
||||
m_autocomplete_box->close();
|
||||
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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
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.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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
213
Libraries/LibGUI/VimEditingEngine.cpp
Normal file
213
Libraries/LibGUI/VimEditingEngine.cpp
Normal file
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
58
Libraries/LibGUI/VimEditingEngine.h
Normal file
58
Libraries/LibGUI/VimEditingEngine.h
Normal file
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue