diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index ce76d11b6e8..0c765fe23c5 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -337,6 +337,7 @@ static const _BuiltinActionDisplayName _builtin_action_display_names[] = { { "ui_text_scroll_down.macos", TTRC("Scroll Down") }, { "ui_text_select_all", TTRC("Select All") }, { "ui_text_select_word_under_caret", TTRC("Select Word Under Caret") }, + { "ui_text_add_selection_for_next_occurrence", TTRC("Add Selection for Next Occurrence") }, { "ui_text_toggle_insert_mode", TTRC("Toggle Insert Mode") }, { "ui_text_submit", TTRC("Text Submitted") }, { "ui_graph_duplicate", TTRC("Duplicate Nodes") }, @@ -641,9 +642,13 @@ const HashMap>> &InputMap::get_builtins() { default_builtin_cache.insert("ui_text_select_all", inputs); inputs = List>(); - inputs.push_back(InputEventKey::create_reference(Key::D | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::G | KeyModifierMask::ALT)); default_builtin_cache.insert("ui_text_select_word_under_caret", inputs); + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::D | KeyModifierMask::CMD_OR_CTRL)); + default_builtin_cache.insert("ui_text_add_selection_for_next_occurrence", inputs); + inputs = List>(); inputs.push_back(InputEventKey::create_reference(Key::INSERT)); default_builtin_cache.insert("ui_text_toggle_insert_mode", inputs); diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index e2502f1fb81..7843104a598 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -830,6 +830,13 @@ + + If a selection is currently active with the last caret in text fields, searches for the next occurrence of the selection, adds a caret and selects the next occurrence. + If no selection is currently active with the last caret in text fields, selects the word currently under the caret. + The action can be performed sequentially for all occurrences of the selection of the last caret and for all existing carets. + The viewport is adjusted to the latest newly added caret. + [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + Default [InputEventAction] to delete the character before the text cursor. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. @@ -984,7 +991,7 @@ If no selection is currently active, selects the word currently under the caret in text fields. If a selection is currently active, deselects the current selection. - [b]Note:[/b] Currently, this is only implemented in [TextEdit], not [LineEdit]. + [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. Default [InputEventAction] to submit a text field. diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index b1fda25ecd5..879a355f25d 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -70,6 +70,12 @@ Register a new gutter to this [TextEdit]. Use [param at] to have a specific gutter order. A value of [code]-1[/code] appends the gutter to the right. + + + + Adds a selection and a caret for the next occurrence of the current selection. If there is no active selection, selects word under caret. + + diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index b1c2676bc14..48853cde9ca 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -2051,7 +2051,7 @@ void TextEdit::gui_input(const Ref &p_gui_input) { } if (is_shortcut_keys_enabled()) { - // SELECT ALL, SELECT WORD UNDER CARET, CUT, COPY, PASTE. + // SELECT ALL, SELECT WORD UNDER CARET, ADD SELECTION FOR NEXT OCCURRENCE, CUT, COPY, PASTE. if (k->is_action("ui_text_select_all", true)) { select_all(); accept_event(); @@ -2062,6 +2062,11 @@ void TextEdit::gui_input(const Ref &p_gui_input) { accept_event(); return; } + if (k->is_action("ui_text_add_selection_for_next_occurrence", true)) { + add_selection_for_next_occurrence(); + accept_event(); + return; + } if (k->is_action("ui_cut", true)) { cut(); accept_event(); @@ -4832,6 +4837,45 @@ void TextEdit::select_word_under_caret(int p_caret) { merge_overlapping_carets(); } +void TextEdit::add_selection_for_next_occurrence() { + if (!selecting_enabled || !is_multiple_carets_enabled()) { + return; + } + + if (text.size() == 1 && text[0].length() == 0) { + return; + } + + // Always use the last caret, to correctly search for + // the next occurrence that comes after this caret. + int caret = get_caret_count() - 1; + + if (!has_selection(caret)) { + select_word_under_caret(caret); + return; + } + + const String &highlighted_text = get_selected_text(caret); + int column = get_selection_from_column(caret) + 1; + int line = get_caret_line(caret); + + const Point2i next_occurrence = search(highlighted_text, SEARCH_MATCH_CASE, line, column); + + if (next_occurrence.x == -1 || next_occurrence.y == -1) { + return; + } + + int to_column = get_selection_to_column(caret) + 1; + int end = next_occurrence.x + (to_column - column); + int new_caret = add_caret(next_occurrence.y, end); + + if (new_caret != -1) { + select(next_occurrence.y, next_occurrence.x, next_occurrence.y, end, new_caret); + adjust_viewport_to_caret(new_caret); + merge_overlapping_carets(); + } +} + void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret) { ERR_FAIL_INDEX(p_caret, carets.size()); if (!selecting_enabled) { @@ -6000,6 +6044,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("select_all"), &TextEdit::select_all); ClassDB::bind_method(D_METHOD("select_word_under_caret", "caret_index"), &TextEdit::select_word_under_caret, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("add_selection_for_next_occurrence"), &TextEdit::add_selection_for_next_occurrence); ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column", "caret_index"), &TextEdit::select, DEFVAL(0)); ClassDB::bind_method(D_METHOD("has_selection", "caret_index"), &TextEdit::has_selection, DEFVAL(-1)); diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index e4af621b738..86b838e3873 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -851,6 +851,7 @@ public: void select_all(); void select_word_under_caret(int p_caret = -1); + void add_selection_for_next_occurrence(); void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret = 0); bool has_selection(int p_caret = -1) const; diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 2ef0a3345fe..4b92b9daff6 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -733,6 +733,46 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); } + SUBCASE("[TextEdit] add selection for next occurrence") { + text_edit->set_text("\ntest other_test\nrandom test\nword test word"); + text_edit->set_caret_column(0); + text_edit->set_caret_line(1); + + text_edit->select_word_under_caret(); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_selection_from_line(1) == 1); + CHECK(text_edit->get_selection_from_column(1) == 13); + CHECK(text_edit->get_selection_to_line(1) == 1); + CHECK(text_edit->get_selection_to_column(1) == 17); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 17); + + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_selected_text(2) == "test"); + CHECK(text_edit->get_selection_from_line(2) == 2); + CHECK(text_edit->get_selection_from_column(2) == 9); + CHECK(text_edit->get_selection_to_line(2) == 2); + CHECK(text_edit->get_selection_to_column(2) == 13); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 13); + + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 4); + CHECK(text_edit->get_selected_text(3) == "test"); + CHECK(text_edit->get_selection_from_line(3) == 3); + CHECK(text_edit->get_selection_from_column(3) == 5); + CHECK(text_edit->get_selection_to_line(3) == 3); + CHECK(text_edit->get_selection_to_column(3) == 9); + CHECK(text_edit->get_caret_line(3) == 3); + CHECK(text_edit->get_caret_column(3) == 9); + } + SUBCASE("[TextEdit] deselect on focus loss") { text_edit->set_text("test");