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/ActionGroup.h>
#include <LibGUI/BoxLayout.h> #include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h> #include <LibGUI/Button.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/FilePicker.h> #include <LibGUI/FilePicker.h>
#include <LibGUI/FontPicker.h> #include <LibGUI/FontPicker.h>
#include <LibGUI/GMLSyntaxHighlighter.h> #include <LibGUI/GMLSyntaxHighlighter.h>
#include <LibGUI/GroupBox.h>
#include <LibGUI/INISyntaxHighlighter.h> #include <LibGUI/INISyntaxHighlighter.h>
#include <LibGUI/Menu.h> #include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h> #include <LibGUI/MenuBar.h>
@ -60,7 +62,6 @@
#include <LibMarkdown/Document.h> #include <LibMarkdown/Document.h>
#include <LibWeb/OutOfProcessWebView.h> #include <LibWeb/OutOfProcessWebView.h>
#include <Shell/SyntaxHighlighter.h> #include <Shell/SyntaxHighlighter.h>
#include <string.h>
TextEditorWidget::TextEditorWidget() 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_find_widget = *find_descendant_of_type_named<GUI::Widget>("find_widget");
m_replace_widget = *find_descendant_of_type_named<GUI::Widget>("replace_widget"); m_replace_widget = *find_descendant_of_type_named<GUI::Widget>("replace_widget");
m_find_textbox = m_find_widget->add<GUI::TextBox>(); m_find_textbox = *find_descendant_of_type_named<GUI::TextBox>("find_textbox");
m_replace_textbox = m_replace_widget->add<GUI::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&) { 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(); auto needle = m_find_textbox->text();
if (needle.is_empty()) { if (needle.is_empty())
dbgln("find_next(\"\")");
return; return;
} if (m_use_regex)
if (m_find_use_regex)
m_editor->document().update_regex_matches(needle); 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); dbgln("find_next('{}') returned {}", needle, found_range);
if (found_range.is_valid()) { if (found_range.is_valid()) {
m_editor->set_selection(found_range); 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_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&) {
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&) {
auto needle = m_find_textbox->text(); auto needle = m_find_textbox->text();
if (needle.is_empty()) { if (needle.is_empty())
dbgln("find_prev(\"\")");
return; return;
} if (m_use_regex)
m_editor->document().update_regex_matches(needle);
auto selection_start = m_editor->normalized_selection().start(); auto selection_start = m_editor->normalized_selection().start();
if (!selection_start.is_valid()) if (!selection_start.is_valid())
selection_start = m_editor->normalized_selection().end(); selection_start = m_editor->normalized_selection().end();
if (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);
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);
dbgln("find_prev(\"{}\") returned {}", needle, found_range); dbgln("find_prev(\"{}\") returned {}", needle, found_range);
if (found_range.is_valid()) { if (found_range.is_valid()) {
m_editor->set_selection(found_range); 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&) { 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)
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&) {
auto needle = m_find_textbox->text(); auto needle = m_find_textbox->text();
auto substitute = m_replace_textbox->text(); auto substitute = m_replace_textbox->text();
if (needle.is_empty()) if (needle.is_empty())
return; return;
if (m_use_regex)
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); 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()) { if (found_range.is_valid()) {
m_editor->set_selection(found_range); m_editor->set_selection(found_range);
m_editor->insert_at_cursor_or_replace_selection(substitute); m_editor->insert_at_cursor_or_replace_selection(substitute);
@ -229,47 +204,42 @@ TextEditorWidget::TextEditorWidget()
auto substitute = m_replace_textbox->text(); auto substitute = m_replace_textbox->text();
if (needle.is_empty()) if (needle.is_empty())
return; return;
if (m_find_use_regex) if (m_use_regex)
m_editor->document().update_regex_matches(needle); 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()) { while (found_range.is_valid()) {
m_editor->set_selection(found_range); m_editor->set_selection(found_range);
m_editor->insert_at_cursor_or_replace_selection(substitute); 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 = *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_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 = *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_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_textbox->on_return_pressed = [this] {
m_find_next_button->click(); 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_textbox->on_escape_pressed = [this] {
m_find_replace_widget->set_visible(false); m_find_replace_widget->set_visible(false);
m_editor->set_focus(true); m_editor->set_focus(true);
}; };
m_replace_previous_button = *find_descendant_of_type_named<GUI::Button>("replace_previous_button"); m_replace_button = *find_descendant_of_type_named<GUI::Button>("replace_button");
m_replace_previous_button->set_action(*m_replace_previous_action); m_replace_button->set_action(*m_replace_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_all_button = *find_descendant_of_type_named<GUI::Button>("replace_all_button"); 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_all_button->set_action(*m_replace_all_action);
m_replace_textbox->on_return_pressed = [this] { m_replace_textbox->on_return_pressed = [this] {
m_replace_next_button->click(); m_replace_button->click();
}; };
m_replace_textbox->on_escape_pressed = [this] { m_replace_textbox->on_escape_pressed = [this] {
@ -393,10 +363,8 @@ TextEditorWidget::TextEditorWidget()
edit_menu.add_separator(); edit_menu.add_separator();
edit_menu.add_action(*m_find_replace_action); edit_menu.add_action(*m_find_replace_action);
edit_menu.add_action(*m_find_next_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_find_previous_action);
edit_menu.add_action(*m_replace_next_action); edit_menu.add_action(*m_replace_action);
edit_menu.add_action(*m_replace_previous_action);
edit_menu.add_action(*m_replace_all_action); edit_menu.add_action(*m_replace_all_action);
m_no_preview_action = GUI::Action::create_checkable( 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_vim_emulation_setting_action;
RefPtr<GUI::Action> m_find_next_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_find_previous_action;
RefPtr<GUI::Action> m_replace_next_action; RefPtr<GUI::Action> m_replace_action;
RefPtr<GUI::Action> m_replace_previous_action;
RefPtr<GUI::Action> m_replace_all_action; RefPtr<GUI::Action> m_replace_all_action;
RefPtr<GUI::Action> m_layout_toolbar_action; RefPtr<GUI::Action> m_layout_toolbar_action;
@ -99,13 +97,14 @@ private:
RefPtr<GUI::TextBox> m_replace_textbox; RefPtr<GUI::TextBox> m_replace_textbox;
RefPtr<GUI::Button> m_find_previous_button; RefPtr<GUI::Button> m_find_previous_button;
RefPtr<GUI::Button> m_find_next_button; RefPtr<GUI::Button> m_find_next_button;
RefPtr<GUI::Button> m_find_regex_button; RefPtr<GUI::Button> m_replace_button;
RefPtr<GUI::Button> m_replace_previous_button;
RefPtr<GUI::Button> m_replace_next_button;
RefPtr<GUI::Button> m_replace_all_button; RefPtr<GUI::Button> m_replace_all_button;
RefPtr<GUI::Widget> m_find_replace_widget; RefPtr<GUI::Widget> m_find_replace_widget;
RefPtr<GUI::Widget> m_find_widget; RefPtr<GUI::Widget> m_find_widget;
RefPtr<GUI::Widget> m_replace_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; GUI::ActionGroup m_wrapping_mode_actions;
RefPtr<GUI::Action> m_no_wrapping_action; RefPtr<GUI::Action> m_no_wrapping_action;
@ -126,7 +125,9 @@ private:
bool m_document_dirty { false }; bool m_document_dirty { false };
bool m_document_opening { false }; bool m_document_opening { false };
bool m_auto_detect_preview_mode { 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 }; PreviewMode m_preview_mode { PreviewMode::None };
}; };

View file

@ -25,14 +25,15 @@
} }
} }
@GUI::Widget { @GUI::GroupBox {
name: "find_replace_widget" name: "find_replace_widget"
visible: false visible: false
fill_with_background_color: true fill_with_background_color: true
fixed_height: 48 fixed_height: 56
layout: @GUI::VerticalBoxLayout { layout: @GUI::VerticalBoxLayout {
margins: [2, 2, 2, 4] spacing: 2
margins: [5, 5, 5, 5]
} }
@GUI::Widget { @GUI::Widget {
@ -41,18 +42,33 @@
fixed_height: 22 fixed_height: 22
layout: @GUI::HorizontalBoxLayout { layout: @GUI::HorizontalBoxLayout {
spacing: 4
} }
@GUI::Button { @GUI::Button {
name: "find_previous_button" name: "find_previous_button"
text: "Find previous" fixed_width: 38
fixed_width: 150
} }
@GUI::Button { @GUI::Button {
name: "find_next_button" name: "find_next_button"
text: "Find next" fixed_width: 38
fixed_width: 150 }
@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 fixed_height: 22
layout: @GUI::HorizontalBoxLayout { layout: @GUI::HorizontalBoxLayout {
spacing: 4
} }
@GUI::Button { @GUI::Button {
name: "replace_previous_button" name: "replace_button"
text: "Replace previous" text: "Replace"
fixed_width: 100 fixed_width: 80
} }
@GUI::Button { @GUI::TextBox {
name: "replace_next_button" name: "replace_textbox"
text: "Replace next"
fixed_width: 100
} }
@GUI::Button { @GUI::Button {
name: "replace_all_button" name: "replace_all_button"
text: "Replace all" 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()) if (needle.is_empty())
return {}; return {};
@ -450,7 +450,7 @@ TextRange TextDocument::find_next(const StringView& needle, const TextPosition&
do { do {
auto ch = code_point_at(position); auto ch = code_point_at(position);
// FIXME: This is not the right way to use a Unicode needle! // 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) if (needle_index == 0)
start_of_potential_match = position; start_of_potential_match = position;
++needle_index; ++needle_index;
@ -467,7 +467,7 @@ TextRange TextDocument::find_next(const StringView& needle, const TextPosition&
return {}; 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()) if (needle.is_empty())
return {}; return {};
@ -524,6 +524,8 @@ TextRange TextDocument::find_previous(const StringView& needle, const TextPositi
TextPosition position = start.is_valid() ? start : TextPosition(0, 0); TextPosition position = start.is_valid() ? start : TextPosition(0, 0);
position = previous_position_before(position, should_wrap); position = previous_position_before(position, should_wrap);
if (position.line() >= line_count())
return {};
TextPosition original_position = position; TextPosition original_position = position;
TextPosition end_of_potential_match; TextPosition end_of_potential_match;
@ -532,7 +534,7 @@ TextRange TextDocument::find_previous(const StringView& needle, const TextPositi
do { do {
auto ch = code_point_at(position); auto ch = code_point_at(position);
// FIXME: This is not the right way to use a Unicode needle! // 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) if (needle_index == needle.length() - 1)
end_of_potential_match = position; end_of_potential_match = position;
if (needle_index == 0) if (needle_index == 0)

View file

@ -109,8 +109,8 @@ public:
Vector<TextRange> find_all(const StringView& needle, bool regmatch = false); Vector<TextRange> find_all(const StringView& needle, bool regmatch = false);
void update_regex_matches(const StringView&); void update_regex_matches(const StringView&);
TextRange find_next(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); 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 next_position_after(const TextPosition&, SearchShouldWrap = SearchShouldWrap::Yes) const;
TextPosition previous_position_before(const TextPosition&, SearchShouldWrap = SearchShouldWrap::Yes) const; TextPosition previous_position_before(const TextPosition&, SearchShouldWrap = SearchShouldWrap::Yes) const;