diff options
Diffstat (limited to 'scene/gui/text_edit.cpp')
-rw-r--r-- | scene/gui/text_edit.cpp | 2442 |
1 files changed, 1354 insertions, 1088 deletions
diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 38b4ffc8ae..4fda49a877 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "text_edit.h" +#include "text_edit.compat.inc" #include "core/config/project_settings.h" #include "core/input/input.h" @@ -451,7 +452,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 +566,9 @@ void TextEdit::_notification(int p_what) { Vector<BraceMatchingData> 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 +1105,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 +1258,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 +1272,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 +1563,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 +1582,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 +1593,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 +1720,17 @@ void TextEdit::gui_input(const Ref<InputEvent> &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 +1738,14 @@ void TextEdit::gui_input(const Ref<InputEvent> &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,121 +1753,86 @@ void TextEdit::gui_input(const Ref<InputEvent> &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++) { - // 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)) { - remove_caret(i); - last_dblclk = 0; - return; - } + if (get_caret_count() > 1) { + // Deselect if clicked on caret or its selection. + int clicked_caret = get_selection_at_line_column(line, col, true, false); + if (clicked_caret != -1) { + remove_caret(clicked_caret); + last_dblclk = 0; + return; } } - 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 { - carets.write[caret].selection.shiftclick_left = true; - } - 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 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 { - deselect(caret); + // A regular click clears all other carets. + caret = 0; + remove_secondary_carets(); + deselect(); } - 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<InputEvent> &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<InputEvent> &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<InputEvent> &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<InputEvent> &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<InputEvent> &p_gui_input) { return; } + _cancel_drag_and_drop_text(); + _reset_caret_blink_timer(); // Allow unicode handling if: @@ -2321,42 +2302,36 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { - bool first_line = false; - if (!p_split_current_line) { - deselect(i); - if (p_above) { - if (get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - 1, false, true, 0, i); - set_caret_column(text[get_caret_line(i)].length(), i == 0, i); - } else { - set_caret_column(0, i == 0, i); - first_line = true; - } - } else { - set_caret_column(text[get_caret_line(i)].length(), i == 0, i); - } - } - - insert_text_at_caret("\n", i); + begin_multicaret_edit(); - if (first_line) { - set_caret_line(0, i == 0, true, 0, i); + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + if (p_split_current_line) { + insert_text_at_caret("\n", i); + } else { + int line = get_caret_line(i); + insert_text("\n", line, p_above ? 0 : text[line].length(), p_above, p_above); + deselect(i); + set_caret_line(p_above ? line : line + 1, false, true, -1, i); + set_caret_column(0, i == 0, i); } } + + end_multicaret_edit(); end_complex_operation(); } 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 +2343,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 +2364,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 +2376,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 +2400,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 +2421,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 +2433,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 +2459,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 +2481,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 +2512,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 +2537,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 +2553,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 +2569,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(); } @@ -2634,58 +2579,47 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } start_action(EditAction::ACTION_BACKSPACE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size(); i++) { - int caret_idx = caret_edit_order[i]; - if (get_caret_column(caret_idx) == 0 && get_caret_line(caret_idx) == 0 && !has_selection(caret_idx)) { + Vector<int> sorted_carets = get_sorted_carets(); + sorted_carets.reverse(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - if (has_selection(caret_idx) || (!p_all_to_left && !p_word) || get_caret_column(caret_idx) == 0) { - backspace(caret_idx); + if (get_caret_column(caret_index) == 0 && get_caret_line(caret_index) == 0 && !has_selection(caret_index)) { continue; } - if (p_all_to_left) { - int caret_current_column = get_caret_column(caret_idx); - set_caret_column(0, caret_idx == 0, caret_idx); - _remove_text(get_caret_line(caret_idx), 0, get_caret_line(caret_idx), caret_current_column); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), caret_current_column, get_caret_line(caret_idx), get_caret_column(caret_idx)); - - // Check for any overlapping carets since we removed the entire line. - for (int j = i + 1; j < caret_edit_order.size(); j++) { - // Selection only end on this line, only the one as carets cannot overlap. - if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx) && get_selection_to_line(caret_edit_order[j]) == get_caret_line(caret_idx)) { - carets.write[caret_edit_order[j]].selection.to_column = 0; - break; - } - - // Check for caret. - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) || (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { - break; - } + if (has_selection(caret_index) || (!p_all_to_left && !p_word) || get_caret_column(caret_index) == 0) { + backspace(caret_index); + continue; + } - deselect(caret_edit_order[j]); - carets_to_remove.push_back(caret_edit_order[j]); - set_caret_column(0, caret_idx == 0, caret_idx); - i = j; - } + if (p_all_to_left) { + // Remove everything to left of caret to the start of the line. + int caret_current_column = get_caret_column(caret_index); + _remove_text(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + collapse_carets(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + set_caret_column(0, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), caret_current_column, get_caret_line(caret_index), 0); continue; } if (p_word) { - // Save here as the caret may change when resolving overlaps. - int from_column = get_caret_column(caret_idx); - int column = get_caret_column(caret_idx); + // Remove text to the start of the word left of the caret. + int from_column = get_caret_column(caret_index); + int column = get_caret_column(caret_index); // Check for the case "<word><space><caret>" and ignore the space. // No need to check for column being 0 since it is checked above. - if (is_whitespace(text[get_caret_line(caret_idx)][get_caret_column(caret_idx) - 1])) { + if (is_whitespace(text[get_caret_line(caret_index)][get_caret_column(caret_index) - 1])) { column -= 1; } + // Get a list with the indices of the word bounds of the given text line. - const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_idx))->get_rid()); + const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->get_rid()); if (words.is_empty() || column <= words[0]) { // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. column = 0; @@ -2699,57 +2633,14 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } } - // Check for any other carets in this range. - int overlapping_caret_index = -1; - for (int j = i + 1; j < caret_edit_order.size(); j++) { - // Check caret and selection in on the right line. - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) && (!has_selection(caret_edit_order[j]) || get_selection_to_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { - break; - } - - // If it has a selection, check it ends with in the range. - if ((has_selection(caret_edit_order[j]) && get_selection_to_column(caret_edit_order[j]) < column)) { - break; - } - - // If it has a selection and it starts outside our word, we need to adjust the selection, and handle it later to prevent overlap. - if ((has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) < column)) { - carets.write[caret_edit_order[j]].selection.to_column = column; - overlapping_caret_index = caret_edit_order[j]; - break; - } - - // Otherwise we can remove it. - if (get_caret_column(caret_edit_order[j]) > column || (has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) > column)) { - deselect(caret_edit_order[j]); - carets_to_remove.push_back(caret_edit_order[j]); - set_caret_column(0, caret_idx == 0, caret_idx); - i = j; - } - } - - _remove_text(get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); - - set_caret_line(get_caret_line(caret_idx), false, true, 0, caret_idx); - set_caret_column(column, caret_idx == 0, caret_idx); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); - - // Now we can clean up the overlapping caret. - if (overlapping_caret_index != -1) { - backspace(overlapping_caret_index); - i++; - carets_to_remove.push_back(overlapping_caret_index); - set_caret_column(get_caret_column(overlapping_caret_index), caret_idx == 0, caret_idx); - } - continue; + _remove_text(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + collapse_carets(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + set_caret_column(column, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), from_column, get_caret_line(caret_index), column); } } - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); - } + end_multicaret_edit(); end_action(); } @@ -2759,61 +2650,40 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { } start_action(EditAction::ACTION_DELETE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size(); i++) { - int caret_idx = caret_edit_order[i]; - if (has_selection(caret_idx)) { - delete_selection(caret_idx); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - int curline_len = text[get_caret_line(caret_idx)].length(); - if (get_caret_line(caret_idx) == text.size() - 1 && get_caret_column(caret_idx) == curline_len) { + if (has_selection(caret_index)) { + delete_selection(caret_index); + continue; + } + + int curline_len = text[get_caret_line(caret_index)].length(); + if (get_caret_line(caret_index) == text.size() - 1 && get_caret_column(caret_index) == curline_len) { continue; // Last line, last column: Nothing to do. } - int next_line = get_caret_column(caret_idx) < curline_len ? get_caret_line(caret_idx) : get_caret_line(caret_idx) + 1; + int next_line = get_caret_column(caret_index) < curline_len ? get_caret_line(caret_index) : get_caret_line(caret_index) + 1; int next_column; if (p_all_to_right) { - // Get caret furthest to the left. - for (int j = i + 1; j < caret_edit_order.size(); j++) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (!has_selection(caret_edit_order[j])) { - i = j; - caret_idx = caret_edit_order[i]; - } - } - - if (get_caret_column(caret_idx) == curline_len) { + if (get_caret_column(caret_index) == curline_len) { continue; } // Delete everything to right of caret. next_column = curline_len; - next_line = get_caret_line(caret_idx); - - // Remove overlapping carets. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - carets_to_remove.push_back(caret_edit_order[j]); - } - - } else if (p_word && get_caret_column(caret_idx) < curline_len - 1) { + next_line = get_caret_line(caret_index); + } else if (p_word && get_caret_column(caret_index) < curline_len - 1) { // Delete next word to right of caret. - int line = get_caret_line(caret_idx); - int column = get_caret_column(caret_idx); + int line = get_caret_line(caret_index); + int column = get_caret_column(caret_index); PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); for (int j = 1; j < words.size(); j = j + 2) { @@ -2825,49 +2695,22 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { next_line = line; next_column = column; - - // Remove overlapping carets. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (get_caret_column(caret_edit_order[j]) > column) { - break; - } - carets_to_remove.push_back(caret_edit_order[j]); - } } else { // Delete one character. if (caret_mid_grapheme_enabled) { - next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0; + next_column = get_caret_column(caret_index) < curline_len ? (get_caret_column(caret_index) + 1) : 0; } else { - next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0; - } - - // Remove overlapping carets. - if (i > 0) { - int prev_caret_idx = caret_edit_order[i - 1]; - if (get_caret_line(prev_caret_idx) == next_line && get_caret_column(prev_caret_idx) == next_column) { - carets_to_remove.push_back(prev_caret_idx); - } + next_column = get_caret_column(caret_index) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_index))->get_rid(), (get_caret_column(caret_index))) : 0; } } - _remove_text(get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); - } - - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); + _remove_text(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + collapse_carets(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + _offset_carets_after(next_line, next_column, get_caret_line(caret_index), get_caret_column(caret_index)); } - // If we are deleting from the end of a line, due to column preservation we could still overlap with another caret. - merge_overlapping_carets(); + end_multicaret_edit(); end_action(); - queue_redraw(); } void TextEdit::_move_caret_document_start(bool p_select) { @@ -2878,12 +2721,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 +2733,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() { @@ -2917,51 +2752,6 @@ bool TextEdit::_clear_carets_and_selection() { return false; } -void TextEdit::_get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x) const { - if (p_last_fit_x == -1) { - p_last_fit_x = _get_column_x_offset_for_line(p_old_column, p_old_line, p_old_column); - } - - // Calculate the new line and wrap index. - p_new_line = p_old_line; - int caret_wrap_index = p_old_wrap_index; - if (p_below) { - if (caret_wrap_index < get_line_wrap_count(p_new_line)) { - caret_wrap_index++; - } else { - p_new_line++; - caret_wrap_index = 0; - } - } else { - if (caret_wrap_index == 0) { - p_new_line--; - caret_wrap_index = get_line_wrap_count(p_new_line); - } else { - caret_wrap_index--; - } - } - - // Boundary checks. - if (p_new_line < 0) { - p_new_line = 0; - } - if (p_new_line >= text.size()) { - p_new_line = text.size() - 1; - } - - p_new_column = _get_char_pos_for_line(p_last_fit_x, p_new_line, caret_wrap_index); - if (p_new_column != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && caret_wrap_index < get_line_wrap_count(p_new_line)) { - Vector<String> rows = get_line_wrapped_text(p_new_line); - int row_end_col = 0; - for (int i = 0; i < caret_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (p_new_column >= row_end_col) { - p_new_column -= 1; - } - } -} - void TextEdit::_update_placeholder() { if (theme_cache.font.is_null() || theme_cache.font_size <= 0) { return; // Not in tree? @@ -3127,53 +2917,48 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { if (p_data.get_type() == Variant::STRING && is_editable()) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int caret_row_tmp = pos.y; - int caret_column_tmp = pos.x; + int drop_at_line = pos.y; + int drop_at_column = pos.x; + int selection_index = get_selection_at_line_column(drop_at_line, drop_at_column, !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)); + + // Remove drag caret before the complex operation starts so it won't appear in undo. + remove_caret(drag_caret_index); + + if (selection_drag_attempt && selection_index >= 0 && selection_index == drag_and_drop_origin_caret_index) { + // Dropped onto original selection, do nothing. + selection_drag_attempt = false; + return; + } + + begin_complex_operation(); + begin_multicaret_edit(); if (selection_drag_attempt) { + // Drop from self. selection_drag_attempt = false; - if (!is_mouse_over_selection(!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL))) { - // Set caret back at selection for undo / redo. - set_caret_line(get_selection_to_line(), false, false); - set_caret_column(get_selection_to_column()); - - begin_complex_operation(); - if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { - if (caret_row_tmp > get_selection_to_line()) { - caret_row_tmp = caret_row_tmp - (get_selection_to_line() - get_selection_from_line()); - } else if (caret_row_tmp == get_selection_to_line() && caret_column_tmp >= get_selection_to_column()) { - caret_column_tmp = caret_column_tmp - (get_selection_to_column() - get_selection_from_column()); - } - delete_selection(); - } else { - deselect(); - } + if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { + // Delete all selections. + int temp_caret = add_caret(drop_at_line, drop_at_column); - remove_secondary_carets(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - end_complex_operation(); - } - } else if (is_mouse_over_selection()) { - remove_secondary_carets(); - caret_row_tmp = get_selection_from_line(); - caret_column_tmp = get_selection_from_column(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - grab_focus(); - } else { - remove_secondary_carets(); - deselect(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - grab_focus(); - } + delete_selection(); - if (caret_row_tmp != get_caret_line() || caret_column_tmp != get_caret_column()) { - select(caret_row_tmp, caret_column_tmp, get_caret_line(), get_caret_column()); + // Use a temporary caret to update the drop at position. + drop_at_line = get_caret_line(temp_caret); + drop_at_column = get_caret_column(temp_caret); + } } + remove_secondary_carets(); + deselect(); + + // Insert the dragged text. + set_caret_line(drop_at_line, true, false, -1); + set_caret_column(drop_at_column); + insert_text_at_caret(p_data); + + select(drop_at_line, drop_at_column, get_caret_line(), get_caret_column()); + grab_focus(); + adjust_viewport_to_caret(); + end_multicaret_edit(); + end_complex_operation(); } } @@ -3459,7 +3244,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; @@ -3532,17 +3317,36 @@ void TextEdit::set_line(int p_line, const String &p_new_text) { return; } begin_complex_operation(); - _remove_text(p_line, 0, p_line, text[p_line].length()); - _insert_text(p_line, 0, p_new_text); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) == p_line && get_caret_column(i) > p_new_text.length()) { - set_caret_column(p_new_text.length(), false, i); + + int old_column = text[p_line].length(); + + // Set the affected carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_column(get_caret_column(i), false, i); } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_column(get_selection_origin_column(i), i); + } + } + + _remove_text(p_line, 0, p_line, old_column); + int new_line, new_column; + _insert_text(p_line, 0, p_new_text, &new_line, &new_column); - if (has_selection(i) && p_line == get_selection_to_line(i) && get_selection_to_column(i) > text[p_line].length()) { - carets.write[i].selection.to_column = text[p_line].length(); + // Don't offset carets that were on the old line. + _offset_carets_after(p_line, old_column, new_line, new_column, false, false); + + // Set the caret lines to update the column to match visually. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_line(get_caret_line(i), false, true, 0, i); + } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_line(get_selection_origin_line(i), true, 0, i); } } + merge_overlapping_carets(); end_complex_operation(); } @@ -3596,71 +3400,163 @@ void TextEdit::swap_lines(int p_from_line, int p_to_line) { ERR_FAIL_INDEX(p_from_line, text.size()); ERR_FAIL_INDEX(p_to_line, text.size()); - String tmp = get_line(p_from_line); - String tmp2 = get_line(p_to_line); + if (p_from_line == p_to_line) { + return; + } + + String from_line_text = get_line(p_from_line); + String to_line_text = get_line(p_to_line); begin_complex_operation(); - set_line(p_to_line, tmp); - set_line(p_from_line, tmp2); + begin_multicaret_edit(); + // Don't use set_line to avoid clamping and updating carets. + _remove_text(p_to_line, 0, p_to_line, text[p_to_line].length()); + _insert_text(p_to_line, 0, from_line_text); + _remove_text(p_from_line, 0, p_from_line, text[p_from_line].length()); + _insert_text(p_from_line, 0, to_line_text); + + // Swap carets. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_from_line || get_caret_line(i) == p_to_line) { + int caret_new_line = get_caret_line(i) == p_from_line ? p_to_line : p_from_line; + int caret_column = get_caret_column(i); + set_caret_line(caret_new_line, false, true, -1, i); + set_caret_column(caret_column, false, i); + } + if (selected && (get_selection_origin_line(i) == p_from_line || get_selection_origin_line(i) == p_to_line)) { + int origin_new_line = get_selection_origin_line(i) == p_from_line ? p_to_line : p_from_line; + int origin_column = get_selection_origin_column(i); + select(origin_new_line, origin_column, get_caret_line(i), get_caret_column(i), i); + } + } + // If only part of a selection was changed, it may now overlap. + merge_overlapping_carets(); + + end_multicaret_edit(); end_complex_operation(); } -void TextEdit::insert_line_at(int p_at, const String &p_text) { - ERR_FAIL_INDEX(p_at, text.size()); +void TextEdit::insert_line_at(int p_line, const String &p_text) { + ERR_FAIL_INDEX(p_line, text.size()); - _insert_text(p_at, 0, p_text + "\n"); + // Use a complex operation so subsequent calls aren't merged together. + begin_complex_operation(); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) >= p_at) { - // Offset caret when located after inserted line. - set_caret_line(get_caret_line(i) + 1, false, true, 0, i); - } - if (has_selection(i)) { - if (get_selection_from_line(i) >= p_at) { - // Offset selection when located after inserted line. - select(get_selection_from_line(i) + 1, get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); - } else if (get_selection_to_line(i) >= p_at) { - // Extend selection that includes inserted line. - select(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); + int new_line, new_column; + _insert_text(p_line, 0, p_text + "\n", &new_line, &new_column); + _offset_carets_after(p_line, 0, new_line, new_column); + + end_complex_operation(); +} + +void TextEdit::remove_line_at(int p_line, bool p_move_carets_down) { + ERR_FAIL_INDEX(p_line, text.size()); + + if (get_line_count() == 1) { + // Only one line, just remove contents. + begin_complex_operation(); + int line_length = get_line(p_line).length(); + _remove_text(p_line, 0, p_line, line_length); + collapse_carets(p_line, 0, p_line, line_length, true); + end_complex_operation(); + return; + } + + begin_complex_operation(); + + bool is_last_line = p_line == get_line_count() - 1; + int from_line = is_last_line ? p_line - 1 : p_line; + int next_line = is_last_line ? p_line : p_line + 1; + int from_column = is_last_line ? get_line(from_line).length() : 0; + int next_column = is_last_line ? get_line(next_line).length() : 0; + + if ((!is_last_line && p_move_carets_down) || (p_line != 0 && !p_move_carets_down)) { + // Set the carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (get_caret_line(i) == p_line) { + set_caret_column(get_caret_column(i), false, i); + } + if (has_selection(i) && get_selection_origin_line(i) == p_line) { + set_selection_origin_column(get_selection_origin_column(i), i); } } } - // Need to apply the above adjustments to the undo / redo carets. - current_op.end_carets = carets; - queue_redraw(); + // Remove line. + _remove_text(from_line, from_column, next_line, next_column); + + begin_multicaret_edit(); + if ((is_last_line && p_move_carets_down) || (p_line == 0 && !p_move_carets_down)) { + // Collapse carets. + collapse_carets(from_line, from_column, next_line, next_column, true); + } else { + // Move carets to visually line up. + int target_line = p_move_carets_down ? p_line : p_line - 1; + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_line) { + set_caret_line(target_line, i == 0, true, 0, i); + } + if (selected && get_selection_origin_line(i) == p_line) { + set_selection_origin_line(target_line, true, 0, i); + select(get_selection_origin_line(i), get_selection_origin_column(i), get_caret_line(i), get_caret_column(i), i); + } + } + + merge_overlapping_carets(); + } + _offset_carets_after(next_line, next_column, from_line, from_column); + end_multicaret_edit(); + end_complex_operation(); } void TextEdit::insert_text_at_caret(const String &p_text, 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<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } delete_selection(i); int from_line = get_caret_line(i); int from_col = get_caret_column(i); - int new_column, new_line; + int new_line, new_column; _insert_text(from_line, from_col, p_text, &new_line, &new_column); _update_scrollbars(); + _offset_carets_after(from_line, from_col, new_line, new_column); - set_caret_line(new_line, false, true, 0, i); + set_caret_line(new_line, false, true, -1, i); set_caret_column(new_column, i == 0, i); - - adjust_carets_after_edit(i, new_line, new_column, from_line, from_col); } if (has_ime_text()) { _update_ime_text(); } + end_multicaret_edit(); + end_complex_operation(); +} + +void TextEdit::insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin, bool p_before_selection_end) { + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_INDEX(p_column, text[p_line].length() + 1); + + begin_complex_operation(); + + int new_line, new_column; + _insert_text(p_line, p_column, p_text, &new_line, &new_column); + + _offset_carets_after(p_line, p_column, new_line, new_column, p_before_selection_begin, p_before_selection_end); + end_complex_operation(); - queue_redraw(); } void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { @@ -3671,7 +3567,13 @@ void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, in ERR_FAIL_COND(p_to_line < p_from_line); ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column); + begin_complex_operation(); + _remove_text(p_from_line, p_from_column, p_to_line, p_to_column); + collapse_carets(p_from_line, p_from_column, p_to_line, p_to_column); + _offset_carets_after(p_to_line, p_to_column, p_from_line, p_from_column); + + end_complex_operation(); } int TextEdit::get_last_unhidden_line() const { @@ -4040,7 +3942,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 +3961,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 +3975,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 +3987,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 +4016,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 +4030,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 +4043,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 +4262,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,29 +4408,13 @@ 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++) { - 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; - } - } + Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); + int line = pos.y; + int column = pos.x; - 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))) { - return true; - } + if ((p_caret == -1 && get_selection_at_line_column(line, column, p_edges) != -1) || (p_caret != -1 && _selection_contains(p_caret, line, column, p_edges))) { + return true; } - return false; } @@ -4619,270 +4501,401 @@ 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 (!is_in_mulitcaret_edit()) { + // Carets cannot overlap. + if (get_selection_at_line_column(p_line, p_column, true, false) != -1) { 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))) { - return -1; - } - } } carets.push_back(Caret()); - set_caret_line(p_line, false, false, 0, carets.size() - 1); - set_caret_column(p_col, false, carets.size() - 1); - caret_index_edit_dirty = true; - return carets.size() - 1; + int new_index = carets.size() - 1; + set_caret_line(p_line, false, false, -1, new_index); + set_caret_column(p_column, false, new_index); + _caret_changed(new_index); + + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(new_index); + merge_overlapping_carets(); + } + return new_index; } 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; + } } -void TextEdit::merge_overlapping_carets() { - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size() - 1; i++) { - int first_caret = caret_edit_order[i]; - int second_caret = caret_edit_order[i + 1]; +int TextEdit::get_caret_count() const { + // Don't include drag caret. + if (drag_caret_index >= 0) { + return carets.size() - 1; + } + return carets.size(); +} - // Both have selection. - if (has_selection(first_caret) && has_selection(second_caret)) { - bool should_merge = false; - if (get_selection_from_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_from_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_from_line(first_caret) > get_selection_from_line(second_caret) || get_selection_from_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_from_line(first_caret) < get_selection_to_line(second_caret) || get_selection_from_column(first_caret) <= get_selection_to_column(second_caret))) { - should_merge = true; - } +void TextEdit::add_caret_at_carets(bool p_below) { + const int last_line_max_wrap = get_line_wrap_count(text.size() - 1); + + begin_multicaret_edit(); + int view_target_caret = -1; + int view_line = p_below ? -1 : INT_MAX; + int num_carets = get_caret_count(); + for (int i = 0; i < num_carets; i++) { + const int caret_line = get_caret_line(i); + const int caret_column = get_caret_column(i); + const bool is_selected = has_selection(i) || carets[i].last_fit_x != carets[i].selection.origin_last_fit_x; + const int selection_origin_line = get_selection_origin_line(i); + const int selection_origin_column = get_selection_origin_column(i); + const int caret_wrap_index = get_caret_wrap_index(i); + const int selection_origin_wrap_index = !is_selected ? -1 : get_line_wrap_index_at_column(selection_origin_line, selection_origin_column); + + if (caret_line == 0 && !p_below && (caret_wrap_index == 0 || selection_origin_wrap_index == 0)) { + // Can't add above the first line. + continue; + } + if (caret_line == text.size() - 1 && p_below && (caret_wrap_index == last_line_max_wrap || selection_origin_wrap_index == last_line_max_wrap)) { + // Can't add below the last line. + continue; + } - if (get_selection_to_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_to_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_to_line(first_caret) > get_selection_from_line(second_caret) || get_selection_to_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_to_line(first_caret) < get_selection_to_line(second_caret) || get_selection_to_column(first_caret) <= get_selection_to_column(second_caret))) { - should_merge = true; - } + // Add a new caret. + int new_caret_index = add_caret(caret_line, caret_column); - if (!should_merge) { - continue; - } + // Copy the selection origin and last fit. + set_selection_origin_line(selection_origin_line, true, -1, new_caret_index); + set_selection_origin_column(selection_origin_column, new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; - // Save the newest one for Click + Drag. - int caret_to_save = first_caret; - int caret_to_remove = second_caret; - if (first_caret < second_caret) { - caret_to_save = second_caret; - caret_to_remove = first_caret; + // Move the caret up or down one visible line. + if (!p_below) { + // Move caret up. + if (caret_wrap_index > 0) { + set_caret_line(caret_line, false, false, caret_wrap_index - 1, new_caret_index); + } else { + int new_line = caret_line - get_next_visible_line_offset_from(caret_line - 1, -1); + if (is_line_wrapped(new_line)) { + set_caret_line(new_line, false, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_caret_line(new_line, false, false, 0, new_caret_index); + } } - - int from_line = MIN(get_selection_from_line(caret_to_save), get_selection_from_line(caret_to_remove)); - int to_line = MAX(get_selection_to_line(caret_to_save), get_selection_to_line(caret_to_remove)); - int from_col = get_selection_from_column(caret_to_save); - int to_col = get_selection_to_column(caret_to_save); - int selection_line = get_selection_line(caret_to_save); - int selection_col = get_selection_column(caret_to_save); - - bool at_from = (get_caret_line(caret_to_save) == get_selection_from_line(caret_to_save) && get_caret_column(caret_to_save) == get_selection_from_column(caret_to_save)); - - if (at_from) { - if (get_selection_line(caret_to_remove) > get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) >= get_selection_column(caret_to_save))) { - selection_line = get_selection_line(caret_to_remove); - selection_col = get_selection_column(caret_to_remove); + // Move selection origin up. + if (is_selected) { + if (selection_origin_wrap_index > 0) { + set_selection_origin_line(caret_line, false, selection_origin_wrap_index - 1, new_caret_index); + } else { + int new_line = selection_origin_line - get_next_visible_line_offset_from(selection_origin_line - 1, -1); + if (is_line_wrapped(new_line)) { + set_selection_origin_line(new_line, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_selection_origin_line(new_line, false, 0, new_caret_index); + } } - } else if (get_selection_line(caret_to_remove) < get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) <= get_selection_column(caret_to_save))) { - selection_line = get_selection_line(caret_to_remove); - selection_col = get_selection_column(caret_to_remove); } - - if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save) || (get_selection_from_line(caret_to_remove) == get_selection_from_line(caret_to_save) && get_selection_from_column(caret_to_remove) <= get_selection_from_column(caret_to_save))) { - from_col = get_selection_from_column(caret_to_remove); + if (get_caret_line(new_caret_index) < view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; + } + } else { + // Move caret down. + if (caret_wrap_index < get_line_wrap_count(caret_line)) { + set_caret_line(caret_line, false, false, caret_wrap_index + 1, new_caret_index); } else { - to_col = get_selection_to_column(caret_to_remove); + int new_line = caret_line + get_next_visible_line_offset_from(CLAMP(caret_line + 1, 0, text.size() - 1), 1); + set_caret_line(new_line, false, false, 0, new_caret_index); + } + // Move selection origin down. + if (is_selected) { + if (selection_origin_wrap_index < get_line_wrap_count(selection_origin_line)) { + set_selection_origin_line(selection_origin_line, false, selection_origin_wrap_index + 1, new_caret_index); + } else { + int new_line = selection_origin_line + get_next_visible_line_offset_from(CLAMP(selection_origin_line + 1, 0, text.size() - 1), 1); + set_selection_origin_line(new_line, false, 0, new_caret_index); + } + } + if (get_caret_line(new_caret_index) > view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; } + } + if (is_selected) { + // Make sure selection is active. + select(get_selection_origin_line(new_caret_index), get_selection_origin_column(new_caret_index), get_caret_line(new_caret_index), get_caret_column(new_caret_index), new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; + } - select(from_line, from_col, to_line, to_col, caret_to_save); - set_selection_mode(selecting_mode, selection_line, selection_col, caret_to_save); - set_caret_line((at_from ? from_line : to_line), caret_to_save == 0, true, 0, caret_to_save); - set_caret_column((at_from ? from_col : to_col), caret_to_save == 0, caret_to_save); - remove_caret(caret_to_remove); - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; + bool check_edges = !has_selection(0) || !has_selection(new_caret_index); + bool will_merge_with_main_caret = _selection_contains(0, get_caret_line(new_caret_index), get_caret_column(new_caret_index), check_edges, false) || _selection_contains(new_caret_index, get_caret_line(0), get_caret_column(0), check_edges, false); + if (will_merge_with_main_caret) { + // Move next to the main caret so it stays the main caret after merging. + Caret new_caret = carets[new_caret_index]; + carets.remove_at(new_caret_index); + carets.insert(0, new_caret); + i++; } + } - // Only first has selection. - if (has_selection(first_caret)) { - if (get_caret_line(second_caret) >= get_selection_from_line(first_caret) && get_caret_line(second_caret) <= get_selection_to_line(first_caret) && (get_caret_line(second_caret) > get_selection_from_line(first_caret) || get_caret_column(second_caret) >= get_selection_from_column(first_caret)) && (get_caret_line(second_caret) < get_selection_to_line(first_caret) || get_caret_column(second_caret) <= get_selection_to_column(first_caret))) { - remove_caret(second_caret); - caret_edit_order = get_caret_index_edit_order(); - i--; - } - continue; + // Show the topmost caret if added above or bottommost caret if added below. + if (view_target_caret >= 0 && view_target_caret < get_caret_count()) { + adjust_viewport_to_caret(view_target_caret); + } + + merge_overlapping_carets(); + end_multicaret_edit(); +} + +struct _CaretSortComparator { + _FORCE_INLINE_ bool operator()(const Vector3i &a, const Vector3i &b) const { + // x is column, y is line, z is caret index. + if (a.y == b.y) { + return a.x < b.x; } + return a.y < b.y; + } +}; - // Only second has selection. - if (has_selection(second_caret)) { - if (get_caret_line(first_caret) >= get_selection_from_line(second_caret) && get_caret_line(first_caret) <= get_selection_to_line(second_caret) && (get_caret_line(first_caret) > get_selection_from_line(second_caret) || get_caret_column(first_caret) >= get_selection_from_column(second_caret)) && (get_caret_line(first_caret) < get_selection_to_line(second_caret) || get_caret_column(first_caret) <= get_selection_to_column(second_caret))) { - remove_caret(first_caret); - caret_edit_order = get_caret_index_edit_order(); - i--; - } +Vector<int> TextEdit::get_sorted_carets(bool p_include_ignored_carets) const { + // Returns caret indexes sorted by selection start or caret position from top to bottom of text. + Vector<Vector3i> caret_line_col_indexes; + for (int i = 0; i < get_caret_count(); i++) { + if (!p_include_ignored_carets && multicaret_edit_ignore_caret(i)) { continue; } + caret_line_col_indexes.push_back(Vector3i(get_selection_from_column(i), get_selection_from_line(i), i)); + } + caret_line_col_indexes.sort_custom<_CaretSortComparator>(); + Vector<int> sorted; + sorted.resize(caret_line_col_indexes.size()); + for (int i = 0; i < caret_line_col_indexes.size(); i++) { + sorted.set(i, caret_line_col_indexes[i].z); + } + return sorted; +} - // Both have no selection. - if (get_caret_line(first_caret) == get_caret_line(second_caret) && get_caret_column(first_caret) == get_caret_column(second_caret)) { - // Save the newest one for Click + Drag. - if (first_caret < second_caret) { - remove_caret(first_caret); - } else { - remove_caret(second_caret); +void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive) { + // Collapse carets in the selected range to the from position. + + // Clamp the collapse target position. + int collapse_line = CLAMP(p_from_line, 0, text.size() - 1); + int collapse_column = CLAMP(p_from_column, 0, text[collapse_line].length()); + + // Swap the lines if they are in the wrong order. + if (p_from_line > p_to_line) { + SWAP(p_from_line, p_to_line); + SWAP(p_from_column, p_to_column); + } + if (p_from_line == p_to_line && p_from_column > p_to_column) { + SWAP(p_from_column, p_to_column); + } + bool any_collapsed = false; + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool is_caret_in = _is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + if (!has_selection(i)) { + if (is_caret_in) { + // Caret was in the collapsed area. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } + } else { + bool is_origin_in = _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + + if (is_caret_in && is_origin_in) { + // Selection was completely encapsulated. + deselect(i); + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } else if (is_caret_in) { + // Only caret was inside. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + any_collapsed = true; + } else if (is_origin_in) { + // Only selection origin was inside. + set_selection_origin_line(collapse_line, true, -1, i); + set_selection_origin_column(collapse_column, i); + any_collapsed = true; } - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; } + if (!p_inclusive && !any_collapsed) { + if ((get_caret_line(i) == collapse_line && get_caret_column(i) == collapse_column) || (get_selection_origin_line(i) == collapse_line && get_selection_origin_column(i) == collapse_column)) { + // Make sure to queue a merge, even if we didn't include it. + any_collapsed = true; + } + } + } + if (any_collapsed) { + merge_overlapping_carets(); } } -int TextEdit::get_caret_count() const { - return carets.size(); -} +void TextEdit::merge_overlapping_carets() { + if (is_in_mulitcaret_edit()) { + // Queue merge to be performed the end of the multicaret edit. + multicaret_edit_merge_queued = true; + return; + } -void TextEdit::add_caret_at_carets(bool p_below) { - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &caret_index : caret_edit_order) { - const int caret_line = get_caret_line(caret_index); - const int caret_column = get_caret_column(caret_index); - - // The last fit x will be cleared if the caret has a selection, - // but if it does not have a selection the last fit x will be - // transferred to the new caret. - int caret_from_column = 0, caret_to_column = 0, caret_last_fit_x = carets[caret_index].last_fit_x; - if (has_selection(caret_index)) { - // If the selection goes over multiple lines, deselect it. - if (get_selection_from_line(caret_index) != get_selection_to_line(caret_index)) { - deselect(caret_index); + multicaret_edit_merge_queued = false; + multicaret_edit_ignore_carets.clear(); + + if (get_caret_count() == 1) { + return; + } + + Vector<int> sorted_carets = get_sorted_carets(true); + for (int i = 0; i < sorted_carets.size() - 1; i++) { + int first_caret = sorted_carets[i]; + int second_caret = sorted_carets[i + 1]; + + bool merge_carets; + if (!has_selection(first_caret) || !has_selection(second_caret)) { + // Merge if touching. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) <= get_selection_to_column(first_caret)); + } else { + // Merge two selections if overlapping. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) < get_selection_to_column(first_caret)); + } + + if (!merge_carets) { + continue; + } + + // Save the newest one for Click + Drag. + int caret_to_save = first_caret; + int caret_to_remove = second_caret; + if (first_caret < second_caret) { + caret_to_save = second_caret; + caret_to_remove = first_caret; + } + + if (get_selection_from_line(caret_to_save) != get_selection_from_line(caret_to_remove) || get_selection_to_line(caret_to_save) != get_selection_to_line(caret_to_remove) || get_selection_from_column(caret_to_save) != get_selection_from_column(caret_to_remove) || get_selection_to_column(caret_to_save) != get_selection_to_column(caret_to_remove)) { + // Selections are not the same, merge them into one bigger selection. + int new_from_line = MIN(get_selection_from_line(caret_to_remove), get_selection_from_line(caret_to_save)); + int new_to_line = MAX(get_selection_to_line(caret_to_remove), get_selection_to_line(caret_to_save)); + int new_from_col; + int new_to_col; + if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_remove); + } else if (get_selection_from_line(caret_to_remove) > get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_save); } else { - caret_from_column = get_selection_from_column(caret_index); - caret_to_column = get_selection_to_column(caret_index); - caret_last_fit_x = -1; - carets.write[caret_index].last_fit_x = _get_column_x_offset_for_line(caret_column, caret_line, caret_column); + new_from_col = MIN(get_selection_from_column(caret_to_remove), get_selection_from_column(caret_to_save)); + } + if (get_selection_to_line(caret_to_remove) < get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_save); + } else if (get_selection_to_line(caret_to_remove) > get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_remove); + } else { + new_to_col = MAX(get_selection_to_column(caret_to_remove), get_selection_to_column(caret_to_save)); } - } - // Get the line and column of the new caret as if you would move the caret by pressing the arrow keys. - int new_caret_line, new_caret_column, new_caret_from_column = 0, new_caret_to_column = 0; - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_column, p_below, new_caret_line, new_caret_column, caret_last_fit_x); + // Use the direction from the last caret or the saved one. + int caret_dir_to_copy; + if (has_selection(caret_to_remove) && has_selection(caret_to_save)) { + caret_dir_to_copy = caret_to_remove == get_caret_count() - 1 ? caret_to_remove : caret_to_save; + } else { + caret_dir_to_copy = !has_selection(caret_to_remove) ? caret_to_save : caret_to_remove; + } - // If the caret does have a selection calculate the new from and to columns. - if (caret_from_column != caret_to_column) { - // We only need to calculate the selection columns if the column of the caret changed. - if (caret_column != new_caret_column) { - int _; // Unused placeholder for p_new_line. - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_from_column, p_below, _, new_caret_from_column); - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_to_column, p_below, _, new_caret_to_column); + if (is_caret_after_selection_origin(caret_dir_to_copy)) { + select(new_from_line, new_from_col, new_to_line, new_to_col, caret_to_save); } else { - new_caret_from_column = caret_from_column; - new_caret_to_column = caret_to_column; + select(new_to_line, new_to_col, new_from_line, new_from_col, caret_to_save); } } - // Add the new caret. - const int new_caret_index = add_caret(new_caret_line, new_caret_column); - - if (new_caret_index == -1) { - continue; + if (caret_to_save == 0) { + adjust_viewport_to_caret(caret_to_save); } - // Also add the selection if there should be one. - if (new_caret_from_column != new_caret_to_column) { - select(new_caret_line, new_caret_from_column, new_caret_line, new_caret_to_column, new_caret_index); - // Necessary to properly modify the selection after adding the new caret. - carets.write[new_caret_index].selection.selecting_line = new_caret_line; - carets.write[new_caret_index].selection.selecting_column = new_caret_column == new_caret_from_column ? new_caret_to_column : new_caret_from_column; - continue; + remove_caret(caret_to_remove); + + // Update the rest of the sorted list. + for (int j = i; j < sorted_carets.size(); j++) { + if (sorted_carets[j] > caret_to_remove) { + // Shift the index since a caret before it was removed. + sorted_carets.write[j] -= 1; + } } + // Remove the caret from the sorted array. + sorted_carets.remove_at(caret_to_remove == first_caret ? i : i + 1); - // Copy the last fit x over. - carets.write[new_caret_index].last_fit_x = carets[caret_index].last_fit_x; + // Process the caret again, since it and the next caret might also overlap. + i--; } +} - merge_overlapping_carets(); - queue_redraw(); +// Starts a multicaret edit operation. Call this before iterating over the carets and call [end_multicaret_edit] afterwards. +void TextEdit::begin_multicaret_edit() { + multicaret_edit_count++; } -Vector<int> TextEdit::get_caret_index_edit_order() { - if (!caret_index_edit_dirty) { - return caret_index_edit_order; +void TextEdit::end_multicaret_edit() { + if (multicaret_edit_count > 0) { + multicaret_edit_count--; + } + if (multicaret_edit_count != 0) { + return; } - caret_index_edit_order.clear(); - caret_index_edit_order.push_back(0); - for (int i = 1; i < carets.size(); i++) { - int j = 0; - - int line = has_selection(i) ? get_selection_to_line(i) : carets[i].line; - int col = has_selection(i) ? get_selection_to_column(i) : carets[i].column; - - for (; j < caret_index_edit_order.size(); j++) { - int idx = caret_index_edit_order[j]; - int other_line = has_selection(idx) ? get_selection_to_line(idx) : carets[idx].line; - int other_col = has_selection(idx) ? get_selection_to_column(idx) : carets[idx].column; - if (line > other_line || (line == other_line && col > other_col)) { - break; - } - } - caret_index_edit_order.insert(j, i); + // This was the last multicaret edit operation. + if (multicaret_edit_merge_queued) { + merge_overlapping_carets(); } - caret_index_edit_dirty = false; - return caret_index_edit_order; + multicaret_edit_ignore_carets.clear(); } -void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) { - int edit_height = p_from_line - p_to_line; - int edit_size = ((edit_height == 0) ? p_from_col : 0) - p_to_col; - - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int j = 0; j < caret_edit_order.size(); j++) { - if (caret_edit_order[j] == p_caret) { - return; - } - - // Adjust caret. - // set_caret_line could adjust the column, so save here. - int cc = get_caret_column(caret_edit_order[j]); - if (edit_height != 0) { - set_caret_line(get_caret_line(caret_edit_order[j]) + edit_height, false, true, 0, caret_edit_order[j]); - } - if (get_caret_line(p_caret) == get_caret_line(caret_edit_order[j])) { - set_caret_column(cc + edit_size, false, caret_edit_order[j]); - } +bool TextEdit::is_in_mulitcaret_edit() const { + return multicaret_edit_count > 0; +} - // Adjust selection. - if (!has_selection(caret_edit_order[j])) { - continue; - } - if (edit_height != 0) { - carets.write[caret_edit_order[j]].selection.from_line += edit_height; - carets.write[caret_edit_order[j]].selection.to_line += edit_height; - } - if (get_caret_line(p_caret) == get_selection_from_line(caret_edit_order[j])) { - carets.write[caret_edit_order[j]].selection.from_column += edit_size; - } - } +bool TextEdit::multicaret_edit_ignore_caret(int p_caret) const { + return multicaret_edit_ignore_carets.has(p_caret); } bool TextEdit::is_caret_visible(int p_caret) const { @@ -4902,16 +4915,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 +4927,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 +4935,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<String> 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 +4973,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 +5013,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 +5074,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 +5093,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) { @@ -5140,8 +5139,6 @@ void TextEdit::select_word_under_caret(int p_caret) { } select(get_caret_line(c), begin, get_caret_line(c), end, c); - // Move the caret to the end of the word for easier editing. - set_caret_column(end, false, c); } merge_overlapping_carets(); } @@ -5234,53 +5231,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()); - - 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; + 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.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; } @@ -5289,100 +5270,268 @@ bool TextEdit::has_selection(int p_caret) const { } String TextEdit::get_selected_text(int p_caret) { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); - StringBuilder selected_text; - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; + 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<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; - if (!has_selection(caret_idx)) { + if (!has_selection(caret_index)) { continue; } - selected_text += _base_get_text(get_selection_from_line(caret_idx), get_selection_from_column(caret_idx), get_selection_to_line(caret_idx), get_selection_to_column(caret_idx)); - if (p_caret == -1 && i != 0) { + 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_line(int p_caret) const { +int TextEdit::get_selection_at_line_column(int p_line, int p_column, bool p_include_edges, bool p_only_selections) 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, p_only_selections)) { + return i; + } + } + return -1; +} + +Vector<Point2i> 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<Point2i> ret; + int last_to_line = INT_MIN; + + Vector<int> 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<Vector2i> 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<Vector2i> ret; + Vector<Point2i> 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); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +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<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (!has_selection(i)) { continue; } - selecting_mode = SelectionMode::SELECTION_MODE_NONE; - _remove_text(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i), get_selection_to_column(i)); - set_caret_line(get_selection_from_line(i), false, false, 0, i); - set_caret_column(get_selection_from_column(i), i == 0, i); - carets.write[i].selection.active = false; + int selection_from_line = get_selection_from_line(i); + int selection_from_column = get_selection_from_column(i); + int selection_to_line = get_selection_to_line(i); + int selection_to_column = get_selection_to_column(i); + + _remove_text(selection_from_line, selection_from_column, selection_to_line, selection_to_column); + _offset_carets_after(selection_to_line, selection_to_column, selection_from_line, selection_from_column); + merge_overlapping_carets(); - adjust_carets_after_edit(i, carets[i].selection.from_line, carets[i].selection.from_column, carets[i].selection.to_line, carets[i].selection.to_column); + deselect(i); + set_caret_line(selection_from_line, false, false, -1, i); + set_caret_column(selection_from_column, i == 0, i); } + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } /* Line wrapping. */ @@ -6224,8 +6373,10 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("swap_lines", "from_line", "to_line"), &TextEdit::swap_lines); ClassDB::bind_method(D_METHOD("insert_line_at", "line", "text"), &TextEdit::insert_line_at); + ClassDB::bind_method(D_METHOD("remove_line_at", "line", "move_carets_down"), &TextEdit::remove_line_at, DEFVAL(true)); ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text", "caret_index"), &TextEdit::insert_text_at_caret, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("insert_text", "text", "line", "column", "before_selection_begin", "before_selection_end"), &TextEdit::insert_text, DEFVAL(true), DEFVAL(false)); ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text); ClassDB::bind_method(D_METHOD("get_last_unhidden_line"), &TextEdit::get_last_unhidden_line); @@ -6311,7 +6462,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,15 +6506,20 @@ 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); ClassDB::bind_method(D_METHOD("get_caret_count"), &TextEdit::get_caret_count); ClassDB::bind_method(D_METHOD("add_caret_at_carets", "below"), &TextEdit::add_caret_at_carets); - ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); - ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit); + ClassDB::bind_method(D_METHOD("get_sorted_carets", "include_ignored_carets"), &TextEdit::get_sorted_carets, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("collapse_carets", "from_line", "from_column", "to_line", "to_column", "inclusive"), &TextEdit::collapse_carets, DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); + ClassDB::bind_method(D_METHOD("begin_multicaret_edit"), &TextEdit::begin_multicaret_edit); + ClassDB::bind_method(D_METHOD("end_multicaret_edit"), &TextEdit::end_multicaret_edit); + ClassDB::bind_method(D_METHOD("is_in_mulitcaret_edit"), &TextEdit::is_in_mulitcaret_edit); + ClassDB::bind_method(D_METHOD("multicaret_edit_ignore_caret", "caret_index"), &TextEdit::multicaret_edit_ignore_caret); ClassDB::bind_method(D_METHOD("is_caret_visible", "caret_index"), &TextEdit::is_caret_visible, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_caret_draw_pos", "caret_index"), &TextEdit::get_caret_draw_pos, DEFVAL(0)); @@ -6394,27 +6550,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", "only_selections"), &TextEdit::get_selection_at_line_column, DEFVAL(true), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_line_ranges_from_carets", "only_selections", "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)); @@ -6550,6 +6712,14 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_menu_visible"), &TextEdit::is_menu_visible); ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option); + /* Deprecated */ +#ifndef DISABLE_DEPRECATED + ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit); + ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); + 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)); +#endif + /* Inspector */ ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text", PROPERTY_HINT_MULTILINE_TEXT), "set_placeholder", "get_placeholder"); @@ -6617,7 +6787,7 @@ void TextEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("gutter_added")); ADD_SIGNAL(MethodInfo("gutter_removed")); - /* Theme items */ + // Theme items /* Search */ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_color); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_border_color); @@ -6690,6 +6860,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()); @@ -6717,14 +6891,17 @@ void TextEdit::_set_symbol_lookup_word(const String &p_symbol) { // Overridable actions void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } start_action(EditAction::ACTION_TYPING); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } @@ -6742,11 +6919,12 @@ void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca const char32_t chr[2] = { (char32_t)p_unicode, 0 }; insert_text_at_caret(chr, i); } + end_multicaret_edit(); end_action(); } 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; } @@ -6757,194 +6935,163 @@ void TextEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } - int cc = get_caret_column(i); - int cl = get_caret_line(i); + int to_line = get_caret_line(i); + int to_column = get_caret_column(i); - if (cc == 0 && cl == 0) { + if (to_column == 0 && to_line == 0) { continue; } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (text[cl - 1].length()); + int from_line = to_column > 0 ? to_line : to_line - 1; + int from_column = to_column > 0 ? (to_column - 1) : (text[to_line - 1].length()); - merge_gutters(prev_line, cl); + merge_gutters(from_line, to_line); - if (_is_line_hidden(cl)) { - _set_line_as_hidden(prev_line, true); - } - _remove_text(prev_line, prev_column, cl, cc); - - set_caret_line(prev_line, false, true, 0, i); - set_caret_column(prev_column, i == 0, i); + _remove_text(from_line, from_column, to_line, to_column); + collapse_carets(from_line, from_column, to_line, to_column); + _offset_carets_after(to_line, to_column, from_line, from_column); - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + set_caret_line(from_line, false, true, -1, i); + set_caret_column(from_column, i == 0, i); } - merge_overlapping_carets(); + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_cut_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); + + _copy_internal(p_caret); + if (!editable) { return; } if (has_selection(p_caret)) { - DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); delete_selection(p_caret); - cut_copy_line = ""; return; } + // Remove full lines. begin_complex_operation(); - Vector<int> carets_to_remove; - - StringBuilder clipboard; - // This is the exception and has to edit in reverse order else the string copied to the clipboard will be backwards. - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; - } - - int cl = get_caret_line(caret_idx); - int cc = get_caret_column(caret_idx); - int indent_level = get_indent_level(cl); - double hscroll = get_h_scroll(); - - // Check for overlapping carets. - // We don't need to worry about selections as that is caught before this entire section. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) == cl) { - carets_to_remove.push_back(caret_edit_order[j]); - i = j; - } - } - - clipboard += text[cl]; - if (p_caret == -1 && caret_idx != 0) { - clipboard += "\n"; - } - - if (cl == 0 && get_line_count() > 1) { - _remove_text(cl, 0, cl + 1, 0); - adjust_carets_after_edit(caret_idx, cl, 0, cl + 1, text[cl].length()); - } else { - _remove_text(cl, 0, cl, text[cl].length()); - set_caret_column(0, false, caret_idx); - backspace(caret_idx); - set_caret_line(get_caret_line(caret_idx) + 1, caret_idx == 0, 0, 0, caret_idx); - } - - // Correct the visually perceived caret column taking care of indentation level of the lines. - int diff_indent = indent_level - get_indent_level(get_caret_line(caret_idx)); - cc += diff_indent; - if (diff_indent != 0) { - cc += diff_indent > 0 ? -1 : 1; - } - - // Restore horizontal scroll and caret column modified by the backspace() call. - set_h_scroll(hscroll); - set_caret_column(cc, caret_idx == 0, caret_idx); + begin_multicaret_edit(); + Vector<Point2i> line_ranges; + if (p_caret == -1) { + line_ranges = get_line_ranges_from_carets(); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); } - - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // Preserve carets on the last line. + remove_line_at(line_range.y + line_offset); + if (line_range.x != line_range.y) { + remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0); + } + line_offset += line_range.x - line_range.y - 1; } + end_multicaret_edit(); end_complex_operation(); - - String clipboard_string = clipboard.as_string(); - DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; } void TextEdit::_copy_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (has_selection(p_caret)) { DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); cut_copy_line = ""; return; } + // Copy full lines. StringBuilder clipboard; - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; - } - - int cl = get_caret_line(caret_idx); - if (text[cl].length() != 0) { - clipboard += _base_get_text(cl, 0, cl, text[cl].length()); - if (p_caret == -1 && i != 0) { - clipboard += "\n"; + Vector<Point2i> line_ranges; + if (p_caret == -1) { + // When there are multiple carets on a line, only copy it once. + line_ranges = get_line_ranges_from_carets(false, true); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); + } + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { + if (text[i].length() != 0) { + clipboard += _base_get_text(i, 0, i, text[i].length()); } + clipboard += "\n"; } } String clipboard_string = clipboard.as_string(); DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; + // Set the cut copy line so we know to paste as a line. + if (get_caret_count() == 1) { + cut_copy_line = clipboard_string; + } else { + cut_copy_line = ""; + } } void TextEdit::_paste_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; } String clipboard = DisplayServer::get_singleton()->clipboard_get(); + + // Paste a full line. Ignore '\r' characters that may have been added to the clipboard by the OS. + if (get_caret_count() == 1 && !has_selection(0) && !cut_copy_line.is_empty() && cut_copy_line == clipboard.replace("\r", "")) { + insert_text(clipboard, get_caret_line(), 0); + return; + } + + // Paste text at each caret or one line per caret. Vector<String> clipboad_lines = clipboard.split("\n"); - bool insert_line_per_caret = p_caret == -1 && carets.size() > 1 && clipboad_lines.size() == carets.size(); + bool insert_line_per_caret = p_caret == -1 && get_caret_count() > 1 && clipboad_lines.size() == get_caret_count(); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - int clipboad_line = clipboad_lines.size() - 1; - for (const int &i : caret_edit_order) { - if (p_caret != -1 && p_caret != i) { + begin_multicaret_edit(); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < get_caret_count(); i++) { + int caret_index = sorted_carets[i]; + if (p_caret != -1 && p_caret != caret_index) { continue; } - if (has_selection(i)) { - delete_selection(i); - } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { - set_caret_column(0, i == 0, i); - String ins = "\n"; - clipboard += ins; + if (has_selection(caret_index)) { + delete_selection(caret_index); } if (insert_line_per_caret) { - clipboard = clipboad_lines[clipboad_line]; + clipboard = clipboad_lines[i]; } - insert_text_at_caret(clipboard, i); - clipboad_line--; + insert_text_at_caret(clipboard, caret_index); } + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_paste_primary_clipboard_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!is_editable() || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { return; } String paste_buffer = DisplayServer::get_singleton()->clipboard_get_primary(); - if (carets.size() == 1) { + if (get_caret_count() == 1) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); deselect(); - set_caret_line(pos.y, true, false); + set_caret_line(pos.y, true, false, -1); set_caret_column(pos.x); } @@ -7203,10 +7350,26 @@ 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; - caret_index_edit_dirty = true; } void TextEdit::_reset_caret_blink_timer() { @@ -7251,60 +7414,152 @@ 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; + } + } + return false; +} + +void TextEdit::_offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin, bool p_include_selection_end) { + // Moves all carets at or after old_line and old_column. + // Called after deleting or inserting text so that the carets stay with the text they are at. + + int edit_height = p_new_line - p_old_line; + int edit_size = p_new_column - p_old_column; + if (edit_height == 0 && edit_size == 0) { + return; + } + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + bool caret_at_end = selected && is_caret_after_selection_origin(i); + bool include_caret_at = caret_at_end ? p_include_selection_end : p_include_selection_begin; + + // Move caret. + int caret_line = get_caret_line(i); + int caret_column = get_caret_column(i); + bool caret_after = caret_line > p_old_line || (caret_line == p_old_line && caret_column > p_old_column); + bool caret_at = caret_line == p_old_line && caret_column == p_old_column; + if (caret_after || (caret_at && include_caret_at)) { + caret_line += edit_height; + if (caret_line == p_new_line) { + caret_column += edit_size; } + + if (edit_height != 0) { + set_caret_line(caret_line, false, true, -1, i); + } + set_caret_column(caret_column, false, i); } - } else { + + // Move selection origin. + if (!selected) { + continue; + } + bool include_selection_origin_at = !caret_at_end ? p_include_selection_end : p_include_selection_begin; + + int selection_origin_line = get_selection_origin_line(i); + int selection_origin_column = get_selection_origin_column(i); + bool selection_origin_after = selection_origin_line > p_old_line || (selection_origin_line == p_old_line && selection_origin_column > p_old_column); + bool selection_origin_at = selection_origin_line == p_old_line && selection_origin_column == p_old_column; + if (selection_origin_after || (selection_origin_at && include_selection_origin_at)) { + selection_origin_line += edit_height; + if (selection_origin_line == p_new_line) { + selection_origin_column += edit_size; + } + select(selection_origin_line, selection_origin_column, caret_line, caret_column, i); + } + } + if (!p_include_selection_begin && p_include_selection_end && has_selection()) { + // It is possible that two adjacent selections now overlap. + merge_overlapping_carets(); + } +} + +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(); + } +} + +/* 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() { - dragging_selection = true; +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; - - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); + int column = pos.x; + int caret_index = get_caret_count() - 1; + + 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 +7571,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; - - 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 caret_index = get_caret_count() - 1; + + 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 +7631,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 */ @@ -7780,9 +8022,43 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { return syntax_highlighter.is_null() && !setting_text ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line); } +/* Deprecated. */ +#ifndef DISABLE_DEPRECATED +Vector<int> TextEdit::get_caret_index_edit_order() { + Vector<int> carets_order = get_sorted_carets(); + carets_order.reverse(); + return carets_order; +} + +void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) { +} + +int TextEdit::get_selection_line(int p_caret) const { + return get_selection_origin_line(p_caret); +} + +int TextEdit::get_selection_column(int p_caret) const { + return get_selection_origin_column(p_caret); +} +#endif + /*** 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 +8194,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 +8235,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); } |