From 3b7884ee8a7dca2ff7732e28a117143590717efe Mon Sep 17 00:00:00 2001 From: Emanuel Sprung Date: Thu, 23 Apr 2020 20:29:38 +0200 Subject: [PATCH] TextEditor: Add button to match regular expression during search --- Applications/TextEditor/CMakeLists.txt | 2 +- Applications/TextEditor/TextEditorWidget.cpp | 40 +++++- Applications/TextEditor/TextEditorWidget.h | 3 + DevTools/HackStudio/ProjectFile.cpp | 2 +- DevTools/HackStudio/ProjectFile.h | 2 +- Libraries/LibGUI/CMakeLists.txt | 2 +- Libraries/LibGUI/TextDocument.cpp | 128 ++++++++++++++++++- Libraries/LibGUI/TextDocument.h | 15 ++- 8 files changed, 176 insertions(+), 18 deletions(-) diff --git a/Applications/TextEditor/CMakeLists.txt b/Applications/TextEditor/CMakeLists.txt index 6a28b82c324..eadf6decfbc 100644 --- a/Applications/TextEditor/CMakeLists.txt +++ b/Applications/TextEditor/CMakeLists.txt @@ -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) diff --git a/Applications/TextEditor/TextEditorWidget.cpp b/Applications/TextEditor/TextEditorWidget.cpp index 09099ebe3da..9d8e6e9e4b9 100644 --- a/Applications/TextEditor/TextEditorWidget.cpp +++ b/Applications/TextEditor/TextEditorWidget.cpp @@ -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(".*"); + 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); diff --git a/Applications/TextEditor/TextEditorWidget.h b/Applications/TextEditor/TextEditorWidget.h index 4b44902b3f8..70cbadbd570 100644 --- a/Applications/TextEditor/TextEditorWidget.h +++ b/Applications/TextEditor/TextEditorWidget.h @@ -77,6 +77,7 @@ private: RefPtr m_line_wrapping_setting_action; RefPtr m_find_next_action; + RefPtr m_find_regex_action; RefPtr m_find_previous_action; RefPtr m_replace_next_action; RefPtr m_replace_previous_action; @@ -93,6 +94,7 @@ private: RefPtr m_replace_textbox; RefPtr m_find_previous_button; RefPtr m_find_next_button; + RefPtr m_find_regex_button; RefPtr m_replace_previous_button; RefPtr m_replace_next_button; RefPtr 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 }; }; diff --git a/DevTools/HackStudio/ProjectFile.cpp b/DevTools/HackStudio/ProjectFile.cpp index 1c755f68de4..b8748c4f13c 100644 --- a/DevTools/HackStudio/ProjectFile.cpp +++ b/DevTools/HackStudio/ProjectFile.cpp @@ -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)); diff --git a/DevTools/HackStudio/ProjectFile.h b/DevTools/HackStudio/ProjectFile.h index 65510ac21fa..c679cd79e05 100644 --- a/DevTools/HackStudio/ProjectFile.h +++ b/DevTools/HackStudio/ProjectFile.h @@ -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); diff --git a/Libraries/LibGUI/CMakeLists.txt b/Libraries/LibGUI/CMakeLists.txt index 8cadd07b4f1..a412c228bbd 100644 --- a/Libraries/LibGUI/CMakeLists.txt +++ b/Libraries/LibGUI/CMakeLists.txt @@ -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) diff --git a/Libraries/LibGUI/TextDocument.cpp b/Libraries/LibGUI/TextDocument.cpp index 137cbdab944..e3fa5d804e0 100644 --- a/Libraries/LibGUI/TextDocument.cpp +++ b/Libraries/LibGUI/TextDocument.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include 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 re(needle); + + Vector 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 TextDocument::find_all(const StringView& needle) const +Vector TextDocument::find_all(const StringView& needle, bool regmatch) { Vector 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); diff --git a/Libraries/LibGUI/TextDocument.h b/Libraries/LibGUI/TextDocument.h index 27b9f4b10f9..d20ef059daf 100644 --- a/Libraries/LibGUI/TextDocument.h +++ b/Libraries/LibGUI/TextDocument.h @@ -39,6 +39,7 @@ #include #include #include +#include namespace GUI { @@ -108,10 +109,11 @@ public: String text() const; String text_in_range(const TextRange&) const; - Vector find_all(const StringView& needle) const; + Vector 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 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 {