Overhaul multiple caret editing in TextEdit.

Use a multicaret edit to delay merging overlapping carets until the end.
This commit is contained in:
kit 2024-01-22 18:27:19 -05:00
parent 154f727c7a
commit 773a473807
17 changed files with 7562 additions and 2706 deletions

View File

@ -14,7 +14,7 @@
<return type="void" />
<param index="0" name="replace" type="bool" />
<description>
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.
</description>
</method>
<method name="_filter_code_completion_candidates" qualifiers="virtual const">
@ -29,7 +29,7 @@
<return type="void" />
<param index="0" name="force" type="bool" />
<description>
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.
</description>
</method>
<method name="add_auto_brace_completion_pair">
@ -123,7 +123,7 @@
<return type="void" />
<param index="0" name="replace" type="bool" default="false" />
<description>
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.
</description>
</method>
<method name="convert_indent">
@ -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])
</description>
</method>
<method name="delete_lines">
<return type="void" />
<description>
Deletes all lines that are selected or have a caret on them.
</description>
</method>
<method name="do_indent">
<return type="void" />
<description>
@ -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.
</description>
</method>
<method name="duplicate_selection">
<return type="void" />
<description>
Duplicates all selected text and duplicates all lines with a caret on them.
</description>
</method>
<method name="fold_all_lines">
<return type="void" />
<description>
@ -379,6 +391,18 @@
Returns whether the line at the specified index is folded or not.
</description>
</method>
<method name="move_lines_down">
<return type="void" />
<description>
Moves all lines down that are selected or have a caret on them.
</description>
</method>
<method name="move_lines_up">
<return type="void" />
<description>
Moves all lines up that are selected or have a caret on them.
</description>
</method>
<method name="remove_comment_delimiter">
<return type="void" />
<param index="0" name="start_key" type="String" />
@ -397,7 +421,7 @@
<return type="void" />
<param index="0" name="force" type="bool" default="false" />
<description>
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.
</description>
</method>
<method name="set_code_completion_selected_index">
@ -467,6 +491,12 @@
Toggle the folding of the code block at the given line.
</description>
</method>
<method name="toggle_foldable_lines_at_carets">
<return type="void" />
<description>
Toggle the folding of the code block on all lines with a caret on them.
</description>
</method>
<method name="unfold_all_lines">
<return type="void" />
<description>

View File

@ -5,7 +5,7 @@
</brief_description>
<description>
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.
</description>
<tutorials>
@ -67,7 +67,7 @@
<return type="void" />
<param index="0" name="below" type="bool" />
<description>
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.
</description>
</method>
<method name="add_gutter">
@ -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.
</description>
</method>
<method name="adjust_carets_after_edit">
<method name="adjust_carets_after_edit" deprecated="No longer necessary since methods now adjust carets themselves.">
<return type="void" />
<param index="0" name="caret" type="int" />
<param index="1" name="from_line" type="int" />
@ -91,7 +91,7 @@
<param index="3" name="to_line" type="int" />
<param index="4" name="to_col" type="int" />
<description>
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.
</description>
</method>
<method name="adjust_viewport_to_caret">
@ -120,6 +120,23 @@
Starts a multipart edit. All edits will be treated as one action until [method end_complex_operation] is called.
</description>
</method>
<method name="begin_multicaret_edit">
<return type="void" />
<description>
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]
</description>
</method>
<method name="cancel_ime">
<return type="void" />
<description>
@ -145,6 +162,20 @@
Clears the undo history.
</description>
</method>
<method name="collapse_carets">
<return type="void" />
<param index="0" name="from_line" type="int" />
<param index="1" name="from_column" type="int" />
<param index="2" name="to_line" type="int" />
<param index="3" name="to_column" type="int" />
<param index="4" name="inclusive" type="bool" default="false" />
<description>
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.
</description>
</method>
<method name="copy">
<return type="void" />
<param index="0" name="caret_index" type="int" default="-1" />
@ -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.
</description>
</method>
<method name="end_multicaret_edit">
<return type="void" />
<description>
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.
</description>
</method>
<method name="get_caret_column" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
@ -205,7 +242,7 @@
Returns the caret pixel draw position.
</description>
</method>
<method name="get_caret_index_edit_order">
<method name="get_caret_index_edit_order" deprecated="Carets no longer need to be edited in any specific order. If the carets need to be sorted, use [method get_sorted_carets] instead.">
<return type="PackedInt32Array" />
<description>
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 @@
</method>
<method name="get_line_ranges_from_carets" qualifiers="const">
<return type="Vector2i[]" />
<param index="0" name="p_only_selections" type="bool" default="false" />
<param index="1" name="p_merge_adjacent" type="bool" default="true" />
<param index="0" name="only_selections" type="bool" default="false" />
<param index="1" name="merge_adjacent" type="bool" default="true" />
<description>
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.
</description>
</method>
<method name="get_line_width" qualifiers="const">
@ -528,12 +565,13 @@
<param index="0" name="line" type="int" />
<param index="1" name="column" type="int" />
<param index="2" name="include_edges" type="bool" default="true" />
<param index="3" name="only_selections" type="bool" default="true" />
<description>
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.
</description>
</method>
<method name="get_selection_column" qualifiers="const" is_deprecated="true">
<method name="get_selection_column" qualifiers="const" deprecated="Use [method get_selection_origin_column] instead.">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
@ -544,17 +582,17 @@
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
Returns the selection begin column.
Returns the selection begin column. Returns the caret column if there is no selection.
</description>
</method>
<method name="get_selection_from_line" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
Returns the selection begin line.
Returns the selection begin line. Returns the caret line if there is no selection.
</description>
</method>
<method name="get_selection_line" qualifiers="const">
<method name="get_selection_line" qualifiers="const" deprecated="Use [method get_selection_origin_line] instead.">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
@ -567,18 +605,40 @@
Returns the current selection mode.
</description>
</method>
<method name="get_selection_origin_column" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
Returns the origin column of the selection. This is the opposite end from the caret.
</description>
</method>
<method name="get_selection_origin_line" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
Returns the origin line of the selection. This is the opposite end from the caret.
</description>
</method>
<method name="get_selection_to_column" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
Returns the selection end column.
Returns the selection end column. Returns the caret column if there is no selection.
</description>
</method>
<method name="get_selection_to_line" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
Returns the selection end line.
Returns the selection end line. Returns the caret line if there is no selection.
</description>
</method>
<method name="get_sorted_carets" qualifiers="const">
<return type="PackedInt32Array" />
<param index="0" name="include_ignored_carets" type="bool" default="false" />
<description>
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.
</description>
</method>
<method name="get_tab_size" qualifiers="const">
@ -672,6 +732,19 @@
Inserts a new line with [param text] at [param line].
</description>
</method>
<method name="insert_text">
<return type="void" />
<param index="0" name="text" type="String" />
<param index="1" name="line" type="int" />
<param index="2" name="column" type="int" />
<param index="3" name="before_selection_begin" type="bool" default="true" />
<param index="4" name="before_selection_end" type="bool" default="false" />
<description>
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.
</description>
</method>
<method name="insert_text_at_caret">
<return type="void" />
<param index="0" name="text" type="String" />
@ -680,6 +753,13 @@
Insert the specified text at the caret position.
</description>
</method>
<method name="is_caret_after_selection_origin" qualifiers="const">
<return type="bool" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
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.
</description>
</method>
<method name="is_caret_visible" qualifiers="const">
<return type="bool" />
<param index="0" name="caret_index" type="int" default="0" />
@ -690,7 +770,7 @@
<method name="is_dragging_cursor" qualifiers="const">
<return type="bool" />
<description>
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.
</description>
</method>
<method name="is_gutter_clickable" qualifiers="const">
@ -714,6 +794,12 @@
Returns whether the gutter is overwritable.
</description>
</method>
<method name="is_in_mulitcaret_edit" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if a [method begin_multicaret_edit] has been called and [method end_multicaret_edit] has not yet been called.
</description>
</method>
<method name="is_line_gutter_clickable" qualifiers="const">
<return type="bool" />
<param index="0" name="line" type="int" />
@ -768,9 +854,18 @@
<return type="void" />
<description>
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.
</description>
</method>
<method name="multicaret_edit_ignore_caret" qualifiers="const">
<return type="bool" />
<param index="0" name="caret_index" type="int" />
<description>
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.
</description>
</method>
<method name="paste">
<return type="void" />
<param index="0" name="caret_index" type="int" default="-1" />
@ -806,6 +901,15 @@
Removes the gutter from this [TextEdit].
</description>
</method>
<method name="remove_line_at">
<return type="void" />
<param index="0" name="line" type="int" />
<param index="1" name="move_carets_down" type="bool" default="true" />
<description>
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.
</description>
</method>
<method name="remove_secondary_carets">
<return type="void" />
<description>
@ -820,7 +924,6 @@
<param index="3" name="to_column" type="int" />
<description>
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.
</description>
</method>
<method name="search" qualifiers="const">
@ -854,14 +957,15 @@
</method>
<method name="select">
<return type="void" />
<param index="0" name="from_line" type="int" />
<param index="1" name="from_column" type="int" />
<param index="2" name="to_line" type="int" />
<param index="3" name="to_column" type="int" />
<param index="0" name="origin_line" type="int" />
<param index="1" name="origin_column" type="int" />
<param index="2" name="caret_line" type="int" />
<param index="3" name="caret_column" type="int" />
<param index="4" name="caret_index" type="int" default="0" />
<description>
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].
</description>
</method>
<method name="select_all">
@ -897,9 +1001,10 @@
<param index="3" name="wrap_index" type="int" default="0" />
<param index="4" name="caret_index" type="int" default="0" />
<description>
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].
</description>
</method>
@ -964,7 +1069,8 @@
<param index="0" name="line" type="int" />
<param index="1" name="new_text" type="String" />
<description>
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.
</description>
</method>
<method name="set_line_as_center_visible">
@ -1072,6 +1178,26 @@
Sets the current selection mode.
</description>
</method>
<method name="set_selection_origin_column">
<return type="void" />
<param index="0" name="column" type="int" />
<param index="1" name="caret_index" type="int" default="0" />
<description>
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.
</description>
</method>
<method name="set_selection_origin_line">
<return type="void" />
<param index="0" name="line" type="int" />
<param index="1" name="can_be_hidden" type="bool" default="true" />
<param index="2" name="wrap_index" type="int" default="-1" />
<param index="3" name="caret_index" type="int" default="0" />
<description>
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.
</description>
</method>
<method name="set_tab_size">
<return type="void" />
<param index="0" name="size" type="int" />
@ -1105,7 +1231,7 @@
<param index="0" name="from_line" type="int" />
<param index="1" name="to_line" type="int" />
<description>
Swaps the two lines.
Swaps the two lines. Carets will be swapped with the lines.
</description>
</method>
<method name="tag_saved_version">
@ -1156,7 +1282,7 @@
If [code]true[/code], the selected text will be deselected when focus is lost.
</member>
<member name="drag_and_drop_selection_enabled" type="bool" setter="set_drag_and_drop_selection_enabled" getter="is_drag_and_drop_selection_enabled" default="true">
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.
</member>
<member name="draw_control_chars" type="bool" setter="set_draw_control_chars" getter="get_draw_control_chars" default="false">
If [code]true[/code], control characters are displayed.
@ -1247,7 +1373,7 @@
<signals>
<signal name="caret_changed">
<description>
Emitted when the caret changes position.
Emitted when any caret changes position.
</description>
</signal>
<signal name="gutter_added">

View File

@ -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<InputEvent> &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<int> 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<int> caret_edit_order = text_editor->get_caret_index_edit_order();
// Lists of carets representing each group.
Vector<Vector<int>> caret_groups;
Vector<Pair<int, int>> 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<int> new_group{ c };
Pair<int, int> 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<Vector<int>> 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<int>{ -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<int>{ 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<int> &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<int> caret_edit_order = text_editor->get_caret_index_edit_order();
// Lists of carets representing each group.
Vector<Vector<int>> caret_groups;
Vector<Pair<int, int>> group_borders;
Vector<int> 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<int> new_group{ c };
Pair<int, int> 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<Vector<int>> 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<int>{ -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<int>{ 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<int> &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<int> caret_edit_order = text_editor->get_caret_index_edit_order();
Vector<int> 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<int> 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<int> caret_edit_order = text_editor->get_caret_index_edit_order();
caret_edit_order.reverse();
int last_line = -1;
Vector<Point2i> 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<int> caret_cols;
Vector<int> 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<InputEvent> &p_event) {
Ref<InputEventMouseButton> 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<int> caret_edit_order = text_editor->get_caret_index_edit_order();
caret_edit_order.reverse();
Vector<int> 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;
}

View File

@ -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);

View File

@ -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<Script> scr = script;
@ -1355,24 +1358,16 @@ void ScriptTextEditor::_edit_option(int p_op) {
tx->unindent_lines();
} break;
case EDIT_DELETE_LINE: {
code_editor->delete_lines();
code_editor->get_text_editor()->delete_lines();
} break;
case EDIT_DUPLICATE_SELECTION: {
code_editor->duplicate_selection();
code_editor->get_text_editor()->duplicate_selection();
} break;
case EDIT_DUPLICATE_LINES: {
code_editor->get_text_editor()->duplicate_lines();
} break;
case EDIT_TOGGLE_FOLD_LINE: {
int prev_line = -1;
for (int caret_idx : tx->get_caret_index_edit_order()) {
int line_idx = tx->get_caret_line(caret_idx);
if (line_idx != prev_line) {
tx->toggle_foldable_line(line_idx);
prev_line = line_idx;
}
}
tx->queue_redraw();
tx->toggle_foldable_lines_at_carets();
} break;
case EDIT_FOLD_ALL_LINES: {
tx->fold_all_lines();
@ -1399,24 +1394,34 @@ void ScriptTextEditor::_edit_option(int p_op) {
}
tx->begin_complex_operation();
int begin, end;
tx->begin_multicaret_edit();
int begin = tx->get_line_count() - 1, end = 0;
if (tx->has_selection()) {
begin = tx->get_selection_from_line();
end = tx->get_selection_to_line();
// ignore if the cursor is not past the first column
if (tx->get_selection_to_column() == 0) {
end--;
// Auto indent all lines that have a caret or selection on it.
Vector<Point2i> line_ranges = tx->get_line_ranges_from_carets();
for (Point2i line_range : line_ranges) {
scr->get_language()->auto_indent_code(text, line_range.x, line_range.y);
if (line_range.x < begin) {
begin = line_range.x;
}
if (line_range.y > end) {
end = line_range.y;
}
}
} else {
// Auto indent entire text.
begin = 0;
end = tx->get_line_count() - 1;
scr->get_language()->auto_indent_code(text, begin, end);
}
scr->get_language()->auto_indent_code(text, begin, end);
// Apply auto indented code.
Vector<String> lines = text.split("\n");
for (int i = begin; i <= end; ++i) {
tx->set_line(i, lines[i]);
}
tx->end_multicaret_edit();
tx->end_complex_operation();
} break;
case EDIT_TRIM_TRAILING_WHITESAPCE: {
@ -1515,13 +1520,12 @@ void ScriptTextEditor::_edit_option(int p_op) {
code_editor->remove_all_bookmarks();
} break;
case DEBUG_TOGGLE_BREAKPOINT: {
Vector<int> caret_edit_order = tx->get_caret_index_edit_order();
caret_edit_order.reverse();
Vector<int> sorted_carets = tx->get_sorted_carets();
int last_line = -1;
for (const int &c : caret_edit_order) {
int from = tx->has_selection(c) ? tx->get_selection_from_line(c) : tx->get_caret_line(c);
for (const int &c : sorted_carets) {
int from = tx->get_selection_from_line(c);
from += from == last_line ? 1 : 0;
int to = tx->has_selection(c) ? tx->get_selection_to_line(c) : tx->get_caret_line(c);
int to = tx->get_selection_to_line(c);
if (to < from) {
continue;
}

View File

@ -380,10 +380,10 @@ void TextEditor::_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: {
tx->indent_lines();
@ -392,24 +392,16 @@ void TextEditor::_edit_option(int p_op) {
tx->unindent_lines();
} break;
case EDIT_DELETE_LINE: {
code_editor->delete_lines();
code_editor->get_text_editor()->delete_lines();
} break;
case EDIT_DUPLICATE_SELECTION: {
code_editor->duplicate_selection();
code_editor->get_text_editor()->duplicate_selection();
} break;
case EDIT_DUPLICATE_LINES: {
code_editor->get_text_editor()->duplicate_lines();
} break;
case EDIT_TOGGLE_FOLD_LINE: {
int previous_line = -1;
for (int caret_idx : tx->get_caret_index_edit_order()) {
int line_idx = tx->get_caret_line(caret_idx);
if (line_idx != previous_line) {
tx->toggle_foldable_line(line_idx);
previous_line = line_idx;
}
}
tx->queue_redraw();
tx->toggle_foldable_lines_at_carets();
} break;
case EDIT_FOLD_ALL_LINES: {
tx->fold_all_lines();

View File

@ -650,10 +650,10 @@ void TextShaderEditor::_menu_option(int p_option) {
code_editor->get_text_editor()->select_all();
} 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: {
if (shader.is_null() && shader_inc.is_null()) {
@ -668,10 +668,10 @@ void TextShaderEditor::_menu_option(int p_option) {
code_editor->get_text_editor()->unindent_lines();
} break;
case EDIT_DELETE_LINE: {
code_editor->delete_lines();
code_editor->get_text_editor()->delete_lines();
} break;
case EDIT_DUPLICATE_SELECTION: {
code_editor->duplicate_selection();
code_editor->get_text_editor()->duplicate_selection();
} break;
case EDIT_DUPLICATE_LINES: {
code_editor->get_text_editor()->duplicate_lines();

View File

@ -315,3 +315,11 @@ Validate extension JSON: Error: Field 'classes/TextServer/methods/shaped_text_ge
Validate extension JSON: Error: Field 'classes/TextServerExtension/methods/_shaped_text_get_word_breaks/arguments': size changed value in new API, from 2 to 3.
Added optional argument. Compatibility method registered.
GH-86978
--------
Validate extension JSON: Error: Field 'classes/TextEdit/methods/set_selection_mode/arguments': size changed value in new API, from 4 to 1.
Removed optional arguments set_selection_mode, use set_selection_origin_line/column instead.
Compatibility methods registered.

View File

@ -641,11 +641,14 @@ void CodeEdit::_unhide_carets() {
// Overridable actions
void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) {
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 && p_caret != i) {
continue;
}
if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
continue;
}
bool had_selection = has_selection(i);
String selection_text = (had_selection ? get_selected_text(i) : "");
@ -703,6 +706,7 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca
insert_text_at_caret(chr, i);
}
}
end_multicaret_edit();
end_action();
}
@ -717,66 +721,80 @@ void CodeEdit::_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 && p_caret != i) {
continue;
}
int cc = get_caret_column(i);
int cl = get_caret_line(i);
if (cc == 0 && cl == 0) {
if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
continue;
}
if (cl > 0 && _is_line_hidden(cl - 1)) {
unfold_line(get_caret_line(i) - 1);
int to_line = get_caret_line(i);
int to_column = get_caret_column(i);
if (to_column == 0 && to_line == 0) {
continue;
}
int prev_line = cc ? cl : cl - 1;
int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length());
if (to_line > 0 && _is_line_hidden(to_line - 1)) {
unfold_line(to_line - 1);
}
merge_gutters(prev_line, cl);
int from_line = to_column > 0 ? to_line : to_line - 1;
int from_column = to_column > 0 ? (to_column - 1) : (get_line(to_line - 1).length());
if (auto_brace_completion_enabled && cc > 0) {
int idx = _get_auto_brace_pair_open_at_pos(cl, cc);
merge_gutters(from_line, to_line);
if (auto_brace_completion_enabled && to_column > 0) {
int idx = _get_auto_brace_pair_open_at_pos(to_line, to_column);
if (idx != -1) {
prev_column = cc - auto_brace_completion_pairs[idx].open_key.length();
from_column = to_column - auto_brace_completion_pairs[idx].open_key.length();
if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) {
cc += auto_brace_completion_pairs[idx].close_key.length();
if (_get_auto_brace_pair_close_at_pos(to_line, to_column) == idx) {
to_column += auto_brace_completion_pairs[idx].close_key.length();
}
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);
adjust_carets_after_edit(i, prev_line, prev_column, cl, cc);
continue;
}
}
// For space indentation we need to do a basic unindent if there are no chars to the left, acting the same way as tabs.
if (indent_using_spaces && cc != 0) {
if (get_first_non_whitespace_column(cl) >= cc) {
prev_column = cc - _calculate_spaces_till_next_left_indent(cc);
prev_line = cl;
if (indent_using_spaces && to_column != 0) {
if (get_first_non_whitespace_column(to_line) >= to_column) {
from_column = to_column - _calculate_spaces_till_next_left_indent(to_column);
from_line = to_line;
}
}
remove_text(prev_line, prev_column, cl, cc);
remove_text(from_line, from_column, to_line, to_column);
set_caret_line(prev_line, false, true, 0, i);
set_caret_column(prev_column, i == 0, i);
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 CodeEdit::_cut_internal(int p_caret) {
// Overridden to unfold lines.
_copy_internal(p_caret);
if (!is_editable()) {
return;
}
if (has_selection(p_caret)) {
delete_selection(p_caret);
return;
}
if (p_caret == -1) {
delete_lines();
} else {
unfold_line(get_caret_line(p_caret));
remove_line_at(get_caret_line(p_caret));
}
}
/* Indent management */
void CodeEdit::set_indent_size(const int p_size) {
ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0.");
@ -850,13 +868,17 @@ void CodeEdit::do_indent() {
}
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 (multicaret_edit_ignore_caret(i)) {
continue;
}
int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column(i));
if (spaces_to_add > 0) {
insert_text_at_caret(String(" ").repeat(spaces_to_add), i);
}
}
end_multicaret_edit();
end_complex_operation();
}
@ -866,51 +888,28 @@ void CodeEdit::indent_lines() {
}
begin_complex_operation();
Vector<int> caret_edit_order = get_caret_index_edit_order();
for (const int &c : caret_edit_order) {
// This value informs us by how much we changed selection position by indenting right.
// Default is 1 for tab indentation.
int selection_offset = 1;
begin_multicaret_edit();
int start_line = get_caret_line(c);
int end_line = start_line;
if (has_selection(c)) {
start_line = get_selection_from_line(c);
end_line = get_selection_to_line(c);
// Ignore the last line if the selection is not past the first column.
if (get_selection_to_column(c) == 0) {
selection_offset = 0;
end_line--;
}
}
for (int i = start_line; i <= end_line; i++) {
Vector<Point2i> line_ranges = get_line_ranges_from_carets();
for (Point2i line_range : line_ranges) {
for (int i = line_range.x; i <= line_range.y; i++) {
const String line_text = get_line(i);
if (line_text.size() == 0 && has_selection(c)) {
if (line_text.size() == 0) {
// Ignore empty lines.
continue;
}
if (!indent_using_spaces) {
set_line(i, '\t' + line_text);
continue;
if (indent_using_spaces) {
int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i));
insert_text(String(" ").repeat(spaces_to_add), i, 0, false);
} else {
insert_text("\t", i, 0, false);
}
// We don't really care where selection is - we just need to know indentation level at the beginning of the line.
// Since we will add this many spaces, we want to move the whole selection and caret by this much.
int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i));
set_line(i, String(" ").repeat(spaces_to_add) + line_text);
selection_offset = spaces_to_add;
}
// Fix selection and caret being off after shifting selection right.
if (has_selection(c)) {
select(start_line, get_selection_from_column(c) + selection_offset, get_selection_to_line(c), get_selection_to_column(c) + selection_offset, c);
}
set_caret_column(get_caret_column(c) + selection_offset, false, c);
}
end_multicaret_edit();
end_complex_operation();
queue_redraw();
}
void CodeEdit::unindent_lines() {
@ -919,76 +918,25 @@ void CodeEdit::unindent_lines() {
}
begin_complex_operation();
begin_multicaret_edit();
Vector<int> caret_edit_order = get_caret_index_edit_order();
for (const int &c : caret_edit_order) {
// Moving caret and selection after unindenting can get tricky because
// changing content of line can move caret and selection on its own (if new line ends before previous position of either)
// therefore we just remember initial values and at the end of the operation offset them by number of removed characters.
int removed_characters = 0;
int initial_selection_end_column = 0;
int initial_cursor_column = get_caret_column(c);
int start_line = get_caret_line(c);
int end_line = start_line;
if (has_selection(c)) {
start_line = get_selection_from_line(c);
end_line = get_selection_to_line(c);
// Ignore the last line if the selection is not past the first column.
initial_selection_end_column = get_selection_to_column(c);
if (initial_selection_end_column == 0) {
end_line--;
}
}
bool first_line_edited = false;
bool last_line_edited = false;
for (int i = start_line; i <= end_line; i++) {
String line_text = get_line(i);
Vector<Point2i> line_ranges = get_line_ranges_from_carets();
for (Point2i line_range : line_ranges) {
for (int i = line_range.x; i <= line_range.y; i++) {
const String line_text = get_line(i);
if (line_text.begins_with("\t")) {
line_text = line_text.substr(1, line_text.length());
set_line(i, line_text);
removed_characters = 1;
first_line_edited = (i == start_line) ? true : first_line_edited;
last_line_edited = (i == end_line) ? true : last_line_edited;
continue;
}
if (line_text.begins_with(" ")) {
// When unindenting we aim to remove spaces before line that has selection no matter what is selected.
// Here we remove only enough spaces to align text to nearest full multiple of indentation_size.
// In case where selection begins at the start of indentation_size multiple we remove whole indentation level.
remove_text(i, 0, i, 1);
} else if (line_text.begins_with(" ")) {
// Remove only enough spaces to align text to nearest full multiple of indentation_size.
int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i));
line_text = line_text.substr(spaces_to_remove, line_text.length());
set_line(i, line_text);
removed_characters = spaces_to_remove;
first_line_edited = (i == start_line) ? true : first_line_edited;
last_line_edited = (i == end_line) ? true : last_line_edited;
remove_text(i, 0, i, spaces_to_remove);
}
}
if (has_selection(c)) {
// Fix selection being off by one on the first line.
if (first_line_edited) {
select(get_selection_from_line(c), get_selection_from_column(c) - removed_characters, get_selection_to_line(c), initial_selection_end_column, c);
}
// Fix selection being off by one on the last line.
if (last_line_edited) {
select(get_selection_from_line(c), get_selection_from_column(c), get_selection_to_line(c), initial_selection_end_column - removed_characters, c);
}
}
set_caret_column(initial_cursor_column - removed_characters, false, c);
}
end_multicaret_edit();
end_complex_operation();
queue_redraw();
}
void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
@ -1004,27 +952,6 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
ERR_FAIL_COND(p_to_line >= get_line_count());
ERR_FAIL_COND(p_to_line < p_from_line);
// Store caret states.
Vector<int> caret_columns;
Vector<Pair<int, int>> from_selections;
Vector<Pair<int, int>> to_selections;
caret_columns.resize(get_caret_count());
from_selections.resize(get_caret_count());
to_selections.resize(get_caret_count());
for (int c = 0; c < get_caret_count(); c++) {
caret_columns.write[c] = get_caret_column(c);
// Set "selection_from_line" to -1 to allow checking if there was a selection later.
if (!has_selection(c)) {
from_selections.write[c].first = -1;
continue;
}
from_selections.write[c].first = get_selection_from_line(c);
from_selections.write[c].second = get_selection_from_column(c);
to_selections.write[c].first = get_selection_to_line(c);
to_selections.write[c].second = get_selection_to_column(c);
}
// Check lines within range.
const char32_t from_indent_char = indent_using_spaces ? '\t' : ' ';
int size_diff = indent_using_spaces ? indent_size - 1 : -(indent_size - 1);
@ -1056,23 +983,10 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
line_changed = true;
if (!changed_indentation) {
begin_complex_operation();
begin_multicaret_edit();
changed_indentation = true;
}
// Calculate new caret state.
for (int c = 0; c < get_caret_count(); c++) {
if (get_caret_line(c) != i || caret_columns[c] <= j) {
continue;
}
caret_columns.write[c] += size_diff;
if (from_selections.write[c].first == -1) {
continue;
}
from_selections.write[c].second = from_selections[c].first == i ? from_selections[c].second + size_diff : from_selections[c].second;
to_selections.write[c].second = to_selections[c].first == i ? to_selections[c].second + size_diff : to_selections[c].second;
}
// Calculate new line.
line = line.left(j + ((size_diff < 0) ? size_diff : 0)) + indent_text + line.substr(j + 1);
@ -1081,6 +995,7 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
}
if (line_changed) {
// Use set line to preserve carets visual position.
set_line(i, line);
}
}
@ -1089,16 +1004,9 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
return;
}
// Restore caret states.
for (int c = 0; c < get_caret_count(); c++) {
set_caret_column(caret_columns[c], c == 0, c);
if (from_selections.write[c].first != -1) {
select(from_selections.write[c].first, from_selections.write[c].second, to_selections.write[c].first, to_selections.write[c].second, c);
}
}
merge_overlapping_carets();
end_multicaret_edit();
end_complex_operation();
queue_redraw();
}
int CodeEdit::_calculate_spaces_till_next_left_indent(int p_column) const {
@ -1119,15 +1027,22 @@ void CodeEdit::_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) {
begin_multicaret_edit();
for (int i = 0; i < get_caret_count(); i++) {
if (multicaret_edit_ignore_caret(i)) {
continue;
}
// When not splitting the line, we need to factor in indentation from the end of the current line.
const int cc = p_split_current_line ? get_caret_column(i) : get_line(get_caret_line(i)).length();
const int cl = get_caret_line(i);
const String line = get_line(cl);
String ins = "\n";
String ins = "";
if (!p_above) {
ins = "\n";
}
// Append current indentation.
int space_count = 0;
@ -1150,6 +1065,9 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) {
}
break;
}
if (p_above) {
ins += "\n";
}
if (is_line_folded(cl)) {
unfold_line(cl);
@ -1195,33 +1113,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) {
}
}
bool first_line = false;
if (!p_split_current_line) {
if (p_split_current_line) {
insert_text_at_caret(ins, i);
} else {
insert_text(ins, cl, p_above ? 0 : get_line(cl).length(), p_above, p_above);
deselect(i);
if (p_above) {
if (cl > 0) {
set_caret_line(cl - 1, false, true, 0, i);
set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i);
} else {
set_caret_column(0, i == 0, i);
first_line = true;
}
} else {
set_caret_column(line.length(), i == 0, i);
}
set_caret_line(p_above ? cl : cl + 1, false, true, -1, i);
set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i);
}
insert_text_at_caret(ins, i);
if (first_line) {
set_caret_line(0, i == 0, true, 0, i);
} else if (brace_indent) {
if (brace_indent) {
// Move to inner indented line.
set_caret_line(get_caret_line(i) - 1, false, true, 0, i);
set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i);
}
}
end_multicaret_edit();
end_complex_operation();
}
@ -1712,27 +1619,8 @@ void CodeEdit::fold_line(int p_line) {
_set_line_as_hidden(i, true);
}
for (int i = 0; i < get_caret_count(); i++) {
// Fix selection.
if (has_selection(i)) {
if (_is_line_hidden(get_selection_from_line(i)) && _is_line_hidden(get_selection_to_line(i))) {
deselect(i);
} else if (_is_line_hidden(get_selection_from_line(i))) {
select(p_line, 9999, get_selection_to_line(i), get_selection_to_column(i), i);
} else if (_is_line_hidden(get_selection_to_line(i))) {
select(get_selection_from_line(i), get_selection_from_column(i), p_line, 9999, i);
}
}
// Reset caret.
if (_is_line_hidden(get_caret_line(i))) {
set_caret_line(p_line, false, false, 0, i);
set_caret_column(get_line(p_line).length(), false, i);
}
}
merge_overlapping_carets();
queue_redraw();
// Collapse any carets in the hidden area.
collapse_carets(p_line, get_line(p_line).length(), end_line, get_line(end_line).length(), true);
}
void CodeEdit::unfold_line(int p_line) {
@ -1781,6 +1669,23 @@ void CodeEdit::toggle_foldable_line(int p_line) {
fold_line(p_line);
}
void CodeEdit::toggle_foldable_lines_at_carets() {
begin_multicaret_edit();
int previous_line = -1;
Vector<int> sorted = get_sorted_carets();
for (int caret_idx : sorted) {
if (multicaret_edit_ignore_caret(caret_idx)) {
continue;
}
int line_idx = get_caret_line(caret_idx);
if (line_idx != previous_line) {
toggle_foldable_line(line_idx);
previous_line = line_idx;
}
}
end_multicaret_edit();
}
bool CodeEdit::is_line_folded(int p_line) const {
ERR_FAIL_INDEX_V(p_line, get_line_count(), false);
return p_line + 1 < get_line_count() && !_is_line_hidden(p_line) && _is_line_hidden(p_line + 1);
@ -1807,49 +1712,29 @@ void CodeEdit::create_code_region() {
WARN_PRINT_ONCE("Cannot create code region without any one line comment delimiters");
return;
}
begin_complex_operation();
// Merge selections if selection starts on the same line the previous one ends.
Vector<int> caret_edit_order = get_caret_index_edit_order();
Vector<int> carets_to_remove;
for (int i = 1; i < caret_edit_order.size(); i++) {
int current_caret = caret_edit_order[i - 1];
int next_caret = caret_edit_order[i];
if (get_selection_from_line(current_caret) == get_selection_to_line(next_caret)) {
select(get_selection_from_line(next_caret), get_selection_from_column(next_caret), get_selection_to_line(current_caret), get_selection_to_column(current_caret), next_caret);
carets_to_remove.append(current_caret);
}
}
// Sort and remove backwards to preserve indices.
carets_to_remove.sort();
for (int i = carets_to_remove.size() - 1; i >= 0; i--) {
remove_caret(carets_to_remove[i]);
}
String region_name = atr(ETR("New Code Region"));
// Adding start and end region tags.
int first_region_start = -1;
for (int caret_idx : get_caret_index_edit_order()) {
if (!has_selection(caret_idx)) {
continue;
}
int from_line = get_selection_from_line(caret_idx);
if (first_region_start == -1 || from_line < first_region_start) {
first_region_start = from_line;
}
int to_line = get_selection_to_line(caret_idx);
set_line(to_line, get_line(to_line) + "\n" + code_region_end_string);
insert_line_at(from_line, code_region_start_string + " " + atr(ETR("New Code Region")));
fold_line(from_line);
begin_complex_operation();
begin_multicaret_edit();
Vector<Point2i> line_ranges = get_line_ranges_from_carets(true, false);
// Add start and end region tags.
int line_offset = 0;
for (Point2i line_range : line_ranges) {
insert_text("\n" + code_region_end_string, line_range.y + line_offset, get_line(line_range.y + line_offset).length());
insert_line_at(line_range.x + line_offset, code_region_start_string + " " + region_name);
fold_line(line_range.x + line_offset);
line_offset += 2;
}
int first_region_start = line_ranges[0].x;
// Select name of the first region to allow quick edit.
remove_secondary_carets();
set_caret_line(first_region_start);
int tag_length = code_region_start_string.length() + atr(ETR("New Code Region")).length() + 1;
set_caret_column(tag_length);
int tag_length = code_region_start_string.length() + region_name.length() + 1;
select(first_region_start, code_region_start_string.length() + 1, first_region_start, tag_length);
end_multicaret_edit();
end_complex_operation();
queue_redraw();
}
String CodeEdit::get_code_region_start_tag() const {
@ -2248,8 +2133,12 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
char32_t caret_last_completion_char = 0;
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 (multicaret_edit_ignore_caret(i)) {
continue;
}
int caret_line = get_caret_line(i);
const String &insert_text = code_completion_options[code_completion_current_selected].insert_text;
@ -2282,8 +2171,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
// Replace.
remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_remove_line, caret_col);
adjust_carets_after_edit(i, caret_line, caret_col - code_completion_base.length(), caret_remove_line, caret_col);
set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i);
insert_text_at_caret(insert_text, i);
} else {
// Get first non-matching char.
@ -2299,8 +2186,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
// Remove base completion text.
remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i));
adjust_carets_after_edit(i, caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i));
set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i);
// Merge with text.
insert_text_at_caret(insert_text.substr(0, code_completion_base.length()), i);
@ -2325,12 +2210,10 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
if (has_string_delimiter(String::chr(last_completion_char))) {
if (post_brace_pair != -1 && last_char_matches) {
remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
}
} else {
if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && last_char_matches) {
remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
} else if (auto_brace_completion_enabled && pre_brace_pair != -1) {
insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key, i);
set_caret_column(get_caret_column(i) - auto_brace_completion_pairs[pre_brace_pair].close_key.length(), i == 0, i);
@ -2341,13 +2224,16 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column(i) + 1);
if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1)) {
remove_text(caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i));
adjust_carets_after_edit(i, caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i));
if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1) != pre_brace_pair) {
set_caret_column(get_caret_column(i) - 1, i == 0, i);
if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) + 1) != pre_brace_pair) {
set_caret_column(get_caret_column(i) + 1, i == 0, i);
} else {
set_caret_column(get_caret_column(i) + 2, i == 0, i);
}
}
}
}
end_multicaret_edit();
end_complex_operation();
cancel_code_completion();
@ -2430,65 +2316,154 @@ void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) {
}
/* Text manipulation */
void CodeEdit::duplicate_lines() {
void CodeEdit::move_lines_up() {
begin_complex_operation();
begin_multicaret_edit();
Vector<int> caret_edit_order = get_caret_index_edit_order();
for (const int &caret_index : caret_edit_order) {
// The text that will be inserted. All lines in one string.
String insert_text;
// The new line position of the caret after the operation.
int new_caret_line = get_caret_line(caret_index);
// The new column position of the caret after the operation.
int new_caret_column = get_caret_column(caret_index);
// The caret positions of the selection. Stays -1 if there is no selection.
int select_from_line = -1;
int select_to_line = -1;
int select_from_column = -1;
int select_to_column = -1;
// Number of lines of the selection.
int select_num_lines = -1;
if (has_selection(caret_index)) {
select_from_line = get_selection_from_line(caret_index);
select_to_line = get_selection_to_line(caret_index);
select_from_column = get_selection_from_column(caret_index);
select_to_column = get_selection_to_column(caret_index);
select_num_lines = select_to_line - select_from_line + 1;
for (int i = select_from_line; i <= select_to_line; i++) {
insert_text += "\n" + get_line(i);
unfold_line(i);
}
new_caret_line = select_to_line + select_num_lines;
} else {
insert_text = "\n" + get_line(new_caret_line);
new_caret_line++;
unfold_line(get_caret_line(caret_index));
// Move lines up by swapping each line with the one above it.
Vector<Point2i> line_ranges = get_line_ranges_from_carets();
for (Point2i line_range : line_ranges) {
if (line_range.x == 0) {
continue;
}
// The text will be inserted at the end of the current line.
set_caret_column(get_line(get_caret_line(caret_index)).length(), false, caret_index);
deselect(caret_index);
insert_text_at_caret(insert_text, caret_index);
set_caret_line(new_caret_line, false, true, 0, caret_index);
set_caret_column(new_caret_column, true, caret_index);
if (select_from_line != -1) {
// Advance the selection by the number of duplicated lines.
select_from_line += select_num_lines;
select_to_line += select_num_lines;
select(select_from_line, select_from_column, select_to_line, select_to_column, caret_index);
unfold_line(line_range.x - 1);
for (int line = line_range.x; line <= line_range.y; line++) {
unfold_line(line);
swap_lines(line - 1, line);
}
}
// Fix selection if it ended at column 0, since it wasn't moved.
for (int i = 0; i < get_caret_count(); i++) {
if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != 0) {
if (is_caret_after_selection_origin(i)) {
set_caret_line(get_caret_line(i) - 1, false, true, -1, i);
} else {
set_selection_origin_line(get_selection_origin_line(i) - 1, true, -1, i);
}
}
}
end_multicaret_edit();
end_complex_operation();
}
void CodeEdit::move_lines_down() {
begin_complex_operation();
begin_multicaret_edit();
Vector<Point2i> line_ranges = get_line_ranges_from_carets();
// Fix selection if it ended at column 0, since it won't be moved.
for (int i = 0; i < get_caret_count(); i++) {
if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != get_line_count() - 1) {
if (is_caret_after_selection_origin(i)) {
set_caret_line(get_caret_line(i) + 1, false, true, -1, i);
} else {
set_selection_origin_line(get_selection_origin_line(i) + 1, true, -1, i);
}
}
}
// Move lines down by swapping each line with the one below it.
for (Point2i line_range : line_ranges) {
if (line_range.y == get_line_count() - 1) {
continue;
}
unfold_line(line_range.y + 1);
for (int line = line_range.y; line >= line_range.x; line--) {
unfold_line(line);
swap_lines(line + 1, line);
}
}
end_multicaret_edit();
end_complex_operation();
}
void CodeEdit::delete_lines() {
begin_complex_operation();
begin_multicaret_edit();
Vector<Point2i> line_ranges = get_line_ranges_from_carets();
int line_offset = 0;
for (Point2i line_range : line_ranges) {
// Remove last line of range separately to preserve carets.
unfold_line(line_range.y + line_offset);
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;
}
// Deselect all.
deselect();
end_multicaret_edit();
end_complex_operation();
}
void CodeEdit::duplicate_selection() {
begin_complex_operation();
begin_multicaret_edit();
// Duplicate lines from carets without selections first.
for (int i = 0; i < get_caret_count(); i++) {
if (multicaret_edit_ignore_caret(i)) {
continue;
}
for (int l = get_selection_from_line(i); l <= get_selection_to_line(i); l++) {
unfold_line(l);
}
if (has_selection(i)) {
continue;
}
String text_to_insert = get_line(get_caret_line(i)) + "\n";
// Insert new text before the line, so the caret is on the second one.
insert_text(text_to_insert, get_caret_line(i), 0);
}
// Duplicate selections.
for (int i = 0; i < get_caret_count(); i++) {
if (multicaret_edit_ignore_caret(i)) {
continue;
}
if (!has_selection(i)) {
continue;
}
// Insert new text before the selection, so the caret is on the second one.
insert_text(get_selected_text(i), get_selection_from_line(i), get_selection_from_column(i));
}
end_multicaret_edit();
end_complex_operation();
}
void CodeEdit::duplicate_lines() {
begin_complex_operation();
begin_multicaret_edit();
Vector<Point2i> line_ranges = get_line_ranges_from_carets(false, false);
int line_offset = 0;
for (Point2i line_range : line_ranges) {
// The text that will be inserted. All lines in one string.
String text_to_insert;
for (int i = line_range.x + line_offset; i <= line_range.y + line_offset; i++) {
text_to_insert += get_line(i) + "\n";
unfold_line(i);
}
// Insert new text before the line.
insert_text(text_to_insert, line_range.x + line_offset, 0);
line_offset += line_range.y - line_range.x + 1;
}
end_multicaret_edit();
end_complex_operation();
queue_redraw();
}
/* Visual */
@ -2590,6 +2565,7 @@ void CodeEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("fold_all_lines"), &CodeEdit::fold_all_lines);
ClassDB::bind_method(D_METHOD("unfold_all_lines"), &CodeEdit::unfold_all_lines);
ClassDB::bind_method(D_METHOD("toggle_foldable_line", "line"), &CodeEdit::toggle_foldable_line);
ClassDB::bind_method(D_METHOD("toggle_foldable_lines_at_carets"), &CodeEdit::toggle_foldable_lines_at_carets);
ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded);
ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines);
@ -2691,6 +2667,10 @@ void CodeEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid);
/* Text manipulation */
ClassDB::bind_method(D_METHOD("move_lines_up"), &CodeEdit::move_lines_up);
ClassDB::bind_method(D_METHOD("move_lines_down"), &CodeEdit::move_lines_down);
ClassDB::bind_method(D_METHOD("delete_lines"), &CodeEdit::delete_lines);
ClassDB::bind_method(D_METHOD("duplicate_selection"), &CodeEdit::duplicate_selection);
ClassDB::bind_method(D_METHOD("duplicate_lines"), &CodeEdit::duplicate_lines);
/* Inspector */

View File

@ -316,6 +316,7 @@ protected:
// Overridable actions
virtual void _handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) override;
virtual void _backspace_internal(int p_caret) override;
virtual void _cut_internal(int p_caret) override;
GDVIRTUAL1(_confirm_code_completion, bool)
GDVIRTUAL1(_request_code_completion, bool)
@ -411,6 +412,7 @@ public:
void fold_all_lines();
void unfold_all_lines();
void toggle_foldable_line(int p_line);
void toggle_foldable_lines_at_carets();
bool is_line_folded(int p_line) const;
TypedArray<int> get_folded_lines() const;
@ -491,6 +493,10 @@ public:
void set_symbol_lookup_word_as_valid(bool p_valid);
/* Text manipulation */
void move_lines_up();
void move_lines_down();
void delete_lines();
void duplicate_selection();
void duplicate_lines();
CodeEdit();

View File

@ -0,0 +1,41 @@
/**************************************************************************/
/* text_edit.compat.inc */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef DISABLE_DEPRECATED
void TextEdit::_set_selection_mode_compat_86978(SelectionMode p_mode, int p_line, int p_column, int p_caret) {
set_selection_mode(p_mode);
}
void TextEdit::_bind_compatibility_methods() {
ClassDB::bind_compatibility_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::_set_selection_mode_compat_86978, DEFVAL(-1), DEFVAL(-1), DEFVAL(0));
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -409,11 +409,13 @@ private:
// Vector containing all the carets, index '0' is the "main caret" and should never be removed.
Vector<Caret> carets;
Vector<int> caret_index_edit_order;
bool setting_caret_line = false;
bool caret_pos_dirty = false;
bool caret_index_edit_dirty = true;
int multicaret_edit_count = 0;
bool multicaret_edit_merge_queued = false;
HashSet<int> multicaret_edit_ignore_carets;
CaretType caret_type = CaretType::CARET_TYPE_LINE;
@ -441,6 +443,8 @@ private:
int _get_column_x_offset_for_line(int p_char, int p_line, int p_column) const;
bool _is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges = true) const;
void _offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin = true, bool p_include_selection_end = true);
void _cancel_drag_and_drop_text();
/* Selection. */
@ -629,13 +633,15 @@ private:
void _move_caret_document_end(bool p_select);
bool _clear_carets_and_selection();
// Used in add_caret_at_carets
void _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 = -1) const;
protected:
void _notification(int p_what);
static void _bind_methods();
#ifndef DISABLE_DEPRECATED
void _set_selection_mode_compat_86978(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0);
static void _bind_compatibility_methods();
#endif // DISABLE_DEPRECATED
virtual void _update_theme_item_cache() override;
/* Internal API for CodeEdit, pending public API. */
@ -770,9 +776,11 @@ public:
void swap_lines(int p_from_line, int p_to_line);
void insert_line_at(int p_at, const String &p_text);
void insert_text_at_caret(const String &p_text, int p_caret = -1);
void insert_line_at(int p_line, const String &p_text);
void remove_line_at(int p_line, bool p_move_carets_down = true);
void insert_text_at_caret(const String &p_text, int p_caret = -1);
void insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin = true, bool p_before_selection_end = false);
void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column);
int get_last_unhidden_line() const;
@ -859,12 +867,17 @@ public:
int add_caret(int p_line, int p_column);
void remove_caret(int p_caret);
void remove_secondary_carets();
void merge_overlapping_carets();
int get_caret_count() const;
void add_caret_at_carets(bool p_below);
Vector<int> get_caret_index_edit_order();
void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col);
Vector<int> get_sorted_carets(bool p_include_ignored_carets = false) const;
void collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive = false);
void merge_overlapping_carets();
void begin_multicaret_edit();
void end_multicaret_edit();
bool is_in_mulitcaret_edit() const;
bool multicaret_edit_ignore_caret(int p_caret) const;
bool is_caret_visible(int p_caret = 0) const;
Point2 get_caret_draw_pos(int p_caret = 0) const;
@ -872,7 +885,7 @@ public:
void set_caret_line(int p_line, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0, int p_caret = 0);
int get_caret_line(int p_caret = 0) const;
void set_caret_column(int p_col, bool p_adjust_viewport = true, int p_caret = 0);
void set_caret_column(int p_column, bool p_adjust_viewport = true, int p_caret = 0);
int get_caret_column(int p_caret = 0) const;
int get_caret_wrap_index(int p_caret = 0) const;
@ -901,7 +914,7 @@ public:
bool has_selection(int p_caret = -1) const;
String get_selected_text(int p_caret = -1);
int get_selection_at_line_column(int p_line, int p_column, bool p_include_edges = true) const;
int get_selection_at_line_column(int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const;
Vector<Point2i> get_line_ranges_from_carets(bool p_only_selections = false, bool p_merge_adjacent = true) const;
TypedArray<Vector2i> get_line_ranges_from_carets_typed_array(bool p_only_selections = false, bool p_merge_adjacent = true) const;
@ -1055,6 +1068,15 @@ public:
Color get_font_color() const;
/* Deprecated. */
#ifndef DISABLE_DEPRECATED
Vector<int> get_caret_index_edit_order();
void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col);
int get_selection_line(int p_caret = 0) const;
int get_selection_column(int p_caret = 0) const;
#endif
TextEdit(const String &p_placeholder = String());
};

View File

@ -47,6 +47,9 @@ private:
Callable event_callback;
Callable input_event_callback;
String clipboard_text;
String primary_clipboard_text;
static Vector<String> get_rendering_drivers_func() {
Vector<String> drivers;
drivers.push_back("dummy");
@ -97,6 +100,8 @@ public:
switch (p_feature) {
case FEATURE_MOUSE:
case FEATURE_CURSOR_SHAPE:
case FEATURE_CLIPBOARD:
case FEATURE_CLIPBOARD_PRIMARY:
return true;
default: {
}
@ -131,6 +136,11 @@ public:
virtual Point2i mouse_get_position() const override { return mouse_position; }
virtual void clipboard_set(const String &p_text) override { clipboard_text = p_text; }
virtual String clipboard_get() const override { return clipboard_text; }
virtual void clipboard_set_primary(const String &p_text) override { primary_clipboard_text = p_text; }
virtual String clipboard_get_primary() const override { return primary_clipboard_text; }
virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override {
return Size2i(1920, 1080);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -136,6 +136,7 @@ int register_test_command(String p_command, TestFunc p_function);
// Requires Message Queue and InputMap to be setup.
// SEND_GUI_ACTION - takes an input map key. e.g SEND_GUI_ACTION("ui_text_newline").
// SEND_GUI_KEY_EVENT - takes a keycode set. e.g SEND_GUI_KEY_EVENT(Key::A | KeyModifierMask::META).
// SEND_GUI_KEY_UP_EVENT - takes a keycode set. e.g SEND_GUI_KEY_UP_EVENT(Key::A | KeyModifierMask::META).
// SEND_GUI_MOUSE_BUTTON_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None);
// SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None);
// SEND_GUI_MOUSE_MOTION_EVENT - takes a position, mouse mask and modifiers e.g SEND_GUI_MOUSE_MOTION_EVENT(Vector2(50, 50), MouseButtonMask::LEFT, KeyModifierMask::META);
@ -161,6 +162,14 @@ int register_test_command(String p_command, TestFunc p_function);
MessageQueue::get_singleton()->flush(); \
}
#define SEND_GUI_KEY_UP_EVENT(m_input) \
{ \
Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \
event->set_pressed(false); \
_SEND_DISPLAYSERVER_EVENT(event); \
MessageQueue::get_singleton()->flush(); \
}
#define _UPDATE_EVENT_MODIFERS(m_event, m_modifers) \
m_event->set_shift_pressed(((m_modifers) & KeyModifierMask::SHIFT) != Key::NONE); \
m_event->set_alt_pressed(((m_modifers) & KeyModifierMask::ALT) != Key::NONE); \