Merge pull request #75746 from ajreckof/order_autocomplete

Sort code autocompletion with rules
This commit is contained in:
Rémi Verschelde 2023-06-08 18:14:31 +02:00 committed by GitHub
commit 577ab3c565
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 291 additions and 202 deletions

View file

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

View file

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

View file

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

View file

@ -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.

View file

@ -943,7 +943,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);
}

View file

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

View file

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

View file

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

View file

@ -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:
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];
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];
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;
}
// Check subsequence.
if (*ssq == *tgt) {
ssq++;
if (ssq_match_len == 0) {
ssq_match_start = i;
}
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;
}
} 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;
}
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;
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++;
/* 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));
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);
}
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));
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);
}
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);
}
}
option.matches.append_array(ssq_lower_matches);
completion_options_subseq_casei.push_back(option);
}
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();
}
}
}
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;
}

View file

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

View file

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