Merge pull request #75746 from ajreckof/order_autocomplete
Sort code autocompletion with rules
This commit is contained in:
commit
577ab3c565
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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
Block a user