1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 03:37:43 +00:00

TextEditor+LibGUI: Add case matching and wrap around optionality

Adds simple ASCII case matching and wrap around toggles to
TextEditor's find/replace widget and reorganizes its layout
This commit is contained in:
thankyouverycool 2021-02-21 19:01:26 -05:00 committed by Andreas Kling
parent b4c0314f1d
commit 3e987eba2b
5 changed files with 98 additions and 106 deletions

View file

@ -40,9 +40,11 @@
#include <LibGUI/ActionGroup.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/FontPicker.h>
#include <LibGUI/GMLSyntaxHighlighter.h>
#include <LibGUI/GroupBox.h>
#include <LibGUI/INISyntaxHighlighter.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
@ -60,7 +62,6 @@
#include <LibMarkdown/Document.h>
#include <LibWeb/OutOfProcessWebView.h>
#include <Shell/SyntaxHighlighter.h>
#include <string.h>
TextEditorWidget::TextEditorWidget()
{
@ -108,26 +109,42 @@ TextEditorWidget::TextEditorWidget()
}
};
m_find_replace_widget = *find_descendant_of_type_named<GUI::Widget>("find_replace_widget");
m_find_replace_widget = *find_descendant_of_type_named<GUI::GroupBox>("find_replace_widget");
m_find_widget = *find_descendant_of_type_named<GUI::Widget>("find_widget");
m_replace_widget = *find_descendant_of_type_named<GUI::Widget>("replace_widget");
m_find_textbox = m_find_widget->add<GUI::TextBox>();
m_replace_textbox = m_replace_widget->add<GUI::TextBox>();
m_find_textbox = *find_descendant_of_type_named<GUI::TextBox>("find_textbox");
m_find_textbox->set_placeholder("Find");
m_replace_textbox = *find_descendant_of_type_named<GUI::TextBox>("replace_textbox");
m_replace_textbox->set_placeholder("Replace");
m_match_case_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("match_case_checkbox");
m_match_case_checkbox->on_checked = [this] {
m_match_case = m_match_case_checkbox->is_checked();
};
m_match_case_checkbox->set_checked(true);
m_regex_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("regex_checkbox");
m_regex_checkbox->on_checked = [this] {
m_use_regex = m_regex_checkbox->is_checked();
};
m_regex_checkbox->set_checked(false);
m_wrap_around_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("wrap_around_checkbox");
m_wrap_around_checkbox->on_checked = [this] {
m_should_wrap = m_wrap_around_checkbox->is_checked();
};
m_wrap_around_checkbox->set_checked(true);
m_find_next_action = GUI::Action::create("Find next", { Mod_Ctrl, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find-next.png"), [&](auto&) {
auto needle = m_find_textbox->text();
if (needle.is_empty()) {
dbgln("find_next(\"\")");
if (needle.is_empty())
return;
}
if (m_find_use_regex)
if (m_use_regex)
m_editor->document().update_regex_matches(needle);
auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().end(), GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().end(), m_should_wrap ? GUI::TextDocument::SearchShouldWrap::Yes : GUI::TextDocument::SearchShouldWrap::No, m_use_regex, m_match_case);
dbgln("find_next('{}') returned {}", needle, found_range);
if (found_range.is_valid()) {
m_editor->set_selection(found_range);
@ -139,27 +156,18 @@ TextEditorWidget::TextEditorWidget()
}
});
m_find_regex_action = GUI::Action::create("Find regex", { Mod_Ctrl | Mod_Shift, Key_R }, [&](auto&) {
m_find_regex_button->set_checked(!m_find_regex_button->is_checked());
m_find_use_regex = m_find_regex_button->is_checked();
});
m_find_previous_action = GUI::Action::create("Find previous", { Mod_Ctrl | Mod_Shift, Key_G }, [&](auto&) {
m_find_previous_action = GUI::Action::create("Find previous", { Mod_Ctrl | Mod_Shift, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find-previous.png"), [&](auto&) {
auto needle = m_find_textbox->text();
if (needle.is_empty()) {
dbgln("find_prev(\"\")");
if (needle.is_empty())
return;
}
if (m_use_regex)
m_editor->document().update_regex_matches(needle);
auto selection_start = m_editor->normalized_selection().start();
if (!selection_start.is_valid())
selection_start = m_editor->normalized_selection().end();
if (m_find_use_regex)
m_editor->document().update_regex_matches(needle);
auto found_range = m_editor->document().find_previous(needle, selection_start, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
auto found_range = m_editor->document().find_previous(needle, selection_start, m_should_wrap ? GUI::TextDocument::SearchShouldWrap::Yes : GUI::TextDocument::SearchShouldWrap::No, m_use_regex, m_match_case);
dbgln("find_prev(\"{}\") returned {}", needle, found_range);
if (found_range.is_valid()) {
m_editor->set_selection(found_range);
@ -171,48 +179,15 @@ TextEditorWidget::TextEditorWidget()
}
});
m_replace_next_action = GUI::Action::create("Replace next", { Mod_Ctrl, Key_F1 }, [&](auto&) {
auto needle = m_find_textbox->text();
auto substitute = m_replace_textbox->text();
if (needle.is_empty())
return;
auto selection_start = m_editor->normalized_selection().start();
if (!selection_start.is_valid())
selection_start = m_editor->normalized_selection().start();
if (m_find_use_regex)
m_editor->document().update_regex_matches(needle);
auto found_range = m_editor->document().find_next(needle, selection_start, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
if (found_range.is_valid()) {
m_editor->set_selection(found_range);
m_editor->insert_at_cursor_or_replace_selection(substitute);
} else {
GUI::MessageBox::show(window(),
String::formatted("Not found: \"{}\"", needle),
"Not found",
GUI::MessageBox::Type::Information);
}
});
m_replace_previous_action = GUI::Action::create("Replace previous", { Mod_Ctrl | Mod_Shift, Key_F1 }, [&](auto&) {
m_replace_action = GUI::Action::create("Replace", { Mod_Ctrl, Key_F1 }, [&](auto&) {
auto needle = m_find_textbox->text();
auto substitute = m_replace_textbox->text();
if (needle.is_empty())
return;
auto selection_start = m_editor->normalized_selection().start();
if (!selection_start.is_valid())
selection_start = m_editor->normalized_selection().start();
if (m_find_use_regex)
if (m_use_regex)
m_editor->document().update_regex_matches(needle);
auto found_range = m_editor->document().find_previous(needle, selection_start);
auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().start(), m_should_wrap ? GUI::TextDocument::SearchShouldWrap::Yes : GUI::TextDocument::SearchShouldWrap::No, m_use_regex, m_match_case);
if (found_range.is_valid()) {
m_editor->set_selection(found_range);
m_editor->insert_at_cursor_or_replace_selection(substitute);
@ -229,47 +204,42 @@ TextEditorWidget::TextEditorWidget()
auto substitute = m_replace_textbox->text();
if (needle.is_empty())
return;
if (m_find_use_regex)
if (m_use_regex)
m_editor->document().update_regex_matches(needle);
auto found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
auto found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_use_regex, m_match_case);
while (found_range.is_valid()) {
m_editor->set_selection(found_range);
m_editor->insert_at_cursor_or_replace_selection(substitute);
found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_use_regex, m_match_case);
}
});
m_find_previous_button = *find_descendant_of_type_named<GUI::Button>("find_previous_button");
m_find_previous_button->set_action(*m_find_previous_action);
m_find_previous_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/find-previous.png"));
m_find_next_button = *find_descendant_of_type_named<GUI::Button>("find_next_button");
m_find_next_button->set_action(*m_find_next_action);
m_find_next_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/find-next.png"));
m_find_textbox->on_return_pressed = [this] {
m_find_next_button->click();
};
m_find_regex_button = m_find_widget->add<GUI::Button>(".*");
m_find_regex_button->set_fixed_width(20);
m_find_regex_button->set_action(*m_find_regex_action);
m_find_textbox->on_escape_pressed = [this] {
m_find_replace_widget->set_visible(false);
m_editor->set_focus(true);
};
m_replace_previous_button = *find_descendant_of_type_named<GUI::Button>("replace_previous_button");
m_replace_previous_button->set_action(*m_replace_previous_action);
m_replace_next_button = *find_descendant_of_type_named<GUI::Button>("replace_next_button");
m_replace_next_button->set_action(*m_replace_next_action);
m_replace_button = *find_descendant_of_type_named<GUI::Button>("replace_button");
m_replace_button->set_action(*m_replace_action);
m_replace_all_button = *find_descendant_of_type_named<GUI::Button>("replace_all_button");
m_replace_all_button->set_action(*m_replace_all_action);
m_replace_textbox->on_return_pressed = [this] {
m_replace_next_button->click();
m_replace_button->click();
};
m_replace_textbox->on_escape_pressed = [this] {
@ -393,10 +363,8 @@ TextEditorWidget::TextEditorWidget()
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);
edit_menu.add_action(*m_find_previous_action);
edit_menu.add_action(*m_replace_next_action);
edit_menu.add_action(*m_replace_previous_action);
edit_menu.add_action(*m_replace_action);
edit_menu.add_action(*m_replace_all_action);
m_no_preview_action = GUI::Action::create_checkable(

View file

@ -78,10 +78,8 @@ private:
RefPtr<GUI::Action> m_vim_emulation_setting_action;
RefPtr<GUI::Action> m_find_next_action;
RefPtr<GUI::Action> m_find_regex_action;
RefPtr<GUI::Action> m_find_previous_action;
RefPtr<GUI::Action> m_replace_next_action;
RefPtr<GUI::Action> m_replace_previous_action;
RefPtr<GUI::Action> m_replace_action;
RefPtr<GUI::Action> m_replace_all_action;
RefPtr<GUI::Action> m_layout_toolbar_action;
@ -99,13 +97,14 @@ private:
RefPtr<GUI::TextBox> m_replace_textbox;
RefPtr<GUI::Button> m_find_previous_button;
RefPtr<GUI::Button> m_find_next_button;
RefPtr<GUI::Button> m_find_regex_button;
RefPtr<GUI::Button> m_replace_previous_button;
RefPtr<GUI::Button> m_replace_next_button;
RefPtr<GUI::Button> m_replace_button;
RefPtr<GUI::Button> m_replace_all_button;
RefPtr<GUI::Widget> m_find_replace_widget;
RefPtr<GUI::Widget> m_find_widget;
RefPtr<GUI::Widget> m_replace_widget;
RefPtr<GUI::CheckBox> m_regex_checkbox;
RefPtr<GUI::CheckBox> m_match_case_checkbox;
RefPtr<GUI::CheckBox> m_wrap_around_checkbox;
GUI::ActionGroup m_wrapping_mode_actions;
RefPtr<GUI::Action> m_no_wrapping_action;
@ -126,7 +125,9 @@ private:
bool m_document_dirty { false };
bool m_document_opening { false };
bool m_auto_detect_preview_mode { false };
bool m_find_use_regex { false };
bool m_use_regex { false };
bool m_match_case { true };
bool m_should_wrap { true };
PreviewMode m_preview_mode { PreviewMode::None };
};

View file

@ -25,14 +25,15 @@
}
}
@GUI::Widget {
@GUI::GroupBox {
name: "find_replace_widget"
visible: false
fill_with_background_color: true
fixed_height: 48
fixed_height: 56
layout: @GUI::VerticalBoxLayout {
margins: [2, 2, 2, 4]
spacing: 2
margins: [5, 5, 5, 5]
}
@GUI::Widget {
@ -41,18 +42,33 @@
fixed_height: 22
layout: @GUI::HorizontalBoxLayout {
spacing: 4
}
@GUI::Button {
name: "find_previous_button"
text: "Find previous"
fixed_width: 150
fixed_width: 38
}
@GUI::Button {
name: "find_next_button"
text: "Find next"
fixed_width: 150
fixed_width: 38
}
@GUI::TextBox {
name: "find_textbox"
}
@GUI::CheckBox {
name: "regex_checkbox"
text: "Use RegEx"
fixed_width: 80
}
@GUI::CheckBox {
name: "match_case_checkbox"
text: "Match case"
fixed_width: 85
}
}
@ -62,24 +78,29 @@
fixed_height: 22
layout: @GUI::HorizontalBoxLayout {
spacing: 4
}
@GUI::Button {
name: "replace_previous_button"
text: "Replace previous"
fixed_width: 100
name: "replace_button"
text: "Replace"
fixed_width: 80
}
@GUI::Button {
name: "replace_next_button"
text: "Replace next"
fixed_width: 100
@GUI::TextBox {
name: "replace_textbox"
}
@GUI::Button {
name: "replace_all_button"
text: "Replace all"
fixed_width: 100
fixed_width: 80
}
@GUI::CheckBox {
name: "wrap_around_checkbox"
text: "Wrap around"
fixed_width: 85
}
}
}

View file

@ -387,7 +387,7 @@ void TextDocument::update_regex_matches(const StringView& needle)
}
}
TextRange TextDocument::find_next(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap, bool regmatch)
TextRange TextDocument::find_next(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap, bool regmatch, bool match_case)
{
if (needle.is_empty())
return {};
@ -450,7 +450,7 @@ TextRange TextDocument::find_next(const StringView& needle, const TextPosition&
do {
auto ch = code_point_at(position);
// FIXME: This is not the right way to use a Unicode needle!
if (ch == (u32)needle[needle_index]) {
if (match_case ? ch == (u32)needle[needle_index] : tolower(ch) == tolower((u32)needle[needle_index])) {
if (needle_index == 0)
start_of_potential_match = position;
++needle_index;
@ -467,7 +467,7 @@ TextRange TextDocument::find_next(const StringView& needle, const TextPosition&
return {};
}
TextRange TextDocument::find_previous(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap, bool regmatch)
TextRange TextDocument::find_previous(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap, bool regmatch, bool match_case)
{
if (needle.is_empty())
return {};
@ -524,6 +524,8 @@ TextRange TextDocument::find_previous(const StringView& needle, const TextPositi
TextPosition position = start.is_valid() ? start : TextPosition(0, 0);
position = previous_position_before(position, should_wrap);
if (position.line() >= line_count())
return {};
TextPosition original_position = position;
TextPosition end_of_potential_match;
@ -532,7 +534,7 @@ TextRange TextDocument::find_previous(const StringView& needle, const TextPositi
do {
auto ch = code_point_at(position);
// FIXME: This is not the right way to use a Unicode needle!
if (ch == (u32)needle[needle_index]) {
if (match_case ? ch == (u32)needle[needle_index] : tolower(ch) == tolower((u32)needle[needle_index])) {
if (needle_index == needle.length() - 1)
end_of_potential_match = position;
if (needle_index == 0)

View file

@ -109,8 +109,8 @@ public:
Vector<TextRange> find_all(const StringView& needle, bool regmatch = false);
void update_regex_matches(const StringView&);
TextRange find_next(const StringView&, const TextPosition& start = {}, SearchShouldWrap = SearchShouldWrap::Yes, bool regmatch = false);
TextRange find_previous(const StringView&, const TextPosition& start = {}, SearchShouldWrap = SearchShouldWrap::Yes, bool regmatch = false);
TextRange find_next(const StringView&, const TextPosition& start = {}, SearchShouldWrap = SearchShouldWrap::Yes, bool regmatch = false, bool match_case = true);
TextRange find_previous(const StringView&, const TextPosition& start = {}, SearchShouldWrap = SearchShouldWrap::Yes, bool regmatch = false, bool match_case = true);
TextPosition next_position_after(const TextPosition&, SearchShouldWrap = SearchShouldWrap::Yes) const;
TextPosition previous_position_before(const TextPosition&, SearchShouldWrap = SearchShouldWrap::Yes) const;