mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 10:12:45 +00:00 
			
		
		
		
	TextEditor: Add button to match regular expression during search
This commit is contained in:
		
							parent
							
								
									4a630d4b63
								
							
						
					
					
						commit
						3b7884ee8a
					
				
					 8 changed files with 176 additions and 18 deletions
				
			
		|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Emanuel Sprung
						Emanuel Sprung