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:
parent
fb10f45efe
commit
006e899bb3
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
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 } });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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));
|
||||
}
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue