diff options
-rw-r--r-- | doc/classes/CodeEdit.xml | 6 | ||||
-rw-r--r-- | editor/code_editor.cpp | 5 | ||||
-rw-r--r-- | editor/plugins/script_text_editor.cpp | 6 | ||||
-rw-r--r-- | editor/plugins/script_text_editor.h | 1 | ||||
-rw-r--r-- | editor/plugins/text_editor.cpp | 4 | ||||
-rw-r--r-- | editor/plugins/text_editor.h | 1 | ||||
-rw-r--r-- | editor/plugins/text_shader_editor.cpp | 4 | ||||
-rw-r--r-- | editor/plugins/text_shader_editor.h | 1 | ||||
-rw-r--r-- | scene/gui/code_edit.cpp | 65 | ||||
-rw-r--r-- | scene/gui/code_edit.h | 3 | ||||
-rw-r--r-- | tests/scene/test_code_edit.h | 78 |
11 files changed, 174 insertions, 0 deletions
diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index d9146cf604..9b4b3aa36e 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -152,6 +152,12 @@ Perform an indent as if the user activated the "ui_text_indent" action. </description> </method> + <method name="duplicate_lines"> + <return type="void" /> + <description> + 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="fold_all_lines"> <return type="void" /> <description> diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 876fef078b..9d2b54ef1a 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -805,6 +805,11 @@ void CodeTextEditor::input(const Ref<InputEvent> &event) { accept_event(); return; } + if (ED_IS_SHORTCUT("script_text_editor/duplicate_lines", key_event)) { + text_editor->duplicate_lines(); + accept_event(); + return; + } } void CodeTextEditor::_text_editor_gui_input(const Ref<InputEvent> &p_event) { diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index d5ad21e346..dc3f88705b 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -1306,6 +1306,9 @@ void ScriptTextEditor::_edit_option(int p_op) { case EDIT_DUPLICATE_SELECTION: { code_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()) { @@ -2173,6 +2176,7 @@ void ScriptTextEditor::_enable_code_editor() { edit_menu->get_popup()->add_separator(); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_select_all"), EDIT_SELECT_ALL); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION); + edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_lines"), EDIT_DUPLICATE_LINES); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP); edit_menu->get_popup()->add_separator(); @@ -2395,6 +2399,8 @@ void ScriptTextEditor::register_editor() { ED_SHORTCUT("script_text_editor/unfold_all_lines", TTR("Unfold All Lines"), Key::NONE); ED_SHORTCUT("script_text_editor/duplicate_selection", TTR("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D); ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C); + ED_SHORTCUT("script_text_editor/duplicate_lines", TTR("Duplicate Lines"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::DOWN); + ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_lines", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::DOWN); ED_SHORTCUT("script_text_editor/evaluate_selection", TTR("Evaluate Selection"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::E); ED_SHORTCUT("script_text_editor/toggle_word_wrap", TTR("Toggle Word Wrap"), KeyModifierMask::ALT | Key::Z); ED_SHORTCUT("script_text_editor/trim_trailing_whitespace", TTR("Trim Trailing Whitespace"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::T); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index 0efe7d54e3..8c0ba6d7e6 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -126,6 +126,7 @@ class ScriptTextEditor : public ScriptEditorBase { EDIT_UNINDENT, EDIT_DELETE_LINE, EDIT_DUPLICATE_SELECTION, + EDIT_DUPLICATE_LINES, EDIT_PICK_COLOR, EDIT_TO_UPPERCASE, EDIT_TO_LOWERCASE, diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index 92ee468bc2..b0a69cfb5c 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -392,6 +392,9 @@ void TextEditor::_edit_option(int p_op) { case EDIT_DUPLICATE_SELECTION: { code_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()) { @@ -651,6 +654,7 @@ TextEditor::TextEditor() { edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unfold_all_lines"), EDIT_UNFOLD_ALL_LINES); edit_menu->get_popup()->add_separator(); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION); + edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_lines"), EDIT_DUPLICATE_LINES); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/trim_trailing_whitespace"), EDIT_TRIM_TRAILING_WHITESAPCE); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_indent_to_spaces"), EDIT_CONVERT_INDENT_TO_SPACES); diff --git a/editor/plugins/text_editor.h b/editor/plugins/text_editor.h index d1aa2a9047..a4ec2d882b 100644 --- a/editor/plugins/text_editor.h +++ b/editor/plugins/text_editor.h @@ -71,6 +71,7 @@ private: EDIT_UNINDENT, EDIT_DELETE_LINE, EDIT_DUPLICATE_SELECTION, + EDIT_DUPLICATE_LINES, EDIT_TO_UPPERCASE, EDIT_TO_LOWERCASE, EDIT_CAPITALIZE, diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index b68b283a61..27f42608c6 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -671,6 +671,9 @@ void TextShaderEditor::_menu_option(int p_option) { case EDIT_DUPLICATE_SELECTION: { shader_editor->duplicate_selection(); } break; + case EDIT_DUPLICATE_LINES: { + shader_editor->get_text_editor()->duplicate_lines(); + } break; case EDIT_TOGGLE_WORD_WRAP: { TextEdit::LineWrappingMode wrap = shader_editor->get_text_editor()->get_line_wrapping_mode(); shader_editor->get_text_editor()->set_line_wrapping_mode(wrap == TextEdit::LINE_WRAPPING_BOUNDARY ? TextEdit::LINE_WRAPPING_NONE : TextEdit::LINE_WRAPPING_BOUNDARY); @@ -1122,6 +1125,7 @@ TextShaderEditor::TextShaderEditor() { edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/delete_line"), EDIT_DELETE_LINE); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION); + edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_lines"), EDIT_DUPLICATE_LINES); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP); edit_menu->get_popup()->add_separator(); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_completion_query"), EDIT_COMPLETE); diff --git a/editor/plugins/text_shader_editor.h b/editor/plugins/text_shader_editor.h index 03827f1b83..eba2ec0bb9 100644 --- a/editor/plugins/text_shader_editor.h +++ b/editor/plugins/text_shader_editor.h @@ -120,6 +120,7 @@ class TextShaderEditor : public MarginContainer { EDIT_UNINDENT, EDIT_DELETE_LINE, EDIT_DUPLICATE_SELECTION, + EDIT_DUPLICATE_LINES, EDIT_TOGGLE_WORD_WRAP, EDIT_TOGGLE_COMMENT, EDIT_COMPLETE, diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index d35d35d36d..e7a2a26a29 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -2399,6 +2399,68 @@ void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) { } } +/* Text manipulation */ +void CodeEdit::duplicate_lines() { + begin_complex_operation(); + + 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)); + } + + // 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); + } + } + + end_complex_operation(); + queue_redraw(); +} + /* Visual */ Color CodeEdit::_get_brace_mismatch_color() const { return theme_cache.brace_mismatch_color; @@ -2598,6 +2660,9 @@ 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("duplicate_lines"), &CodeEdit::duplicate_lines); + /* Inspector */ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "symbol_lookup_on_click"), "set_symbol_lookup_on_click_enabled", "is_symbol_lookup_on_click_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "line_folding"), "set_line_folding_enabled", "is_line_folding_enabled"); diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index e688af2bda..97c435b52d 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -486,6 +486,9 @@ public: void set_symbol_lookup_word_as_valid(bool p_valid); + /* Text manipulation */ + void duplicate_lines(); + CodeEdit(); ~CodeEdit(); }; diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h index 8576b38ce2..b44a47bf8a 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -3886,6 +3886,84 @@ TEST_CASE("[SceneTree][CodeEdit] New Line") { memdelete(code_edit); } +TEST_CASE("[SceneTree][CodeEdit] Duplicate Lines") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + code_edit->set_text(R"(extends Node + +func _ready(): + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"); + + /* Duplicate a single line without selection. */ + code_edit->set_caret_line(0); + code_edit->duplicate_lines(); + CHECK(code_edit->get_line(0) == "extends Node"); + CHECK(code_edit->get_line(1) == "extends Node"); + CHECK(code_edit->get_line(2) == ""); + + /* Duplicate multiple lines with selection. */ + code_edit->set_caret_line(6); + code_edit->set_caret_column(15); + code_edit->select(4, 8, 6, 15); + code_edit->duplicate_lines(); + CHECK(code_edit->get_line(6) == "\tvar c := a + b"); + CHECK(code_edit->get_line(7) == "\tvar a := len(OS.get_cmdline_args())"); + CHECK(code_edit->get_line(8) == "\tvar b := get_child_count()"); + CHECK(code_edit->get_line(9) == "\tvar c := a + b"); + CHECK(code_edit->get_line(10) == "\tfor i in range(c):"); + + /* Duplicate single lines with multiple carets. */ + code_edit->deselect(); + code_edit->set_caret_line(10); + code_edit->set_caret_column(1); + code_edit->add_caret(11, 2); + code_edit->add_caret(12, 1); + code_edit->duplicate_lines(); + CHECK(code_edit->get_line(9) == "\tvar c := a + b"); + CHECK(code_edit->get_line(10) == "\tfor i in range(c):"); + CHECK(code_edit->get_line(11) == "\tfor i in range(c):"); + CHECK(code_edit->get_line(12) == "\t\tprint(\"This is the solution: \", sin(i))"); + CHECK(code_edit->get_line(13) == "\t\tprint(\"This is the solution: \", sin(i))"); + CHECK(code_edit->get_line(14) == "\tvar pos = get_index() - 1"); + CHECK(code_edit->get_line(15) == "\tvar pos = get_index() - 1"); + CHECK(code_edit->get_line(16) == "\tprint(\"Make sure this exits: %b\" % pos)"); + + /* Duplicate multiple lines with multiple carets. */ + code_edit->select(0, 0, 1, 2, 0); + code_edit->select(3, 0, 4, 2, 1); + code_edit->select(16, 0, 17, 0, 2); + code_edit->set_caret_line(1, false, true, 0, 0); + code_edit->set_caret_column(2, false, 0); + code_edit->set_caret_line(4, false, true, 0, 1); + code_edit->set_caret_column(2, false, 1); + code_edit->set_caret_line(17, false, true, 0, 2); + code_edit->set_caret_column(0, false, 2); + code_edit->duplicate_lines(); + CHECK(code_edit->get_line(1) == "extends Node"); + CHECK(code_edit->get_line(2) == "extends Node"); + CHECK(code_edit->get_line(3) == "extends Node"); + CHECK(code_edit->get_line(4) == ""); + CHECK(code_edit->get_line(6) == "\tvar a := len(OS.get_cmdline_args())"); + CHECK(code_edit->get_line(7) == "func _ready():"); + CHECK(code_edit->get_line(8) == "\tvar a := len(OS.get_cmdline_args())"); + CHECK(code_edit->get_line(9) == "\tvar b := get_child_count()"); + CHECK(code_edit->get_line(20) == "\tprint(\"Make sure this exits: %b\" % pos)"); + CHECK(code_edit->get_line(21) == ""); + CHECK(code_edit->get_line(22) == "\tprint(\"Make sure this exits: %b\" % pos)"); + CHECK(code_edit->get_line(23) == ""); + + memdelete(code_edit); +} + } // namespace TestCodeEdit #endif // TEST_CODE_EDIT_H |