sort code completions with rules

Fixups

Add levenshtein distance for comparisons, remove kind sort order, try to improve as many different use cases as possible

Trying again to improve code completion

Sort code autocompletion options by similarity based on input

To make it really brief, uses a combination `String.similiary`, the category system introduced in a previous PR, and some filtering to yield more predictable results, instead of scattering every completion option at seemingly random.

It also gives much higher priority to strings that contain the base in full, closer to the beginning or are perfect matches.

Also moves CodeCompletionOptionCompare to code_edit.cpp

Co-Authored-By: Micky <66727710+Mickeon@users.noreply.github.com>
Co-Authored-By: Eric M <41730826+EricEzaM@users.noreply.github.com>
This commit is contained in:
ajreckof 2023-05-23 05:12:34 +02:00
parent fb10f45efe
commit 006e899bb3
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

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

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