diff --git a/core/object/script_language.h b/core/object/script_language.h index 10d8716c89..daf21609d9 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -305,10 +305,27 @@ public: LOCATION_OTHER = 1 << 10, }; + struct TextEdit { + String new_text; + int start_line = -1; + int start_column; + int end_line; + int end_column; + + _FORCE_INLINE_ bool is_set() const { return start_line != -1; } + }; + struct CodeCompletionOption { CodeCompletionKind kind = CODE_COMPLETION_KIND_PLAIN_TEXT; String display; String insert_text; + /** + * Optional server side calculated insertion. + * + * In contrast to `insert_text`, the editor must not do matching of preexisting text on `text_edit`. + * Note: This is used by the language server, there is no support in the builtin editor for this property at the moment. + */ + TextEdit text_edit; Color font_color; Ref icon; Variant default_value; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index ddf368ed30..bfe4b8320a 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -894,7 +894,63 @@ static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_functio return arghint; } -static void _get_directory_contents(EditorFileSystemDirectory *p_dir, HashMap &r_list, const StringName &p_required_type = StringName()) { +/** + * Creates a completion option that inserts a string or fully replaces the string content of a preexisting literal. + * + * @param p_existing - An existing node at the position. If this is a string literal the option will replace its content. Can be `nullptr`. + * @param p_str - The string literal to be inserted. This should only contain the strings content, not the quotes that are used to declare it syntactically. + * @param p_expected - The expected variant type at the context were the string is to be insert. Use `Variant::Type::NIL` if this does not apply. + */ +static ScriptLanguage::CodeCompletionOption _calculate_string_insertion(const GDScriptParser::Node *p_existing, const String &p_content, Variant::Type p_expected = Variant::NIL) { + const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; + + const GDScriptParser::LiteralNode *existing_literal = p_existing && p_existing->type == GDScriptParser::Node::LITERAL ? static_cast(p_existing) : nullptr; + + const bool has_string_name = existing_literal && existing_literal->value.get_type() == Variant::STRING_NAME; + const bool has_node_path = existing_literal && existing_literal->value.get_type() == Variant::NODE_PATH; + + const bool should_have_string_name = (p_expected == Variant::STRING_NAME && EDITOR_GET("text_editor/completion/add_string_name_literals")) || has_string_name; + const bool should_have_node_path = (p_expected == Variant::NODE_PATH && EDITOR_GET("text_editor/completion/add_node_path_literals")) || has_node_path; + + String final_code = p_content.quote(quote_style); + if (should_have_string_name) { + final_code = "&" + final_code; + } else if (should_have_node_path) { + final_code = "^" + final_code; + } + + // For the builtin editor. + String insert_text = p_content.quote(quote_style); + if (should_have_string_name && !has_string_name) { + insert_text = "&" + insert_text; + } else if (should_have_node_path && !has_node_path) { + insert_text = "^" + insert_text; + } + + ScriptLanguage::CodeCompletionOption option(insert_text, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); + option.display = final_code; + + // For LSP. See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit. + if (existing_literal == nullptr || (existing_literal->value.get_type() != Variant::STRING && existing_literal->value.get_type() != Variant::STRING_NAME && existing_literal->value.get_type() != Variant::NODE_PATH)) { + // No existing string literal. Use default client behavior. + return option; + } + + if (existing_literal->start_line != existing_literal->end_line) { + // Preexisting multiline string. Not relevant for how we are using the method. + return option; + } + + option.text_edit.new_text = final_code; + option.text_edit.start_line = existing_literal->start_line; + option.text_edit.start_column = existing_literal->start_column; + option.text_edit.end_line = existing_literal->end_line; + option.text_edit.end_column = existing_literal->end_column; + + return option; +} + +static void _get_directory_contents(const GDScriptParser::Node *p_current, EditorFileSystemDirectory *p_dir, HashMap &r_list, const StringName &p_required_type = StringName()) { const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; const bool requires_type = !p_required_type.is_empty(); @@ -902,50 +958,47 @@ static void _get_directory_contents(EditorFileSystemDirectory *p_dir, HashMapget_file_type(i), p_required_type)) { continue; } - ScriptLanguage::CodeCompletionOption option(p_dir->get_file_path(i).quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(p_current, p_dir->get_file_path(i), Variant::NIL); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH; r_list.insert(option.display, option); } for (int i = 0; i < p_dir->get_subdir_count(); i++) { - _get_directory_contents(p_dir->get_subdir(i), r_list, p_required_type); + _get_directory_contents(p_current, p_dir->get_subdir(i), r_list, p_required_type); } } static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_annotation, int p_argument, const String p_quote_style, HashMap &r_result, String &r_arghint) { ERR_FAIL_NULL(p_annotation); + const GDScriptParser::Node *existing_argument = p_annotation->arguments.size() > p_argument ? p_annotation->arguments[p_argument] : nullptr; + if (p_annotation->info != nullptr) { r_arghint = _make_arguments_hint(p_annotation->info->info, p_argument, true); } if (p_annotation->name == SNAME("@export_range")) { if (p_argument == 3 || p_argument == 4 || p_argument == 5) { // Slider hint. - ScriptLanguage::CodeCompletionOption slider1("or_greater", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - slider1.insert_text = slider1.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption slider1 = _calculate_string_insertion(existing_argument, "or_greater"); r_result.insert(slider1.display, slider1); - ScriptLanguage::CodeCompletionOption slider2("or_less", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - slider2.insert_text = slider2.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption slider2 = _calculate_string_insertion(existing_argument, "or_less"); r_result.insert(slider2.display, slider2); - ScriptLanguage::CodeCompletionOption slider3("prefer_slider", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - slider3.insert_text = slider3.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption slider3 = _calculate_string_insertion(existing_argument, "prefer_slider"); r_result.insert(slider3.display, slider3); - ScriptLanguage::CodeCompletionOption slider4("hide_control", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - slider4.insert_text = slider4.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption slider4 = _calculate_string_insertion(existing_argument, "hide_control"); r_result.insert(slider4.display, slider4); } } else if (p_annotation->name == SNAME("@export_exp_easing")) { if (p_argument == 0 || p_argument == 1) { // Easing hint. - ScriptLanguage::CodeCompletionOption hint1("attenuation", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - hint1.insert_text = hint1.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption hint1 = _calculate_string_insertion(existing_argument, "attenuation"); r_result.insert(hint1.display, hint1); - ScriptLanguage::CodeCompletionOption hint2("positive_only", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - hint2.insert_text = hint2.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption hint2 = _calculate_string_insertion(existing_argument, "positive_only"); r_result.insert(hint2.display, hint2); } } else if (p_annotation->name == SNAME("@export_node_path")) { - ScriptLanguage::CodeCompletionOption node("Node", ScriptLanguage::CODE_COMPLETION_KIND_CLASS); - node.insert_text = node.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption node = _calculate_string_insertion(existing_argument, "Node", Variant::NIL); + node.kind = ScriptLanguage::CODE_COMPLETION_KIND_CLASS; r_result.insert(node.display, node); LocalVector native_classes; @@ -954,8 +1007,8 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a if (!ClassDB::is_class_exposed(E)) { continue; } - ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_CLASS); - option.insert_text = option.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, E, Variant::NIL); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_CLASS; r_result.insert(option.display, option); } @@ -965,8 +1018,8 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a if (!ClassDB::is_parent_class(ScriptServer::get_global_class_native_base(class_name), "Node")) { continue; } - ScriptLanguage::CodeCompletionOption option(class_name, ScriptLanguage::CODE_COMPLETION_KIND_CLASS); - option.insert_text = option.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, class_name, Variant::NIL); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_CLASS; r_result.insert(option.display, option); } } else if (p_annotation->name == SNAME("@export_tool_button")) { @@ -976,8 +1029,8 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a List icon_list; theme->get_icon_list(EditorStringName(EditorIcons), &icon_list); for (const StringName &E : icon_list) { - ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_CLASS); - option.insert_text = option.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, E, Variant::NIL); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_CLASS; r_result.insert(option.display, option); } } @@ -1012,16 +1065,14 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a break; // Don't suggest deprecated warnings as they are never produced. } #endif // DISABLE_DEPRECATED - ScriptLanguage::CodeCompletionOption warning(GDScriptWarning::get_name_from_code((GDScriptWarning::Code)warning_code).to_lower(), ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - warning.insert_text = warning.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption warning = _calculate_string_insertion(existing_argument, GDScriptWarning::get_name_from_code((GDScriptWarning::Code)warning_code).to_lower()); r_result.insert(warning.display, warning); } } else if (p_annotation->name == SNAME("@rpc")) { if (p_argument == 0 || p_argument == 1 || p_argument == 2) { static const char *options[7] = { "call_local", "call_remote", "any_peer", "authority", "reliable", "unreliable", "unreliable_ordered" }; for (int i = 0; i < 7; i++) { - ScriptLanguage::CodeCompletionOption option(options[i], ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - option.insert_text = option.display.quote(p_quote_style); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, options[i]); r_result.insert(option.display, option); } } @@ -2947,8 +2998,8 @@ static void _list_call_arguments(GDScriptParser::CompletionContext &p_context, c const StringName &method = p_call->function_name; const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; - const bool use_string_names = EDITOR_GET("text_editor/completion/add_string_name_literals"); - const bool use_node_paths = EDITOR_GET("text_editor/completion/add_node_path_literals"); + + const GDScriptParser::Node *existing_argument = p_call->arguments.size() > p_argidx ? p_call->arguments[p_argidx] : nullptr; while (base_type.is_set() && !base_type.is_variant()) { switch (base_type.kind) { @@ -3014,32 +3065,13 @@ static void _list_call_arguments(GDScriptParser::CompletionContext &p_context, c if (obj) { List options; obj->get_argument_options(method, p_argidx, &options); + const GDScriptParser::Node *existing_node = p_call->arguments.size() > p_argidx ? p_call->arguments[p_argidx] : nullptr; + const Variant::Type expected_type = info.arguments.size() > p_argidx ? info.arguments[p_argidx].type : Variant::NIL; for (String &opt : options) { - // Handle user preference. if (opt.is_quoted()) { - opt = opt.unquote().quote(quote_style); - if (use_string_names && info.arguments[p_argidx].type == Variant::STRING_NAME) { - if (p_call->arguments.size() > p_argidx && p_call->arguments[p_argidx] && p_call->arguments[p_argidx]->type == GDScriptParser::Node::LITERAL) { - GDScriptParser::LiteralNode *literal = static_cast(p_call->arguments[p_argidx]); - if (literal->value.get_type() == Variant::STRING) { - opt = "&" + opt; - } - } else { - opt = "&" + opt; - } - } else if (use_node_paths && info.arguments[p_argidx].type == Variant::NODE_PATH) { - if (p_call->arguments.size() > p_argidx && p_call->arguments[p_argidx] && p_call->arguments[p_argidx]->type == GDScriptParser::Node::LITERAL) { - GDScriptParser::LiteralNode *literal = static_cast(p_call->arguments[p_argidx]); - if (literal->value.get_type() == Variant::STRING) { - opt = "^" + opt; - } - } else { - opt = "^" + opt; - } - } + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_node, opt.unquote(), expected_type); + r_result.insert(option.display, option); } - ScriptLanguage::CodeCompletionOption option(opt, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); - r_result.insert(option.display, option); } } } @@ -3078,18 +3110,9 @@ static void _list_call_arguments(GDScriptParser::CompletionContext &p_context, c if (E.usage & (PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_INTERNAL)) { continue; } - String name = E.name.quote(quote_style); - if (use_node_paths) { - if (p_call->arguments.size() > p_argidx && p_call->arguments[p_argidx] && p_call->arguments[p_argidx]->type == GDScriptParser::Node::LITERAL) { - GDScriptParser::LiteralNode *literal = static_cast(p_call->arguments[p_argidx]); - if (literal->value.get_type() == Variant::STRING) { - name = "^" + name; - } - } else { - name = "^" + name; - } - } - ScriptLanguage::CodeCompletionOption option(name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, ScriptLanguage::CodeCompletionLocation::LOCATION_LOCAL + n); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, E.name, Variant::NODE_PATH); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_MEMBER; + option.location = ScriptLanguage::CodeCompletionLocation::LOCATION_LOCAL + n; r_result.insert(option.display, option); } script = script->get_base_script(); @@ -3103,18 +3126,9 @@ static void _list_call_arguments(GDScriptParser::CompletionContext &p_context, c while (clss) { for (GDScriptParser::ClassNode::Member member : clss->members) { if (member.type == GDScriptParser::ClassNode::Member::VARIABLE) { - String name = member.get_name().quote(quote_style); - if (use_node_paths) { - if (p_call->arguments.size() > p_argidx && p_call->arguments[p_argidx] && p_call->arguments[p_argidx]->type == GDScriptParser::Node::LITERAL) { - GDScriptParser::LiteralNode *literal = static_cast(p_call->arguments[p_argidx]); - if (literal->value.get_type() == Variant::STRING) { - name = "^" + name; - } - } else { - name = "^" + name; - } - } - ScriptLanguage::CodeCompletionOption option(name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, ScriptLanguage::CodeCompletionLocation::LOCATION_LOCAL + n); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, member.get_name(), Variant::NODE_PATH); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_MEMBER; + option.location = ScriptLanguage::CodeCompletionLocation::LOCATION_LOCAL + n; r_result.insert(option.display, option); } } @@ -3137,18 +3151,8 @@ static void _list_call_arguments(GDScriptParser::CompletionContext &p_context, c if (E.usage & (PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_INTERNAL)) { continue; } - String name = E.name.quote(quote_style); - if (use_node_paths) { - if (p_call->arguments.size() > p_argidx && p_call->arguments[p_argidx] && p_call->arguments[p_argidx]->type == GDScriptParser::Node::LITERAL) { - GDScriptParser::LiteralNode *literal = static_cast(p_call->arguments[p_argidx]); - if (literal->value.get_type() == Variant::STRING) { - name = "^" + name; - } - } else { - name = "^" + name; - } - } - ScriptLanguage::CodeCompletionOption option(name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, E.name, Variant::NODE_PATH); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_MEMBER; r_result.insert(option.display, option); } } @@ -3164,18 +3168,8 @@ static void _list_call_arguments(GDScriptParser::CompletionContext &p_context, c continue; } String name = s.get_slicec('/', 1); - String path = ("/root/" + name).quote(quote_style); - if (use_node_paths) { - if (p_call->arguments.size() > p_argidx && p_call->arguments[p_argidx] && p_call->arguments[p_argidx]->type == GDScriptParser::Node::LITERAL) { - GDScriptParser::LiteralNode *literal = static_cast(p_call->arguments[p_argidx]); - if (literal->value.get_type() == Variant::STRING) { - path = "^" + path; - } - } else { - path = "^" + path; - } - } - ScriptLanguage::CodeCompletionOption option(path, ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, "/root/" + name, Variant::NODE_PATH); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH; r_result.insert(option.display, option); } } @@ -3185,29 +3179,18 @@ static void _list_call_arguments(GDScriptParser::CompletionContext &p_context, c List props; ProjectSettings::get_singleton()->get_property_list(&props); for (const PropertyInfo &E : props) { - String s = E.name; - if (!s.begins_with("input/")) { + if (!E.name.begins_with("input/")) { continue; } - String name = s.get_slicec('/', 1).quote(quote_style); - if (use_string_names) { - if (p_call->arguments.size() > p_argidx && p_call->arguments[p_argidx] && p_call->arguments[p_argidx]->type == GDScriptParser::Node::LITERAL) { - GDScriptParser::LiteralNode *literal = static_cast(p_call->arguments[p_argidx]); - if (literal->value.get_type() == Variant::STRING) { - name = "&" + name; - } - } else { - name = "&" + name; - } - } - ScriptLanguage::CodeCompletionOption option(name, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT); + + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_argument, E.name.get_slicec('/', 1), Variant::STRING_NAME); r_result.insert(option.display, option); } } if (EDITOR_GET("text_editor/completion/complete_file_paths")) { if (p_argidx == 0 && method == SNAME("change_scene_to_file") && ClassDB::is_parent_class(class_name, SNAME("SceneTree"))) { HashMap list; - _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), list, SNAME("PackedScene")); + _get_directory_contents(existing_argument, EditorFileSystem::get_singleton()->get_filesystem(), list, SNAME("PackedScene")); for (const KeyValue &key_value_pair : list) { ScriptLanguage::CodeCompletionOption option = key_value_pair.value; r_result.insert(option.display, option); @@ -3341,7 +3324,7 @@ static bool _get_subscript_type(GDScriptParser::CompletionContext &p_context, co static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, const GDScriptParser::Node *p_call, int p_argidx, HashMap &r_result, bool &r_forced, String &r_arghint) { if (p_call->type == GDScriptParser::Node::PRELOAD) { if (p_argidx == 0 && bool(EDITOR_GET("text_editor/completion/complete_file_paths"))) { - _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), r_result); + _get_directory_contents(static_cast(p_call)->path, EditorFileSystem::get_singleton()->get_filesystem(), r_result); } MethodInfo mi(PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, Resource::get_class_static()), "preload", PropertyInfo(Variant::STRING, "path")); @@ -3612,6 +3595,8 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c GDScriptCompletionIdentifier base; const bool res = _guess_expression_type(completion_context, subscript->base, base); + const GDScriptParser::Node *existing_index = subscript->index; + // If the type is not known, we assume it is BUILTIN, since indices on arrays is the most common use case. if (!subscript->is_attribute && (!res || base.type.kind == GDScriptParser::DataType::BUILTIN || base.type.is_variant())) { if (base.value.get_type() == Variant::DICTIONARY) { @@ -3619,7 +3604,9 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c base.value.get_property_list(&members); for (const PropertyInfo &E : members) { - ScriptLanguage::CodeCompletionOption option(E.name.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, ScriptLanguage::LOCATION_LOCAL); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_index, E.name); + option.kind = ScriptLanguage::CODE_COMPLETION_KIND_MEMBER; + option.location = ScriptLanguage::LOCATION_LOCAL; options.insert(option.display, option); } } @@ -3633,7 +3620,9 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c HashMap opt; _find_identifiers_in_base(base, false, false, false, opt, 0); for (const KeyValue &E : opt) { - ScriptLanguage::CodeCompletionOption option(E.value.insert_text.quote(quote_style), E.value.kind, E.value.location); + ScriptLanguage::CodeCompletionOption option = _calculate_string_insertion(existing_index, E.value.insert_text); + option.kind = E.value.kind; + option.location = E.value.location; options.insert(option.display, option); } } else { @@ -3669,8 +3658,17 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c r_forced = true; } break; case GDScriptParser::COMPLETION_RESOURCE_PATH: { + GDScriptParser::Node *existing = nullptr; + if (completion_context.node && completion_context.node->type == GDScriptParser::Node::CALL && completion_context.current_argument >= 0) { + const GDScriptParser::CallNode *call = static_cast(completion_context.node); + existing = call->arguments.size() > completion_context.current_argument ? call->arguments[completion_context.current_argument] : nullptr; + } else if (completion_context.node && completion_context.node->type == GDScriptParser::Node::PRELOAD) { + const GDScriptParser::PreloadNode *preload = static_cast(completion_context.node); + existing = preload->path; + } + if (EDITOR_GET("text_editor/completion/complete_file_paths")) { - _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), options); + _get_directory_contents(existing, EditorFileSystem::get_singleton()->get_filesystem(), options); r_forced = true; } } break; diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp index 34856482e4..095b56dd0d 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.cpp +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -546,6 +546,8 @@ Array GDScriptLanguageProtocol::lsp_completion(const Dictionary &p_params) { List options; get_workspace()->completion(params, &options); + const Vector &lines = get_parse_result(workspace->get_file_path(params.textDocument.uri))->get_lines(); + if (!options.is_empty()) { int i = 0; arr.resize(options.size()); @@ -568,6 +570,12 @@ Array GDScriptLanguageProtocol::lsp_completion(const Dictionary &p_params) { item.insertText = item.insertText.trim_suffix("("); } + if (option.text_edit.is_set()) { + GodotRange range(GodotPosition(option.text_edit.start_line, option.text_edit.start_column), GodotPosition(option.text_edit.end_line, option.text_edit.end_column)); + item.textEdit.newText = option.text_edit.new_text; + item.textEdit.range = range.to_lsp(lines); + } + switch (option.kind) { case ScriptLanguage::CODE_COMPLETION_KIND_ENUM: item.kind = LSP::CompletionItemKind::Enum; diff --git a/modules/gdscript/language_server/godot_lsp.h b/modules/gdscript/language_server/godot_lsp.h index 581e54f888..6cde97118e 100644 --- a/modules/gdscript/language_server/godot_lsp.h +++ b/modules/gdscript/language_server/godot_lsp.h @@ -319,6 +319,13 @@ struct TextEdit { * empty string. */ String newText; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["newText"] = newText; + dict["range"] = range.to_json(); + return dict; + } }; /** @@ -1079,6 +1086,9 @@ struct CompletionItem { if (insertTextFormat) { dict["insertTextFormat"] = insertTextFormat; } + if (!textEdit.newText.is_empty()) { + dict["textEdit"] = textEdit.to_json(); + } if (resolved) { if (!detail.is_empty()) { dict["detail"] = detail;