mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-05 01:55:21 +03:00
TextEditor: Add button to match regular expression during search
This commit is contained in:
parent
4a630d4b63
commit
3b7884ee8a
Notes:
sideshowbarker
2024-07-19 01:15:06 +09:00
Author: https://github.com/lnzero1dev Commit: https://github.com/SerenityOS/serenity/commit/3b7884ee8a7 Pull-request: https://github.com/SerenityOS/serenity/pull/4103 Reviewed-by: https://github.com/alimpfard Reviewed-by: https://github.com/linusg ✅
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user