From 154f727c7a7b766e5a2013c6eeb17fb843e9d7a4 Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 22 Jan 2024 18:26:16 -0500 Subject: [PATCH 1/2] Overhaul TextEdit selection. The caret is now a part of the selection. --- doc/classes/TextEdit.xml | 28 +- editor/code_editor.cpp | 10 +- editor/plugins/script_text_editor.cpp | 44 +- editor/plugins/text_editor.cpp | 2 +- editor/plugins/text_shader_editor.cpp | 2 +- scene/gui/code_edit.cpp | 22 +- scene/gui/code_edit.h | 2 + scene/gui/text_edit.cpp | 1056 ++++++++++++++----------- scene/gui/text_edit.h | 54 +- scene/main/viewport.cpp | 7 + scene/main/viewport.h | 1 + 11 files changed, 709 insertions(+), 519 deletions(-) diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index db0c1f17b0e..72893c913a6 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -58,7 +58,7 @@ - + Adds a new caret at the given location. Returns the index of the new caret, or [code]-1[/code] if the location is invalid. @@ -363,6 +363,15 @@ [b]Note:[/b] The return value is influenced by [theme_item line_spacing] and [theme_item font_size]. And it will not be less than [code]1[/code]. + + + + + + Returns an [Array] of line ranges where [code]x[/code] is the first line and [code]y[/code] is the last line. All lines within these ranges will have a caret on them or be part of a selection. Each line will only be part of one line range, even if it has multiple carets on it. + If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param p_merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections. + + @@ -514,7 +523,17 @@ Returns the text inside the selection of a caret, or all the carets if [param caret_index] is its default value [code]-1[/code]. - + + + + + + + Returns the caret index of the selection at the given [param line] and [param column], or [code]-1[/code] if there is none. + If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. + + + @@ -809,7 +828,7 @@ - + Perform a search inside the text. Search flags can be specified in the [enum SearchFlags] enum. In the returned vector, [code]x[/code] is the column, [code]y[/code] is the line. If no results are found, both are equal to [code]-1[/code]. @@ -1049,9 +1068,6 @@ - - - Sets the current selection mode. diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 49896d66d82..238fd00089e 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -173,10 +173,8 @@ bool FindReplaceBar::_search(uint32_t p_flags, int p_from_line, int p_from_col) if (pos.x != -1) { if (!preserve_cursor && !is_selection_only()) { text_editor->unfold_line(pos.y); - text_editor->set_caret_line(pos.y, false); - text_editor->set_caret_column(pos.x + text.length(), false); - text_editor->center_viewport_to_caret(0); text_editor->select(pos.y, pos.x, pos.y, pos.x + text.length()); + text_editor->center_viewport_to_caret(0); line_col_changed_for_result = true; } @@ -216,7 +214,7 @@ void FindReplaceBar::_replace() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { // Restrict search_current() to selected region. - text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_line(selection_begin.width, false, true, -1, 0); text_editor->set_caret_column(selection_begin.height, true, 0); } @@ -285,10 +283,10 @@ void FindReplaceBar::_replace_all() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { - text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_line(selection_begin.width, false, true, -1, 0); text_editor->set_caret_column(selection_begin.height, true, 0); } else { - text_editor->set_caret_line(0, false, true, 0, 0); + text_editor->set_caret_line(0, false, true, -1, 0); text_editor->set_caret_column(0, true, 0); } diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index a642f35d6fd..752ccecd91c 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -2008,45 +2008,32 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref &ev) { tx->apply_ime(); Point2i pos = tx->get_line_column_at_pos(local_pos); - int row = pos.y; - int col = pos.x; + int mouse_line = pos.y; + int mouse_column = pos.x; tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click")); - int caret_clicked = -1; + int selection_clicked = -1; if (tx->is_move_caret_on_right_click_enabled()) { - if (tx->has_selection()) { - for (int i = 0; i < tx->get_caret_count(); i++) { - int from_line = tx->get_selection_from_line(i); - int to_line = tx->get_selection_to_line(i); - int from_column = tx->get_selection_from_column(i); - int to_column = tx->get_selection_to_column(i); - - if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) { - // Right click in one of the selected text - caret_clicked = i; - break; - } - } - } - if (caret_clicked < 0) { + selection_clicked = tx->get_selection_at_line_column(mouse_line, mouse_column, true); + if (selection_clicked < 0) { tx->deselect(); tx->remove_secondary_carets(); - caret_clicked = 0; - tx->set_caret_line(row, false, false); - tx->set_caret_column(col); + selection_clicked = 0; + tx->set_caret_line(mouse_line, false, false, -1); + tx->set_caret_column(mouse_column); } } String word_at_pos = tx->get_word_at_pos(local_pos); if (word_at_pos.is_empty()) { - word_at_pos = tx->get_word_under_caret(caret_clicked); + word_at_pos = tx->get_word_under_caret(selection_clicked); } if (word_at_pos.is_empty()) { - word_at_pos = tx->get_selected_text(caret_clicked); + word_at_pos = tx->get_selected_text(selection_clicked); } bool has_color = (word_at_pos == "Color"); - bool foldable = tx->can_fold_line(row) || tx->is_line_folded(row); + bool foldable = tx->can_fold_line(mouse_line) || tx->is_line_folded(mouse_line); bool open_docs = false; bool goto_definition = false; @@ -2064,9 +2051,9 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref &ev) { } if (has_color) { - String line = tx->get_line(row); - color_position.x = row; - color_position.y = col; + String line = tx->get_line(mouse_line); + color_position.x = mouse_line; + color_position.y = mouse_column; int begin = -1; int end = -1; @@ -2076,7 +2063,7 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref &ev) { COLOR_NAME, // Color.COLOR_NAME } expression_pattern = NOT_PARSED; - for (int i = col; i < line.length(); i++) { + for (int i = mouse_column; i < line.length(); i++) { if (line[i] == '(') { if (expression_pattern == NOT_PARSED) { begin = i; @@ -2155,7 +2142,6 @@ void ScriptTextEditor::_color_changed(const Color &p_color) { code_editor->get_text_editor()->begin_complex_operation(); code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args); code_editor->get_text_editor()->end_complex_operation(); - code_editor->get_text_editor()->queue_redraw(); } void ScriptTextEditor::_prepare_edit_menu() { diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index 6070e08739e..0eb0e469916 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -531,7 +531,7 @@ void TextEditor::_text_edit_gui_input(const Ref &ev) { } } if (!tx->has_selection()) { - tx->set_caret_line(row, true, false); + tx->set_caret_line(row, true, false, -1); tx->set_caret_column(col); } } diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index 6e786c1d94c..d605edbf750 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -1007,7 +1007,7 @@ void TextShaderEditor::_text_edit_gui_input(const Ref &ev) { } } if (!tx->has_selection()) { - tx->set_caret_line(row, true, false); + tx->set_caret_line(row, true, false, -1); tx->set_caret_column(col); } } diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 4f90504e35f..ab61123da2c 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -624,6 +624,18 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { return TextEdit::get_cursor_shape(p_pos); } +void CodeEdit::_unhide_carets() { + // Unfold caret and selection origin. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_hidden(get_caret_line(i))) { + unfold_line(get_caret_line(i)); + } + if (has_selection(i) && _is_line_hidden(get_selection_origin_line(i))) { + unfold_line(get_selection_origin_line(i)); + } + } +} + /* Text manipulation */ // Overridable actions @@ -2846,10 +2858,12 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { if (p_gutter == line_number_gutter) { remove_secondary_carets(); - set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0); - select(p_line, 0, p_line + 1, 0); - set_caret_line(p_line + 1); - set_caret_column(0); + set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE); + if (p_line == get_line_count() - 1) { + select(p_line, 0, p_line, INT_MAX); + } else { + select(p_line, 0, p_line + 1, 0); + } return; } diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 1770d4f4d89..1a40f949660 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -309,6 +309,8 @@ protected: static void _bind_compatibility_methods(); #endif + virtual void _unhide_carets() override; + /* Text manipulation */ // Overridable actions diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 38b4ffc8ae9..20d17dfcf8c 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -451,7 +451,7 @@ void TextEdit::_notification(int p_what) { callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); } if (text_changed_dirty) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); } _update_wrap_at_column(true); } break; @@ -565,9 +565,9 @@ void TextEdit::_notification(int p_what) { Vector brace_matching; if (highlight_matching_braces_enabled) { - brace_matching.resize(carets.size()); + brace_matching.resize(get_caret_count()); - for (int caret = 0; caret < carets.size(); caret++) { + for (int caret = 0; caret < get_caret_count(); caret++) { if (get_caret_line(caret) < 0 || get_caret_line(caret) >= text.size() || get_caret_column(caret) < 0) { continue; } @@ -1104,7 +1104,7 @@ void TextEdit::_notification(int p_what) { // Draw selections. float char_w = theme_cache.font->get_char_size(' ', theme_cache.font_size).width; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if (!clipped && has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); @@ -1257,7 +1257,7 @@ void TextEdit::_notification(int p_what) { } Color gl_color = current_color; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if (has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { // Selection int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); @@ -1271,7 +1271,7 @@ void TextEdit::_notification(int p_what) { float char_pos = char_ofs + char_margin + ofs_x; if (char_pos >= xmargin_beg) { if (highlight_matching_braces_enabled) { - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if ((brace_matching[c].open_match_line == line && brace_matching[c].open_match_column == glyphs[j].start) || (get_caret_column(c) == glyphs[j].start && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].open_matching || brace_matching[c].open_mismatch))) { if (brace_matching[c].open_mismatch) { @@ -1562,10 +1562,15 @@ void TextEdit::_notification(int p_what) { case MainLoop::NOTIFICATION_OS_IME_UPDATE: { if (has_focus()) { + bool had_ime_text = has_ime_text(); ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); - if (!ime_text.is_empty() && has_selection()) { + if (!had_ime_text && has_ime_text()) { + _cancel_drag_and_drop_text(); + } + + if (has_ime_text() && has_selection()) { delete_selection(); } @@ -1576,7 +1581,7 @@ void TextEdit::_notification(int p_what) { } break; case NOTIFICATION_DRAG_BEGIN: { - selecting_mode = SelectionMode::SELECTION_MODE_NONE; + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); drag_action = true; dragging_minimap = false; dragging_selection = false; @@ -1587,19 +1592,31 @@ void TextEdit::_notification(int p_what) { case NOTIFICATION_DRAG_END: { if (is_drag_successful()) { if (selection_drag_attempt) { - selection_drag_attempt = false; + // Dropped elsewhere. if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { delete_selection(); } else if (deselect_on_focus_loss_enabled) { deselect(); } } - } else { - selection_drag_attempt = false; } + if (drag_caret_index >= 0) { + if (drag_caret_index < carets.size()) { + remove_caret(drag_caret_index); + } + drag_caret_index = -1; + } + selection_drag_attempt = false; drag_action = false; drag_caret_force_displayed = false; } break; + + case NOTIFICATION_MOUSE_EXIT_SELF: { + if (drag_caret_force_displayed) { + drag_caret_force_displayed = false; + queue_redraw(); + } + } break; } } @@ -1702,15 +1719,17 @@ void TextEdit::gui_input(const Ref &p_gui_input) { if (mb->get_button_index() == MouseButton::WHEEL_RIGHT) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); } + if (mb->get_button_index() == MouseButton::LEFT) { _reset_caret_blink_timer(); apply_ime(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; + int line = pos.y; int col = pos.x; + // Gutters. int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); for (int i = 0; i < gutters.size(); i++) { if (!gutters[i].draw || gutters[i].width <= 0) { @@ -1718,14 +1737,14 @@ void TextEdit::gui_input(const Ref &p_gui_input) { } if (mpos.x >= left_margin && mpos.x <= left_margin + gutters[i].width) { - emit_signal(SNAME("gutter_clicked"), row, i); + emit_signal(SNAME("gutter_clicked"), line, i); return; } left_margin += gutters[i].width; } - // Minimap + // Minimap. if (draw_minimap) { _update_minimap_click(); if (dragging_minimap) { @@ -1733,24 +1752,28 @@ void TextEdit::gui_input(const Ref &p_gui_input) { } } + // Update caret. + int caret = carets.size() - 1; int prev_col = get_caret_column(caret); int prev_line = get_caret_line(caret); + int mouse_over_selection_caret = get_selection_at_line_column(line, col, true); + const int triple_click_timeout = 600; const int triple_click_tolerance = 5; bool is_triple_click = (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && mb->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance); if (!mb->is_double_click() && !is_triple_click) { if (mb->is_alt_pressed()) { - prev_line = row; + prev_line = line; prev_col = col; // Remove caret at clicked location. - if (carets.size() > 1) { - for (int i = 0; i < carets.size(); i++) { + if (get_caret_count() > 1) { + for (int i = 0; i < get_caret_count(); i++) { // Deselect if clicked on caret or its selection. - if ((get_caret_column(i) == col && get_caret_line(i) == row) || is_mouse_over_selection(true, i)) { + if (_selection_contains(i, line, col, true, false)) { remove_caret(i); last_dblclk = 0; return; @@ -1758,96 +1781,58 @@ void TextEdit::gui_input(const Ref &p_gui_input) { } } - if (is_mouse_over_selection()) { + if (mouse_over_selection_caret >= 0) { + // Did not remove selection under mouse, don't add a new caret. return; } - caret = add_caret(row, col); + // Create new caret at clicked location. + caret = add_caret(line, col); if (caret == -1) { return; } - carets.write[caret].selection.selecting_line = row; - carets.write[caret].selection.selecting_column = col; - last_dblclk = 0; - } else if (!mb->is_shift_pressed() && !is_mouse_over_selection()) { - caret = 0; - remove_secondary_carets(); - } - } - - _push_current_op(); - set_caret_line(row, false, true, 0, caret); - set_caret_column(col, false, caret); - selection_drag_attempt = false; - - if (selecting_enabled && mb->is_shift_pressed() && (get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line)) { - if (!has_selection(caret)) { - carets.write[caret].selection.active = true; - selecting_mode = SelectionMode::SELECTION_MODE_POINTER; - carets.write[caret].selection.from_column = prev_col; - carets.write[caret].selection.from_line = prev_line; - carets.write[caret].selection.to_column = carets[caret].column; - carets.write[caret].selection.to_line = carets[caret].line; - - if (get_selection_from_line(caret) > get_selection_to_line(caret) || (get_selection_from_line(caret) == get_selection_to_line(caret) && get_selection_from_column(caret) > get_selection_to_column(caret))) { - SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); - SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); - carets.write[caret].selection.shiftclick_left = false; + } else if (!mb->is_shift_pressed()) { + if (drag_and_drop_selection_enabled && mouse_over_selection_caret >= 0) { + // Try to drag and drop. + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); + selection_drag_attempt = true; + drag_and_drop_origin_caret_index = mouse_over_selection_caret; + last_dblclk = 0; + // Don't update caret until we know if it is not drag and drop. + return; } else { - carets.write[caret].selection.shiftclick_left = true; + // A regular click clears all other carets. + caret = 0; + remove_secondary_carets(); + deselect(); } - carets.write[caret].selection.selecting_line = prev_line; - carets.write[caret].selection.selecting_column = prev_col; - caret_index_edit_dirty = true; - merge_overlapping_carets(); - queue_redraw(); - } else { - if (carets[caret].line < get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column < get_selection_column(caret))) { - if (carets[caret].selection.shiftclick_left) { - carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; - } - carets.write[caret].selection.from_column = carets[caret].column; - carets.write[caret].selection.from_line = carets[caret].line; - - } else if (carets[caret].line > get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column > get_selection_column(caret))) { - if (!carets[caret].selection.shiftclick_left) { - SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); - SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); - carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; - } - carets.write[caret].selection.to_column = carets[caret].column; - carets.write[caret].selection.to_line = carets[caret].line; - - } else { - deselect(caret); - } - caret_index_edit_dirty = true; - merge_overlapping_carets(); - queue_redraw(); } - } else if (drag_and_drop_selection_enabled && is_mouse_over_selection()) { - set_selection_mode(SelectionMode::SELECTION_MODE_NONE, get_selection_line(caret), get_selection_column(caret), caret); - // We use the main caret for dragging, so reset this one. - set_caret_line(prev_line, false, true, 0, caret); - set_caret_column(prev_col, false, caret); - selection_drag_attempt = true; - } else if (caret == 0) { - deselect(); - set_selection_mode(SelectionMode::SELECTION_MODE_POINTER, row, col); - } - if (is_triple_click) { - // Triple-click select line. - selecting_mode = SelectionMode::SELECTION_MODE_LINE; + _push_current_op(); + set_caret_line(line, false, true, -1, caret); + set_caret_column(col, false, caret); selection_drag_attempt = false; - _update_selection_mode_line(); + bool caret_moved = get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line; + + if (selecting_enabled && mb->is_shift_pressed() && !has_selection(caret) && caret_moved) { + // Select from the previous caret position. + select(prev_line, prev_col, line, col, caret); + } + + // Start regular select mode. + set_selection_mode(SelectionMode::SELECTION_MODE_POINTER); + _update_selection_mode_pointer(true); + } else if (is_triple_click) { + // Start triple-click select line mode. + set_selection_mode(SelectionMode::SELECTION_MODE_LINE); + _update_selection_mode_line(true); last_dblclk = 0; - } else if (mb->is_double_click() && text[get_caret_line(caret)].length()) { - // Double-click select word. - selecting_mode = SelectionMode::SELECTION_MODE_WORD; - _update_selection_mode_word(); + } else if (mb->is_double_click()) { + // Start double-click select word mode. + set_selection_mode(SelectionMode::SELECTION_MODE_WORD); + _update_selection_mode_word(true); last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); } @@ -1863,34 +1848,20 @@ void TextEdit::gui_input(const Ref &p_gui_input) { _push_current_op(); _reset_caret_blink_timer(); apply_ime(); + _cancel_drag_and_drop_text(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; - int col = pos.x; + int mouse_line = pos.y; + int mouse_column = pos.x; - bool selection_clicked = false; if (is_move_caret_on_right_click_enabled()) { - if (has_selection()) { - for (int i = 0; i < get_caret_count(); i++) { - int from_line = get_selection_from_line(i); - int to_line = get_selection_to_line(i); - int from_column = get_selection_from_column(i); - int to_column = get_selection_to_column(i); - - if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) { - // Right click in one of the selected text - selection_clicked = true; - break; - } - } - } + bool selection_clicked = get_selection_at_line_column(mouse_line, mouse_column, true) >= 0; if (!selection_clicked) { deselect(); remove_secondary_carets(); - set_caret_line(row, false, false); - set_caret_column(col); + set_caret_line(mouse_line, false, false, -1); + set_caret_column(mouse_column); } - merge_overlapping_carets(); } if (context_menu_enabled) { @@ -1908,22 +1879,20 @@ void TextEdit::gui_input(const Ref &p_gui_input) { } if (mb->get_button_index() == MouseButton::LEFT) { - if (selection_drag_attempt && is_mouse_over_selection()) { + if (!drag_action && selection_drag_attempt && is_mouse_over_selection()) { + // This is not a drag and drop attempt, update the caret. + selection_drag_attempt = false; remove_secondary_carets(); + deselect(); Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); + set_caret_line(pos.y, false, true, -1, 0); set_caret_column(pos.x, true, 0); - - deselect(); } dragging_minimap = false; dragging_selection = false; can_drag_minimap = false; click_select_held->stop(); - if (!drag_action) { - selection_drag_attempt = false; - } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } @@ -1958,7 +1927,8 @@ void TextEdit::gui_input(const Ref &p_gui_input) { mpos.x = get_size().x - mpos.x; } - if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. + if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { + // Update if not in drag and drop. _reset_caret_blink_timer(); if (draw_minimap && !dragging_selection) { @@ -2011,10 +1981,19 @@ void TextEdit::gui_input(const Ref &p_gui_input) { if (drag_action && can_drop_data(mpos, get_viewport()->gui_get_drag_data())) { apply_ime(); + // Update drag and drop caret. drag_caret_force_displayed = true; Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); - set_caret_column(pos.x, true, 0); + + if (drag_caret_index == -1) { + // Force create a new caret for drag and drop. + carets.push_back(Caret()); + drag_caret_index = carets.size() - 1; + } + + drag_caret_force_displayed = true; + set_caret_line(pos.y, false, true, -1, drag_caret_index); + set_caret_column(pos.x, true, drag_caret_index); dragging_selection = true; } } @@ -2043,6 +2022,8 @@ void TextEdit::gui_input(const Ref &p_gui_input) { return; } + _cancel_drag_and_drop_text(); + _reset_caret_blink_timer(); // Allow unicode handling if: @@ -2350,13 +2331,13 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) { void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { // Handle selection. if (p_select) { _pre_shift_selection(i); } else if (has_selection(i) && !p_move_by_word) { // If a selection is active, move caret to start of selection. - set_caret_line(get_selection_from_line(i), false, true, 0, i); + set_caret_line(get_selection_from_line(i), false, true, -1, i); set_caret_column(get_selection_from_column(i), i == 0, i); deselect(i); continue; @@ -2368,7 +2349,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { int cc = get_caret_column(i); // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. if (cc == 0 && get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - 1, false, true, 0, i); + set_caret_line(get_caret_line(i) - 1, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2389,7 +2370,8 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. if (get_caret_column(i) == 0) { if (get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1), false, true, 0, i); + int new_caret_line = get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1); + set_caret_line(new_caret_line, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } } else { @@ -2400,23 +2382,19 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { // Handle selection. if (p_select) { _pre_shift_selection(i); } else if (has_selection(i) && !p_move_by_word) { // If a selection is active, move caret to end of selection. - set_caret_line(get_selection_to_line(i), false, true, 0, i); + set_caret_line(get_selection_to_line(i), false, true, -1, i); set_caret_column(get_selection_to_column(i), i == 0, i); deselect(i); continue; @@ -2428,7 +2406,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { int cc = get_caret_column(i); // If the caret is at the end of the line, and not on the last line, move it down to the beginning of the next line. if (cc == text[get_caret_line(i)].length() && get_caret_line(i) < text.size() - 1) { - set_caret_line(get_caret_line(i) + 1, false, true, 0, i); + set_caret_line(get_caret_line(i) + 1, false, true, -1, i); set_caret_column(0, i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2449,7 +2427,8 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { // If we are at the end of the line, move the caret to the next line down. if (get_caret_column(i) == text[get_caret_line(i)].length()) { if (get_caret_line(i) < text.size() - 1) { - set_caret_line(get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1), false, false, 0, i); + int new_caret_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1); + set_caret_line(new_caret_line, false, false, -1, i); set_caret_column(0, i == 0, i); } } else { @@ -2460,17 +2439,13 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2490,17 +2465,13 @@ void TextEdit::_move_caret_up(bool p_select) { set_caret_line(new_line, i == 0, false, 0, i); } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2516,17 +2487,13 @@ void TextEdit::_move_caret_down(bool p_select) { int new_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1); set_caret_line(new_line, i == 0, false, 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_start(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2551,17 +2518,13 @@ void TextEdit::_move_caret_to_line_start(bool p_select) { } else { set_caret_column(row_start_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_end(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2580,17 +2543,13 @@ void TextEdit::_move_caret_to_line_end(bool p_select) { } else { set_caret_column(row_end_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2600,17 +2559,13 @@ void TextEdit::_move_caret_page_up(bool p_select) { Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), -get_visible_line_count()); int n_line = get_caret_line(i) - next_line.x + 1; set_caret_line(n_line, i == 0, false, next_line.y, i); - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2620,10 +2575,6 @@ void TextEdit::_move_caret_page_down(bool p_select) { Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), get_visible_line_count()); int n_line = get_caret_line(i) + next_line.x - 1; set_caret_line(n_line, i == 0, false, next_line.y, i); - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } @@ -2878,12 +2829,8 @@ void TextEdit::_move_caret_document_start(bool p_select) { deselect(); } - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); - - if (p_select) { - _post_shift_selection(0); - } } void TextEdit::_move_caret_document_end(bool p_select) { @@ -2894,12 +2841,8 @@ void TextEdit::_move_caret_document_end(bool p_select) { deselect(); } - set_caret_line(get_last_unhidden_line(), true, false, 9999); + set_caret_line(get_last_unhidden_line(), true, false, -1); set_caret_column(text[get_caret_line()].length()); - - if (p_select) { - _post_shift_selection(0); - } } bool TextEdit::_clear_carets_and_selection() { @@ -3459,7 +3402,7 @@ void TextEdit::_clear() { clear_undo_history(); text.clear(); remove_secondary_carets(); - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); first_visible_col = 0; first_visible_line = 0; @@ -4040,7 +3983,7 @@ void TextEdit::undo() { _push_current_op(); if (undo_stack_pos == nullptr) { - if (!undo_stack.size()) { + if (undo_stack.is_empty()) { return; // Nothing to undo. } @@ -4059,6 +4002,7 @@ void TextEdit::undo() { current_op.version = op.prev_version; if (undo_stack_pos->get().chain_backward) { + // This was part of a complex operation, undo until the chain forward at the start of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->prev()); undo_stack_pos = undo_stack_pos->prev(); @@ -4072,9 +4016,9 @@ void TextEdit::undo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().start_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().start_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (carets[i].line != undo_stack_pos->get().start_carets[i].line || carets[i].column != undo_stack_pos->get().start_carets[i].column) { dirty_carets = true; break; @@ -4084,11 +4028,11 @@ void TextEdit::undo() { carets = undo_stack_pos->get().start_carets; - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4113,6 +4057,7 @@ void TextEdit::redo() { _do_text_op(op, false); current_op.version = op.version; if (undo_stack_pos->get().chain_forward) { + // This was part of a complex operation, redo until the chain backward at the end of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->next()); undo_stack_pos = undo_stack_pos->next(); @@ -4126,9 +4071,9 @@ void TextEdit::redo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().end_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().end_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (carets[i].line != undo_stack_pos->get().end_carets[i].line || carets[i].column != undo_stack_pos->get().end_carets[i].column) { dirty_carets = true; break; @@ -4139,11 +4084,11 @@ void TextEdit::redo() { carets = undo_stack_pos->get().end_carets; undo_stack_pos = undo_stack_pos->next(); - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4358,13 +4303,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ } } - if (row < 0) { - row = 0; - } - - if (row >= text.size()) { - row = text.size() - 1; - } + row = CLAMP(row, 0, text.size() - 1); int visible_lines = get_visible_line_count_in_range(first_vis_line, row); if (rows > visible_lines) { @@ -4510,25 +4449,16 @@ bool TextEdit::is_dragging_cursor() const { } bool TextEdit::is_mouse_over_selection(bool p_edges, int p_caret) const { - for (int i = 0; i < carets.size(); i++) { + Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); + int line = pos.y; + int column = pos.x; + + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } - if (!has_selection(i)) { - continue; - } - - Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int row = pos.y; - int col = pos.x; - if (p_edges) { - if ((row == get_selection_from_line(i) && col == get_selection_from_column(i)) || (row == get_selection_to_line(i) && col == get_selection_to_column(i))) { - return true; - } - } - - if (row >= get_selection_from_line(i) && row <= get_selection_to_line(i) && (row > get_selection_from_line(i) || col > get_selection_from_column(i)) && (row < get_selection_to_line(i) || col < get_selection_to_column(i))) { + if (_selection_contains(i, line, column, p_edges)) { return true; } } @@ -4619,21 +4549,22 @@ bool TextEdit::is_multiple_carets_enabled() const { return multi_carets_enabled; } -int TextEdit::add_caret(int p_line, int p_col) { +int TextEdit::add_caret(int p_line, int p_column) { if (!multi_carets_enabled) { return -1; } + _cancel_drag_and_drop_text(); p_line = CLAMP(p_line, 0, text.size() - 1); - p_col = CLAMP(p_col, 0, get_line(p_line).length()); + p_column = CLAMP(p_column, 0, get_line(p_line).length()); for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) == p_line && get_caret_column(i) == p_col) { + if (get_caret_line(i) == p_line && get_caret_column(i) == p_column) { return -1; } if (has_selection(i)) { - if (p_line >= get_selection_from_line(i) && p_line <= get_selection_to_line(i) && (p_line > get_selection_from_line(i) || p_col >= get_selection_from_column(i)) && (p_line < get_selection_to_line(i) || p_col <= get_selection_to_column(i))) { + if (p_line >= get_selection_from_line(i) && p_line <= get_selection_to_line(i) && (p_line > get_selection_from_line(i) || p_column >= get_selection_from_column(i)) && (p_line < get_selection_to_line(i) || p_column <= get_selection_to_column(i))) { return -1; } } @@ -4641,7 +4572,7 @@ int TextEdit::add_caret(int p_line, int p_col) { carets.push_back(Caret()); set_caret_line(p_line, false, false, 0, carets.size() - 1); - set_caret_column(p_col, false, carets.size() - 1); + set_caret_column(p_column, false, carets.size() - 1); caret_index_edit_dirty = true; return carets.size() - 1; } @@ -4649,14 +4580,39 @@ int TextEdit::add_caret(int p_line, int p_col) { void TextEdit::remove_caret(int p_caret) { ERR_FAIL_COND_MSG(carets.size() <= 1, "The main caret should not be removed."); ERR_FAIL_INDEX(p_caret, carets.size()); + + _caret_changed(p_caret); carets.remove_at(p_caret); caret_index_edit_dirty = true; + + if (drag_caret_index >= 0) { + if (p_caret == drag_caret_index) { + drag_caret_index = -1; + } else if (p_caret < drag_caret_index) { + drag_caret_index -= 1; + } + } } void TextEdit::remove_secondary_carets() { + if (carets.size() == 1) { + return; + } + + _caret_changed(); carets.resize(1); - caret_index_edit_dirty = true; - queue_redraw(); + + if (drag_caret_index >= 0) { + drag_caret_index = -1; + } +} + +int TextEdit::get_caret_count() const { + // Don't include drag caret. + if (drag_caret_index >= 0) { + return carets.size() - 1; + } + return carets.size(); } void TextEdit::merge_overlapping_carets() { @@ -4758,10 +4714,6 @@ void TextEdit::merge_overlapping_carets() { } } -int TextEdit::get_caret_count() const { - return carets.size(); -} - void TextEdit::add_caret_at_carets(bool p_below) { Vector caret_edit_order = get_caret_index_edit_order(); for (const int &caret_index : caret_edit_order) { @@ -4902,16 +4854,10 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ } setting_caret_line = true; - if (p_line < 0) { - p_line = 0; - } - - if (p_line >= text.size()) { - p_line = text.size() - 1; - } + p_line = CLAMP(p_line, 0, text.size() - 1); if (!p_can_be_hidden) { - if (_is_line_hidden(CLAMP(p_line, 0, text.size() - 1))) { + if (_is_line_hidden(p_line)) { int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { p_line += move_down; @@ -4920,7 +4866,7 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { p_line -= move_up; } else { - WARN_PRINT(("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines.")); + WARN_PRINT("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); } } } @@ -4928,31 +4874,36 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ bool caret_moved = get_caret_line(p_caret) != p_line; carets.write[p_caret].line = p_line; - int n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index); - if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { - Vector rows = get_line_wrapped_text(p_line); - int row_end_col = 0; - for (int i = 0; i < p_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (n_col >= row_end_col) { - n_col -= 1; + int n_col; + if (p_wrap_index >= 0) { + // Keep caret in same visual x position it was at previously. + n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index); + if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } } + } else { + // Clamp the column. + n_col = MIN(get_caret_column(p_caret), get_line(p_line).length()); } caret_moved = (caret_moved || get_caret_column(p_caret) != n_col); carets.write[p_caret].column = n_col; + // Unselect if the caret moved to the selection origin. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } setting_caret_line = false; - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4961,29 +4912,32 @@ int TextEdit::get_caret_line(int p_caret) const { return carets[p_caret].line; } -void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport, int p_caret) { +void TextEdit::set_caret_column(int p_column, bool p_adjust_viewport, int p_caret) { ERR_FAIL_INDEX(p_caret, carets.size()); - if (p_col < 0) { - p_col = 0; - } - if (p_col > get_line(get_caret_line(p_caret)).length()) { - p_col = get_line(get_caret_line(p_caret)).length(); - } - bool caret_moved = get_caret_column(p_caret) != p_col; - carets.write[p_caret].column = p_col; + p_column = CLAMP(p_column, 0, get_line(get_caret_line(p_caret)).length()); + + bool caret_moved = get_caret_column(p_caret) != p_column; + carets.write[p_caret].column = p_column; carets.write[p_caret].last_fit_x = _get_column_x_offset_for_line(get_caret_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret)); + if (!has_selection(p_caret)) { + // Set the selection origin last fit x to be the same, so we can tell if there was a selection. + carets.write[p_caret].selection.origin_last_fit_x = carets[p_caret].last_fit_x; + } + + // Unselect if the caret moved to the selection origin. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4998,7 +4952,7 @@ int TextEdit::get_caret_wrap_index(int p_caret) const { } String TextEdit::get_word_under_caret(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); StringBuilder selected_text; for (int c = 0; c < carets.size(); c++) { @@ -5059,20 +5013,8 @@ bool TextEdit::is_drag_and_drop_selection_enabled() const { return drag_and_drop_selection_enabled; } -void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column, int p_caret) { - ERR_FAIL_INDEX(p_caret, carets.size()); - +void TextEdit::set_selection_mode(SelectionMode p_mode) { selecting_mode = p_mode; - if (p_line >= 0) { - ERR_FAIL_INDEX(p_line, text.size()); - carets.write[p_caret].selection.selecting_line = p_line; - carets.write[p_caret].selection.selecting_column = CLAMP(carets[p_caret].selection.selecting_column, 0, text[carets[p_caret].selection.selecting_line].length()); - } - if (p_column >= 0) { - ERR_FAIL_INDEX(carets[p_caret].selection.selecting_line, text.size()); - ERR_FAIL_INDEX(p_column, text[carets[p_caret].selection.selecting_line].length() + 1); - carets.write[p_caret].selection.selecting_column = p_column; - } } TextEdit::SelectionMode TextEdit::get_selection_mode() const { @@ -5090,16 +5032,12 @@ void TextEdit::select_all() { } remove_secondary_carets(); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); select(0, 0, text.size() - 1, text[text.size() - 1].length()); - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, 0, 0); - carets.write[0].selection.shiftclick_left = true; - set_caret_line(get_selection_to_line(), false); - set_caret_column(get_selection_to_column(), false); - queue_redraw(); } void TextEdit::select_word_under_caret(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); _push_current_op(); if (!selecting_enabled) { @@ -5234,53 +5172,37 @@ void TextEdit::skip_selection_for_next_occurrence() { } } -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()); +void TextEdit::select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret) { + ERR_FAIL_INDEX(p_caret, get_caret_count()); + + p_caret_line = CLAMP(p_caret_line, 0, text.size() - 1); + p_caret_column = CLAMP(p_caret_column, 0, text[p_caret_line].length()); + set_caret_line(p_caret_line, false, true, -1, p_caret); + set_caret_column(p_caret_column, false, p_caret); + if (!selecting_enabled) { return; } - p_from_line = CLAMP(p_from_line, 0, text.size() - 1); - p_from_column = CLAMP(p_from_column, 0, text[p_from_line].length()); - p_to_line = CLAMP(p_to_line, 0, text.size() - 1); - p_to_column = CLAMP(p_to_column, 0, text[p_to_line].length()); + p_origin_line = CLAMP(p_origin_line, 0, text.size() - 1); + p_origin_column = CLAMP(p_origin_column, 0, text[p_origin_line].length()); + set_selection_origin_line(p_origin_line, true, -1, p_caret); + set_selection_origin_column(p_origin_column, p_caret); - carets.write[p_caret].selection.from_line = p_from_line; - carets.write[p_caret].selection.from_column = p_from_column; - carets.write[p_caret].selection.to_line = p_to_line; - carets.write[p_caret].selection.to_column = p_to_column; - - carets.write[p_caret].selection.active = true; - - if (get_selection_from_line(p_caret) == get_selection_to_line(p_caret)) { - if (get_selection_from_column(p_caret) == get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.active = false; - - } else if (get_selection_from_column(p_caret) > get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.shiftclick_left = false; - SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); - } else { - carets.write[p_caret].selection.shiftclick_left = true; - } - } else if (get_selection_from_line(p_caret) > get_selection_to_line(p_caret)) { - carets.write[p_caret].selection.shiftclick_left = false; - SWAP(carets.write[p_caret].selection.from_line, carets.write[p_caret].selection.to_line); - SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); - } else { - carets.write[p_caret].selection.shiftclick_left = true; + bool had_selection = has_selection(p_caret); + bool activate = p_origin_line != p_caret_line || p_origin_column != p_caret_column; + carets.write[p_caret].selection.active = activate; + if (had_selection != activate) { + _selection_changed(p_caret); } - - caret_index_edit_dirty = true; - queue_redraw(); } bool TextEdit::has_selection(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), false); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, false); + if (p_caret >= 0) { + return carets[p_caret].selection.active; + } for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; - } - if (carets[i].selection.active) { return true; } @@ -5288,6 +5210,154 @@ bool TextEdit::has_selection(int p_caret) const { return false; } +String TextEdit::get_selected_text(int p_caret) { + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); + + if (p_caret >= 0) { + if (!has_selection(p_caret)) { + return ""; + } + return _base_get_text(get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret)); + } + + StringBuilder selected_text; + Vector sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + + if (!has_selection(caret_index)) { + continue; + } + if (selected_text.get_string_length() != 0) { + selected_text += "\n"; + } + selected_text += _base_get_text(get_selection_from_line(caret_index), get_selection_from_column(caret_index), get_selection_to_line(caret_index), get_selection_to_column(caret_index)); + } + + return selected_text.as_string(); +} + +int TextEdit::get_selection_at_line_column(int p_line, int p_column, bool p_include_edges) const { + // Return the caret index of the found selection, or -1. + for (int i = 0; i < get_caret_count(); i++) { + if (_selection_contains(i, p_line, p_column, p_include_edges, true)) { + return i; + } + } + return -1; +} + +Vector TextEdit::get_line_ranges_from_carets(bool p_only_selections, bool p_merge_adjacent) const { + // Get a series of line ranges that cover all lines that have a caret or selection. + // For each Point2i range, x is the first line and y is the last line. + Vector ret; + int last_to_line = INT_MIN; + + Vector sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (p_only_selections && !has_selection(caret_index)) { + continue; + } + Point2i range = Point2i(get_selection_from_line(caret_index), get_selection_to_line(caret_index)); + if (has_selection(caret_index) && get_selection_to_column(caret_index) == 0) { + // Dont include selection end line if it ends at column 0. + range.y--; + } + if (range.x == last_to_line || (p_merge_adjacent && range.x - 1 == last_to_line)) { + // Merge if starts on the same line or adjacent line. + ret.write[ret.size() - 1].y = range.y; + } else { + ret.append(range); + } + last_to_line = range.y; + } + return ret; +} + +TypedArray TextEdit::get_line_ranges_from_carets_typed_array(bool p_only_selections, bool p_merge_adjacent) const { + // Wrapper for `get_line_ranges_from_carets` to return a datatype that can be exposed. + TypedArray ret; + Vector ranges = get_line_ranges_from_carets(p_only_selections, p_merge_adjacent); + for (const Point2i &range : ranges) { + ret.push_back(range); + } + return ret; +} + +void TextEdit::set_selection_origin_line(int p_line, bool p_can_be_hidden, int p_wrap_index, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + p_line = CLAMP(p_line, 0, text.size() - 1); + + if (!p_can_be_hidden) { + if (_is_line_hidden(p_line)) { + int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; + if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { + p_line += move_down; + } else { + int move_up = get_next_visible_line_offset_from(p_line, -1) - 1; + if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { + p_line -= move_up; + } else { + WARN_PRINT("Selection origin set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); + } + } + } + } + + bool selection_moved = get_selection_origin_line(p_caret) != p_line; + carets.write[p_caret].selection.origin_line = p_line; + + int n_col; + if (p_wrap_index >= 0) { + // Keep selection origin in same visual x position it was at previously. + n_col = _get_char_pos_for_line(carets[p_caret].selection.origin_last_fit_x, p_line, p_wrap_index); + if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } + } + } else { + // Clamp the column. + n_col = MIN(get_selection_origin_column(p_caret), get_line(p_line).length()); + } + selection_moved = (selection_moved || get_selection_origin_column(p_caret) != n_col); + carets.write[p_caret].selection.origin_column = n_col; + + // Unselect if the selection origin moved to the caret. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +void TextEdit::set_selection_origin_column(int p_column, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + + p_column = CLAMP(p_column, 0, get_line(get_selection_origin_line(p_caret)).length()); + + bool selection_moved = get_selection_origin_column(p_caret) != p_column; + + carets.write[p_caret].selection.origin_column = p_column; + + carets.write[p_caret].selection.origin_last_fit_x = _get_column_x_offset_for_line(get_selection_origin_column(p_caret), get_selection_origin_line(p_caret), get_selection_origin_column(p_caret)); + + // Unselect if the selection origin moved to the caret. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } +} + String TextEdit::get_selected_text(int p_caret) { ERR_FAIL_COND_V(p_caret > carets.size(), ""); @@ -5311,56 +5381,87 @@ String TextEdit::get_selected_text(int p_caret) { return selected_text.as_string(); } -int TextEdit::get_selection_line(int p_caret) const { +int TextEdit::get_selection_origin_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.selecting_line; + return carets[p_caret].selection.origin_line; } -int TextEdit::get_selection_column(int p_caret) const { +int TextEdit::get_selection_origin_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.selecting_column; + return carets[p_caret].selection.origin_column; } int TextEdit::get_selection_from_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.from_line; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MIN(carets[p_caret].selection.origin_line, carets[p_caret].line); } int TextEdit::get_selection_from_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.from_column; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].column; + } else { + return MIN(carets[p_caret].selection.origin_column, carets[p_caret].column); + } } int TextEdit::get_selection_to_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.to_line; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MAX(carets[p_caret].selection.origin_line, carets[p_caret].line); } int TextEdit::get_selection_to_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.to_column; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else { + return MAX(carets[p_caret].selection.origin_column, carets[p_caret].column); + } +} + +bool TextEdit::is_caret_after_selection_origin(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), false); + if (!has_selection(p_caret)) { + return true; + } + return carets[p_caret].line > carets[p_caret].selection.origin_line || (carets[p_caret].line == carets[p_caret].selection.origin_line && carets[p_caret].column >= carets[p_caret].selection.origin_column); } void TextEdit::deselect(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); - for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); + bool selection_changed = false; + if (p_caret >= 0) { + selection_changed = carets.write[p_caret].selection.active; + carets.write[p_caret].selection.active = false; + } else { + for (int i = 0; i < carets.size(); i++) { + selection_changed |= carets.write[i].selection.active; + carets.write[i].selection.active = false; } - carets.write[i].selection.active = false; } - caret_index_edit_dirty = true; - queue_redraw(); + if (selection_changed) { + _selection_changed(p_caret); + } } void TextEdit::delete_selection(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); begin_complex_operation(); Vector caret_edit_order = get_caret_index_edit_order(); @@ -6311,7 +6412,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_search_text", "search_text"), &TextEdit::set_search_text); ClassDB::bind_method(D_METHOD("set_search_flags", "flags"), &TextEdit::set_search_flags); - ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search); + ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_column"), &TextEdit::search); /* Tooltip */ ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "callback"), &TextEdit::set_tooltip_request_func); @@ -6355,7 +6456,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_multiple_carets_enabled", "enabled"), &TextEdit::set_multiple_carets_enabled); ClassDB::bind_method(D_METHOD("is_multiple_carets_enabled"), &TextEdit::is_multiple_carets_enabled); - ClassDB::bind_method(D_METHOD("add_caret", "line", "col"), &TextEdit::add_caret); + ClassDB::bind_method(D_METHOD("add_caret", "line", "column"), &TextEdit::add_caret); ClassDB::bind_method(D_METHOD("remove_caret", "caret"), &TextEdit::remove_caret); ClassDB::bind_method(D_METHOD("remove_secondary_carets"), &TextEdit::remove_secondary_carets); ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); @@ -6394,27 +6495,33 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_drag_and_drop_selection_enabled", "enable"), &TextEdit::set_drag_and_drop_selection_enabled); ClassDB::bind_method(D_METHOD("is_drag_and_drop_selection_enabled"), &TextEdit::is_drag_and_drop_selection_enabled); - ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_mode", "mode"), &TextEdit::set_selection_mode); ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode); 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("select", "origin_line", "origin_column", "caret_line", "caret_column", "caret_index"), &TextEdit::select, DEFVAL(0)); ClassDB::bind_method(D_METHOD("has_selection", "caret_index"), &TextEdit::has_selection, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_selected_text", "caret_index"), &TextEdit::get_selected_text, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_selection_at_line_column", "line", "column", "include_edges"), &TextEdit::get_selection_at_line_column, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_line_ranges_from_carets", "p_only_selections", "p_merge_adjacent"), &TextEdit::get_line_ranges_from_carets_typed_array, DEFVAL(false), DEFVAL(true)); - ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_origin_line", "caret_index"), &TextEdit::get_selection_origin_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_origin_column", "caret_index"), &TextEdit::get_selection_origin_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_line", "line", "can_be_hidden", "wrap_index", "caret_index"), &TextEdit::set_selection_origin_line, DEFVAL(true), DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_column", "column", "caret_index"), &TextEdit::set_selection_origin_column, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_from_line", "caret_index"), &TextEdit::get_selection_from_line, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_from_column", "caret_index"), &TextEdit::get_selection_from_column, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_to_line", "caret_index"), &TextEdit::get_selection_to_line, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_to_column", "caret_index"), &TextEdit::get_selection_to_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("is_caret_after_selection_origin", "caret_index"), &TextEdit::is_caret_after_selection_origin, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("deselect", "caret_index"), &TextEdit::deselect, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("delete_selection", "caret_index"), &TextEdit::delete_selection, DEFVAL(-1)); @@ -6690,6 +6797,10 @@ void TextEdit::_unhide_all_lines() { queue_redraw(); } +void TextEdit::_unhide_carets() { + // Override for functionality. +} + void TextEdit::_set_line_as_hidden(int p_line, bool p_hidden) { ERR_FAIL_INDEX(p_line, text.size()); @@ -6746,7 +6857,7 @@ void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca } void TextEdit::_backspace_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } @@ -7203,6 +7314,23 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con } /* Caret */ +void TextEdit::_caret_changed(int p_caret) { + queue_redraw(); + + if (has_selection(p_caret)) { + _selection_changed(p_caret); + } + + if (caret_pos_dirty) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); + } + caret_pos_dirty = true; +} + void TextEdit::_emit_caret_changed() { emit_signal(SNAME("caret_changed")); caret_pos_dirty = false; @@ -7251,60 +7379,95 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line, int p_column } } -/* Selection */ -void TextEdit::_click_selection_held() { - // Warning: is_mouse_button_pressed(MouseButton::LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD - // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem. - // I'm unsure if there's an actual fix that doesn't have a ton of side effects. - if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) && get_selection_mode() != SelectionMode::SELECTION_MODE_NONE) { - switch (get_selection_mode()) { - case SelectionMode::SELECTION_MODE_POINTER: { - _update_selection_mode_pointer(); - } break; - case SelectionMode::SELECTION_MODE_WORD: { - _update_selection_mode_word(); - } break; - case SelectionMode::SELECTION_MODE_LINE: { - _update_selection_mode_line(); - } break; - default: { - break; - } +bool TextEdit::_is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges) const { + if (p_line >= p_from_line && p_line <= p_to_line && (p_line > p_from_line || p_column > p_from_column) && (p_line < p_to_line || p_column < p_to_column)) { + return true; + } + if (p_include_edges) { + if ((p_line == p_from_line && p_column == p_from_column) || (p_line == p_to_line && p_column == p_to_column)) { + return true; } - } else { - click_select_held->stop(); + } + return false; +} + +void TextEdit::_cancel_drag_and_drop_text() { + // Cancel the drag operation if drag originated from here. + if (selection_drag_attempt && get_viewport()) { + get_viewport()->gui_cancel_drag(); } } -void TextEdit::_update_selection_mode_pointer() { - dragging_selection = true; +/* Selection */ +void TextEdit::_selection_changed(int p_caret) { + if (!selecting_enabled) { + return; + } + + _cancel_drag_and_drop_text(); + queue_redraw(); +} + +void TextEdit::_click_selection_held() { + // Update the selection mode on a timer so it is updated when the view scrolls even if the mouse isn't moving. + if (!Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { + click_select_held->stop(); + return; + } + switch (get_selection_mode()) { + case SelectionMode::SELECTION_MODE_POINTER: { + _update_selection_mode_pointer(); + } break; + case SelectionMode::SELECTION_MODE_WORD: { + _update_selection_mode_word(); + } break; + case SelectionMode::SELECTION_MODE_LINE: { + _update_selection_mode_line(); + } break; + default: { + break; + } + } +} + +void TextEdit::_update_selection_mode_pointer(bool p_initial) { Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; + int column = pos.x; + int caret_index = get_caret_count() - 1; - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); + if (p_initial && !has_selection(caret_index)) { + set_selection_origin_line(line, true, -1, caret_index); + set_selection_origin_column(column, caret_index); + // Set the word begin and end to the column in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = column; + carets.write[caret_index].selection.word_end_column = column; + } else { + select(get_selection_origin_line(caret_index), get_selection_origin_column(caret_index), line, column, caret_index); + } + adjust_viewport_to_caret(caret_index); - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(col, true, caret_idx); - queue_redraw(); + if (has_selection(caret_index)) { + // Only set to true if any selection has been made. + dragging_selection = true; + } click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_word() { +void TextEdit::_update_selection_mode_word(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; + int column = pos.x; + int caret_index = get_caret_count() - 1; - int caret_pos = CLAMP(col, 0, text[line].length()); + int caret_pos = CLAMP(column, 0, text[line].length()); int beg = caret_pos; int end = beg; PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); @@ -7316,70 +7479,57 @@ void TextEdit::_update_selection_mode_word() { } } - /* Initial selection. */ - if (!has_selection(caret_idx)) { - select(line, beg, line, end, caret_idx); - carets.write[caret_idx].selection.selecting_column = beg; - carets.write[caret_idx].selection.selected_word_beg = beg; - carets.write[caret_idx].selection.selected_word_end = end; - carets.write[caret_idx].selection.selected_word_origin = beg; - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(end, true, caret_idx); + if (p_initial && !has_selection(caret_index)) { + // Set the selection origin if there is no existing selection. + select(line, beg, line, end, caret_index); + carets.write[caret_index].selection.word_begin_column = beg; + carets.write[caret_index].selection.word_end_column = end; } else { - if ((col <= carets[caret_idx].selection.selected_word_origin && line == get_selection_line(caret_idx)) || line < get_selection_line(caret_idx)) { - carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_end; - select(line, beg, get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_end, caret_idx); - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(beg, true, caret_idx); - } else { - carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_beg; - select(get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_beg, line, end, caret_idx); - set_caret_line(get_selection_to_line(caret_idx), false, true, 0, caret_idx); - set_caret_column(get_selection_to_column(caret_idx), true, caret_idx); - } + // Expand the word selection to the mouse. + int origin_line = get_selection_origin_line(caret_index); + bool is_new_selection_dir_right = line > origin_line || (line == origin_line && column >= carets[caret_index].selection.word_begin_column); + int origin_col = is_new_selection_dir_right ? carets[caret_index].selection.word_begin_column : carets[caret_index].selection.word_end_column; + int caret_col = is_new_selection_dir_right ? end : beg; + + select(origin_line, origin_col, line, caret_col, caret_index); } + adjust_viewport_to_caret(caret_index); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_line() { +void TextEdit::_update_selection_mode_line(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; + int caret_index = get_caret_count() - 1; - col = 0; - if (line < carets[caret_idx].selection.selecting_line) { - // Caret is above us. - set_caret_line(line - 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = has_selection(caret_idx) - ? text[get_selection_line(caret_idx)].length() - : 0; - } else { - // Caret is below us. - set_caret_line(line + 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = 0; - col = text[line].length(); + int origin_line = p_initial && !has_selection(caret_index) ? line : get_selection_origin_line(); + bool line_below = line >= origin_line; + int origin_col = line_below ? 0 : get_line(origin_line).length(); + int caret_line = line_below ? line + 1 : line; + int caret_col = caret_line < text.size() ? 0 : get_line(text.size() - 1).length(); + + select(origin_line, origin_col, caret_line, caret_col, caret_index); + adjust_viewport_to_caret(caret_index); + + if (p_initial) { + // Set the word begin and end to the start and end of the origin line in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = 0; + carets.write[caret_index].selection.word_end_column = get_line(origin_line).length(); } - set_caret_column(0, false, caret_idx); - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } @@ -7389,23 +7539,23 @@ void TextEdit::_pre_shift_selection(int p_caret) { return; } - if (!has_selection(p_caret) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { - carets.write[p_caret].selection.active = true; - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_caret_line(p_caret), get_caret_column(p_caret), p_caret); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); + if (has_selection(p_caret)) { return; } - - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_selection_line(p_caret), get_selection_column(p_caret), p_caret); + // Prepare selection to start at current caret position. + set_selection_origin_line(get_caret_line(p_caret), true, -1, p_caret); + set_selection_origin_column(get_caret_column(p_caret), p_caret); + carets.write[p_caret].selection.active = true; + carets.write[p_caret].selection.word_begin_column = get_caret_column(p_caret); + carets.write[p_caret].selection.word_end_column = get_caret_column(p_caret); } -void TextEdit::_post_shift_selection(int p_caret) { - if (!selecting_enabled) { - return; - } - - if (has_selection(p_caret) && get_selection_mode() == SelectionMode::SELECTION_MODE_SHIFT) { - select(get_selection_line(p_caret), get_selection_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret), p_caret); +bool TextEdit::_selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges, bool p_only_selections) const { + if (!has_selection(p_caret)) { + return !p_only_selections && p_line == get_caret_line(p_caret) && p_column == get_caret_column(p_caret); } + return _is_line_col_in_range(p_line, p_column, get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret), p_include_edges); } /* Line Wrapping */ @@ -7782,7 +7932,21 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { /*** Super internal Core API. Everything builds on it. ***/ -void TextEdit::_text_changed_emit() { +void TextEdit::_text_changed() { + _cancel_drag_and_drop_text(); + queue_redraw(); + + if (text_changed_dirty || setting_text) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); + } + text_changed_dirty = true; +} + +void TextEdit::_emit_text_changed() { emit_signal(SNAME("text_changed")); text_changed_dirty = false; } @@ -7918,12 +8082,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i input_direction = (TextDirection)dir; } - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_line, r_end_line); } @@ -7964,12 +8123,7 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li text.remove_range(p_from_line, p_to_line); text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text)); - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_to_line, p_from_line); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 1099295d3b5..ed68eb954b1 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -389,18 +389,12 @@ private: /* Caret. */ struct Selection { bool active = false; - bool shiftclick_left = false; - int selecting_line = 0; - int selecting_column = 0; - int selected_word_beg = 0; - int selected_word_end = 0; - int selected_word_origin = 0; - - int from_line = 0; - int from_column = 0; - int to_line = 0; - int to_column = 0; + int origin_line = 0; + int origin_column = 0; + int origin_last_fit_x = 0; + int word_begin_column = 0; + int word_end_column = 0; }; struct Caret { @@ -438,12 +432,16 @@ private: bool drag_action = false; bool drag_caret_force_displayed = false; + void _caret_changed(int p_caret = -1); void _emit_caret_changed(); void _reset_caret_blink_timer(); void _toggle_draw_caret(); int _get_column_x_offset_for_line(int p_char, int p_line, int p_column) const; + bool _is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges = true) const; + + void _cancel_drag_and_drop_text(); /* Selection. */ SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; @@ -456,18 +454,23 @@ private: bool selection_drag_attempt = false; bool dragging_selection = false; + int drag_and_drop_origin_caret_index = -1; + int drag_caret_index = -1; Timer *click_select_held = nullptr; uint64_t last_dblclk = 0; Vector2 last_dblclk_pos; + + void _selection_changed(int p_caret = -1); void _click_selection_held(); - void _update_selection_mode_pointer(); - void _update_selection_mode_word(); - void _update_selection_mode_line(); + void _update_selection_mode_pointer(bool p_initial = false); + void _update_selection_mode_word(bool p_initial = false); + void _update_selection_mode_line(bool p_initial = false); void _pre_shift_selection(int p_caret); - void _post_shift_selection(int p_caret); + + bool _selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const; /* Line wrapping. */ LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE; @@ -599,7 +602,8 @@ private: /*** Super internal Core API. Everything builds on it. ***/ bool text_changed_dirty = false; - void _text_changed_emit(); + void _text_changed(); + void _emit_text_changed(); void _insert_text(int p_line, int p_char, const String &p_text, int *r_end_line = nullptr, int *r_end_char = nullptr); void _remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); @@ -659,6 +663,7 @@ protected: bool _is_line_hidden(int p_line) const; void _unhide_all_lines(); + virtual void _unhide_carets(); // Symbol lookup. String lookup_symbol_word; @@ -851,7 +856,7 @@ public: void set_multiple_carets_enabled(bool p_enabled); bool is_multiple_carets_enabled() const; - int add_caret(int p_line, int p_col); + int add_caret(int p_line, int p_column); void remove_caret(int p_caret); void remove_secondary_carets(); void merge_overlapping_carets(); @@ -884,27 +889,34 @@ public: void set_drag_and_drop_selection_enabled(const bool p_enabled); bool is_drag_and_drop_selection_enabled() const; - void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); + void set_selection_mode(SelectionMode p_mode); SelectionMode get_selection_mode() const; 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); + void select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret = 0); bool has_selection(int p_caret = -1) const; String get_selected_text(int p_caret = -1); + int get_selection_at_line_column(int p_line, int p_column, bool p_include_edges = true) const; + Vector get_line_ranges_from_carets(bool p_only_selections = false, bool p_merge_adjacent = true) const; + TypedArray get_line_ranges_from_carets_typed_array(bool p_only_selections = false, bool p_merge_adjacent = true) const; - int get_selection_line(int p_caret = 0) const; - int get_selection_column(int p_caret = 0) const; + void set_selection_origin_line(int p_line, bool p_can_be_hidden = true, int p_wrap_index = -1, int p_caret = 0); + void set_selection_origin_column(int p_column, int p_caret = 0); + int get_selection_origin_line(int p_caret = 0) const; + int get_selection_origin_column(int p_caret = 0) const; int get_selection_from_line(int p_caret = 0) const; int get_selection_from_column(int p_caret = 0) const; int get_selection_to_line(int p_caret = 0) const; int get_selection_to_column(int p_caret = 0) const; + bool is_caret_after_selection_origin(int p_caret = 0) const; + void deselect(int p_caret = -1); void delete_selection(int p_caret = -1); diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 73ce1661237..9f7a941e89e 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -3578,6 +3578,13 @@ bool Viewport::gui_is_drag_successful() const { return gui.drag_successful; } +void Viewport::gui_cancel_drag() { + ERR_MAIN_THREAD_GUARD; + if (gui_is_dragging()) { + _perform_drop(); + } +} + void Viewport::set_input_as_handled() { ERR_MAIN_THREAD_GUARD; if (!handle_input_locally) { diff --git a/scene/main/viewport.h b/scene/main/viewport.h index 21832a454c4..c6a757acd06 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -616,6 +616,7 @@ public: bool gui_is_dragging() const; bool gui_is_drag_successful() const; + void gui_cancel_drag(); Control *gui_find_control(const Point2 &p_global); From 773a473807c75975b5820b30a4f0eccaece7cfa2 Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 22 Jan 2024 18:27:19 -0500 Subject: [PATCH 2/2] Overhaul multiple caret editing in TextEdit. Use a multicaret edit to delay merging overlapping carets until the end. --- doc/classes/CodeEdit.xml | 38 +- doc/classes/TextEdit.xml | 180 +- editor/code_editor.cpp | 404 +- editor/code_editor.h | 8 - editor/plugins/script_text_editor.cpp | 80 +- editor/plugins/text_editor.cpp | 18 +- editor/plugins/text_shader_editor.cpp | 8 +- .../4.2-stable.expected | 8 + scene/gui/code_edit.cpp | 624 +- scene/gui/code_edit.h | 6 + scene/gui/text_edit.compat.inc | 41 + scene/gui/text_edit.cpp | 1468 ++--- scene/gui/text_edit.h | 46 +- tests/display_server_mock.h | 10 + tests/scene/test_code_edit.h | 1994 +++++- tests/scene/test_text_edit.h | 5326 ++++++++++++++--- tests/test_macros.h | 9 + 17 files changed, 7562 insertions(+), 2706 deletions(-) create mode 100644 scene/gui/text_edit.compat.inc diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index 7c6f1a51c43..d455799c298 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -14,7 +14,7 @@ - Override this method to define how the selected entry should be inserted. If [param replace] is true, any existing text should be replaced. + Override this method to define how the selected entry should be inserted. If [param replace] is [code]true[/code], any existing text should be replaced. @@ -29,7 +29,7 @@ - Override this method to define what happens when the user requests code completion. If [param force] is true, any checks should be bypassed. + Override this method to define what happens when the user requests code completion. If [param force] is [code]true[/code], any checks should be bypassed. @@ -123,7 +123,7 @@ - Inserts the selected entry into the text. If [param replace] is true, any existing text is replaced rather than merged. + Inserts the selected entry into the text. If [param replace] is [code]true[/code], any existing text is replaced rather than merged. @@ -144,6 +144,12 @@ Code regions are delimited using start and end tags (respectively [code]region[/code] and [code]endregion[/code] by default) preceded by one line comment delimiter. (eg. [code]#region[/code] and [code]#endregion[/code]) + + + + Deletes all lines that are selected or have a caret on them. + + @@ -156,6 +162,12 @@ Duplicates all lines currently selected with any caret. Duplicates the entire line beneath the current one no matter where the caret is within the line. + + + + Duplicates all selected text and duplicates all lines with a caret on them. + + @@ -379,6 +391,18 @@ Returns whether the line at the specified index is folded or not. + + + + Moves all lines down that are selected or have a caret on them. + + + + + + Moves all lines up that are selected or have a caret on them. + + @@ -397,7 +421,7 @@ - Emits [signal code_completion_requested], if [param force] is true will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path or signal. + Emits [signal code_completion_requested], if [param force] is [code]true[/code] will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path, or signal. @@ -467,6 +491,12 @@ Toggle the folding of the code block at the given line. + + + + Toggle the folding of the code block on all lines with a caret on them. + + diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index 72893c913a6..2959ec4cfa9 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -5,7 +5,7 @@ A multiline text editor. It also has limited facilities for editing code, such as syntax highlighting support. For more advanced facilities for editing code, see [CodeEdit]. - [b]Note:[/b] Most viewport, caret and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets. + [b]Note:[/b] Most viewport, caret, and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets in the order they were created. [b]Note:[/b] When holding down [kbd]Alt[/kbd], the vertical scroll wheel will scroll 5 times as fast as it would normally do. This also works in the Godot script editor. @@ -67,7 +67,7 @@ - Adds an additional caret above or below every caret. If [param below] is true the new caret will be added below and above otherwise. + Adds an additional caret above or below every caret. If [param below] is [code]true[/code] the new caret will be added below and above otherwise. @@ -83,7 +83,7 @@ Adds a selection and a caret for the next occurrence of the current selection. If there is no active selection, selects word under caret. - + @@ -91,7 +91,7 @@ - Reposition the carets affected by the edit. This assumes edits are applied in edit order, see [method get_caret_index_edit_order]. + This method does nothing. @@ -120,6 +120,23 @@ Starts a multipart edit. All edits will be treated as one action until [method end_complex_operation] is called. + + + + Starts an edit for multiple carets. The edit must be ended with [method end_multicaret_edit]. Multicaret edits can be used to edit text at multiple carets and delay merging the carets until the end, so the caret indexes aren't affected immediately. [method begin_multicaret_edit] and [method end_multicaret_edit] can be nested, and the merge will happen at the last [method end_multicaret_edit]. + Example usage: + [codeblock] + begin_complex_operation() + begin_multicaret_edit() + for i in range(get_caret_count()): + if multicaret_edit_ignore_caret(i): + continue + # Logic here. + end_multicaret_edit() + end_complex_operation() + [/codeblock] + + @@ -145,6 +162,20 @@ Clears the undo history. + + + + + + + + + Collapse all carets in the given range to the [param from_line] and [param from_column] position. + [param inclusive] applies to both ends. + If [method is_in_mulitcaret_edit] is [code]true[/code], carets that are collapsed will be [code]true[/code] for [method multicaret_edit_ignore_caret]. + [method merge_overlapping_carets] will be called if any carets were collapsed. + + @@ -185,6 +216,12 @@ Ends a multipart edit, started with [method begin_complex_operation]. If called outside a complex operation, the current operation is pushed onto the undo/redo stack. + + + + Ends an edit for multiple carets, that was started with [method begin_multicaret_edit]. If this was the last [method end_multicaret_edit] and [method merge_overlapping_carets] was called, carets will be merged. + + @@ -205,7 +242,7 @@ Returns the caret pixel draw position. - + Returns a list of caret indexes in their edit order, this done from bottom to top. Edit order refers to the way actions such as [method insert_text_at_caret] are applied. @@ -365,11 +402,11 @@ - - + + Returns an [Array] of line ranges where [code]x[/code] is the first line and [code]y[/code] is the last line. All lines within these ranges will have a caret on them or be part of a selection. Each line will only be part of one line range, even if it has multiple carets on it. - If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param p_merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections. + If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections. @@ -528,12 +565,13 @@ + Returns the caret index of the selection at the given [param line] and [param column], or [code]-1[/code] if there is none. - If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. + If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. If [param only_selections] is [code]false[/code], carets without a selection will also be considered. - + @@ -544,17 +582,17 @@ - Returns the selection begin column. + Returns the selection begin column. Returns the caret column if there is no selection. - Returns the selection begin line. + Returns the selection begin line. Returns the caret line if there is no selection. - + @@ -567,18 +605,40 @@ Returns the current selection mode. + + + + + Returns the origin column of the selection. This is the opposite end from the caret. + + + + + + + Returns the origin line of the selection. This is the opposite end from the caret. + + - Returns the selection end column. + Returns the selection end column. Returns the caret column if there is no selection. - Returns the selection end line. + Returns the selection end line. Returns the caret line if there is no selection. + + + + + + + Returns the carets sorted by selection beginning from lowest line and column to highest (from top to bottom of text). + If [param include_ignored_carets] is [code]false[/code], carets from [method multicaret_edit_ignore_caret] will be ignored. @@ -672,6 +732,19 @@ Inserts a new line with [param text] at [param line]. + + + + + + + + + Inserts the [param text] at [param line] and [param column]. + If [param before_selection_begin] is [code]true[/code], carets and selections that begin at [param line] and [param column] will moved to the end of the inserted text, along with all carets after it. + If [param before_selection_end] is [code]true[/code], selections that end at [param line] and [param column] will be extended to the end of the inserted text. These parameters can be used to insert text inside of or outside of selections. + + @@ -680,6 +753,13 @@ Insert the specified text at the caret position. + + + + + Returns [code]true[/code] if the caret of the selection is after the selection origin. This can be used to determine the direction of the selection. + + @@ -690,7 +770,7 @@ - Returns [code]true[/code] if the user is dragging their mouse for scrolling or selecting. + Returns [code]true[/code] if the user is dragging their mouse for scrolling, selecting, or text dragging. @@ -714,6 +794,12 @@ Returns whether the gutter is overwritable. + + + + Returns [code]true[/code] if a [method begin_multicaret_edit] has been called and [method end_multicaret_edit] has not yet been called. + + @@ -768,9 +854,18 @@ Merges any overlapping carets. Will favor the newest caret, or the caret with a selection. + If [method is_in_mulitcaret_edit] is [code]true[/code], the merge will be queued to happen at the end of the multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. [b]Note:[/b] This is not called when a caret changes position but after certain actions, so it is possible to get into a state where carets overlap. + + + + + Returns [code]true[/code] if the given [param caret_index] should be ignored as part of a multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. Carets that should be ignored are ones that were part of removed text and will likely be merged at the end of the edit, or carets that were added during the edit. + It is recommended to [code]continue[/code] within a loop iterating on multiple carets if a caret should be ignored. + + @@ -806,6 +901,15 @@ Removes the gutter from this [TextEdit]. + + + + + + Removes the line of text at [param line]. Carets on this line will attempt to match their previous visual x position. + If [param move_carets_down] is [code]true[/code] carets will move to the next line down, otherwise carets will move up. + + @@ -820,7 +924,6 @@ Removes text between the given positions. - [b]Note:[/b] This does not adjust the caret or selection, which as a result it can end up in an invalid position. @@ -854,14 +957,15 @@ - - - - + + + + - Perform selection, from line/column to line/column. + Selects text from [param origin_line] and [param origin_column] to [param caret_line] and [param caret_column] for the given [param caret_index]. This moves the selection origin and the caret. If the positions are the same, the selection will be deselected. If [member selecting_enabled] is [code]false[/code], no selection will occur. + [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. @@ -897,9 +1001,10 @@ - Moves the caret to the specified [param line] index. + Moves the caret to the specified [param line] index. The caret column will be moved to the same visual position it was at the last time [method set_caret_column] was called, or clamped to the end of the line. If [param adjust_viewport] is [code]true[/code], the viewport will center at the caret position after the move occurs. If [param can_be_hidden] is [code]true[/code], the specified [param line] can be hidden. + If [param wrap_index] is [code]-1[/code], the caret column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_caret_column] was called. [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. @@ -964,7 +1069,8 @@ - Sets the text for a specific line. + Sets the text for a specific [param line]. + Carets on the line will attempt to keep their visual x position. @@ -1072,6 +1178,26 @@ Sets the current selection mode. + + + + + + Sets the selection origin column to the [param column] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + + + + + + + + + + Sets the selection origin line to the [param line] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + If [param can_be_hidden] is [code]false[/code], The line will be set to the nearest unhidden line below or above. + If [param wrap_index] is [code]-1[/code], the selection origin column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_selection_origin_column] or [method select] was called. + + @@ -1105,7 +1231,7 @@ - Swaps the two lines. + Swaps the two lines. Carets will be swapped with the lines. @@ -1156,7 +1282,7 @@ If [code]true[/code], the selected text will be deselected when focus is lost. - If [code]true[/code], allow drag and drop of selected text. + If [code]true[/code], allow drag and drop of selected text. Text can still be dropped from other sources. If [code]true[/code], control characters are displayed. @@ -1247,7 +1373,7 @@ - Emitted when the caret changes position. + Emitted when any caret changes position. diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 238fd00089e..cfeb495690b 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -33,7 +33,6 @@ #include "core/input/input.h" #include "core/os/keyboard.h" #include "core/string/string_builder.h" -#include "core/templates/pair.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/plugins/script_editor_plugin.h" @@ -810,22 +809,22 @@ void CodeTextEditor::input(const Ref &event) { } if (ED_IS_SHORTCUT("script_text_editor/move_up", key_event)) { - move_lines_up(); + text_editor->move_lines_up(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/move_down", key_event)) { - move_lines_down(); + text_editor->move_lines_down(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/delete_line", key_event)) { - delete_lines(); + text_editor->delete_lines(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/duplicate_selection", key_event)) { - duplicate_selection(); + text_editor->duplicate_selection(); accept_event(); return; } @@ -1114,31 +1113,23 @@ void CodeTextEditor::trim_trailing_whitespace() { break; } } - text_editor->set_line(i, line.substr(0, end)); + text_editor->remove_text(i, end, i, line.length()); } } if (trimmed_whitespace) { text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } } void CodeTextEditor::insert_final_newline() { int final_line = text_editor->get_line_count() - 1; - String line = text_editor->get_line(final_line); // Length 0 means it's already an empty line, no need to add a newline. if (line.length() > 0 && !line.ends_with("\n")) { - text_editor->begin_complex_operation(); - - line += "\n"; - text_editor->set_line(final_line, line); - - text_editor->end_complex_operation(); - text_editor->queue_redraw(); + text_editor->insert_text("\n", final_line, line.length(), false); } } @@ -1147,9 +1138,12 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { return; } text_editor->begin_complex_operation(); + text_editor->begin_multicaret_edit(); - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { + for (int c = 0; c < text_editor->get_caret_count(); c++) { + if (text_editor->multicaret_edit_ignore_caret(c)) { + continue; + } if (!text_editor->has_selection(c)) { continue; } @@ -1190,6 +1184,7 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { text_editor->set_line(i, new_line); } } + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); } @@ -1198,308 +1193,24 @@ void CodeTextEditor::set_indent_using_spaces(bool p_use_spaces) { indentation_txt->set_text(p_use_spaces ? TTR("Spaces", "Indentation") : TTR("Tabs", "Indentation")); } -void CodeTextEditor::move_lines_up() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector> caret_groups; - Vector> group_borders; - - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector new_group{ c }; - Pair group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos != current_start_pos && next_end_pos + 1 != current_start_pos) { - break; - } - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } - group_borders.push_back(group_border); - caret_groups.push_back(new_group); - } - - for (int i = group_borders.size() - 1; i >= 0; i--) { - if (group_borders[i].first - 1 < 0) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i < group_borders.size() - 1 && group_borders[i].first - 1 <= group_borders[i + 1].second) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - int from_line = text_editor->get_selection_from_line(c); - int from_col = text_editor->get_selection_from_column(c); - int to_line = text_editor->get_selection_to_line(c); - int to_column = text_editor->get_selection_to_column(c); - caret_group_parameters.push_back(Vector{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].first; line_id <= group_borders[i].second; line_id++) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id - 1); - - text_editor->swap_lines(line_id - 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] - 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] - 1, caret_parameters[1], caret_parameters[2] - 1, caret_parameters[3], c); - } - } - } - - text_editor->end_complex_operation(); - text_editor->merge_overlapping_carets(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::move_lines_down() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector> caret_groups; - Vector> group_borders; - Vector group_border_ends; - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector new_group{ c }; - Pair group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos == current_start_pos || next_end_pos + 1 == current_start_pos) { - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } else { - break; - } - } - group_borders.push_back(group_border); - group_border_ends.push_back(text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c)); - caret_groups.push_back(new_group); - } - - for (int i = 0; i < group_borders.size(); i++) { - if (group_border_ends[i] + 1 > text_editor->get_line_count() - 1) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i > 0 && group_border_ends[i] + 1 >= group_borders[i - 1].first) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - int from_line = text_editor->get_selection_from_line(c); - int from_col = text_editor->get_selection_from_column(c); - int to_line = text_editor->get_selection_to_line(c); - int to_column = text_editor->get_selection_to_column(c); - caret_group_parameters.push_back(Vector{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].second; line_id >= group_borders[i].first; line_id--) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id + 1); - - text_editor->swap_lines(line_id + 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] + 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] + 1, caret_parameters[1], caret_parameters[2] + 1, caret_parameters[3], c); - } - } - } - - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::delete_lines() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - Vector lines; - int last_line = INT_MAX; - for (const int &c : caret_edit_order) { - for (int line = _get_affected_lines_to(c); line >= _get_affected_lines_from(c); line--) { - if (line >= last_line) { - continue; - } - last_line = line; - lines.append(line); - } - } - - for (const int &line : lines) { - if (line != text_editor->get_line_count() - 1) { - text_editor->remove_text(line, 0, line + 1, 0); - } else { - text_editor->remove_text(line - 1, text_editor->get_line(line - 1).length(), line, text_editor->get_line(line).length()); - } - // Readjust carets. - int new_line = MIN(line, text_editor->get_line_count() - 1); - text_editor->unfold_line(new_line); - for (const int &c : caret_edit_order) { - if (text_editor->get_caret_line(c) == line || (text_editor->get_caret_line(c) == line + 1 && text_editor->get_caret_column(c) == 0)) { - text_editor->deselect(c); - text_editor->set_caret_line(new_line, c == 0, true, 0, c); - continue; - } - if (text_editor->get_caret_line(c) > line) { - text_editor->set_caret_line(text_editor->get_caret_line(c) - 1, c == 0, true, 0, c); - continue; - } - break; - } - } - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); -} - -void CodeTextEditor::duplicate_selection() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { - const int cursor_column = text_editor->get_caret_column(c); - int from_line = text_editor->get_caret_line(c); - int to_line = text_editor->get_caret_line(c); - int from_column = 0; - int to_column = 0; - int cursor_new_line = to_line + 1; - int cursor_new_column = text_editor->get_caret_column(c); - String new_text = "\n" + text_editor->get_line(from_line); - bool selection_active = false; - - text_editor->set_caret_column(text_editor->get_line(from_line).length(), c == 0, c); - if (text_editor->has_selection(c)) { - from_column = text_editor->get_selection_from_column(c); - to_column = text_editor->get_selection_to_column(c); - - from_line = text_editor->get_selection_from_line(c); - to_line = text_editor->get_selection_to_line(c); - cursor_new_line = to_line + text_editor->get_caret_line(c) - from_line; - cursor_new_column = to_column == cursor_column ? 2 * to_column - from_column : to_column; - new_text = text_editor->get_selected_text(c); - selection_active = true; - - text_editor->set_caret_line(to_line, c == 0, true, 0, c); - text_editor->set_caret_column(to_column, c == 0, c); - } - - for (int i = from_line; i <= to_line; i++) { - text_editor->unfold_line(i); - } - text_editor->deselect(c); - text_editor->insert_text_at_caret(new_text, c); - text_editor->set_caret_line(cursor_new_line, c == 0, true, 0, c); - text_editor->set_caret_column(cursor_new_column, c == 0, c); - if (selection_active) { - text_editor->select(to_line, to_column, 2 * to_line - from_line, to_line == from_line ? 2 * to_column - from_column : to_column, c); - } - } - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); - text_editor->queue_redraw(); -} - void CodeTextEditor::toggle_inline_comment(const String &delimiter) { text_editor->begin_complex_operation(); + text_editor->begin_multicaret_edit(); - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); - int last_line = -1; + Vector line_ranges = text_editor->get_line_ranges_from_carets(); int folded_to = 0; - for (const int &c1 : caret_edit_order) { - int from = _get_affected_lines_from(c1); - from += from == last_line ? 1 + folded_to : 0; - int to = _get_affected_lines_to(c1); - last_line = to; + for (Point2i line_range : line_ranges) { + int from_line = line_range.x; + int to_line = line_range.y; // If last line is folded, extends to the end of the folded section - if (text_editor->is_line_folded(to)) { - folded_to = text_editor->get_next_visible_line_offset_from(to + 1, 1) - 1; - to += folded_to; + if (text_editor->is_line_folded(to_line)) { + folded_to = text_editor->get_next_visible_line_offset_from(to_line + 1, 1) - 1; + to_line += folded_to; } // Check first if there's any uncommented lines in selection. bool is_commented = true; bool is_all_empty = true; - for (int line = from; line <= to; line++) { + for (int line = from_line; line <= to_line; line++) { // `+ delimiter.length()` here because comment delimiter is not actually `in comment` so we check first character after it int delimiter_idx = text_editor->is_in_comment(line, text_editor->get_first_non_whitespace_column(line) + delimiter.length()); // Empty lines should not be counted. @@ -1515,58 +1226,24 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { // Special case for commenting empty lines, treat it/them as uncommented lines. is_commented = is_commented && !is_all_empty; - // Caret positions need to be saved since they could be moved at the eol. - Vector caret_cols; - Vector selection_to_cols; - for (const int &c2 : caret_edit_order) { - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - caret_cols.append(text_editor->get_caret_column(c2)); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - selection_to_cols.append(text_editor->get_selection_to_column(c2)); - } - } - // Comment/uncomment. - for (int line = from; line <= to; line++) { - String line_text = text_editor->get_line(line); + for (int line = from_line; line <= to_line; line++) { if (is_all_empty) { - text_editor->set_line(line, delimiter); + text_editor->insert_text(delimiter, line, 0); continue; } if (is_commented) { - text_editor->set_line(line, line_text.replace_first(delimiter, "")); + int delimiter_column = text_editor->get_line(line).find(delimiter); + text_editor->remove_text(line, delimiter_column, line, delimiter_column + delimiter.length()); } else { - text_editor->set_line(line, line_text.insert(text_editor->get_first_non_whitespace_column(line), delimiter)); - } - } - - // Readjust carets and selections. - int caret_i = 0; - int selection_i = 0; - int offset = (is_commented ? -1 : 1) * delimiter.length(); - for (const int &c2 : caret_edit_order) { - bool is_line_selection = text_editor->has_selection(c2) && text_editor->get_selection_from_line(c2) < text_editor->get_selection_to_line(c2); - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - int caret_col = caret_cols[caret_i++]; - caret_col += (is_line_selection && caret_col == 0) ? 0 : offset; - text_editor->set_caret_column(caret_col, c2 == 0, c2); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - int from_col = text_editor->get_selection_from_column(c2); - from_col += (is_line_selection && from_col == 0) ? 0 : offset; - int to_col = selection_to_cols[selection_i++]; - to_col += (to_col == 0) ? 0 : offset; - text_editor->select( - text_editor->get_selection_from_line(c2), from_col, - text_editor->get_selection_to_line(c2), to_col, c2); + text_editor->insert_text(delimiter, line, text_editor->get_first_non_whitespace_column(line)); } } } - text_editor->merge_overlapping_carets(); + + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } void CodeTextEditor::goto_line(int p_line) { @@ -1813,22 +1490,6 @@ void CodeTextEditor::_toggle_scripts_pressed() { update_toggle_scripts_button(); } -int CodeTextEditor::_get_affected_lines_from(int p_caret) { - return text_editor->has_selection(p_caret) ? text_editor->get_selection_from_line(p_caret) : text_editor->get_caret_line(p_caret); -} - -int CodeTextEditor::_get_affected_lines_to(int p_caret) { - if (!text_editor->has_selection(p_caret)) { - return text_editor->get_caret_line(p_caret); - } - int line = text_editor->get_selection_to_line(p_caret); - // Don't affect a line with no selected characters. - if (text_editor->get_selection_to_column(p_caret) == 0) { - line--; - } - return line; -} - void CodeTextEditor::_error_pressed(const Ref &p_event) { Ref mb = p_event; if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { @@ -1877,13 +1538,12 @@ void CodeTextEditor::set_warning_count(int p_warning_count) { } void CodeTextEditor::toggle_bookmark() { - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); + Vector sorted_carets = text_editor->get_sorted_carets(); int last_line = -1; - for (const int &c : caret_edit_order) { - int from = text_editor->has_selection(c) ? text_editor->get_selection_from_line(c) : text_editor->get_caret_line(c); + for (const int &c : sorted_carets) { + int from = text_editor->get_selection_from_line(c); from += from == last_line ? 1 : 0; - int to = text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c); + int to = text_editor->get_selection_to_line(c); if (to < from) { continue; } diff --git a/editor/code_editor.h b/editor/code_editor.h index c36eedb580f..75a2a68d585 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -207,9 +207,6 @@ class CodeTextEditor : public VBoxContainer { void _toggle_scripts_pressed(); - int _get_affected_lines_from(int p_caret); - int _get_affected_lines_to(int p_caret); - protected: virtual void _load_theme_settings() {} virtual void _validate_script() {} @@ -238,11 +235,6 @@ public: void set_indent_using_spaces(bool p_use_spaces); - void move_lines_up(); - void move_lines_down(); - void delete_lines(); - void duplicate_selection(); - /// Toggle inline comment on currently selected lines, or on current line if nothing is selected, /// by adding or removing comment delimiter void toggle_inline_comment(const String &delimiter); diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 752ccecd91c..561edcf8bf1 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -284,8 +284,7 @@ void ScriptTextEditor::_warning_clicked(const Variant &p_line) { if (prev_line.contains("@warning_ignore")) { const int closing_bracket_idx = prev_line.find(")"); const String text_to_insert = ", " + code.quote(quote_style); - prev_line = prev_line.insert(closing_bracket_idx, text_to_insert); - text_editor->set_line(line - 1, prev_line); + text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx); } else { const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size(); String annotation_indent; @@ -352,22 +351,26 @@ void ScriptTextEditor::add_callback(const String &p_function, const PackedString if (!language->can_make_function()) { return; } - + code_editor->get_text_editor()->begin_complex_operation(); + code_editor->get_text_editor()->remove_secondary_carets(); + code_editor->get_text_editor()->deselect(); String code = code_editor->get_text_editor()->get_text(); int pos = language->find_function(p_function, code); - code_editor->get_text_editor()->remove_secondary_carets(); if (pos == -1) { - //does not exist - code_editor->get_text_editor()->deselect(); - pos = code_editor->get_text_editor()->get_line_count() + 2; + // Function does not exist, create it at the end of the file. + int last_line = code_editor->get_text_editor()->get_line_count() - 1; String func = language->make_function("", p_function, p_args); - //code=code+func; - code_editor->get_text_editor()->set_caret_line(pos + 1); - code_editor->get_text_editor()->set_caret_column(1000000); //none shall be that big - code_editor->get_text_editor()->insert_text_at_caret("\n\n" + func); + code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length()); + pos = last_line + 3; } - code_editor->get_text_editor()->set_caret_line(pos); - code_editor->get_text_editor()->set_caret_column(1); + // Put caret on the line after the function, after the indent. + int indent_column = 1; + if (EDITOR_GET("text_editor/behavior/indent/type")) { + indent_column = EDITOR_GET("text_editor/behavior/indent/size"); + } + code_editor->get_text_editor()->set_caret_line(pos, true, true, -1); + code_editor->get_text_editor()->set_caret_column(indent_column); + code_editor->get_text_editor()->end_complex_operation(); } bool ScriptTextEditor::show_members_overview() { @@ -1335,10 +1338,10 @@ void ScriptTextEditor::_edit_option(int p_op) { callable_mp((Control *)tx, &Control::grab_focus).call_deferred(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { Ref