1
Fork 0
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:
Rok Povsic 2021-01-02 11:59:55 +01:00 committed by Andreas Kling
parent 1c17ecdeb7
commit b4a783d923
13 changed files with 1143 additions and 438 deletions

View file

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

View file

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

View file

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

View file

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

View 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();
};
}

View 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();
};
}

View file

@ -42,6 +42,7 @@ class CheckBox;
class Command;
class DragEvent;
class DropEvent;
class EditingEngine;
class FileSystemModel;
class Frame;
class GroupBox;

View 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();
}
}

View 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();
};
}

View file

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

View file

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

View 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);
};
}

View 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);
};
}