TextEditor: Add button to match regular expression during search

This commit is contained in:
Emanuel Sprung 2020-04-23 20:29:38 +02:00 committed by Andreas Kling
parent 4a630d4b63
commit 3b7884ee8a
8 changed files with 176 additions and 18 deletions

View file

@ -7,4 +7,4 @@ set(SOURCES
)
serenity_bin(TextEditor)
target_link_libraries(TextEditor LibWeb LibMarkdown LibGUI LibShell LibDesktop)
target_link_libraries(TextEditor LibWeb LibMarkdown LibGUI LibShell LibRegex LibDesktop)

View file

@ -117,8 +117,12 @@ TextEditorWidget::TextEditorWidget()
dbgln("find_next(\"\")");
return;
}
auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().end());
dbgln("find_next(\"{}\") returned {}", needle, found_range);
if (m_find_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);
dbg() << "find_next(\"" << needle << "\") returned " << found_range;
if (found_range.is_valid()) {
m_editor->set_selection(found_range);
} else {
@ -129,7 +133,12 @@ TextEditorWidget::TextEditorWidget()
}
});
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_action = GUI::Action::create("Find regex", { Mod_Ctrl, 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&) {
auto needle = m_find_textbox->text();
if (needle.is_empty()) {
dbgln("find_prev(\"\")");
@ -140,7 +149,10 @@ TextEditorWidget::TextEditorWidget()
if (!selection_start.is_valid())
selection_start = m_editor->normalized_selection().end();
auto found_range = m_editor->document().find_previous(needle, selection_start);
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);
dbgln("find_prev(\"{}\") returned {}", needle, found_range);
if (found_range.is_valid()) {
@ -164,7 +176,10 @@ TextEditorWidget::TextEditorWidget()
if (!selection_start.is_valid())
selection_start = m_editor->normalized_selection().start();
auto found_range = m_editor->document().find_next(needle, 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);
@ -187,6 +202,9 @@ TextEditorWidget::TextEditorWidget()
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_previous(needle, selection_start);
if (found_range.is_valid()) {
@ -205,12 +223,14 @@ TextEditorWidget::TextEditorWidget()
auto substitute = m_replace_textbox->text();
if (needle.is_empty())
return;
if (m_find_use_regex)
m_editor->document().update_regex_matches(needle);
auto found_range = m_editor->document().find_next(needle);
auto found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
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);
found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
}
});
@ -224,6 +244,11 @@ TextEditorWidget::TextEditorWidget()
m_find_next_button->click();
};
m_find_regex_button = m_find_widget->add<GUI::Button>(".*");
m_find_regex_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill);
m_find_regex_button->set_preferred_size(20, 0);
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);
@ -358,6 +383,7 @@ 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);

View file

@ -77,6 +77,7 @@ private:
RefPtr<GUI::Action> m_line_wrapping_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;
@ -93,6 +94,7 @@ 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_all_button;
@ -114,6 +116,7 @@ private:
bool m_document_dirty { false };
bool m_document_opening { false };
bool m_auto_detect_preview_mode { false };
bool m_find_use_regex { false };
PreviewMode m_preview_mode { PreviewMode::None };
};

View file

@ -35,7 +35,7 @@ ProjectFile::ProjectFile(const String& name)
{
}
const GUI::TextDocument& ProjectFile::document() const
GUI::TextDocument& ProjectFile::document() const
{
if (!m_document) {
m_document = CodeDocument::create(LexicalPath(m_name));

View file

@ -43,7 +43,7 @@ public:
const String& name() const { return m_name; }
const GUI::TextDocument& document() const;
GUI::TextDocument& document() const;
int vertical_scroll_value() const;
void vertical_scroll_value(int);

View file

@ -94,4 +94,4 @@ set(GENERATED_SOURCES
)
serenity_lib(LibGUI gui)
target_link_libraries(LibGUI LibCore LibGfx LibIPC LibThread LibCpp)
target_link_libraries(LibGUI LibCore LibGfx LibIPC LibThread LibCpp LibRegex)

View file

@ -30,6 +30,7 @@
#include <LibCore/Timer.h>
#include <LibGUI/TextDocument.h>
#include <LibGUI/TextEditor.h>
#include <LibRegex/Regex.h>
#include <ctype.h>
namespace GUI {
@ -272,6 +273,8 @@ void TextDocument::notify_did_change()
for (auto* client : m_clients)
client->document_did_change();
}
m_regex_needs_update = true;
}
void TextDocument::set_all_cursors(const TextPosition& position)
@ -350,11 +353,78 @@ TextPosition TextDocument::previous_position_before(const TextPosition& position
return { position.line(), position.column() - 1 };
}
TextRange TextDocument::find_next(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap) const
void TextDocument::update_regex_matches(const StringView& needle)
{
if (m_regex_needs_update || needle != m_regex_needle) {
Regex<PosixExtended> re(needle);
Vector<RegexStringView> views;
for (size_t line = 0; line < m_lines.size(); ++line) {
views.append(m_lines.at(line).view());
}
re.search(views, m_regex_result);
m_regex_needs_update = false;
m_regex_needle = String { needle };
m_regex_result_match_index = -1;
m_regex_result_match_capture_group_index = -1;
}
}
TextRange TextDocument::find_next(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap, bool regmatch)
{
if (needle.is_empty())
return {};
if (regmatch) {
if (!m_regex_result.matches.size())
return {};
regex::Match match;
bool use_whole_match { false };
auto next_match = [&] {
m_regex_result_match_capture_group_index = 0;
if (m_regex_result_match_index == m_regex_result.matches.size() - 1) {
if (should_wrap == SearchShouldWrap::Yes)
m_regex_result_match_index = 0;
else
++m_regex_result_match_index;
} else
++m_regex_result_match_index;
};
if (m_regex_result.n_capture_groups) {
if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
next_match();
else {
// check if last capture group has been reached
if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size()) {
next_match();
} else {
// get to the next capture group item
++m_regex_result_match_capture_group_index;
}
}
// use whole match, if there is no capture group for current index
if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
use_whole_match = true;
else if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
next_match();
} else {
next_match();
}
if (use_whole_match || !m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
match = m_regex_result.matches.at(m_regex_result_match_index);
else
match = m_regex_result.capture_group_matches.at(m_regex_result_match_index).at(m_regex_result_match_capture_group_index);
return TextRange { { match.line, match.column }, { match.line, match.column + match.view.length() } };
}
TextPosition position = start.is_valid() ? start : TextPosition(0, 0);
TextPosition original_position = position;
@ -381,11 +451,61 @@ TextRange TextDocument::find_next(const StringView& needle, const TextPosition&
return {};
}
TextRange TextDocument::find_previous(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap) const
TextRange TextDocument::find_previous(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap, bool regmatch)
{
if (needle.is_empty())
return {};
if (regmatch) {
if (!m_regex_result.matches.size())
return {};
regex::Match match;
bool use_whole_match { false };
auto next_match = [&] {
if (m_regex_result_match_index == 0) {
if (should_wrap == SearchShouldWrap::Yes)
m_regex_result_match_index = m_regex_result.matches.size() - 1;
else
--m_regex_result_match_index;
} else
--m_regex_result_match_index;
m_regex_result_match_capture_group_index = m_regex_result.capture_group_matches.at(m_regex_result_match_index).size() - 1;
};
if (m_regex_result.n_capture_groups) {
if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
next_match();
else {
// check if last capture group has been reached
if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size()) {
next_match();
} else {
// get to the next capture group item
--m_regex_result_match_capture_group_index;
}
}
// use whole match, if there is no capture group for current index
if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
use_whole_match = true;
else if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
next_match();
} else {
next_match();
}
if (use_whole_match || !m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
match = m_regex_result.matches.at(m_regex_result_match_index);
else
match = m_regex_result.capture_group_matches.at(m_regex_result_match_index).at(m_regex_result_match_capture_group_index);
return TextRange { { match.line, match.column }, { match.line, match.column + match.view.length() } };
}
TextPosition position = start.is_valid() ? start : TextPosition(0, 0);
position = previous_position_before(position, should_wrap);
TextPosition original_position = position;
@ -413,13 +533,13 @@ TextRange TextDocument::find_previous(const StringView& needle, const TextPositi
return {};
}
Vector<TextRange> TextDocument::find_all(const StringView& needle) const
Vector<TextRange> TextDocument::find_all(const StringView& needle, bool regmatch)
{
Vector<TextRange> ranges;
TextPosition position;
for (;;) {
auto range = find_next(needle, position, SearchShouldWrap::No);
auto range = find_next(needle, position, SearchShouldWrap::No, regmatch);
if (!range.is_valid())
break;
ranges.append(range);

View file

@ -39,6 +39,7 @@
#include <LibGUI/UndoStack.h>
#include <LibGfx/Color.h>
#include <LibGfx/Forward.h>
#include <LibRegex/Regex.h>
namespace GUI {
@ -108,10 +109,11 @@ public:
String text() const;
String text_in_range(const TextRange&) const;
Vector<TextRange> find_all(const StringView& needle) const;
Vector<TextRange> find_all(const StringView& needle, bool regmatch = false);
TextRange find_next(const StringView&, const TextPosition& start = {}, SearchShouldWrap = SearchShouldWrap::Yes) const;
TextRange find_previous(const StringView&, const TextPosition& start = {}, SearchShouldWrap = SearchShouldWrap::Yes) const;
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);
TextPosition next_position_after(const TextPosition&, SearchShouldWrap = SearchShouldWrap::Yes) const;
TextPosition previous_position_before(const TextPosition&, SearchShouldWrap = SearchShouldWrap::Yes) const;
@ -158,6 +160,13 @@ private:
UndoStack m_undo_stack;
RefPtr<Core::Timer> m_undo_timer;
RegexResult m_regex_result;
size_t m_regex_result_match_index { 0 };
size_t m_regex_result_match_capture_group_index { 0 };
bool m_regex_needs_update { true };
String m_regex_needle;
};
class TextDocumentLine {