From d2e651f403b7f583a66e37ef0331362ad70fd1c3 Mon Sep 17 00:00:00 2001 From: PucklaMotzer09 Date: Wed, 28 Sep 2022 17:09:45 +0200 Subject: [PATCH] Add Duplicate Lines shortcut to CodeTextEditor This keyboard shortcut has been made with inspiration from the VS Code keyboard shortcut editor.action.copyLinesDownAction. It duplicates all selected lines and inserts them below no matter where the caret is within the line. --- doc/classes/CodeEdit.xml | 6 +++ editor/code_editor.cpp | 5 ++ editor/plugins/script_text_editor.cpp | 6 +++ editor/plugins/script_text_editor.h | 1 + editor/plugins/text_editor.cpp | 4 ++ editor/plugins/text_editor.h | 1 + editor/plugins/text_shader_editor.cpp | 4 ++ editor/plugins/text_shader_editor.h | 1 + scene/gui/code_edit.cpp | 65 ++++++++++++++++++++++ scene/gui/code_edit.h | 3 ++ tests/scene/test_code_edit.h | 78 +++++++++++++++++++++++++++ 11 files changed, 174 insertions(+) diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index d9146cf604b..9b4b3aa36e6 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. + + + + 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. + + diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 876fef078b2..9d2b54ef1a5 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -805,6 +805,11 @@ void CodeTextEditor::input(const Ref &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 &p_event) { diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index d5ad21e3467..dc3f88705bb 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 0efe7d54e3b..8c0ba6d7e66 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 92ee468bc29..b0a69cfb5cb 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 d1aa2a90478..a4ec2d882b0 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 b68b283a61e..27f42608c68 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 03827f1b83d..eba2ec0bb93 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 d35d35d36de..e7a2a26a29c 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 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 e688af2bda9..97c435b52d0 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 8576b38ce26..b44a47bf8ad 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