Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'Skip to next (text) occurrence' feature to text editor #87883

Merged
1 commit merged into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
TontonSancho marked this conversation as resolved.
Show resolved Hide resolved
[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") {
TontonSancho marked this conversation as resolved.
Show resolved Hide resolved
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
Loading