From 773a473807c75975b5820b30a4f0eccaece7cfa2 Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 22 Jan 2024 18:27:19 -0500 Subject: [PATCH] Overhaul multiple caret editing in TextEdit. Use a multicaret edit to delay merging overlapping carets until the end. --- doc/classes/CodeEdit.xml | 38 +- doc/classes/TextEdit.xml | 180 +- editor/code_editor.cpp | 404 +- editor/code_editor.h | 8 - editor/plugins/script_text_editor.cpp | 80 +- editor/plugins/text_editor.cpp | 18 +- editor/plugins/text_shader_editor.cpp | 8 +- .../4.2-stable.expected | 8 + scene/gui/code_edit.cpp | 624 +- scene/gui/code_edit.h | 6 + scene/gui/text_edit.compat.inc | 41 + scene/gui/text_edit.cpp | 1468 ++--- scene/gui/text_edit.h | 46 +- tests/display_server_mock.h | 10 + tests/scene/test_code_edit.h | 1994 +++++- tests/scene/test_text_edit.h | 5326 ++++++++++++++--- tests/test_macros.h | 9 + 17 files changed, 7562 insertions(+), 2706 deletions(-) create mode 100644 scene/gui/text_edit.compat.inc diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index 7c6f1a51c43..d455799c298 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -14,7 +14,7 @@ - Override this method to define how the selected entry should be inserted. If [param replace] is true, any existing text should be replaced. + Override this method to define how the selected entry should be inserted. If [param replace] is [code]true[/code], any existing text should be replaced. @@ -29,7 +29,7 @@ - Override this method to define what happens when the user requests code completion. If [param force] is true, any checks should be bypassed. + Override this method to define what happens when the user requests code completion. If [param force] is [code]true[/code], any checks should be bypassed. @@ -123,7 +123,7 @@ - Inserts the selected entry into the text. If [param replace] is true, any existing text is replaced rather than merged. + Inserts the selected entry into the text. If [param replace] is [code]true[/code], any existing text is replaced rather than merged. @@ -144,6 +144,12 @@ Code regions are delimited using start and end tags (respectively [code]region[/code] and [code]endregion[/code] by default) preceded by one line comment delimiter. (eg. [code]#region[/code] and [code]#endregion[/code]) + + + + Deletes all lines that are selected or have a caret on them. + + @@ -156,6 +162,12 @@ Duplicates all lines currently selected with any caret. Duplicates the entire line beneath the current one no matter where the caret is within the line. + + + + Duplicates all selected text and duplicates all lines with a caret on them. + + @@ -379,6 +391,18 @@ Returns whether the line at the specified index is folded or not. + + + + Moves all lines down that are selected or have a caret on them. + + + + + + Moves all lines up that are selected or have a caret on them. + + @@ -397,7 +421,7 @@ - Emits [signal code_completion_requested], if [param force] is true will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path or signal. + Emits [signal code_completion_requested], if [param force] is [code]true[/code] will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path, or signal. @@ -467,6 +491,12 @@ Toggle the folding of the code block at the given line. + + + + Toggle the folding of the code block on all lines with a caret on them. + + diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index 72893c913a6..2959ec4cfa9 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -5,7 +5,7 @@ A multiline text editor. It also has limited facilities for editing code, such as syntax highlighting support. For more advanced facilities for editing code, see [CodeEdit]. - [b]Note:[/b] Most viewport, caret and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets. + [b]Note:[/b] Most viewport, caret, and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets in the order they were created. [b]Note:[/b] When holding down [kbd]Alt[/kbd], the vertical scroll wheel will scroll 5 times as fast as it would normally do. This also works in the Godot script editor. @@ -67,7 +67,7 @@ - Adds an additional caret above or below every caret. If [param below] is true the new caret will be added below and above otherwise. + Adds an additional caret above or below every caret. If [param below] is [code]true[/code] the new caret will be added below and above otherwise. @@ -83,7 +83,7 @@ Adds a selection and a caret for the next occurrence of the current selection. If there is no active selection, selects word under caret. - + @@ -91,7 +91,7 @@ - Reposition the carets affected by the edit. This assumes edits are applied in edit order, see [method get_caret_index_edit_order]. + This method does nothing. @@ -120,6 +120,23 @@ Starts a multipart edit. All edits will be treated as one action until [method end_complex_operation] is called. + + + + Starts an edit for multiple carets. The edit must be ended with [method end_multicaret_edit]. Multicaret edits can be used to edit text at multiple carets and delay merging the carets until the end, so the caret indexes aren't affected immediately. [method begin_multicaret_edit] and [method end_multicaret_edit] can be nested, and the merge will happen at the last [method end_multicaret_edit]. + Example usage: + [codeblock] + begin_complex_operation() + begin_multicaret_edit() + for i in range(get_caret_count()): + if multicaret_edit_ignore_caret(i): + continue + # Logic here. + end_multicaret_edit() + end_complex_operation() + [/codeblock] + + @@ -145,6 +162,20 @@ Clears the undo history. + + + + + + + + + Collapse all carets in the given range to the [param from_line] and [param from_column] position. + [param inclusive] applies to both ends. + If [method is_in_mulitcaret_edit] is [code]true[/code], carets that are collapsed will be [code]true[/code] for [method multicaret_edit_ignore_caret]. + [method merge_overlapping_carets] will be called if any carets were collapsed. + + @@ -185,6 +216,12 @@ Ends a multipart edit, started with [method begin_complex_operation]. If called outside a complex operation, the current operation is pushed onto the undo/redo stack. + + + + Ends an edit for multiple carets, that was started with [method begin_multicaret_edit]. If this was the last [method end_multicaret_edit] and [method merge_overlapping_carets] was called, carets will be merged. + + @@ -205,7 +242,7 @@ Returns the caret pixel draw position. - + Returns a list of caret indexes in their edit order, this done from bottom to top. Edit order refers to the way actions such as [method insert_text_at_caret] are applied. @@ -365,11 +402,11 @@ - - + + Returns an [Array] of line ranges where [code]x[/code] is the first line and [code]y[/code] is the last line. All lines within these ranges will have a caret on them or be part of a selection. Each line will only be part of one line range, even if it has multiple carets on it. - If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param p_merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections. + If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections. @@ -528,12 +565,13 @@ + Returns the caret index of the selection at the given [param line] and [param column], or [code]-1[/code] if there is none. - If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. + If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. If [param only_selections] is [code]false[/code], carets without a selection will also be considered. - + @@ -544,17 +582,17 @@ - Returns the selection begin column. + Returns the selection begin column. Returns the caret column if there is no selection. - Returns the selection begin line. + Returns the selection begin line. Returns the caret line if there is no selection. - + @@ -567,18 +605,40 @@ Returns the current selection mode. + + + + + Returns the origin column of the selection. This is the opposite end from the caret. + + + + + + + Returns the origin line of the selection. This is the opposite end from the caret. + + - Returns the selection end column. + Returns the selection end column. Returns the caret column if there is no selection. - Returns the selection end line. + Returns the selection end line. Returns the caret line if there is no selection. + + + + + + + Returns the carets sorted by selection beginning from lowest line and column to highest (from top to bottom of text). + If [param include_ignored_carets] is [code]false[/code], carets from [method multicaret_edit_ignore_caret] will be ignored. @@ -672,6 +732,19 @@ Inserts a new line with [param text] at [param line]. + + + + + + + + + Inserts the [param text] at [param line] and [param column]. + If [param before_selection_begin] is [code]true[/code], carets and selections that begin at [param line] and [param column] will moved to the end of the inserted text, along with all carets after it. + If [param before_selection_end] is [code]true[/code], selections that end at [param line] and [param column] will be extended to the end of the inserted text. These parameters can be used to insert text inside of or outside of selections. + + @@ -680,6 +753,13 @@ Insert the specified text at the caret position. + + + + + Returns [code]true[/code] if the caret of the selection is after the selection origin. This can be used to determine the direction of the selection. + + @@ -690,7 +770,7 @@ - Returns [code]true[/code] if the user is dragging their mouse for scrolling or selecting. + Returns [code]true[/code] if the user is dragging their mouse for scrolling, selecting, or text dragging. @@ -714,6 +794,12 @@ Returns whether the gutter is overwritable. + + + + Returns [code]true[/code] if a [method begin_multicaret_edit] has been called and [method end_multicaret_edit] has not yet been called. + + @@ -768,9 +854,18 @@ Merges any overlapping carets. Will favor the newest caret, or the caret with a selection. + If [method is_in_mulitcaret_edit] is [code]true[/code], the merge will be queued to happen at the end of the multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. [b]Note:[/b] This is not called when a caret changes position but after certain actions, so it is possible to get into a state where carets overlap. + + + + + Returns [code]true[/code] if the given [param caret_index] should be ignored as part of a multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. Carets that should be ignored are ones that were part of removed text and will likely be merged at the end of the edit, or carets that were added during the edit. + It is recommended to [code]continue[/code] within a loop iterating on multiple carets if a caret should be ignored. + + @@ -806,6 +901,15 @@ Removes the gutter from this [TextEdit]. + + + + + + Removes the line of text at [param line]. Carets on this line will attempt to match their previous visual x position. + If [param move_carets_down] is [code]true[/code] carets will move to the next line down, otherwise carets will move up. + + @@ -820,7 +924,6 @@ Removes text between the given positions. - [b]Note:[/b] This does not adjust the caret or selection, which as a result it can end up in an invalid position. @@ -854,14 +957,15 @@ - - - - + + + + - Perform selection, from line/column to line/column. + Selects text from [param origin_line] and [param origin_column] to [param caret_line] and [param caret_column] for the given [param caret_index]. This moves the selection origin and the caret. If the positions are the same, the selection will be deselected. If [member selecting_enabled] is [code]false[/code], no selection will occur. + [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. @@ -897,9 +1001,10 @@ - Moves the caret to the specified [param line] index. + Moves the caret to the specified [param line] index. The caret column will be moved to the same visual position it was at the last time [method set_caret_column] was called, or clamped to the end of the line. If [param adjust_viewport] is [code]true[/code], the viewport will center at the caret position after the move occurs. If [param can_be_hidden] is [code]true[/code], the specified [param line] can be hidden. + If [param wrap_index] is [code]-1[/code], the caret column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_caret_column] was called. [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. @@ -964,7 +1069,8 @@ - Sets the text for a specific line. + Sets the text for a specific [param line]. + Carets on the line will attempt to keep their visual x position. @@ -1072,6 +1178,26 @@ Sets the current selection mode. + + + + + + Sets the selection origin column to the [param column] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + + + + + + + + + + Sets the selection origin line to the [param line] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + If [param can_be_hidden] is [code]false[/code], The line will be set to the nearest unhidden line below or above. + If [param wrap_index] is [code]-1[/code], the selection origin column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_selection_origin_column] or [method select] was called. + + @@ -1105,7 +1231,7 @@ - Swaps the two lines. + Swaps the two lines. Carets will be swapped with the lines. @@ -1156,7 +1282,7 @@ If [code]true[/code], the selected text will be deselected when focus is lost. - If [code]true[/code], allow drag and drop of selected text. + If [code]true[/code], allow drag and drop of selected text. Text can still be dropped from other sources. If [code]true[/code], control characters are displayed. @@ -1247,7 +1373,7 @@ - Emitted when the caret changes position. + Emitted when any caret changes position. diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 238fd00089e..cfeb495690b 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -33,7 +33,6 @@ #include "core/input/input.h" #include "core/os/keyboard.h" #include "core/string/string_builder.h" -#include "core/templates/pair.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/plugins/script_editor_plugin.h" @@ -810,22 +809,22 @@ void CodeTextEditor::input(const Ref &event) { } if (ED_IS_SHORTCUT("script_text_editor/move_up", key_event)) { - move_lines_up(); + text_editor->move_lines_up(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/move_down", key_event)) { - move_lines_down(); + text_editor->move_lines_down(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/delete_line", key_event)) { - delete_lines(); + text_editor->delete_lines(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/duplicate_selection", key_event)) { - duplicate_selection(); + text_editor->duplicate_selection(); accept_event(); return; } @@ -1114,31 +1113,23 @@ void CodeTextEditor::trim_trailing_whitespace() { break; } } - text_editor->set_line(i, line.substr(0, end)); + text_editor->remove_text(i, end, i, line.length()); } } if (trimmed_whitespace) { text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } } void CodeTextEditor::insert_final_newline() { int final_line = text_editor->get_line_count() - 1; - String line = text_editor->get_line(final_line); // Length 0 means it's already an empty line, no need to add a newline. if (line.length() > 0 && !line.ends_with("\n")) { - text_editor->begin_complex_operation(); - - line += "\n"; - text_editor->set_line(final_line, line); - - text_editor->end_complex_operation(); - text_editor->queue_redraw(); + text_editor->insert_text("\n", final_line, line.length(), false); } } @@ -1147,9 +1138,12 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { return; } text_editor->begin_complex_operation(); + text_editor->begin_multicaret_edit(); - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { + for (int c = 0; c < text_editor->get_caret_count(); c++) { + if (text_editor->multicaret_edit_ignore_caret(c)) { + continue; + } if (!text_editor->has_selection(c)) { continue; } @@ -1190,6 +1184,7 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { text_editor->set_line(i, new_line); } } + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); } @@ -1198,308 +1193,24 @@ void CodeTextEditor::set_indent_using_spaces(bool p_use_spaces) { indentation_txt->set_text(p_use_spaces ? TTR("Spaces", "Indentation") : TTR("Tabs", "Indentation")); } -void CodeTextEditor::move_lines_up() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector> caret_groups; - Vector> group_borders; - - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector new_group{ c }; - Pair group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos != current_start_pos && next_end_pos + 1 != current_start_pos) { - break; - } - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } - group_borders.push_back(group_border); - caret_groups.push_back(new_group); - } - - for (int i = group_borders.size() - 1; i >= 0; i--) { - if (group_borders[i].first - 1 < 0) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i < group_borders.size() - 1 && group_borders[i].first - 1 <= group_borders[i + 1].second) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - int from_line = text_editor->get_selection_from_line(c); - int from_col = text_editor->get_selection_from_column(c); - int to_line = text_editor->get_selection_to_line(c); - int to_column = text_editor->get_selection_to_column(c); - caret_group_parameters.push_back(Vector{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].first; line_id <= group_borders[i].second; line_id++) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id - 1); - - text_editor->swap_lines(line_id - 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] - 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] - 1, caret_parameters[1], caret_parameters[2] - 1, caret_parameters[3], c); - } - } - } - - text_editor->end_complex_operation(); - text_editor->merge_overlapping_carets(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::move_lines_down() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector> caret_groups; - Vector> group_borders; - Vector group_border_ends; - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector new_group{ c }; - Pair group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos == current_start_pos || next_end_pos + 1 == current_start_pos) { - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } else { - break; - } - } - group_borders.push_back(group_border); - group_border_ends.push_back(text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c)); - caret_groups.push_back(new_group); - } - - for (int i = 0; i < group_borders.size(); i++) { - if (group_border_ends[i] + 1 > text_editor->get_line_count() - 1) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i > 0 && group_border_ends[i] + 1 >= group_borders[i - 1].first) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - int from_line = text_editor->get_selection_from_line(c); - int from_col = text_editor->get_selection_from_column(c); - int to_line = text_editor->get_selection_to_line(c); - int to_column = text_editor->get_selection_to_column(c); - caret_group_parameters.push_back(Vector{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].second; line_id >= group_borders[i].first; line_id--) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id + 1); - - text_editor->swap_lines(line_id + 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] + 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] + 1, caret_parameters[1], caret_parameters[2] + 1, caret_parameters[3], c); - } - } - } - - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::delete_lines() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - Vector lines; - int last_line = INT_MAX; - for (const int &c : caret_edit_order) { - for (int line = _get_affected_lines_to(c); line >= _get_affected_lines_from(c); line--) { - if (line >= last_line) { - continue; - } - last_line = line; - lines.append(line); - } - } - - for (const int &line : lines) { - if (line != text_editor->get_line_count() - 1) { - text_editor->remove_text(line, 0, line + 1, 0); - } else { - text_editor->remove_text(line - 1, text_editor->get_line(line - 1).length(), line, text_editor->get_line(line).length()); - } - // Readjust carets. - int new_line = MIN(line, text_editor->get_line_count() - 1); - text_editor->unfold_line(new_line); - for (const int &c : caret_edit_order) { - if (text_editor->get_caret_line(c) == line || (text_editor->get_caret_line(c) == line + 1 && text_editor->get_caret_column(c) == 0)) { - text_editor->deselect(c); - text_editor->set_caret_line(new_line, c == 0, true, 0, c); - continue; - } - if (text_editor->get_caret_line(c) > line) { - text_editor->set_caret_line(text_editor->get_caret_line(c) - 1, c == 0, true, 0, c); - continue; - } - break; - } - } - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); -} - -void CodeTextEditor::duplicate_selection() { - text_editor->begin_complex_operation(); - - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { - const int cursor_column = text_editor->get_caret_column(c); - int from_line = text_editor->get_caret_line(c); - int to_line = text_editor->get_caret_line(c); - int from_column = 0; - int to_column = 0; - int cursor_new_line = to_line + 1; - int cursor_new_column = text_editor->get_caret_column(c); - String new_text = "\n" + text_editor->get_line(from_line); - bool selection_active = false; - - text_editor->set_caret_column(text_editor->get_line(from_line).length(), c == 0, c); - if (text_editor->has_selection(c)) { - from_column = text_editor->get_selection_from_column(c); - to_column = text_editor->get_selection_to_column(c); - - from_line = text_editor->get_selection_from_line(c); - to_line = text_editor->get_selection_to_line(c); - cursor_new_line = to_line + text_editor->get_caret_line(c) - from_line; - cursor_new_column = to_column == cursor_column ? 2 * to_column - from_column : to_column; - new_text = text_editor->get_selected_text(c); - selection_active = true; - - text_editor->set_caret_line(to_line, c == 0, true, 0, c); - text_editor->set_caret_column(to_column, c == 0, c); - } - - for (int i = from_line; i <= to_line; i++) { - text_editor->unfold_line(i); - } - text_editor->deselect(c); - text_editor->insert_text_at_caret(new_text, c); - text_editor->set_caret_line(cursor_new_line, c == 0, true, 0, c); - text_editor->set_caret_column(cursor_new_column, c == 0, c); - if (selection_active) { - text_editor->select(to_line, to_column, 2 * to_line - from_line, to_line == from_line ? 2 * to_column - from_column : to_column, c); - } - } - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); - text_editor->queue_redraw(); -} - void CodeTextEditor::toggle_inline_comment(const String &delimiter) { text_editor->begin_complex_operation(); + text_editor->begin_multicaret_edit(); - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); - int last_line = -1; + Vector line_ranges = text_editor->get_line_ranges_from_carets(); int folded_to = 0; - for (const int &c1 : caret_edit_order) { - int from = _get_affected_lines_from(c1); - from += from == last_line ? 1 + folded_to : 0; - int to = _get_affected_lines_to(c1); - last_line = to; + for (Point2i line_range : line_ranges) { + int from_line = line_range.x; + int to_line = line_range.y; // If last line is folded, extends to the end of the folded section - if (text_editor->is_line_folded(to)) { - folded_to = text_editor->get_next_visible_line_offset_from(to + 1, 1) - 1; - to += folded_to; + if (text_editor->is_line_folded(to_line)) { + folded_to = text_editor->get_next_visible_line_offset_from(to_line + 1, 1) - 1; + to_line += folded_to; } // Check first if there's any uncommented lines in selection. bool is_commented = true; bool is_all_empty = true; - for (int line = from; line <= to; line++) { + for (int line = from_line; line <= to_line; line++) { // `+ delimiter.length()` here because comment delimiter is not actually `in comment` so we check first character after it int delimiter_idx = text_editor->is_in_comment(line, text_editor->get_first_non_whitespace_column(line) + delimiter.length()); // Empty lines should not be counted. @@ -1515,58 +1226,24 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { // Special case for commenting empty lines, treat it/them as uncommented lines. is_commented = is_commented && !is_all_empty; - // Caret positions need to be saved since they could be moved at the eol. - Vector caret_cols; - Vector selection_to_cols; - for (const int &c2 : caret_edit_order) { - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - caret_cols.append(text_editor->get_caret_column(c2)); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - selection_to_cols.append(text_editor->get_selection_to_column(c2)); - } - } - // Comment/uncomment. - for (int line = from; line <= to; line++) { - String line_text = text_editor->get_line(line); + for (int line = from_line; line <= to_line; line++) { if (is_all_empty) { - text_editor->set_line(line, delimiter); + text_editor->insert_text(delimiter, line, 0); continue; } if (is_commented) { - text_editor->set_line(line, line_text.replace_first(delimiter, "")); + int delimiter_column = text_editor->get_line(line).find(delimiter); + text_editor->remove_text(line, delimiter_column, line, delimiter_column + delimiter.length()); } else { - text_editor->set_line(line, line_text.insert(text_editor->get_first_non_whitespace_column(line), delimiter)); - } - } - - // Readjust carets and selections. - int caret_i = 0; - int selection_i = 0; - int offset = (is_commented ? -1 : 1) * delimiter.length(); - for (const int &c2 : caret_edit_order) { - bool is_line_selection = text_editor->has_selection(c2) && text_editor->get_selection_from_line(c2) < text_editor->get_selection_to_line(c2); - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - int caret_col = caret_cols[caret_i++]; - caret_col += (is_line_selection && caret_col == 0) ? 0 : offset; - text_editor->set_caret_column(caret_col, c2 == 0, c2); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - int from_col = text_editor->get_selection_from_column(c2); - from_col += (is_line_selection && from_col == 0) ? 0 : offset; - int to_col = selection_to_cols[selection_i++]; - to_col += (to_col == 0) ? 0 : offset; - text_editor->select( - text_editor->get_selection_from_line(c2), from_col, - text_editor->get_selection_to_line(c2), to_col, c2); + text_editor->insert_text(delimiter, line, text_editor->get_first_non_whitespace_column(line)); } } } - text_editor->merge_overlapping_carets(); + + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } void CodeTextEditor::goto_line(int p_line) { @@ -1813,22 +1490,6 @@ void CodeTextEditor::_toggle_scripts_pressed() { update_toggle_scripts_button(); } -int CodeTextEditor::_get_affected_lines_from(int p_caret) { - return text_editor->has_selection(p_caret) ? text_editor->get_selection_from_line(p_caret) : text_editor->get_caret_line(p_caret); -} - -int CodeTextEditor::_get_affected_lines_to(int p_caret) { - if (!text_editor->has_selection(p_caret)) { - return text_editor->get_caret_line(p_caret); - } - int line = text_editor->get_selection_to_line(p_caret); - // Don't affect a line with no selected characters. - if (text_editor->get_selection_to_column(p_caret) == 0) { - line--; - } - return line; -} - void CodeTextEditor::_error_pressed(const Ref &p_event) { Ref mb = p_event; if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { @@ -1877,13 +1538,12 @@ void CodeTextEditor::set_warning_count(int p_warning_count) { } void CodeTextEditor::toggle_bookmark() { - Vector caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); + Vector sorted_carets = text_editor->get_sorted_carets(); int last_line = -1; - for (const int &c : caret_edit_order) { - int from = text_editor->has_selection(c) ? text_editor->get_selection_from_line(c) : text_editor->get_caret_line(c); + for (const int &c : sorted_carets) { + int from = text_editor->get_selection_from_line(c); from += from == last_line ? 1 : 0; - int to = text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c); + int to = text_editor->get_selection_to_line(c); if (to < from) { continue; } diff --git a/editor/code_editor.h b/editor/code_editor.h index c36eedb580f..75a2a68d585 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -207,9 +207,6 @@ class CodeTextEditor : public VBoxContainer { void _toggle_scripts_pressed(); - int _get_affected_lines_from(int p_caret); - int _get_affected_lines_to(int p_caret); - protected: virtual void _load_theme_settings() {} virtual void _validate_script() {} @@ -238,11 +235,6 @@ public: void set_indent_using_spaces(bool p_use_spaces); - void move_lines_up(); - void move_lines_down(); - void delete_lines(); - void duplicate_selection(); - /// Toggle inline comment on currently selected lines, or on current line if nothing is selected, /// by adding or removing comment delimiter void toggle_inline_comment(const String &delimiter); diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 752ccecd91c..561edcf8bf1 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -284,8 +284,7 @@ void ScriptTextEditor::_warning_clicked(const Variant &p_line) { if (prev_line.contains("@warning_ignore")) { const int closing_bracket_idx = prev_line.find(")"); const String text_to_insert = ", " + code.quote(quote_style); - prev_line = prev_line.insert(closing_bracket_idx, text_to_insert); - text_editor->set_line(line - 1, prev_line); + text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx); } else { const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size(); String annotation_indent; @@ -352,22 +351,26 @@ void ScriptTextEditor::add_callback(const String &p_function, const PackedString if (!language->can_make_function()) { return; } - + code_editor->get_text_editor()->begin_complex_operation(); + code_editor->get_text_editor()->remove_secondary_carets(); + code_editor->get_text_editor()->deselect(); String code = code_editor->get_text_editor()->get_text(); int pos = language->find_function(p_function, code); - code_editor->get_text_editor()->remove_secondary_carets(); if (pos == -1) { - //does not exist - code_editor->get_text_editor()->deselect(); - pos = code_editor->get_text_editor()->get_line_count() + 2; + // Function does not exist, create it at the end of the file. + int last_line = code_editor->get_text_editor()->get_line_count() - 1; String func = language->make_function("", p_function, p_args); - //code=code+func; - code_editor->get_text_editor()->set_caret_line(pos + 1); - code_editor->get_text_editor()->set_caret_column(1000000); //none shall be that big - code_editor->get_text_editor()->insert_text_at_caret("\n\n" + func); + code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length()); + pos = last_line + 3; } - code_editor->get_text_editor()->set_caret_line(pos); - code_editor->get_text_editor()->set_caret_column(1); + // Put caret on the line after the function, after the indent. + int indent_column = 1; + if (EDITOR_GET("text_editor/behavior/indent/type")) { + indent_column = EDITOR_GET("text_editor/behavior/indent/size"); + } + code_editor->get_text_editor()->set_caret_line(pos, true, true, -1); + code_editor->get_text_editor()->set_caret_column(indent_column); + code_editor->get_text_editor()->end_complex_operation(); } bool ScriptTextEditor::show_members_overview() { @@ -1335,10 +1338,10 @@ void ScriptTextEditor::_edit_option(int p_op) { callable_mp((Control *)tx, &Control::grab_focus).call_deferred(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { Ref