Skip to content

Commit

Permalink
Add 'Skip to next (text) occurrence' feature to text editor
Browse files Browse the repository at this point in the history
Adds `ui_text_skip_selection_for_next_occurrence` action and related implementation to text editor.
This action is bound `Ctrl+Alt+D` shorcut.

Used in conjonction with `ui_add_skip_selection_for_next_occurrence`, it gives the user the ability to select many occurrences of a selection
and avoid some of them.
Used without a previous selection, the action jumps to the next occurrence of the current word under the caret.
  • Loading branch information
TontonSancho committed Mar 25, 2024
1 parent 5d08c26 commit c988bec
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 1 deletion.
5 changes: 5 additions & 0 deletions core/input/input_map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ static const _BuiltinActionDisplayName _builtin_action_display_names[] = {
{ "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_skip_selection_for_next_occurrence", TTRC("Skip Selection for Next Occurrence") },
{ "ui_text_clear_carets_and_selection", TTRC("Clear Carets and Selection") },
{ "ui_text_toggle_insert_mode", TTRC("Toggle Insert Mode") },
{ "ui_text_submit", TTRC("Submit Text") },
Expand Down Expand Up @@ -721,6 +722,10 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() {
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<Ref<InputEvent>>();
inputs.push_back(InputEventKey::create_reference(Key::D | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT));
default_builtin_cache.insert("ui_text_skip_selection_for_next_occurrence", inputs);

inputs = List<Ref<InputEvent>>();
inputs.push_back(InputEventKey::create_reference(Key::ESCAPE));
default_builtin_cache.insert("ui_text_clear_carets_and_selection", inputs);
Expand Down
6 changes: 6 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,12 @@
<member name="input/ui_text_select_word_under_caret.macos" type="Dictionary" setter="" getter="">
macOS specific override for the shortcut to select the word currently under the caret.
</member>
<member name="input/ui_text_skip_selection_for_next_occurrence" type="Dictionary" setter="" getter="">
If no selection is currently active with the last caret in text fields, searches for the next occurrence of the the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret.
If a selection is currently active with the last caret in text fields, searches for the next occurrence of the selection, adds a caret, selects the next occurrence then deselects the previous selection and its associated caret. The action can be performed sequentially for other occurrences of the selection of the last caret.
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.
</member>
<member name="input/ui_text_submit" type="Dictionary" setter="" getter="">
Default [InputEventAction] to submit a text field.
[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.
Expand Down
6 changes: 6 additions & 0 deletions doc/classes/TextEdit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,12 @@
Provide custom tooltip text. The callback method must take the following args: [code]hovered_word: String[/code].
</description>
</method>
<method name="skip_selection_for_next_occurrence">
<return type="void" />
<description>
Moves a selection and a caret for the next occurrence of the current selection. If there is no active selection, moves to the next occurrence of the word under caret.
</description>
</method>
<method name="start_action">
<return type="void" />
<param index="0" name="action" type="int" enum="TextEdit.EditAction" />
Expand Down
56 changes: 55 additions & 1 deletion scene/gui/text_edit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2115,7 +2115,7 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
}

if (is_shortcut_keys_enabled()) {
// SELECT ALL, SELECT WORD UNDER CARET, ADD SELECTION FOR NEXT OCCURRENCE,
// SELECT ALL, SELECT WORD UNDER CARET, ADD SELECTION FOR NEXT OCCURRENCE, SKIP SELECTION FOR NEXT OCCURRENCE,
// CLEAR CARETS AND SELECTIONS, CUT, COPY, PASTE.
if (k->is_action("ui_text_select_all", true)) {
select_all();
Expand All @@ -2132,6 +2132,11 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
accept_event();
return;
}
if (k->is_action("ui_text_skip_selection_for_next_occurrence", true)) {
skip_selection_for_next_occurrence();
accept_event();
return;
}
if (k->is_action("ui_text_clear_carets_and_selection", true)) {
// Since the default shortcut is ESC, accepts the event only if it's actually performed.
if (_clear_carets_and_selection()) {
Expand Down Expand Up @@ -5185,6 +5190,54 @@ void TextEdit::add_selection_for_next_occurrence() {
}
}

void TextEdit::skip_selection_for_next_occurrence() {
if (!selecting_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;

// Supports getting the text under caret without selecting it.
// It allows to use this shortcut to simply jump to the next (under caret) word.
// Due to const and &(reference) presence, ternary operator is a way to avoid errors and warnings.
const String &searched_text = has_selection(caret) ? get_selected_text(caret) : get_word_under_caret(caret);

int column = (has_selection(caret) ? get_selection_from_column(caret) : get_caret_column(caret)) + 1;
int line = get_caret_line(caret);

const Point2i next_occurrence = search(searched_text, SEARCH_MATCH_CASE, line, column);

if (next_occurrence.x == -1 || next_occurrence.y == -1) {
return;
}

int to_column = (has_selection(caret) ? get_selection_to_column(caret) : get_caret_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();
}

// Deselect word under previous caret.
if (has_selection(caret)) {
select_word_under_caret(caret);
}

// Remove previous caret.
if (get_caret_count() > 1) {
remove_caret(caret);
}
}

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) {
Expand Down Expand Up @@ -6351,6 +6404,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("skip_selection_for_next_occurrence"), &TextEdit::skip_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));
Expand Down
1 change: 1 addition & 0 deletions scene/gui/text_edit.h
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ class TextEdit : public Control {
void select_all();
void select_word_under_caret(int p_caret = -1);
void add_selection_for_next_occurrence();
void skip_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;
Expand Down
147 changes: 147 additions & 0 deletions tests/scene/test_text_edit.h
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,153 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_selected_text(3) == "test");
}

SUBCASE("[TextEdit] skip selection for next occurrence") {
text_edit->set_text("\ntest other_test\nrandom test\nword test word nonrandom");
text_edit->set_caret_column(0);
text_edit->set_caret_line(1);

// Without selection on the current caret, the caret as 'jumped' to the next occurrence of the word under the caret.
text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK_FALSE(text_edit->has_selection(0));
CHECK(text_edit->get_caret_line(0) == 1);
CHECK(text_edit->get_caret_column(0) == 13);

// Repeating previous action.
// This time caret is in 'other_test' (other_|test)
// so the searched term will be 'other_test' or not just 'test'
// => no occurrence, as a side effect, the caret will move to start of the term.
text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK_FALSE(text_edit->has_selection(0));
CHECK(text_edit->get_caret_line(0) == 1);
CHECK(text_edit->get_caret_column(0) == 7);

// Repeating action again should do nothing now
text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK_FALSE(text_edit->has_selection(0));
CHECK(text_edit->get_caret_line(0) == 1);
CHECK(text_edit->get_caret_column(0) == 7);

// Moving back to the first 'test' occurrence.
text_edit->set_caret_column(0);
text_edit->set_caret_line(1);

// But this time, create a selection of it.
text_edit->add_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");
CHECK(text_edit->get_selection_from_line(0) == 1);
CHECK(text_edit->get_selection_from_column(0) == 0);
CHECK(text_edit->get_selection_to_line(0) == 1);
CHECK(text_edit->get_selection_to_column(0) == 4);
CHECK(text_edit->get_caret_line(0) == 1);
CHECK(text_edit->get_caret_column(0) == 4);

// Then, skipping it, but this time, selection has been made on the next occurrence.
text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");
CHECK(text_edit->get_selection_from_line(0) == 1);
CHECK(text_edit->get_selection_from_column(0) == 13);
CHECK(text_edit->get_selection_to_line(0) == 1);
CHECK(text_edit->get_selection_to_column(0) == 17);
CHECK(text_edit->get_caret_line(0) == 1);
CHECK(text_edit->get_caret_column(0) == 17);

text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");
CHECK(text_edit->get_selection_from_line(0) == 2);
CHECK(text_edit->get_selection_from_column(0) == 9);
CHECK(text_edit->get_selection_to_line(0) == 2);
CHECK(text_edit->get_selection_to_column(0) == 13);
CHECK(text_edit->get_caret_line(0) == 2);
CHECK(text_edit->get_caret_column(0) == 13);

text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");
CHECK(text_edit->get_selection_from_line(0) == 3);
CHECK(text_edit->get_selection_from_column(0) == 5);
CHECK(text_edit->get_selection_to_line(0) == 3);
CHECK(text_edit->get_selection_to_column(0) == 9);
CHECK(text_edit->get_caret_line(0) == 3);
CHECK(text_edit->get_caret_column(0) == 9);

// Last skip, we are back to the first occurrence.
text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");
CHECK(text_edit->get_selection_from_line(0) == 1);
CHECK(text_edit->get_selection_from_column(0) == 0);
CHECK(text_edit->get_selection_to_line(0) == 1);
CHECK(text_edit->get_selection_to_column(0) == 4);
CHECK(text_edit->get_caret_line(0) == 1);
CHECK(text_edit->get_caret_column(0) == 4);

// Adding first occurrence to selections/carets list
// and select occurrence on 'other_test'.
text_edit->add_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 2);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");

CHECK(text_edit->has_selection(1));
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);

// We don't want this occurrence.
// Let's skip it.
text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 2);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");

CHECK(text_edit->get_selected_text(1) == "test");
CHECK(text_edit->get_selection_from_line(1) == 2);
CHECK(text_edit->get_selection_from_column(1) == 9);
CHECK(text_edit->get_selection_to_line(1) == 2);
CHECK(text_edit->get_selection_to_column(1) == 13);
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 13);

text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 2);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");

CHECK(text_edit->get_selected_text(1) == "test");
CHECK(text_edit->get_selection_from_line(1) == 3);
CHECK(text_edit->get_selection_from_column(1) == 5);
CHECK(text_edit->get_selection_to_line(1) == 3);
CHECK(text_edit->get_selection_to_column(1) == 9);
CHECK(text_edit->get_caret_line(1) == 3);
CHECK(text_edit->get_caret_column(1) == 9);

// We are back the first occurrence.
text_edit->skip_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");
CHECK(text_edit->get_selection_from_line(0) == 1);
CHECK(text_edit->get_selection_from_column(0) == 0);
CHECK(text_edit->get_selection_to_line(0) == 1);
CHECK(text_edit->get_selection_to_column(0) == 4);
CHECK(text_edit->get_caret_line(0) == 1);
CHECK(text_edit->get_caret_column(0) == 4);
}

SUBCASE("[TextEdit] deselect on focus loss") {
text_edit->set_text("test");

Expand Down

0 comments on commit c988bec

Please sign in to comment.