diff options
-rw-r--r-- | editor/editor_resource_picker.cpp | 20 | ||||
-rw-r--r-- | editor/editor_resource_picker.h | 4 | ||||
-rw-r--r-- | editor/import/resource_importer_obj.h | 3 | ||||
-rw-r--r-- | editor/plugins/animation_blend_tree_editor_plugin.cpp | 75 | ||||
-rw-r--r-- | editor/plugins/animation_blend_tree_editor_plugin.h | 18 | ||||
-rw-r--r-- | main/main.cpp | 2 | ||||
-rw-r--r-- | scene/gui/code_edit.cpp | 23 | ||||
-rw-r--r-- | scene/gui/code_edit.h | 2 | ||||
-rw-r--r-- | scene/gui/tab_container.cpp | 2 | ||||
-rw-r--r-- | scene/resources/font.cpp | 51 | ||||
-rw-r--r-- | scene/resources/material.cpp | 6 | ||||
-rw-r--r-- | tests/test_code_edit.h | 2255 | ||||
-rw-r--r-- | tests/test_macros.h | 37 |
13 files changed, 2454 insertions, 44 deletions
diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index a4ab749db4..31c62880e2 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -135,6 +135,10 @@ void EditorResourcePicker::_file_selected(const String &p_path) { _update_resource(); } +void EditorResourcePicker::_file_quick_selected() { + _file_selected(quick_open->get_selected()); +} + void EditorResourcePicker::_update_menu() { _update_menu_items(); @@ -153,7 +157,10 @@ void EditorResourcePicker::_update_menu_items() { // Add options for creating specific subtypes of the base resource type. set_create_options(edit_menu); - // Add an option to load a resource from a file. + // Add an option to load a resource from a file using the QuickOpen dialog. + edit_menu->add_icon_item(get_theme_icon(SNAME("Load"), SNAME("EditorIcons")), TTR("Quick Load"), OBJ_MENU_QUICKLOAD); + + // Add an option to load a resource from a file using the regular file dialog. edit_menu->add_icon_item(get_theme_icon(SNAME("Load"), SNAME("EditorIcons")), TTR("Load"), OBJ_MENU_LOAD); // Add options for changing existing value of the resource. @@ -246,6 +253,17 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { file_dialog->popup_file_dialog(); } break; + case OBJ_MENU_QUICKLOAD: { + if (!quick_open) { + quick_open = memnew(EditorQuickOpen); + add_child(quick_open); + quick_open->connect("quick_open", callable_mp(this, &EditorResourcePicker::_file_quick_selected)); + } + + quick_open->popup_dialog(base_type); + quick_open->set_title(TTR("Resource")); + } break; + case OBJ_MENU_EDIT: { if (edited_resource.is_valid()) { emit_signal(SNAME("resource_selected"), edited_resource); diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h index d77c31f831..d0dad0287b 100644 --- a/editor/editor_resource_picker.h +++ b/editor/editor_resource_picker.h @@ -32,6 +32,7 @@ #define EDITOR_RESOURCE_PICKER_H #include "editor_file_dialog.h" +#include "quick_open.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/popup_menu.h" @@ -54,9 +55,11 @@ class EditorResourcePicker : public HBoxContainer { TextureRect *preview_rect; Button *edit_button; EditorFileDialog *file_dialog = nullptr; + EditorQuickOpen *quick_open = nullptr; enum MenuOption { OBJ_MENU_LOAD, + OBJ_MENU_QUICKLOAD, OBJ_MENU_EDIT, OBJ_MENU_CLEAR, OBJ_MENU_MAKE_UNIQUE, @@ -75,6 +78,7 @@ class EditorResourcePicker : public HBoxContainer { void _update_resource_preview(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, ObjectID p_obj); void _resource_selected(); + void _file_quick_selected(); void _file_selected(const String &p_path); void _update_menu(); diff --git a/editor/import/resource_importer_obj.h b/editor/import/resource_importer_obj.h index 414e0c1fe6..1bb5ef33ce 100644 --- a/editor/import/resource_importer_obj.h +++ b/editor/import/resource_importer_obj.h @@ -64,6 +64,9 @@ public: virtual Error import(const String &p_source_file, const String &p_save_path, const Map<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files = nullptr, Variant *r_metadata = nullptr) override; + // Threaded import can currently cause deadlocks, see GH-48265. + virtual bool can_import_threaded() const override { return false; } + ResourceImporterOBJ(); }; diff --git a/editor/plugins/animation_blend_tree_editor_plugin.cpp b/editor/plugins/animation_blend_tree_editor_plugin.cpp index 030d90eeca..24cb660f7a 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.cpp +++ b/editor/plugins/animation_blend_tree_editor_plugin.cpp @@ -66,9 +66,13 @@ void AnimationNodeBlendTreeEditor::remove_custom_type(const Ref<Script> &p_scrip _update_options_menu(); } -void AnimationNodeBlendTreeEditor::_update_options_menu() { +void AnimationNodeBlendTreeEditor::_update_options_menu(bool p_has_input_ports) { add_node->get_popup()->clear(); + add_node->get_popup()->set_size(Size2i(-1, -1)); for (int i = 0; i < add_options.size(); i++) { + if (p_has_input_ports && add_options[i].input_port_count == 0) { + continue; + } add_node->get_popup()->add_item(add_options[i].name, i); } @@ -309,6 +313,11 @@ void AnimationNodeBlendTreeEditor::_add_node(int p_idx) { return; } + if (!from_node.is_empty() && anode->get_input_count() == 0) { + from_node = ""; + return; + } + Point2 instance_pos = graph->get_scroll_ofs(); if (use_popup_menu_position) { instance_pos += popup_menu_position; @@ -328,11 +337,51 @@ void AnimationNodeBlendTreeEditor::_add_node(int p_idx) { undo_redo->create_action(TTR("Add Node to BlendTree")); undo_redo->add_do_method(blend_tree.ptr(), "add_node", name, anode, instance_pos / EDSCALE); undo_redo->add_undo_method(blend_tree.ptr(), "remove_node", name); + + if (!from_node.is_empty()) { + undo_redo->add_do_method(blend_tree.ptr(), "connect_node", name, 0, from_node); + from_node = ""; + } + if (!to_node.is_empty() && to_slot != -1) { + undo_redo->add_do_method(blend_tree.ptr(), "connect_node", to_node, to_slot, name); + to_node = ""; + to_slot = -1; + } + undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->commit_action(); } +void AnimationNodeBlendTreeEditor::_popup(bool p_has_input_ports, const Vector2 &p_popup_position, const Vector2 &p_node_position) { + _update_options_menu(p_has_input_ports); + use_popup_menu_position = true; + popup_menu_position = p_popup_position; + add_node->get_popup()->set_position(p_node_position); + add_node->get_popup()->popup(); +} + +void AnimationNodeBlendTreeEditor::_popup_request(const Vector2 &p_position) { + _popup(false, graph->get_local_mouse_position(), p_position); +} + +void AnimationNodeBlendTreeEditor::_connection_to_empty(const String &p_from, int p_from_slot, const Vector2 &p_release_position) { + Ref<AnimationNode> node = blend_tree->get_node(p_from); + if (node.is_valid()) { + from_node = p_from; + _popup(true, p_release_position, graph->get_global_mouse_position()); + } +} + +void AnimationNodeBlendTreeEditor::_connection_from_empty(const String &p_to, int p_to_slot, const Vector2 &p_release_position) { + Ref<AnimationNode> node = blend_tree->get_node(p_to); + if (node.is_valid()) { + to_node = p_to; + to_slot = p_to_slot; + _popup(false, p_release_position, graph->get_global_mouse_position()); + } +} + void AnimationNodeBlendTreeEditor::_node_dragged(const Vector2 &p_from, const Vector2 &p_to, const StringName &p_which) { updating = true; undo_redo->create_action(TTR("Node Moved")); @@ -431,14 +480,6 @@ void AnimationNodeBlendTreeEditor::_delete_nodes_request() { undo_redo->commit_action(); } -void AnimationNodeBlendTreeEditor::_popup_request(const Vector2 &p_position) { - _update_options_menu(); - use_popup_menu_position = true; - popup_menu_position = graph->get_local_mouse_position(); - add_node->get_popup()->set_position(p_position); - add_node->get_popup()->popup(); -} - void AnimationNodeBlendTreeEditor::_node_selected(Object *p_node) { GraphNode *gn = Object::cast_to<GraphNode>(p_node); ERR_FAIL_COND(!gn); @@ -890,6 +931,8 @@ AnimationNodeBlendTreeEditor::AnimationNodeBlendTreeEditor() { graph->connect("scroll_offset_changed", callable_mp(this, &AnimationNodeBlendTreeEditor::_scroll_changed)); graph->connect("delete_nodes_request", callable_mp(this, &AnimationNodeBlendTreeEditor::_delete_nodes_request)); graph->connect("popup_request", callable_mp(this, &AnimationNodeBlendTreeEditor::_popup_request)); + graph->connect("connection_to_empty", callable_mp(this, &AnimationNodeBlendTreeEditor::_connection_to_empty)); + graph->connect("connection_from_empty", callable_mp(this, &AnimationNodeBlendTreeEditor::_connection_from_empty)); float graph_minimap_opacity = EditorSettings::get_singleton()->get("editors/visual_editors/minimap_opacity"); graph->set_minimap_opacity(graph_minimap_opacity); @@ -905,13 +948,13 @@ AnimationNodeBlendTreeEditor::AnimationNodeBlendTreeEditor() { add_node->connect("about_to_popup", callable_mp(this, &AnimationNodeBlendTreeEditor::_update_options_menu)); add_options.push_back(AddOption("Animation", "AnimationNodeAnimation")); - add_options.push_back(AddOption("OneShot", "AnimationNodeOneShot")); - add_options.push_back(AddOption("Add2", "AnimationNodeAdd2")); - add_options.push_back(AddOption("Add3", "AnimationNodeAdd3")); - add_options.push_back(AddOption("Blend2", "AnimationNodeBlend2")); - add_options.push_back(AddOption("Blend3", "AnimationNodeBlend3")); - add_options.push_back(AddOption("Seek", "AnimationNodeTimeSeek")); - add_options.push_back(AddOption("TimeScale", "AnimationNodeTimeScale")); + add_options.push_back(AddOption("OneShot", "AnimationNodeOneShot", 2)); + add_options.push_back(AddOption("Add2", "AnimationNodeAdd2", 2)); + add_options.push_back(AddOption("Add3", "AnimationNodeAdd3", 3)); + add_options.push_back(AddOption("Blend2", "AnimationNodeBlend2", 2)); + add_options.push_back(AddOption("Blend3", "AnimationNodeBlend3", 3)); + add_options.push_back(AddOption("Seek", "AnimationNodeTimeSeek", 1)); + add_options.push_back(AddOption("TimeScale", "AnimationNodeTimeScale", 1)); add_options.push_back(AddOption("Transition", "AnimationNodeTransition")); add_options.push_back(AddOption("BlendTree", "AnimationNodeBlendTree")); add_options.push_back(AddOption("BlendSpace1D", "AnimationNodeBlendSpace1D")); diff --git a/editor/plugins/animation_blend_tree_editor_plugin.h b/editor/plugins/animation_blend_tree_editor_plugin.h index 9f09069719..0fcafad40e 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.h +++ b/editor/plugins/animation_blend_tree_editor_plugin.h @@ -64,22 +64,28 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { Map<StringName, ProgressBar *> animations; Vector<EditorProperty *> visible_properties; + String to_node = ""; + int to_slot = -1; + String from_node = ""; + void _update_graph(); struct AddOption { String name; String type; Ref<Script> script; - AddOption(const String &p_name = String(), const String &p_type = String()) : + int input_port_count; + AddOption(const String &p_name = String(), const String &p_type = String(), bool p_input_port_count = 0) : name(p_name), - type(p_type) { + type(p_type), + input_port_count(p_input_port_count) { } }; Vector<AddOption> add_options; void _add_node(int p_idx); - void _update_options_menu(); + void _update_options_menu(bool p_has_input_ports = false); static AnimationNodeBlendTreeEditor *singleton; @@ -98,7 +104,6 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { void _anim_selected(int p_index, Array p_options, const String &p_node); void _delete_request(const String &p_which); void _delete_nodes_request(); - void _popup_request(const Vector2 &p_position); bool _update_filters(const Ref<AnimationNode> &anode); void _edit_filters(const String &p_which); @@ -106,6 +111,11 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { void _filter_toggled(); Ref<AnimationNode> _filter_edit; + void _popup(bool p_has_input_ports, const Vector2 &p_popup_position, const Vector2 &p_node_position); + void _popup_request(const Vector2 &p_position); + void _connection_to_empty(const String &p_from, int p_from_slot, const Vector2 &p_release_position); + void _connection_from_empty(const String &p_to, int p_to_slot, const Vector2 &p_release_position); + void _property_changed(const StringName &p_property, const Variant &p_value, const String &p_field, bool p_changing); void _removed_from_graph(); diff --git a/main/main.cpp b/main/main.cpp index 5513e571d6..532b1e80c9 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -455,10 +455,10 @@ void Main::test_cleanup() { ResourceLoader::remove_custom_loaders(); ResourceSaver::remove_custom_savers(); + unregister_driver_types(); #ifdef TOOLS_ENABLED EditorNode::unregister_editor_types(); #endif - unregister_driver_types(); unregister_module_types(); unregister_platform_apis(); unregister_scene_types(); diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index e7769f9372..4dfd6902e6 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -467,7 +467,7 @@ void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } /* MISC */ - if (k->is_action("ui_cancel", true)) { + if (!code_hint.is_empty() && k->is_action("ui_cancel", true)) { set_code_hint(""); accept_event(); return; @@ -1725,14 +1725,17 @@ bool CodeEdit::is_code_completion_enabled() const { void CodeEdit::set_code_completion_prefixes(const TypedArray<String> &p_prefixes) { code_completion_prefixes.clear(); for (int i = 0; i < p_prefixes.size(); i++) { - code_completion_prefixes.insert(p_prefixes[i]); + const String prefix = p_prefixes[i]; + + ERR_CONTINUE_MSG(prefix.is_empty(), "Code completion prefix cannot be empty."); + code_completion_prefixes.insert(prefix[0]); } } TypedArray<String> CodeEdit::get_code_completion_prefixes() const { TypedArray<String> prefixes; - for (Set<String>::Element *E = code_completion_prefixes.front(); E; E = E->next()) { - prefixes.push_back(E->get()); + for (const Set<char32_t>::Element *E = code_completion_prefixes.front(); E; E = E->next()) { + prefixes.push_back(String::chr(E->get())); } return prefixes; } @@ -1795,9 +1798,9 @@ void CodeEdit::request_code_completion(bool p_force) { String line = get_line(get_caret_line()); int ofs = CLAMP(get_caret_column(), 0, line.length()); - if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || _is_char(line[ofs - 1]) || code_completion_prefixes.has(String::chr(line[ofs - 1])))) { + if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || _is_char(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) { emit_signal(SNAME("request_code_completion")); - } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(String::chr(line[ofs - 2]))) { + } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(line[ofs - 2])) { emit_signal(SNAME("request_code_completion")); } } @@ -1969,7 +1972,7 @@ void CodeEdit::confirm_code_completion(bool p_replace) { end_complex_operation(); cancel_code_completion(); - if (code_completion_prefixes.has(String::chr(last_completion_char))) { + if (code_completion_prefixes.has(last_completion_char)) { request_code_completion(); } } @@ -2764,7 +2767,7 @@ void CodeEdit::_filter_code_completion_candidates_impl() { bool prev_is_word = false; /* Cancel if we are at the close of a string. */ - if (in_string == -1 && first_quote_col == cofs - 1) { + if (caret_column > 0 && in_string == -1 && first_quote_col == cofs - 1) { cancel_code_completion(); return; /* In a string, therefore we are trying to complete the string text. */ @@ -2790,9 +2793,9 @@ void CodeEdit::_filter_code_completion_candidates_impl() { /* If all else fails, check for a prefix. */ /* Single space between caret and prefix is okay. */ bool prev_is_prefix = false; - if (cofs > 0 && code_completion_prefixes.has(String::chr(line[cofs - 1]))) { + if (cofs > 0 && code_completion_prefixes.has(line[cofs - 1])) { prev_is_prefix = true; - } else if (cofs > 1 && line[cofs - 1] == ' ' && code_completion_prefixes.has(String::chr(line[cofs - 2]))) { + } else if (cofs > 1 && line[cofs - 1] == ' ' && code_completion_prefixes.has(line[cofs - 2])) { prev_is_prefix = true; } diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 740548d559..d8eccb7d32 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -214,7 +214,7 @@ private: int code_completion_longest_line = 0; Rect2i code_completion_rect; - Set<String> code_completion_prefixes; + Set<char32_t> code_completion_prefixes; List<ScriptCodeCompletionOption> code_completion_option_submitted; List<ScriptCodeCompletionOption> code_completion_option_sources; String code_completion_base; diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index 137ce7e96f..a423dc0173 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -538,7 +538,6 @@ void TabContainer::_notification(int p_what) { void TabContainer::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x) { Control *control = get_tab_control(p_index); RID canvas = get_canvas_item(); - Ref<Font> font = get_theme_font(SNAME("font")); Color font_outline_color = get_theme_color(SNAME("font_outline_color")); int outline_size = get_theme_constant(SNAME("outline_size")); int icon_text_distance = get_theme_constant(SNAME("icon_separation")); @@ -1134,7 +1133,6 @@ Size2 TabContainer::get_minimum_size() const { Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); - Ref<Font> font = get_theme_font(SNAME("font")); if (tabs_visible) { ms.y += MAX(MAX(tab_unselected->get_minimum_size().y, tab_selected->get_minimum_size().y), tab_disabled->get_minimum_size().y); diff --git a/scene/resources/font.cpp b/scene/resources/font.cpp index 7af8e900fd..29bd56eebd 100644 --- a/scene/resources/font.cpp +++ b/scene/resources/font.cpp @@ -689,227 +689,272 @@ void FontData::remove_cache(int p_cache_index) { } Array FontData::get_size_cache_list(int p_cache_index) const { + ERR_FAIL_COND_V(p_cache_index < 0, Array()); _ensure_rid(p_cache_index); return TS->font_get_size_cache_list(cache[p_cache_index]); } void FontData::clear_size_cache(int p_cache_index) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_clear_size_cache(cache[p_cache_index]); } void FontData::remove_size_cache(int p_cache_index, const Vector2i &p_size) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_remove_size_cache(cache[p_cache_index], p_size); } void FontData::set_variation_coordinates(int p_cache_index, const Dictionary &p_variation_coordinates) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_variation_coordinates(cache[p_cache_index], p_variation_coordinates); emit_changed(); } Dictionary FontData::get_variation_coordinates(int p_cache_index) const { + ERR_FAIL_COND_V(p_cache_index < 0, Dictionary()); _ensure_rid(p_cache_index); return TS->font_get_variation_coordinates(cache[p_cache_index]); } void FontData::set_ascent(int p_cache_index, int p_size, real_t p_ascent) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_ascent(cache[p_cache_index], p_size, p_ascent); } real_t FontData::get_ascent(int p_cache_index, int p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0.f); _ensure_rid(p_cache_index); return TS->font_get_ascent(cache[p_cache_index], p_size); } void FontData::set_descent(int p_cache_index, int p_size, real_t p_descent) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_descent(cache[p_cache_index], p_size, p_descent); } real_t FontData::get_descent(int p_cache_index, int p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0.f); _ensure_rid(p_cache_index); return TS->font_get_descent(cache[p_cache_index], p_size); } void FontData::set_underline_position(int p_cache_index, int p_size, real_t p_underline_position) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_underline_position(cache[p_cache_index], p_size, p_underline_position); } real_t FontData::get_underline_position(int p_cache_index, int p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0.f); _ensure_rid(p_cache_index); return TS->font_get_underline_position(cache[p_cache_index], p_size); } void FontData::set_underline_thickness(int p_cache_index, int p_size, real_t p_underline_thickness) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_underline_thickness(cache[p_cache_index], p_size, p_underline_thickness); } real_t FontData::get_underline_thickness(int p_cache_index, int p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0.f); _ensure_rid(p_cache_index); return TS->font_get_underline_thickness(cache[p_cache_index], p_size); } void FontData::set_scale(int p_cache_index, int p_size, real_t p_scale) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_scale(cache[p_cache_index], p_size, p_scale); } real_t FontData::get_scale(int p_cache_index, int p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0.f); _ensure_rid(p_cache_index); return TS->font_get_scale(cache[p_cache_index], p_size); } void FontData::set_spacing(int p_cache_index, int p_size, TextServer::SpacingType p_spacing, int p_value) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_spacing(cache[p_cache_index], p_size, p_spacing, p_value); } int FontData::get_spacing(int p_cache_index, int p_size, TextServer::SpacingType p_spacing) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0); _ensure_rid(p_cache_index); return TS->font_get_spacing(cache[p_cache_index], p_size, p_spacing); } int FontData::get_texture_count(int p_cache_index, const Vector2i &p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0); _ensure_rid(p_cache_index); return TS->font_get_texture_count(cache[p_cache_index], p_size); } void FontData::clear_textures(int p_cache_index, const Vector2i &p_size) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_clear_textures(cache[p_cache_index], p_size); } void FontData::remove_texture(int p_cache_index, const Vector2i &p_size, int p_texture_index) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_remove_texture(cache[p_cache_index], p_size, p_texture_index); } void FontData::set_texture_image(int p_cache_index, const Vector2i &p_size, int p_texture_index, const Ref<Image> &p_image) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_texture_image(cache[p_cache_index], p_size, p_texture_index, p_image); } Ref<Image> FontData::get_texture_image(int p_cache_index, const Vector2i &p_size, int p_texture_index) const { + ERR_FAIL_COND_V(p_cache_index < 0, Ref<Image>()); _ensure_rid(p_cache_index); return TS->font_get_texture_image(cache[p_cache_index], p_size, p_texture_index); } void FontData::set_texture_offsets(int p_cache_index, const Vector2i &p_size, int p_texture_index, const PackedInt32Array &p_offset) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_texture_offsets(cache[p_cache_index], p_size, p_texture_index, p_offset); } PackedInt32Array FontData::get_texture_offsets(int p_cache_index, const Vector2i &p_size, int p_texture_index) const { + ERR_FAIL_COND_V(p_cache_index < 0, PackedInt32Array()); _ensure_rid(p_cache_index); return TS->font_get_texture_offsets(cache[p_cache_index], p_size, p_texture_index); } Array FontData::get_glyph_list(int p_cache_index, const Vector2i &p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, Array()); _ensure_rid(p_cache_index); return TS->font_get_glyph_list(cache[p_cache_index], p_size); } void FontData::clear_glyphs(int p_cache_index, const Vector2i &p_size) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_clear_glyphs(cache[p_cache_index], p_size); } void FontData::remove_glyph(int p_cache_index, const Vector2i &p_size, int32_t p_glyph) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_remove_glyph(cache[p_cache_index], p_size, p_glyph); } void FontData::set_glyph_advance(int p_cache_index, int p_size, int32_t p_glyph, const Vector2 &p_advance) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_glyph_advance(cache[p_cache_index], p_size, p_glyph, p_advance); } Vector2 FontData::get_glyph_advance(int p_cache_index, int p_size, int32_t p_glyph) const { + ERR_FAIL_COND_V(p_cache_index < 0, Vector2()); _ensure_rid(p_cache_index); return TS->font_get_glyph_advance(cache[p_cache_index], p_size, p_glyph); } void FontData::set_glyph_offset(int p_cache_index, const Vector2i &p_size, int32_t p_glyph, const Vector2 &p_offset) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_glyph_offset(cache[p_cache_index], p_size, p_glyph, p_offset); } Vector2 FontData::get_glyph_offset(int p_cache_index, const Vector2i &p_size, int32_t p_glyph) const { + ERR_FAIL_COND_V(p_cache_index < 0, Vector2()); _ensure_rid(p_cache_index); return TS->font_get_glyph_offset(cache[p_cache_index], p_size, p_glyph); } void FontData::set_glyph_size(int p_cache_index, const Vector2i &p_size, int32_t p_glyph, const Vector2 &p_gl_size) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_glyph_size(cache[p_cache_index], p_size, p_glyph, p_gl_size); } Vector2 FontData::get_glyph_size(int p_cache_index, const Vector2i &p_size, int32_t p_glyph) const { + ERR_FAIL_COND_V(p_cache_index < 0, Vector2()); _ensure_rid(p_cache_index); return TS->font_get_glyph_size(cache[p_cache_index], p_size, p_glyph); } void FontData::set_glyph_uv_rect(int p_cache_index, const Vector2i &p_size, int32_t p_glyph, const Rect2 &p_uv_rect) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_glyph_uv_rect(cache[p_cache_index], p_size, p_glyph, p_uv_rect); } Rect2 FontData::get_glyph_uv_rect(int p_cache_index, const Vector2i &p_size, int32_t p_glyph) const { + ERR_FAIL_COND_V(p_cache_index < 0, Rect2()); _ensure_rid(p_cache_index); return TS->font_get_glyph_uv_rect(cache[p_cache_index], p_size, p_glyph); } void FontData::set_glyph_texture_idx(int p_cache_index, const Vector2i &p_size, int32_t p_glyph, int p_texture_idx) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_glyph_texture_idx(cache[p_cache_index], p_size, p_glyph, p_texture_idx); } int FontData::get_glyph_texture_idx(int p_cache_index, const Vector2i &p_size, int32_t p_glyph) const { + ERR_FAIL_COND_V(p_cache_index < 0, 0); _ensure_rid(p_cache_index); return TS->font_get_glyph_texture_idx(cache[p_cache_index], p_size, p_glyph); } Array FontData::get_kerning_list(int p_cache_index, int p_size) const { + ERR_FAIL_COND_V(p_cache_index < 0, Array()); _ensure_rid(p_cache_index); return TS->font_get_kerning_list(cache[p_cache_index], p_size); } void FontData::clear_kerning_map(int p_cache_index, int p_size) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_clear_kerning_map(cache[p_cache_index], p_size); } void FontData::remove_kerning(int p_cache_index, int p_size, const Vector2i &p_glyph_pair) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_remove_kerning(cache[p_cache_index], p_size, p_glyph_pair); } void FontData::set_kerning(int p_cache_index, int p_size, const Vector2i &p_glyph_pair, const Vector2 &p_kerning) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_set_kerning(cache[p_cache_index], p_size, p_glyph_pair, p_kerning); } Vector2 FontData::get_kerning(int p_cache_index, int p_size, const Vector2i &p_glyph_pair) const { + ERR_FAIL_COND_V(p_cache_index < 0, Vector2()); _ensure_rid(p_cache_index); return TS->font_get_kerning(cache[p_cache_index], p_size, p_glyph_pair); } void FontData::render_range(int p_cache_index, const Vector2i &p_size, char32_t p_start, char32_t p_end) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_render_range(cache[p_cache_index], p_size, p_start, p_end); } void FontData::render_glyph(int p_cache_index, const Vector2i &p_size, int32_t p_index) { + ERR_FAIL_COND(p_cache_index < 0); _ensure_rid(p_cache_index); TS->font_render_glyph(cache[p_cache_index], p_size, p_index); } RID FontData::get_cache_rid(int p_cache_index) const { + ERR_FAIL_COND_V(p_cache_index < 0, RID()); _ensure_rid(p_cache_index); return cache[p_cache_index]; } @@ -1008,10 +1053,8 @@ void Font::_data_changed() { void Font::_ensure_rid(int p_index) const { // Find or create cache record. - for (int i = 0; i < rids.size(); i++) { - if (!rids[i].is_valid() && data[i].is_valid()) { - rids.write[i] = data[i]->find_cache(variation_coordinates); - } + if (!rids[p_index].is_valid() && data[p_index].is_valid()) { + rids.write[p_index] = data[p_index]->find_cache(variation_coordinates); } } diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index 77a68151c4..643b0e34b9 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -790,12 +790,10 @@ void BaseMaterial3D::_update_shader() { } } break; case BILLBOARD_FIXED_Y: { - code += " MODELVIEW_MATRIX = INV_CAMERA_MATRIX * mat4(CAMERA_MATRIX[0],WORLD_MATRIX[1],vec4(normalize(cross(CAMERA_MATRIX[0].xyz,WORLD_MATRIX[1].xyz)), 0.0),WORLD_MATRIX[3]);\n"; + code += " MODELVIEW_MATRIX = INV_CAMERA_MATRIX * mat4(vec4(normalize(cross(vec3(0.0, 1.0, 0.0), CAMERA_MATRIX[2].xyz)),0.0),vec4(0.0, 1.0, 0.0, 0.0),vec4(normalize(cross(CAMERA_MATRIX[0].xyz, vec3(0.0, 1.0, 0.0))),0.0),WORLD_MATRIX[3]);\n"; if (flags[FLAG_BILLBOARD_KEEP_SCALE]) { - code += " MODELVIEW_MATRIX = MODELVIEW_MATRIX * mat4(vec4(length(WORLD_MATRIX[0].xyz), 0.0, 0.0, 0.0),vec4(0.0, 1.0, 0.0, 0.0),vec4(0.0, 0.0, length(WORLD_MATRIX[2].xyz), 0.0), vec4(0.0, 0.0, 0.0, 1.0));\n"; - } else { - code += " MODELVIEW_MATRIX = MODELVIEW_MATRIX * mat4(vec4(1.0, 0.0, 0.0, 0.0),vec4(0.0, 1.0/length(WORLD_MATRIX[1].xyz), 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0),vec4(0.0, 0.0, 0.0 ,1.0));\n"; + code += " MODELVIEW_MATRIX = MODELVIEW_MATRIX * mat4(vec4(length(WORLD_MATRIX[0].xyz), 0.0, 0.0, 0.0),vec4(0.0, length(WORLD_MATRIX[1].xyz), 0.0, 0.0),vec4(0.0, 0.0, length(WORLD_MATRIX[2].xyz), 0.0),vec4(0.0, 0.0, 0.0, 1.0));\n"; } } break; case BILLBOARD_PARTICLES: { diff --git a/tests/test_code_edit.h b/tests/test_code_edit.h index 9579d8ebef..fc8cec60af 100644 --- a/tests/test_code_edit.h +++ b/tests/test_code_edit.h @@ -808,6 +808,2261 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { memdelete(code_edit); } +TEST_CASE("[SceneTree][CodeEdit] delimiters") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + const Point2 OUTSIDE_DELIMETER = Point2(-1, -1); + + code_edit->clear_string_delimiters(); + code_edit->clear_comment_delimiters(); + + SUBCASE("[CodeEdit] add and remove delimiters") { + SUBCASE("[CodeEdit] add and remove string delimiters") { + /* Add a delimiter.*/ + code_edit->add_string_delimiter("\"", "\"", false); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Adding a duplicate start key is not allowed. */ + code_edit->add_string_delimiter("\"", "\'", false); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Adding a duplicate end key is allowed. */ + code_edit->add_string_delimiter("'", "\"", false); + CHECK(code_edit->has_string_delimiter("'")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Both start and end keys have to be symbols. */ + code_edit->add_string_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("@", "f", false); + CHECK_FALSE(code_edit->has_string_delimiter("@")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("f", "f", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Blank start keys are not allowed */ + code_edit->add_string_delimiter("", "#", false); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + ERR_PRINT_ON; + + /* Blank end keys are allowed. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 3); + + /* Remove a delimiter. */ + code_edit->remove_string_delimiter("#"); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Set should override existing, and test multiline */ + TypedArray<String> delimiters; + delimiters.push_back("^^ ^^"); + + code_edit->set_string_delimiters(delimiters); + CHECK_FALSE(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->has_string_delimiter("^^")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* clear should remove all. */ + code_edit->clear_string_delimiters(); + CHECK_FALSE(code_edit->has_string_delimiter("^^")); + CHECK(code_edit->get_string_delimiters().size() == 0); + } + + SUBCASE("[CodeEdit] add and remove comment delimiters") { + /* Add a delimiter.*/ + code_edit->add_comment_delimiter("\"", "\"", false); + CHECK(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Adding a duplicate start key is not allowed. */ + code_edit->add_comment_delimiter("\"", "\'", false); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Adding a duplicate end key is allowed. */ + code_edit->add_comment_delimiter("'", "\"", false); + CHECK(code_edit->has_comment_delimiter("'")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Both start and end keys have to be symbols. */ + code_edit->add_comment_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("@", "f", false); + CHECK_FALSE(code_edit->has_comment_delimiter("@")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("f", "f", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Blank start keys are not allowed. */ + code_edit->add_comment_delimiter("", "#", false); + CHECK_FALSE(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + ERR_PRINT_ON; + + /* Blank end keys are allowed. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 3); + + /* Remove a delimiter. */ + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Set should override existing, and test multiline. */ + TypedArray<String> delimiters; + delimiters.push_back("^^ ^^"); + + code_edit->set_comment_delimiters(delimiters); + CHECK_FALSE(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->has_comment_delimiter("^^")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* clear should remove all. */ + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->has_comment_delimiter("^^")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + } + + SUBCASE("[CodeEdit] add and remove mixed delimiters") { + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Disallow adding a string with the same start key as comment. */ + code_edit->add_string_delimiter("#", "", false); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + code_edit->add_string_delimiter("\"", "\"", false); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Disallow adding a comment with the same start key as string. */ + code_edit->add_comment_delimiter("\"", "", false); + CHECK_FALSE(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_ON; + + /* Cannot remove string with remove comment. */ + code_edit->remove_comment_delimiter("\""); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Cannot remove comment with remove string. */ + code_edit->remove_string_delimiter("#"); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Clear comments leave strings. */ + code_edit->clear_comment_delimiters(); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Clear string leave comments. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + code_edit->clear_string_delimiters(); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + } + } + + SUBCASE("[CodeEdit] single line delimiters") { + SUBCASE("[CodeEdit] single line string delimiters") { + /* Blank end key should set lineonly to true. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Insert line above, line with string then line below. */ + code_edit->insert_text_at_caret(" \n#\n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after start key is in string and start / end positions are correct. */ + CHECK(code_edit->is_in_string(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1)); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check region metadata. */ + int idx = code_edit->is_in_string(1, 1); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == ""); + + /* Check nested strings are handeled correctly. */ + code_edit->set_text(" \n# # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in string and start / end positions are correct. */ + CHECK(code_edit->is_in_string(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first. */ + CHECK(code_edit->is_in_string(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check is in string with no column retruns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # # \n "); + CHECK(code_edit->is_in_string(1) != -1); + + code_edit->set_text(" \n text # # \n "); + CHECK(code_edit->is_in_string(1) == -1); + + /* Removing delimiter should update. */ + code_edit->set_text(" \n # # \n "); + + code_edit->remove_string_delimiter("#"); + CHECK_FALSE(code_edit->has_string_delimiter("$")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + CHECK(code_edit->is_in_string(1) == -1); + + /* Adding and clear should update. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + CHECK(code_edit->is_in_string(1) != -1); + + code_edit->clear_string_delimiters(); + CHECK_FALSE(code_edit->has_string_delimiter("$")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + CHECK(code_edit->is_in_string(1) == -1); + } + + SUBCASE("[CodeEdit] single line comment delimiters") { + /* Blank end key should set lineonly to true. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Insert line above, line with comment then line below. */ + code_edit->insert_text_at_caret(" \n#\n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1)); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check region metadata. */ + int idx = code_edit->is_in_comment(1, 1); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == ""); + + /* Check nested comments are handeled correctly. */ + code_edit->set_text(" \n# # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first. */ + CHECK(code_edit->is_in_comment(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check is in comment with no column retruns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # # \n "); + CHECK(code_edit->is_in_comment(1) != -1); + + code_edit->set_text(" \n text # # \n "); + CHECK(code_edit->is_in_comment(1) == -1); + + /* Removing delimiter should update. */ + code_edit->set_text(" \n # # \n "); + + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + CHECK(code_edit->is_in_comment(1) == -1); + + /* Adding and clear should update. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + CHECK(code_edit->is_in_comment(1) != -1); + + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + CHECK(code_edit->is_in_comment(1) == -1); + } + + SUBCASE("[CodeEdit] single line mixed delimiters") { + /* Blank end key should set lineonly to true. */ + /* Add string delimiter. */ + code_edit->add_string_delimiter("&", "", false); + CHECK(code_edit->has_string_delimiter("&")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Nest a string delimiter inside a comment. */ + code_edit->set_text(" \n# & \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first, and does not state string. */ + CHECK(code_edit->is_in_comment(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + CHECK(code_edit->is_in_string(1, 5) == -1); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Remove the comment delimiter. */ + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + /* The "first" comment region is no longer valid. */ + CHECK(code_edit->is_in_comment(1, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 1) == OUTSIDE_DELIMETER); + + /* The "second" region as string is now valid. */ + CHECK(code_edit->is_in_string(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(4, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + } + } + + SUBCASE("[CodeEdit] multiline delimiters") { + SUBCASE("[CodeEdit] multiline string delimiters") { + code_edit->clear_string_delimiters(); + code_edit->clear_comment_delimiters(); + + /* Add string delimiter. */ + code_edit->add_string_delimiter("#", "#", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* First test over a single line. */ + code_edit->set_text(" \n # # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column before closing delimiter is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(1, 6) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check the region metadata. */ + int idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test over a multiple blank lines. */ + code_edit->set_text(" \n # \n\n # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check blank middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* Next test over a multiple non-blank lines. */ + code_edit->set_text(" \n # \n \n # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test nested strings. */ + code_edit->add_string_delimiter("^", "^", false); + CHECK(code_edit->has_string_delimiter("^")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test no end key. */ + code_edit->set_text(" \n # \n "); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1)); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in string with no column retruns true if entire line is string excluding whitespace. */ + code_edit->set_text(" \n # \n\n #\n "); + CHECK(code_edit->is_in_string(1) != -1); + CHECK(code_edit->is_in_string(2) != -1); + CHECK(code_edit->is_in_string(3) != -1); + + code_edit->set_text(" \n test # \n\n # test \n "); + CHECK(code_edit->is_in_string(1) == -1); + CHECK(code_edit->is_in_string(2) != -1); + CHECK(code_edit->is_in_string(3) == -1); + } + + SUBCASE("[CodeEdit] multiline comment delimiters") { + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "#", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* First test over a single line. */ + code_edit->set_text(" \n # # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column before closing delimiter is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 6) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check the region metadata. */ + int idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test over a multiple blank lines. */ + code_edit->set_text(" \n # \n\n # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check blank middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* Next test over a multiple non-blank lines. */ + code_edit->set_text(" \n # \n \n # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test nested comments. */ + code_edit->add_comment_delimiter("^", "^", false); + CHECK(code_edit->has_comment_delimiter("^")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test no end key. */ + code_edit->set_text(" \n # \n "); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1)); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in comment with no column retruns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # \n\n #\n "); + CHECK(code_edit->is_in_comment(1) != -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) != -1); + + code_edit->set_text(" \n test # \n\n # test \n "); + CHECK(code_edit->is_in_comment(1) == -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) == -1); + } + + SUBCASE("[CodeEdit] multiline mixed delimiters") { + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "#", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Add string delimiter. */ + code_edit->add_string_delimiter("^", "^", false); + CHECK(code_edit->has_string_delimiter("^")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Nest a string inside a comment. */ + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + int idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in comment with no column retruns true as inner delimiter should not be counted. */ + CHECK(code_edit->is_in_comment(1) != -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) != -1); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] indent") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + SUBCASE("[CodeEdit] indent settings") { + code_edit->set_indent_size(10); + CHECK(code_edit->get_indent_size() == 10); + CHECK(code_edit->get_tab_size() == 10); + + code_edit->set_auto_indent_enabled(false); + CHECK_FALSE(code_edit->is_auto_indent_enabled()); + + code_edit->set_auto_indent_enabled(true); + CHECK(code_edit->is_auto_indent_enabled()); + + code_edit->set_indent_using_spaces(false); + CHECK_FALSE(code_edit->is_indent_using_spaces()); + + code_edit->set_indent_using_spaces(true); + CHECK(code_edit->is_indent_using_spaces()); + + /* Only the first char is registered. */ + TypedArray<String> auto_indent_prefixes; + auto_indent_prefixes.push_back("::"); + auto_indent_prefixes.push_back("s"); + auto_indent_prefixes.push_back("1"); + code_edit->set_auto_indent_prefixes(auto_indent_prefixes); + + auto_indent_prefixes = code_edit->get_auto_indent_prefixes(); + CHECK(auto_indent_prefixes.has(":")); + CHECK(auto_indent_prefixes.has("s")); + CHECK(auto_indent_prefixes.has("1")); + } + + SUBCASE("[CodeEdit] indent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Do nothing if not editable. */ + code_edit->set_editable(false); + + code_edit->do_indent(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->indent_lines(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->set_editable(true); + + /* Simple indent. */ + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\t"); + + /* Check input action. */ + SEND_GUI_ACTION(code_edit, "ui_text_indent"); + CHECK(code_edit->get_line(0) == "\t\t"); + + /* Insert in place. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "test\t"); + + /* Indent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->indent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Selection does entire line. */ + code_edit->set_text("test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Handles multiple lines. */ + code_edit->set_text("test\ntext"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "\ttext"); + + /* Do not indent line if last col is zero. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + + /* Indent even if last column of first line. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 4, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_selection_from_column() == 2); + CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->get_line(0) == "\ttest"); + code_edit->undo(); + } + + SUBCASE("[CodeEdit] indent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Do nothing if not editable. */ + code_edit->set_editable(false); + + code_edit->do_indent(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->indent_lines(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->set_editable(true); + + /* Simple indent. */ + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " "); + + /* Check input action. */ + SEND_GUI_ACTION(code_edit, "ui_text_indent"); + CHECK(code_edit->get_line(0) == " "); + + /* Insert in place. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "test "); + + /* Indent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->indent_lines(); + CHECK(code_edit->get_line(0) == " test"); + + /* Selection does entire line. */ + code_edit->set_text("test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + + /* single indent only add required spaces. */ + code_edit->set_text(" test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Handles multiple lines. */ + code_edit->set_text("test\ntext"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == " text"); + + /* Do not indent line if last col is zero. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Indent even if last column of first line. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 4, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_selection_from_column() == 5); + CHECK(code_edit->get_selection_to_column() == 6); + CHECK(code_edit->get_line(0) == " test"); + } + + SUBCASE("[CodeEdit] unindent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Do nothing if not editable. */ + code_edit->set_text("\t"); + + code_edit->set_editable(false); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\t"); + + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\t"); + + code_edit->set_editable(true); + + /* Simple unindent. */ + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == ""); + + /* Should inindent inplace. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test\t"); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + + /* Backspace does a simple unindent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("\t"); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == ""); + + /* Unindent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("\ttest"); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + + /* Caret on col zero unindent line. */ + code_edit->set_text("\t\ttest"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Check input action. */ + code_edit->set_text("\t\ttest"); + SEND_GUI_ACTION(code_edit, "ui_text_dedent"); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Selection does entire line. */ + code_edit->set_text("\t\ttest"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Handles multiple lines. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Do not unindent line if last col is zero. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "\ttext"); + + /* Unindent even if last column of first line. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(0, 5, 1, 1); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("\ttest"); + code_edit->select(0, 1, 0, 2); + code_edit->do_unindent(); + CHECK(code_edit->get_selection_from_column() == 0); + CHECK(code_edit->get_selection_to_column() == 1); + CHECK(code_edit->get_line(0) == "test"); + } + + SUBCASE("[CodeEdit] unindent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Do nothing if not editable. */ + code_edit->set_text(" "); + + code_edit->set_editable(false); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " "); + + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " "); + + code_edit->set_editable(true); + + /* Simple unindent. */ + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == ""); + + /* Should inindent inplace. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test "); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + + /* Backspace does a simple unindent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" "); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == ""); + + /* Backspace with letter. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" a"); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == " "); + + /* Unindent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" test"); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + + /* Caret on col zero unindent line. */ + code_edit->set_text(" test"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Only as far as needed */ + code_edit->set_text(" test"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Check input action. */ + code_edit->set_text(" test"); + SEND_GUI_ACTION(code_edit, "ui_text_dedent"); + CHECK(code_edit->get_line(0) == " test"); + + /* Selection does entire line. */ + code_edit->set_text(" test"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Handles multiple lines. */ + code_edit->set_text(" test\n text"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Do not unindent line if last col is zero. */ + code_edit->set_text(" test\n text"); + code_edit->select(0, 0, 1, 0); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == " text"); + + /* Unindent even if last column of first line. */ + code_edit->set_text(" test\n text"); + code_edit->select(0, 5, 1, 1); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text(" test"); + code_edit->select(0, 4, 0, 5); + code_edit->do_unindent(); + CHECK(code_edit->get_selection_from_column() == 0); + CHECK(code_edit->get_selection_to_column() == 1); + CHECK(code_edit->get_line(0) == "test"); + } + + SUBCASE("[CodeEdit] auto indent") { + SUBCASE("[CodeEdit] auto indent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Simple indent on new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == "\t"); + + /* new blank line should still indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == "\t"); + + /* new line above should not indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test:"); + + /* Whitespace between symbol and caret is okay. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: "); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: "); + CHECK(code_edit->get_line(1) == "\t"); + + /* Comment between symbol and caret is okay. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # comment"); + CHECK(code_edit->get_line(1) == "\t"); + code_edit->remove_comment_delimiter("#"); + + /* Strings between symbol and caret are not okay. */ + code_edit->add_string_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # string"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # string"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_comment_delimiter("#"); + + /* If between brace pairs an extra line is added. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test{"); + CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_line(2) == "}"); + + /* Except when we are going above. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test{}"); + + /* or below. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test{}"); + CHECK(code_edit->get_line(1) == ""); + } + + SUBCASE("[CodeEdit] auto indent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Simple indent on new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == " "); + + /* new blank line should still indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == " "); + + /* new line above should not indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test:"); + + /* Whitespace between symbol and caret is okay. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: "); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: "); + CHECK(code_edit->get_line(1) == " "); + + /* Comment between symbol and caret is okay. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # comment"); + CHECK(code_edit->get_line(1) == " "); + code_edit->remove_comment_delimiter("#"); + + /* Strings between symbol and caret are not okay. */ + code_edit->add_string_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # string"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # string"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_comment_delimiter("#"); + + /* If between brace pairs an extra line is added. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test{"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_line(2) == "}"); + + /* Except when we are going above. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test{}"); + + /* or below. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test{}"); + CHECK(code_edit->get_line(1) == ""); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] folding") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + SUBCASE("[CodeEdit] folding settings") { + code_edit->set_line_folding_enabled(true); + CHECK(code_edit->is_line_folding_enabled()); + + code_edit->set_line_folding_enabled(false); + CHECK_FALSE(code_edit->is_line_folding_enabled()); + } + + SUBCASE("[CodeEdit] folding") { + code_edit->set_line_folding_enabled(true); + + // No indent. + code_edit->set_text("line1\nline2\nline3"); + for (int i = 0; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Indented lines. + code_edit->set_text("\tline1\n\tline2\n\tline3"); + for (int i = 0; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Indent. + code_edit->set_text("line1\n\tline2\nline3"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Nested indents. + code_edit->set_text("line1\n\tline2\n\t\tline3\nline4"); + CHECK(code_edit->can_fold_line(0)); + CHECK(code_edit->can_fold_line(1)); + for (int i = 2; i < 3; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK_FALSE(code_edit->is_line_folded(3)); + CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 2); + + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK_FALSE(code_edit->is_line_folded(3)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Check metadata. + CHECK(code_edit->get_folded_lines().size() == 1); + CHECK((int)code_edit->get_folded_lines()[0] == 0); + + // Cannot unfold nested. + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // (un)Fold all / toggle. + code_edit->unfold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Check metadata. + CHECK(code_edit->get_folded_lines().size() == 0); + + code_edit->fold_all_lines(); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + code_edit->unfold_all_lines(); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->toggle_foldable_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Can also unfold from hidden line. + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Blank lines. + code_edit->set_text("line1\n\tline2\n\n\n\ttest\n\nline3"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < code_edit->get_line_count(); i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + for (int i = 1; i < code_edit->get_line_count(); i++) { + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 6); + + // End of file. + code_edit->set_text("line1\n\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Comment & string blocks. + // Single line block + code_edit->add_comment_delimiter("#", "", true); + code_edit->set_text("#line1\n#\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test #line1\n#\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("#line1\ntest #\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // String. + code_edit->add_string_delimiter("^", "", true); + code_edit->set_text("^line1\n^\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test ^line1\n^\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("^line1\ntest ^\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Multiline blocks. + code_edit->add_comment_delimiter("&", "&", false); + code_edit->set_text("&line1\n\tline2&"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test &line1\n\tline2&"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("&line1\n\tline2& test"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Strings. + code_edit->add_string_delimiter("$", "$", false); + code_edit->set_text("$line1\n\tline2$"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test $line1\n\tline2$"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("$line1\n\tline2$ test"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Non-indented comments/ strings. + // Single line + code_edit->set_text("test\n\tline1\n#line1\n#line2\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + code_edit->set_text("test\n\tline1\n^line1\n^line2\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + // Multiline + code_edit->set_text("test\n\tline1\n&line1\nline2&\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + code_edit->set_text("test\n\tline1\n$line1\nline2$\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] completion") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + SUBCASE("[CodeEdit] auto brace completion") { + code_edit->set_auto_brace_completion_enabled(true); + CHECK(code_edit->is_auto_brace_completion_enabled()); + + code_edit->set_highlight_matching_braces_enabled(true); + CHECK(code_edit->is_highlight_matching_braces_enabled()); + + /* Try setters, any length. */ + Dictionary auto_brace_completion_pairs; + auto_brace_completion_pairs["["] = "]"; + auto_brace_completion_pairs["'"] = "'"; + auto_brace_completion_pairs[";"] = "'"; + auto_brace_completion_pairs["'''"] = "'''"; + code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + CHECK(code_edit->get_auto_brace_completion_pairs()["["] == "]"); + CHECK(code_edit->get_auto_brace_completion_pairs()["'"] == "'"); + CHECK(code_edit->get_auto_brace_completion_pairs()[";"] == "'"); + CHECK(code_edit->get_auto_brace_completion_pairs()["'''"] == "'''"); + + ERR_PRINT_OFF; + + /* No duplicate start keys. */ + code_edit->add_auto_brace_completion_pair("[", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + /* No empty keys. */ + code_edit->add_auto_brace_completion_pair("[", ""); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("", ""); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + /* Must be a symbol. */ + code_edit->add_auto_brace_completion_pair("a", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("[", "a"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("a", "a"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + ERR_PRINT_ON; + + /* Check metadata. */ + CHECK(code_edit->has_auto_brace_completion_open_key("[")); + CHECK(code_edit->has_auto_brace_completion_open_key("'")); + CHECK(code_edit->has_auto_brace_completion_open_key(";")); + CHECK(code_edit->has_auto_brace_completion_open_key("'''")); + CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("(")); + + CHECK(code_edit->has_auto_brace_completion_close_key("]")); + CHECK(code_edit->has_auto_brace_completion_close_key("'")); + CHECK(code_edit->has_auto_brace_completion_close_key("'''")); + CHECK_FALSE(code_edit->has_auto_brace_completion_close_key(")")); + + CHECK(code_edit->get_auto_brace_completion_close_key("[") == "]"); + CHECK(code_edit->get_auto_brace_completion_close_key("'") == "'"); + CHECK(code_edit->get_auto_brace_completion_close_key(";") == "'"); + CHECK(code_edit->get_auto_brace_completion_close_key("'''") == "'''"); + CHECK(code_edit->get_auto_brace_completion_close_key("(").is_empty()); + + /* Check typing inserts closing pair. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETLEFT); + CHECK(code_edit->get_line(0) == "[]"); + + /* Should first match and insert smaller key. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "''"); + CHECK(code_edit->get_caret_column() == 1); + + /* Move out from centre, Should match and insert larger key. */ + SEND_GUI_ACTION(code_edit, "ui_text_caret_right"); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "''''''"); + CHECK(code_edit->get_caret_column() == 3); + + /* Backspace should remove all. */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->get_line(0).is_empty()); + + /* If in between and typing close key should "skip". */ + SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETLEFT); + CHECK(code_edit->get_line(0) == "[]"); + CHECK(code_edit->get_caret_column() == 1); + SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETRIGHT); + CHECK(code_edit->get_line(0) == "[]"); + CHECK(code_edit->get_caret_column() == 2); + + /* If current is char and inserting a string, do not autocomplete. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_A); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "A'"); + + /* If in comment, do not complete. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_NUMBERSIGN); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "#'"); + + /* If in string, and inserting string do not complete. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + SEND_GUI_KEY_EVENT(code_edit, KEY_QUOTEDBL); + CHECK(code_edit->get_line(0) == "'\"'"); + } + + SUBCASE("[CodeEdit] autocomplete") { + code_edit->set_code_completion_enabled(true); + CHECK(code_edit->is_code_completion_enabled()); + + /* Set prefixes, single char only, disallow empty. */ + TypedArray<String> completion_prefixes; + completion_prefixes.push_back(""); + completion_prefixes.push_back("."); + completion_prefixes.push_back("."); + completion_prefixes.push_back(",,"); + + ERR_PRINT_OFF; + code_edit->set_code_completion_prefixes(completion_prefixes); + ERR_PRINT_ON; + completion_prefixes = code_edit->get_code_completion_prefixes(); + CHECK(completion_prefixes.size() == 2); + CHECK(completion_prefixes.has(".")); + CHECK(completion_prefixes.has(",")); + + code_edit->set_text("test\ntest"); + CHECK(code_edit->get_text_for_code_completion() == String::chr(0xFFFF) + "test\ntest"); + } + + SUBCASE("[CodeEdit] autocomplete request") { + SIGNAL_WATCH(code_edit, "request_code_completion"); + code_edit->set_code_completion_enabled(true); + + Array signal_args; + signal_args.push_back(Array()); + + /* Force request. */ + code_edit->request_code_completion(); + SIGNAL_CHECK_FALSE("request_code_completion"); + code_edit->request_code_completion(true); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Manual request should force. */ + SEND_GUI_ACTION(code_edit, "ui_text_completion_query"); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Insert prefix. */ + TypedArray<String> completion_prefixes; + completion_prefixes.push_back("."); + code_edit->set_code_completion_prefixes(completion_prefixes); + + code_edit->insert_text_at_caret("."); + code_edit->request_code_completion(); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Should work with space too. */ + code_edit->insert_text_at_caret(" "); + code_edit->request_code_completion(); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Should work when complete ends with prefix. */ + code_edit->clear(); + code_edit->insert_text_at_caret("t"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "test.", "test."); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test."); + SIGNAL_CHECK("request_code_completion", signal_args); + + SIGNAL_UNWATCH(code_edit, "request_code_completion"); + } + + SUBCASE("[CodeEdit] autocomplete completion") { + CHECK(code_edit->get_code_completion_selected_index() == -1); + code_edit->set_code_completion_enabled(true); + CHECK(code_edit->get_code_completion_selected_index() == -1); + + code_edit->update_code_completion_options(); + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_code_completion_option(0).size() == 0); + CHECK(code_edit->get_code_completion_options().size() == 0); + + /* Adding does not update the list. */ + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_0.", "item_0"); + + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_code_completion_option(0).size() == 0); + CHECK(code_edit->get_code_completion_options().size() == 0); + + /* After update, pending add should not be counted, */ + /* also does not work on col 0 */ + code_edit->insert_text_at_caret("i"); + code_edit->update_code_completion_options(); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0", Color(1, 0, 0), RES(), Color(1, 0, 0)); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_1.", "item_1"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_2.", "item_2"); + + ERR_PRINT_OFF; + code_edit->set_code_completion_selected_index(1); + ERR_PRINT_ON; + CHECK(code_edit->get_code_completion_selected_index() == 0); + CHECK(code_edit->get_code_completion_option(0).size() == 6); + CHECK(code_edit->get_code_completion_options().size() == 1); + + /* Check cancel closes completion. */ + SEND_GUI_ACTION(code_edit, "ui_cancel"); + CHECK(code_edit->get_code_completion_selected_index() == -1); + + code_edit->update_code_completion_options(); + CHECK(code_edit->get_code_completion_selected_index() == 0); + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == 1); + CHECK(code_edit->get_code_completion_option(0).size() == 6); + CHECK(code_edit->get_code_completion_options().size() == 3); + + /* Check data. */ + Dictionary option = code_edit->get_code_completion_option(0); + CHECK((int)option["kind"] == (int)CodeEdit::CodeCompletionKind::KIND_CLASS); + CHECK(option["display_text"] == "item_0."); + CHECK(option["insert_text"] == "item_0"); + CHECK(option["font_color"] == Color(1, 0, 0)); + CHECK(option["icon"] == RES()); + CHECK(option["default_value"] == Color(1, 0, 0)); + + /* Set size for mouse input. */ + code_edit->set_size(Size2(100, 100)); + + /* Check input. */ + SEND_GUI_ACTION(code_edit, "ui_end"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_home"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_page_down"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_page_up"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_up"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_down"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_KEY_EVENT(code_edit, KEY_T); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_left"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_right"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + Point2 caret_pos = code_edit->get_caret_draw_pos(); + caret_pos.y -= code_edit->get_line_height(); + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_NONE); + CHECK(code_edit->get_code_completion_selected_index() == 1); + + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_NONE); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + /* Single click selects. */ + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MASK_LEFT); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + /* Double click inserts. */ + SEND_GUI_DOUBLE_CLICK(code_edit, caret_pos); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_line(0) == "item_2"); + + code_edit->set_auto_brace_completion_enabled(false); + + /* Does nothing in readonly. */ + code_edit->undo(); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + code_edit->set_editable(false); + code_edit->confirm_code_completion(); + code_edit->set_editable(true); + CHECK(code_edit->get_line(0) == "i"); + + /* Replace */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0 test"); + + /* Replace string. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test\""); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "\"item_0\""); + + /* Normal replace if no end is given. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "\"item_0\" test"); + + /* Insert at completion. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_accept"); + CHECK(code_edit->get_line(0) == "item_01 test"); + + /* Insert at completion with string should have same output. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test\""); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_accept"); + CHECK(code_edit->get_line(0) == "\"item_0\"1 test\""); + + /* Merge symbol at end on insert text. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* Full set. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* Autobrace completion. */ + code_edit->set_auto_brace_completion_enabled(true); + + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 7); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* Full set. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] symbol lookup") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + code_edit->set_symbol_lookup_on_click_enabled(true); + CHECK(code_edit->is_symbol_lookup_on_click_enabled()); + + /* Set size for mouse input. */ + code_edit->set_size(Size2(100, 100)); + + code_edit->set_text("this is some text"); + + Point2 caret_pos = code_edit->get_caret_draw_pos(); + caret_pos.x += 55; + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE); + CHECK(code_edit->get_text_for_symbol_lookup() == "this is s" + String::chr(0xFFFF) + "ome text"); + + SIGNAL_WATCH(code_edit, "symbol_validate"); + +#ifdef OSX_ENABLED + SEND_GUI_KEY_EVENT(code_edit, KEY_META); +#else + SEND_GUI_KEY_EVENT(code_edit, KEY_CTRL); +#endif + + Array signal_args; + Array arg; + arg.push_back("some"); + signal_args.push_back(arg); + SIGNAL_CHECK("symbol_validate", signal_args); + + SIGNAL_UNWATCH(code_edit, "symbol_validate"); + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] line length guidelines") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + TypedArray<int> guide_lines; + + code_edit->set_line_length_guidelines(guide_lines); + CHECK(code_edit->get_line_length_guidelines().size() == 0); + + guide_lines.push_back(80); + guide_lines.push_back(120); + + /* Order should be preserved. */ + code_edit->set_line_length_guidelines(guide_lines); + CHECK((int)code_edit->get_line_length_guidelines()[0] == 80); + CHECK((int)code_edit->get_line_length_guidelines()[1] == 120); + + memdelete(code_edit); +} + } // namespace TestCodeEdit #endif // TEST_CODE_EDIT_H diff --git a/tests/test_macros.h b/tests/test_macros.h index bf1001fa67..2f0bc6dcfa 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -131,8 +131,12 @@ int register_test_command(String p_command, TestFunc p_function); register_test_command(m_command, m_function); \ DOCTEST_GLOBAL_NO_WARNINGS_END() -// Utility macro to send an action event to a given object +// Utility macros to send an event actions to a given object // Requires Message Queue and InputMap to be setup. +// SEND_GUI_ACTION - takes an object and a input map key. e.g SEND_GUI_ACTION(code_edit, "ui_text_newline"). +// SEND_GUI_KEY_EVENT - takes an object and a keycode set. e.g SEND_GUI_KEY_EVENT(code_edit, KEY_A | KEY_MASK_CMD). +// SEND_GUI_MOUSE_EVENT - takes an object, position, mouse button and mouse mask e.g SEND_GUI_MOUSE_EVENT(code_edit, Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE); +// SEND_GUI_DOUBLE_CLICK - takes an object and a postion. e.g SEND_GUI_DOUBLE_CLICK(code_edit, Vector2(50, 50)); #define SEND_GUI_ACTION(m_object, m_action) \ { \ @@ -144,6 +148,37 @@ int register_test_command(String p_command, TestFunc p_function); MessageQueue::get_singleton()->flush(); \ } +#define SEND_GUI_KEY_EVENT(m_object, m_input) \ + { \ + Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \ + event->set_pressed(true); \ + m_object->gui_input(event); \ + MessageQueue::get_singleton()->flush(); \ + } + +#define _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask) \ + Ref<InputEventMouseButton> event; \ + event.instantiate(); \ + event->set_position(m_local_pos); \ + event->set_button_index(m_input); \ + event->set_button_mask(m_mask); \ + event->set_pressed(true); + +#define SEND_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask) \ + { \ + _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask); \ + m_object->get_viewport()->push_input(event); \ + MessageQueue::get_singleton()->flush(); \ + } + +#define SEND_GUI_DOUBLE_CLICK(m_object, m_local_pos) \ + { \ + _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_LEFT); \ + event->set_double_click(true); \ + m_object->get_viewport()->push_input(event); \ + MessageQueue::get_singleton()->flush(); \ + } + // Utility class / macros for testing signals // // Use SIGNAL_WATCH(*object, "signal_name") to start watching |