diff options
| -rw-r--r-- | core/object/script_language.cpp | 47 | ||||
| -rw-r--r-- | core/object/script_language.h | 14 | ||||
| -rw-r--r-- | doc/classes/CodeEdit.xml | 14 | ||||
| -rw-r--r-- | doc/classes/ScriptLanguageExtension.xml | 2 | ||||
| -rw-r--r-- | editor/code_editor.cpp | 2 | ||||
| -rw-r--r-- | editor/plugins/script_text_editor.cpp | 2 | ||||
| -rw-r--r-- | editor/plugins/script_text_editor.h | 47 | ||||
| -rw-r--r-- | modules/gdscript/gdscript_editor.cpp | 15 | ||||
| -rw-r--r-- | scene/gui/code_edit.cpp | 228 | ||||
| -rw-r--r-- | scene/gui/code_edit.h | 20 | ||||
| -rw-r--r-- | tests/scene/test_code_edit.h | 96 |
11 files changed, 288 insertions, 199 deletions
diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index 71f40660f4..6f047d80aa 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -34,7 +34,6 @@ #include "core/core_string_names.h" #include "core/debugger/engine_debugger.h" #include "core/debugger/script_debugger.h" -#include "core/variant/typed_array.h" #include <stdint.h> @@ -461,6 +460,52 @@ void ScriptLanguage::get_core_type_words(List<String> *p_core_type_words) const void ScriptLanguage::frame() { } +TypedArray<int> ScriptLanguage::CodeCompletionOption::get_option_characteristics(const String &p_base) { + // Return characacteristics of the match found by order of importance. + // Matches will be ranked by a lexicographical order on the vector returned by this function. + // The lower values indicate better matches and that they should go before in the order of appearance. + if (last_matches == matches) { + return charac; + } + charac.clear(); + // Ensure base is not empty and at the same time that matches is not empty too. + if (p_base.length() == 0) { + last_matches = matches; + charac.push_back(location); + return charac; + } + charac.push_back(matches.size()); + charac.push_back((matches[0].first == 0) ? 0 : 1); + charac.push_back(location); + const char32_t *target_char = &p_base[0]; + int bad_case = 0; + for (const Pair<int, int> &match_segment : matches) { + const char32_t *string_to_complete_char = &display[match_segment.first]; + for (int j = 0; j < match_segment.second; j++, string_to_complete_char++, target_char++) { + if (*string_to_complete_char != *target_char) { + bad_case++; + } + } + } + charac.push_back(bad_case); + charac.push_back(matches[0].first); + last_matches = matches; + return charac; +} + +void ScriptLanguage::CodeCompletionOption::clear_characteristics() { + charac = TypedArray<int>(); +} + +TypedArray<int> ScriptLanguage::CodeCompletionOption::get_option_cached_characteristics() const { + // Only returns the cached value and warns if it was not updated since the last change of matches. + if (last_matches != matches) { + WARN_PRINT("Characteristics are not up to date."); + } + + return charac; +} + bool PlaceHolderScriptInstance::set(const StringName &p_name, const Variant &p_value) { if (script->is_placeholder_fallback_enabled()) { return false; diff --git a/core/object/script_language.h b/core/object/script_language.h index 696c9a94a5..829f01fbcc 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -35,6 +35,7 @@ #include "core/io/resource.h" #include "core/templates/pair.h" #include "core/templates/rb_map.h" +#include "core/variant/typed_array.h" class ScriptLanguage; template <typename T> @@ -305,8 +306,8 @@ public: virtual Error open_in_external_editor(const Ref<Script> &p_script, int p_line, int p_col) { return ERR_UNAVAILABLE; } virtual bool overrides_external_editor() { return false; } - /* Keep enum in Sync with: */ - /* /scene/gui/code_edit.h - CodeEdit::CodeCompletionKind */ + // Keep enums in sync with: + // scene/gui/code_edit.h - CodeEdit::CodeCompletionKind enum CodeCompletionKind { CODE_COMPLETION_KIND_CLASS, CODE_COMPLETION_KIND_FUNCTION, @@ -321,6 +322,7 @@ public: CODE_COMPLETION_KIND_MAX }; + // scene/gui/code_edit.h - CodeEdit::CodeCompletionLocation enum CodeCompletionLocation { LOCATION_LOCAL = 0, LOCATION_PARENT_MASK = 1 << 8, @@ -336,6 +338,7 @@ public: Ref<Resource> icon; Variant default_value; Vector<Pair<int, int>> matches; + Vector<Pair<int, int>> last_matches; int location = LOCATION_OTHER; CodeCompletionOption() {} @@ -346,6 +349,13 @@ public: kind = p_kind; location = p_location; } + + TypedArray<int> get_option_characteristics(const String &p_base); + void clear_characteristics(); + TypedArray<int> get_option_cached_characteristics() const; + + private: + TypedArray<int> charac; }; virtual Error complete_code(const String &p_code, const String &p_path, Object *p_owner, List<CodeCompletionOption> *r_options, bool &r_force, String &r_call_hint) { return ERR_UNAVAILABLE; } diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index 84e1c80900..d88b0db72e 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -49,8 +49,10 @@ <param index="3" name="text_color" type="Color" default="Color(1, 1, 1, 1)" /> <param index="4" name="icon" type="Resource" default="null" /> <param index="5" name="value" type="Variant" default="0" /> + <param index="6" name="location" type="int" default="1024" /> <description> Submits an item to the queue of potential candidates for the autocomplete menu. Call [method update_code_completion_options] to update the list. + [param location] indicates location of the option relative to the location of the code completion query. See [enum CodeEdit.CodeCompletionLocation] for how to set this value. [b]Note:[/b] This list will replace all current candidates. </description> </method> @@ -560,6 +562,18 @@ <constant name="KIND_PLAIN_TEXT" value="9" enum="CodeCompletionKind"> Marks the option as unclassified or plain text. </constant> + <constant name="LOCATION_LOCAL" value="0" enum="CodeCompletionLocation"> + The option is local to the location of the code completion query - e.g. a local variable. Subsequent value of location represent options from the outer class, the exact value represent how far they are (in terms of inner classes). + </constant> + <constant name="LOCATION_PARENT_MASK" value="256" enum="CodeCompletionLocation"> + The option is from the containing class or a parent class, relative to the location of the code completion query. Perform a bitwise OR with the class depth (e.g. 0 for the local class, 1 for the parent, 2 for the grandparent, etc) to store the depth of an option in the class or a parent class. + </constant> + <constant name="LOCATION_OTHER_USER_CODE" value="512" enum="CodeCompletionLocation"> + The option is from user code which is not local and not in a derived class (e.g. Autoload Singletons). + </constant> + <constant name="LOCATION_OTHER" value="1024" enum="CodeCompletionLocation"> + The option is from other engine code, not covered by the other enum constants - e.g. built-in classes. + </constant> </constants> <theme_items> <theme_item name="background_color" data_type="color" type="Color" default="Color(0, 0, 0, 0)"> diff --git a/doc/classes/ScriptLanguageExtension.xml b/doc/classes/ScriptLanguageExtension.xml index 20017291a0..43388554b3 100644 --- a/doc/classes/ScriptLanguageExtension.xml +++ b/doc/classes/ScriptLanguageExtension.xml @@ -358,7 +358,7 @@ <constant name="LOOKUP_RESULT_MAX" value="9" enum="LookupResultType"> </constant> <constant name="LOCATION_LOCAL" value="0" enum="CodeCompletionLocation"> - The option is local to the location of the code completion query - e.g. a local variable. + The option is local to the location of the code completion query - e.g. a local variable. Subsequent value of location represent options from the outer class, the exact value represent how far they are (in terms of inner classes). </constant> <constant name="LOCATION_PARENT_MASK" value="256" enum="CodeCompletionLocation"> The option is from the containing class or a parent class, relative to the location of the code completion query. Perform a bitwise OR with the class depth (e.g. 0 for the local class, 1 for the parent, 2 for the grandparent, etc) to store the depth of an option in the class or a parent class. diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index b188e1faca..50b1de93bf 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -955,7 +955,7 @@ void CodeTextEditor::_complete_request() { } else if (e.insert_text.begins_with("#") || e.insert_text.begins_with("//")) { font_color = completion_comment_color; } - text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value); + text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value, e.location); } text_editor->update_code_completion_options(forced); } diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index ba6c90d157..cd700d953a 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -753,8 +753,6 @@ void ScriptTextEditor::_code_complete_script(const String &p_code, List<ScriptLa String hint; Error err = script->get_language()->complete_code(p_code, script->get_path(), base, r_options, r_force, hint); - r_options->sort_custom_inplace<CodeCompletionOptionCompare>(); - if (err == OK) { code_editor->get_text_editor()->set_code_hint(hint); } diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index 5e167af51a..808a6417e4 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -259,51 +259,4 @@ public: ~ScriptTextEditor(); }; -const int KIND_COUNT = 10; -// The order in which to sort code completion options. -const ScriptLanguage::CodeCompletionKind KIND_SORT_ORDER[KIND_COUNT] = { - ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE, - ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, - ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, - ScriptLanguage::CODE_COMPLETION_KIND_ENUM, - ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, - ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, - ScriptLanguage::CODE_COMPLETION_KIND_CLASS, - ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH, - ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH, - ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT, -}; - -// The custom comparer which will sort completion options. -struct CodeCompletionOptionCompare { - _FORCE_INLINE_ bool operator()(const ScriptLanguage::CodeCompletionOption &l, const ScriptLanguage::CodeCompletionOption &r) const { - if (l.location == r.location) { - // If locations are same, sort on kind - if (l.kind == r.kind) { - // If kinds are same, sort alphanumeric - return l.display < r.display; - } - - // Sort kinds based on the const sorting array defined above. Lower index = higher priority. - int l_index = -1; - int r_index = -1; - for (int i = 0; i < KIND_COUNT; i++) { - const ScriptLanguage::CodeCompletionKind kind = KIND_SORT_ORDER[i]; - l_index = kind == l.kind ? i : l_index; - r_index = kind == r.kind ? i : r_index; - - if (l_index != -1 && r_index != -1) { - return l_index < r_index; - } - } - - // This return should never be hit unless something goes wrong. - // l and r should always have a Kind which is in the sort order array. - return l.display < r.display; - } - - return l.location < r.location; - } -}; - #endif // SCRIPT_TEXT_EDITOR_H diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 829567d734..09af51656c 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -906,19 +906,20 @@ static void _list_available_types(bool p_inherit_only, GDScriptParser::Completio } } -static void _find_identifiers_in_suite(const GDScriptParser::SuiteNode *p_suite, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result) { +static void _find_identifiers_in_suite(const GDScriptParser::SuiteNode *p_suite, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth = 0) { for (int i = 0; i < p_suite->locals.size(); i++) { ScriptLanguage::CodeCompletionOption option; + int location = p_recursion_depth == 0 ? ScriptLanguage::LOCATION_LOCAL : (p_recursion_depth | ScriptLanguage::LOCATION_PARENT_MASK); if (p_suite->locals[i].type == GDScriptParser::SuiteNode::Local::CONSTANT) { - option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, ScriptLanguage::LOCATION_LOCAL); + option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location); option.default_value = p_suite->locals[i].constant->initializer->reduced_value; } else { - option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE, ScriptLanguage::LOCATION_LOCAL); + option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE, location); } r_result.insert(option.display, option); } if (p_suite->parent_block) { - _find_identifiers_in_suite(p_suite->parent_block, r_result); + _find_identifiers_in_suite(p_suite->parent_block, r_result, p_recursion_depth + 1); } } @@ -933,7 +934,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, int classes_processed = 0; while (clss) { for (int i = 0; i < clss->members.size(); i++) { - const int location = (classes_processed + p_recursion_depth) | ScriptLanguage::LOCATION_PARENT_MASK; + const int location = p_recursion_depth == 0 ? classes_processed : (p_recursion_depth | ScriptLanguage::LOCATION_PARENT_MASK); const GDScriptParser::ClassNode::Member &member = clss->members[i]; ScriptLanguage::CodeCompletionOption option; switch (member.type) { @@ -1025,7 +1026,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base while (!base_type.has_no_type()) { switch (base_type.kind) { case GDScriptParser::DataType::CLASS: { - _find_identifiers_in_class(base_type.class_type, p_only_functions, base_type.is_meta_type, false, r_result, p_recursion_depth + 1); + _find_identifiers_in_class(base_type.class_type, p_only_functions, base_type.is_meta_type, false, r_result, p_recursion_depth); // This already finds all parent identifiers, so we are done. base_type = GDScriptParser::DataType(); } break; @@ -1205,7 +1206,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context } if (p_context.current_class) { - _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result, p_recursion_depth + 1); + _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result, p_recursion_depth); } List<StringName> functions; diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index d83182c775..efbc067d20 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -142,13 +142,12 @@ void CodeEdit::_notification(int p_what) { Point2 match_pos = Point2(code_completion_rect.position.x + icon_area_size.x + theme_cache.code_completion_icon_separation, code_completion_rect.position.y + i * row_height); for (int j = 0; j < code_completion_options[l].matches.size(); j++) { - Pair<int, int> match = code_completion_options[l].matches[j]; - int match_offset = theme_cache.font->get_string_size(code_completion_options[l].display.substr(0, match.first), HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width; - int match_len = theme_cache.font->get_string_size(code_completion_options[l].display.substr(match.first, match.second), HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width; + Pair<int, int> match_segment = code_completion_options[l].matches[j]; + int match_offset = theme_cache.font->get_string_size(code_completion_options[l].display.substr(0, match_segment.first), HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width; + int match_len = theme_cache.font->get_string_size(code_completion_options[l].display.substr(match_segment.first, match_segment.second), HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width; draw_rect(Rect2(match_pos + Point2(match_offset, 0), Size2(match_len, row_height)), theme_cache.code_completion_existing_color); } - tl->draw(ci, title_pos, code_completion_options[l].font_color); } @@ -2031,7 +2030,7 @@ void CodeEdit::request_code_completion(bool p_force) { } } -void CodeEdit::add_code_completion_option(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color, const Ref<Resource> &p_icon, const Variant &p_value) { +void CodeEdit::add_code_completion_option(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color, const Ref<Resource> &p_icon, const Variant &p_value, int p_location) { ScriptLanguage::CodeCompletionOption completion_option; completion_option.kind = (ScriptLanguage::CodeCompletionKind)p_type; completion_option.display = p_display_text; @@ -2039,6 +2038,7 @@ void CodeEdit::add_code_completion_option(CodeCompletionKind p_type, const Strin completion_option.font_color = p_text_color; completion_option.icon = p_icon; completion_option.default_value = p_value; + completion_option.location = p_location; code_completion_option_submitted.push_back(completion_option); } @@ -2063,6 +2063,7 @@ TypedArray<Dictionary> CodeEdit::get_code_completion_options() const { option["insert_text"] = code_completion_options[i].insert_text; option["font_color"] = code_completion_options[i].font_color; option["icon"] = code_completion_options[i].icon; + option["location"] = code_completion_options[i].location; option["default_value"] = code_completion_options[i].default_value; completion_options[i] = option; } @@ -2081,6 +2082,7 @@ Dictionary CodeEdit::get_code_completion_option(int p_index) const { option["insert_text"] = code_completion_options[p_index].insert_text; option["font_color"] = code_completion_options[p_index].font_color; option["icon"] = code_completion_options[p_index].icon; + option["location"] = code_completion_options[p_index].location; option["default_value"] = code_completion_options[p_index].default_value; return option; } @@ -2424,9 +2426,14 @@ void CodeEdit::_bind_methods() { BIND_ENUM_CONSTANT(KIND_FILE_PATH); BIND_ENUM_CONSTANT(KIND_PLAIN_TEXT); + BIND_ENUM_CONSTANT(LOCATION_LOCAL); + BIND_ENUM_CONSTANT(LOCATION_PARENT_MASK); + BIND_ENUM_CONSTANT(LOCATION_OTHER_USER_CODE) + BIND_ENUM_CONSTANT(LOCATION_OTHER); + ClassDB::bind_method(D_METHOD("get_text_for_code_completion"), &CodeEdit::get_text_for_code_completion); ClassDB::bind_method(D_METHOD("request_code_completion", "force"), &CodeEdit::request_code_completion, DEFVAL(false)); - ClassDB::bind_method(D_METHOD("add_code_completion_option", "type", "display_text", "insert_text", "text_color", "icon", "value"), &CodeEdit::add_code_completion_option, DEFVAL(Color(1, 1, 1)), DEFVAL(Ref<Resource>()), DEFVAL(Variant::NIL)); + ClassDB::bind_method(D_METHOD("add_code_completion_option", "type", "display_text", "insert_text", "text_color", "icon", "value", "location"), &CodeEdit::add_code_completion_option, DEFVAL(Color(1, 1, 1)), DEFVAL(Ref<Resource>()), DEFVAL(Variant::NIL), DEFVAL(LOCATION_OTHER)); ClassDB::bind_method(D_METHOD("update_code_completion_options", "force"), &CodeEdit::update_code_completion_options); ClassDB::bind_method(D_METHOD("get_code_completion_options"), &CodeEdit::get_code_completion_options); ClassDB::bind_method(D_METHOD("get_code_completion_option", "index"), &CodeEdit::get_code_completion_option); @@ -2954,6 +2961,7 @@ void CodeEdit::_filter_code_completion_candidates_impl() { option["font_color"] = E.font_color; option["icon"] = E.icon; option["default_value"] = E.default_value; + option["location"] = E.location; completion_options_sources[i] = option; i++; } @@ -2977,6 +2985,7 @@ void CodeEdit::_filter_code_completion_candidates_impl() { option.insert_text = completion_options[i].get("insert_text"); option.font_color = completion_options[i].get("font_color"); option.icon = completion_options[i].get("icon"); + option.location = completion_options[i].get("location"); option.default_value = completion_options[i].get("default_value"); int offset = 0; @@ -3063,7 +3072,7 @@ void CodeEdit::_filter_code_completion_candidates_impl() { } /* Filter Options. */ - /* For now handle only tradional quoted strings. */ + /* For now handle only traditional quoted strings. */ bool single_quote = in_string != -1 && first_quote_col > 0 && delimiters[in_string].start_key == "'"; code_completion_options.clear(); @@ -3075,23 +3084,16 @@ void CodeEdit::_filter_code_completion_candidates_impl() { return; } - Vector<ScriptLanguage::CodeCompletionOption> completion_options_casei; - Vector<ScriptLanguage::CodeCompletionOption> completion_options_substr; - Vector<ScriptLanguage::CodeCompletionOption> completion_options_substr_casei; - Vector<ScriptLanguage::CodeCompletionOption> completion_options_subseq; - Vector<ScriptLanguage::CodeCompletionOption> completion_options_subseq_casei; - int max_width = 0; String string_to_complete_lower = string_to_complete.to_lower(); + for (ScriptLanguage::CodeCompletionOption &option : code_completion_option_sources) { + option.matches.clear(); if (single_quote && option.display.is_quoted()) { option.display = option.display.unquote().quote("'"); } - int offset = 0; - if (option.default_value.get_type() == Variant::COLOR) { - offset = line_height; - } + int offset = option.default_value.get_type() == Variant::COLOR ? line_height : 0; if (in_string != -1) { String quote = single_quote ? "'" : "\""; @@ -3104,6 +3106,7 @@ void CodeEdit::_filter_code_completion_candidates_impl() { } if (string_to_complete.length() == 0) { + option.get_option_characteristics(string_to_complete); code_completion_options.push_back(option); if (theme_cache.font.is_valid()) { max_width = MAX(max_width, theme_cache.font->get_string_size(option.display, HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width + offset); @@ -3111,139 +3114,73 @@ void CodeEdit::_filter_code_completion_candidates_impl() { continue; } - /* This code works the same as: - - if (option.display.begins_with(s)) { - completion_options.push_back(option); - } else if (option.display.to_lower().begins_with(s.to_lower())) { - completion_options_casei.push_back(option); - } else if (s.is_subsequence_of(option.display)) { - completion_options_subseq.push_back(option); - } else if (s.is_subsequence_ofn(option.display)) { - completion_options_subseq_casei.push_back(option); - } - - But is more performant due to being inlined and looping over the characters only once - */ - - String display_lower = option.display.to_lower(); - - const char32_t *ssq = &string_to_complete[0]; - const char32_t *ssq_lower = &string_to_complete_lower[0]; - - const char32_t *tgt = &option.display[0]; - const char32_t *tgt_lower = &display_lower[0]; + String target_lower = option.display.to_lower(); + const char32_t *string_to_complete_char_lower = &string_to_complete_lower[0]; + const char32_t *target_char_lower = &target_lower[0]; - const char32_t *sst = &string_to_complete[0]; - const char32_t *sst_lower = &display_lower[0]; - - Vector<Pair<int, int>> ssq_matches; - int ssq_match_start = 0; - int ssq_match_len = 0; - - Vector<Pair<int, int>> ssq_lower_matches; - int ssq_lower_match_start = 0; - int ssq_lower_match_len = 0; - - int sst_start = -1; - int sst_lower_start = -1; - - for (int i = 0; *tgt; tgt++, tgt_lower++, i++) { - // Check substring. - if (*sst == *tgt) { - sst++; - if (sst_start == -1) { - sst_start = i; - } - } else if (sst_start != -1 && *sst) { - sst = &string_to_complete[0]; - sst_start = -1; + Vector<Vector<Pair<int, int>>> all_possible_subsequence_matches; + for (int i = 0; *target_char_lower; i++, target_char_lower++) { + if (*target_char_lower == *string_to_complete_char_lower) { + all_possible_subsequence_matches.push_back({ { i, 1 } }); } + } + string_to_complete_char_lower++; - // Check subsequence. - if (*ssq == *tgt) { - ssq++; - if (ssq_match_len == 0) { - ssq_match_start = i; + for (int i = 1; *string_to_complete_char_lower && (all_possible_subsequence_matches.size() > 0); i++, string_to_complete_char_lower++) { + // find all occurrences of ssq_lower to avoid looking everywhere each time + Vector<int> all_ocurence; + for (int j = i; j < target_lower.length(); j++) { + if (target_lower[j] == *string_to_complete_char_lower) { + all_ocurence.push_back(j); } - ssq_match_len++; - } else if (ssq_match_len > 0) { - ssq_matches.push_back(Pair<int, int>(ssq_match_start, ssq_match_len)); - ssq_match_len = 0; } - - // Check lower substring. - if (*sst_lower == *tgt) { - sst_lower++; - if (sst_lower_start == -1) { - sst_lower_start = i; + Vector<Vector<Pair<int, int>>> next_subsequence_matches; + for (Vector<Pair<int, int>> &subsequence_matches : all_possible_subsequence_matches) { + Pair<int, int> match_last_segment = subsequence_matches[subsequence_matches.size() - 1]; + int next_index = match_last_segment.first + match_last_segment.second; + // get the last index from current sequence + // and look for next char starting from that index + if (target_lower[next_index] == *string_to_complete_char_lower) { + Vector<Pair<int, int>> new_matches = subsequence_matches; + new_matches.write[new_matches.size() - 1].second++; + next_subsequence_matches.push_back(new_matches); } - } else if (sst_lower_start != -1 && *sst_lower) { - sst_lower = &string_to_complete[0]; - sst_lower_start = -1; - } - - // Check lower subsequence. - if (*ssq_lower == *tgt_lower) { - ssq_lower++; - if (ssq_lower_match_len == 0) { - ssq_lower_match_start = i; + for (int index : all_ocurence) { + if (index > next_index) { + Vector<Pair<int, int>> new_matches = subsequence_matches; + new_matches.push_back({ index, 1 }); + next_subsequence_matches.push_back(new_matches); + } } - ssq_lower_match_len++; - } else if (ssq_lower_match_len > 0) { - ssq_lower_matches.push_back(Pair<int, int>(ssq_lower_match_start, ssq_lower_match_len)); - ssq_lower_match_len = 0; } - } - - /* Matched the whole subsequence in s. */ - if (!*ssq) { // Matched the whole subsequence in s. - option.matches.clear(); - - if (sst_start == 0) { // Matched substring in beginning of s. - option.matches.push_back(Pair<int, int>(sst_start, string_to_complete.length())); - code_completion_options.push_back(option); - } else if (sst_start > 0) { // Matched substring in s. - option.matches.push_back(Pair<int, int>(sst_start, string_to_complete.length())); - completion_options_substr.push_back(option); - } else { - if (ssq_match_len > 0) { - ssq_matches.push_back(Pair<int, int>(ssq_match_start, ssq_match_len)); + all_possible_subsequence_matches = next_subsequence_matches; + } + // go through all possible matches to get the best one as defined by CodeCompletionOptionCompare + if (all_possible_subsequence_matches.size() > 0) { + option.matches = all_possible_subsequence_matches[0]; + option.get_option_characteristics(string_to_complete); + all_possible_subsequence_matches = all_possible_subsequence_matches.slice(1); + if (all_possible_subsequence_matches.size() > 0) { + CodeCompletionOptionCompare compare; + ScriptLanguage::CodeCompletionOption compared_option = option; + compared_option.clear_characteristics(); + for (Vector<Pair<int, int>> &matches : all_possible_subsequence_matches) { + compared_option.matches = matches; + compared_option.get_option_characteristics(string_to_complete); + if (compare(compared_option, option)) { + option = compared_option; + compared_option.clear_characteristics(); + } } - option.matches.append_array(ssq_matches); - completion_options_subseq.push_back(option); } - if (theme_cache.font.is_valid()) { - max_width = MAX(max_width, theme_cache.font->get_string_size(option.display, HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width + offset); - } - } else if (!*ssq_lower) { // Matched the whole subsequence in s_lower. - option.matches.clear(); - if (sst_lower_start == 0) { // Matched substring in beginning of s_lower. - option.matches.push_back(Pair<int, int>(sst_lower_start, string_to_complete.length())); - completion_options_casei.push_back(option); - } else if (sst_lower_start > 0) { // Matched substring in s_lower. - option.matches.push_back(Pair<int, int>(sst_lower_start, string_to_complete.length())); - completion_options_substr_casei.push_back(option); - } else { - if (ssq_lower_match_len > 0) { - ssq_lower_matches.push_back(Pair<int, int>(ssq_lower_match_start, ssq_lower_match_len)); - } - option.matches.append_array(ssq_lower_matches); - completion_options_subseq_casei.push_back(option); - } + code_completion_options.push_back(option); if (theme_cache.font.is_valid()) { max_width = MAX(max_width, theme_cache.font->get_string_size(option.display, HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width + offset); } } } - code_completion_options.append_array(completion_options_casei); - code_completion_options.append_array(completion_options_substr); - code_completion_options.append_array(completion_options_substr_casei); - code_completion_options.append_array(completion_options_subseq); - code_completion_options.append_array(completion_options_subseq_casei); - /* No options to complete, cancel. */ if (code_completion_options.size() == 0) { cancel_code_completion(); @@ -3256,6 +3193,8 @@ void CodeEdit::_filter_code_completion_candidates_impl() { return; } + code_completion_options.sort_custom<CodeCompletionOptionCompare>(); + code_completion_longest_line = MIN(max_width, theme_cache.code_completion_max_width * theme_cache.font_size); code_completion_current_selected = 0; code_completion_force_item_center = -1; @@ -3384,3 +3323,26 @@ CodeEdit::CodeEdit() { CodeEdit::~CodeEdit() { } + +// Return true if l should come before r +bool CodeCompletionOptionCompare::operator()(const ScriptLanguage::CodeCompletionOption &l, const ScriptLanguage::CodeCompletionOption &r) const { + // Check if we are not completing an empty string in this case there is no reason to get matches characteristics. + + TypedArray<int> lcharac = l.get_option_cached_characteristics(); + TypedArray<int> rcharac = r.get_option_cached_characteristics(); + + if (lcharac != rcharac) { + return lcharac < rcharac; + } + + // to get here they need to have the same size so we can take the size of whichever we want + for (int i = 0; i < l.matches.size(); ++i) { + if (l.matches[i].first != r.matches[i].first) { + return l.matches[i].first < r.matches[i].first; + } + if (l.matches[i].second != r.matches[i].second) { + return l.matches[i].second > r.matches[i].second; + } + } + return l.display < r.display; +} diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index a086707745..6933eb9392 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -37,8 +37,8 @@ class CodeEdit : public TextEdit { GDCLASS(CodeEdit, TextEdit) public: - /* Keep enum in sync with: */ - /* /core/object/script_language.h - ScriptLanguage::CodeCompletionKind */ + // Keep enums in sync with: + // core/object/script_language.h - ScriptLanguage::CodeCompletionKind enum CodeCompletionKind { KIND_CLASS, KIND_FUNCTION, @@ -52,6 +52,14 @@ public: KIND_PLAIN_TEXT, }; + // core/object/script_language.h - ScriptLanguage::CodeCompletionLocation + enum CodeCompletionLocation { + LOCATION_LOCAL = 0, + LOCATION_PARENT_MASK = 1 << 8, + LOCATION_OTHER_USER_CODE = 1 << 9, + LOCATION_OTHER = 1 << 10, + }; + private: /* Indent management */ int indent_size = 4; @@ -427,7 +435,7 @@ public: void request_code_completion(bool p_force = false); - void add_code_completion_option(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color = Color(1, 1, 1), const Ref<Resource> &p_icon = Ref<Resource>(), const Variant &p_value = Variant::NIL); + void add_code_completion_option(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color = Color(1, 1, 1), const Ref<Resource> &p_icon = Ref<Resource>(), const Variant &p_value = Variant::NIL, int p_location = LOCATION_OTHER); void update_code_completion_options(bool p_forced = false); TypedArray<Dictionary> get_code_completion_options() const; @@ -456,5 +464,11 @@ public: }; VARIANT_ENUM_CAST(CodeEdit::CodeCompletionKind); +VARIANT_ENUM_CAST(CodeEdit::CodeCompletionLocation); + +// The custom comparer which will sort completion options. +struct CodeCompletionOptionCompare { + _FORCE_INLINE_ bool operator()(const ScriptLanguage::CodeCompletionOption &l, const ScriptLanguage::CodeCompletionOption &r) const; +}; #endif // CODE_EDIT_H diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h index a201f0f3b0..c8ce49f318 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -3186,7 +3186,7 @@ TEST_CASE("[SceneTree][CodeEdit] completion") { 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_option(0).size() == 7); CHECK(code_edit->get_code_completion_options().size() == 1); /* Check cancel closes completion. */ @@ -3197,7 +3197,7 @@ TEST_CASE("[SceneTree][CodeEdit] completion") { 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_option(0).size() == 7); CHECK(code_edit->get_code_completion_options().size() == 3); /* Check data. */ @@ -3445,6 +3445,98 @@ TEST_CASE("[SceneTree][CodeEdit] completion") { } } + SUBCASE("[CodeEdit] autocomplete suggestion order") { + /* Favorize less fragmented suggestion. */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "test", "test"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "tset", "tset"); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test"); + + /* Favorize suggestion starting from the string to complete (matching start). */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "test", "test"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "stest", "stest"); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test"); + + /* Favorize less fragment to matching start. */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "tset", "tset"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "stest", "stest"); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "stest"); + + /* Favorize closer location. */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "test", "test"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "test_bis", "test_bis", Color(1, 1, 1), Ref<Resource>(), Variant::NIL, CodeEdit::LOCATION_LOCAL); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test_bis"); + + /* Favorize matching start to location. */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "test", "test"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "stest_bis", "test_bis", Color(1, 1, 1), Ref<Resource>(), Variant::NIL, CodeEdit::LOCATION_LOCAL); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test"); + + /* Favorize good capitalisation. */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "test", "test"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "Test", "Test"); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test"); + + /* Favorize location to good capitalisation. */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "test", "test"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "Test", "Test", Color(1, 1, 1), Ref<Resource>(), Variant::NIL, CodeEdit::LOCATION_LOCAL); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "Test"); + + /* Favorize string to complete being closest to the start of the suggestion (closest to start). */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "stest", "stest"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "sstest", "sstest"); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "stest"); + + /* Favorize good capitalisation to closest to start. */ + code_edit->clear(); + code_edit->insert_text_at_caret("te"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "sTest", "stest"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "sstest", "sstest"); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "sstest"); + } + memdelete(code_edit); } |
