From 7ab735fbbbe043720a5b9d684c6d8ce3f936372a Mon Sep 17 00:00:00 2001 From: HolonProduction Date: Sun, 4 Jan 2026 16:22:57 +0100 Subject: [PATCH 1/2] LSP: When supported use snippet insert mode to add closing braces --- .../doc_classes/GDScriptLanguageProtocol.xml | 2 +- .../gdscript_language_protocol.cpp | 101 +++++++++++++++++- .../gdscript_language_protocol.h | 17 ++- .../gdscript_text_document.cpp | 60 +---------- modules/gdscript/language_server/godot_lsp.h | 4 + 5 files changed, 122 insertions(+), 62 deletions(-) diff --git a/modules/gdscript/doc_classes/GDScriptLanguageProtocol.xml b/modules/gdscript/doc_classes/GDScriptLanguageProtocol.xml index 2553f657904b..e5ef6e95d31a 100644 --- a/modules/gdscript/doc_classes/GDScriptLanguageProtocol.xml +++ b/modules/gdscript/doc_classes/GDScriptLanguageProtocol.xml @@ -23,7 +23,7 @@ - + diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp index fda5f0539ffa..34856482e428 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.cpp +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -192,7 +192,26 @@ void GDScriptLanguageProtocol::_bind_methods() { #endif // !DISABLE_DEPRECATED } -Dictionary GDScriptLanguageProtocol::initialize(const Dictionary &p_params) { +template +Variant get_deep(Variant p_dict, Variant p_default, T p_key) { + if (p_dict.get_type() != Variant::DICTIONARY) { + return p_default; + } + return p_dict.operator Dictionary().get(p_key, p_default); +} + +template +Variant get_deep(Variant p_dict, Variant p_default, T1 p_key1, T2... p_key2) { + if (p_dict.get_type() != Variant::DICTIONARY || !p_dict.operator Dictionary().has(p_key1)) { + return p_default; + } + + return get_deep(p_dict.operator Dictionary()[p_key1], p_default, p_key2...); +} + +Variant GDScriptLanguageProtocol::initialize(const Dictionary &p_params) { + LSP_CLIENT_V(Variant()); + LSP::InitializeResult ret; { @@ -252,6 +271,11 @@ Dictionary GDScriptLanguageProtocol::initialize(const Dictionary &p_params) { _initialized = true; } + // Handle client capabilities. + Dictionary capabilities = p_params["capabilities"]; + client->behavior.use_snippets_for_brace_completion = get_deep(capabilities, false, + "textDocument", "completion", "completionItem", "snippetSupport"); + return ret.to_json(); } @@ -511,6 +535,81 @@ void GDScriptLanguageProtocol::lsp_did_close(const Dictionary &p_params) { scene_cache.unload(path); } +Array GDScriptLanguageProtocol::lsp_completion(const Dictionary &p_params) { + Array arr; + LSP_CLIENT_V(arr); + + LSP::CompletionParams params; + params.load(p_params); + Dictionary request_data = params.to_json(); + + List options; + get_workspace()->completion(params, &options); + + if (!options.is_empty()) { + int i = 0; + arr.resize(options.size()); + + for (const ScriptLanguage::CodeCompletionOption &option : options) { + LSP::CompletionItem item; + item.label = option.display; + item.data = request_data; + item.insertText = option.insert_text; + + // LSP clients won't autoclose brackets. + if (client->behavior.use_snippets_for_brace_completion) { + // Use snippet insert mode to insert closing brace as well. + if (item.insertText.ends_with("(")) { + item.insertText += "$1)"; + item.insertTextFormat = LSP::InsertTextFormat::Snippet; + } + } else { + // Trim braces. + item.insertText = item.insertText.trim_suffix("("); + } + + switch (option.kind) { + case ScriptLanguage::CODE_COMPLETION_KIND_ENUM: + item.kind = LSP::CompletionItemKind::Enum; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_CLASS: + item.kind = LSP::CompletionItemKind::Class; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_MEMBER: + item.kind = LSP::CompletionItemKind::Property; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION: + item.kind = LSP::CompletionItemKind::Method; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL: + item.kind = LSP::CompletionItemKind::Event; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT: + item.kind = LSP::CompletionItemKind::Constant; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE: + item.kind = LSP::CompletionItemKind::Variable; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH: + item.kind = LSP::CompletionItemKind::File; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH: + item.kind = LSP::CompletionItemKind::Snippet; + break; + case ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT: + item.kind = LSP::CompletionItemKind::Text; + break; + default: { + } + } + + arr[i] = item.to_json(); + i++; + } + } + return arr; +} + void GDScriptLanguageProtocol::resolve_related_symbols(const LSP::TextDocumentPositionParams &p_doc_pos, List &r_list) { LSP_CLIENT; diff --git a/modules/gdscript/language_server/gdscript_language_protocol.h b/modules/gdscript/language_server/gdscript_language_protocol.h index 897fe11efbf0..6ba4fc53ee67 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.h +++ b/modules/gdscript/language_server/gdscript_language_protocol.h @@ -49,6 +49,12 @@ class GDScriptLanguageProtocol : public JSONRPC { friend class TestGDScriptLanguageProtocolInitializer; +public: + struct ClientBehavior { + /** If `true` use snippet insert mode to position the cursor between braces of completion options. If `false` strip braces from completion options since we can't provide good UX for them. */ + bool use_snippets_for_brace_completion = false; + }; + private: struct LSPeer : RefCounted { Ref connection; @@ -64,6 +70,12 @@ class GDScriptLanguageProtocol : public JSONRPC { Error handle_data(); Error send_data(); + /** + * Represents how the server should behave towards this client in certain situations. + * This gets derived from client capabilities so the configured behavior is guaranteed to be supported by the client. + */ + ClientBehavior behavior; + /** * Tracks all files that the client claimed, however for files deemed not relevant * to the server the `text` might not be persisted. @@ -112,7 +124,7 @@ class GDScriptLanguageProtocol : public JSONRPC { protected: static void _bind_methods(); - Dictionary initialize(const Dictionary &p_params); + Variant initialize(const Dictionary &p_params); void initialized(const Variant &p_params); public: @@ -138,6 +150,9 @@ class GDScriptLanguageProtocol : public JSONRPC { void lsp_did_change(const Dictionary &p_params); void lsp_did_close(const Dictionary &p_params); + // Completion + Array lsp_completion(const Dictionary &p_params); + /** * Returns a list of symbols that might be related to the document position. * diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp index d931ab4f2eb3..7cb3af553bb1 100644 --- a/modules/gdscript/language_server/gdscript_text_document.cpp +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -175,65 +175,7 @@ Array GDScriptTextDocument::documentHighlight(const Dictionary &p_params) { } Array GDScriptTextDocument::completion(const Dictionary &p_params) { - Array arr; - - LSP::CompletionParams params; - params.load(p_params); - Dictionary request_data = params.to_json(); - - List options; - GDScriptLanguageProtocol::get_singleton()->get_workspace()->completion(params, &options); - - if (!options.is_empty()) { - int i = 0; - arr.resize(options.size()); - - for (const ScriptLanguage::CodeCompletionOption &option : options) { - LSP::CompletionItem item; - item.label = option.display; - item.data = request_data; - item.insertText = option.insert_text; - - switch (option.kind) { - case ScriptLanguage::CODE_COMPLETION_KIND_ENUM: - item.kind = LSP::CompletionItemKind::Enum; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_CLASS: - item.kind = LSP::CompletionItemKind::Class; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_MEMBER: - item.kind = LSP::CompletionItemKind::Property; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION: - item.kind = LSP::CompletionItemKind::Method; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL: - item.kind = LSP::CompletionItemKind::Event; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT: - item.kind = LSP::CompletionItemKind::Constant; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE: - item.kind = LSP::CompletionItemKind::Variable; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH: - item.kind = LSP::CompletionItemKind::File; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH: - item.kind = LSP::CompletionItemKind::Snippet; - break; - case ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT: - item.kind = LSP::CompletionItemKind::Text; - break; - default: { - } - } - - arr[i] = item.to_json(); - i++; - } - } - return arr; + return GDScriptLanguageProtocol::get_singleton()->lsp_completion(p_params); } Dictionary GDScriptTextDocument::rename(const Dictionary &p_params) { diff --git a/modules/gdscript/language_server/godot_lsp.h b/modules/gdscript/language_server/godot_lsp.h index 6ceada6cbe4f..581e54f8885d 100644 --- a/modules/gdscript/language_server/godot_lsp.h +++ b/modules/gdscript/language_server/godot_lsp.h @@ -1076,6 +1076,9 @@ struct CompletionItem { if (!insertText.is_empty()) { dict["insertText"] = insertText; } + if (insertTextFormat) { + dict["insertTextFormat"] = insertTextFormat; + } if (resolved) { if (!detail.is_empty()) { dict["detail"] = detail; @@ -1135,6 +1138,7 @@ struct CompletionItem { if (p_dict.has("insertText")) { insertText = p_dict["insertText"]; } + insertTextFormat = p_dict.get("insertTextFormat", 0); if (p_dict.has("data")) { data = p_dict["data"]; } From 82f308d9571d87e6aac25d58630636cb159e6ae6 Mon Sep 17 00:00:00 2001 From: HolonProduction Date: Sat, 21 Mar 2026 20:53:40 +0100 Subject: [PATCH 2/2] LSP: Calculate string insertions on the server-side --- core/object/script_language.h | 17 ++ modules/gdscript/gdscript_editor.cpp | 236 +++++++++--------- .../gdscript_language_protocol.cpp | 8 + modules/gdscript/language_server/godot_lsp.h | 10 + 4 files changed, 152 insertions(+), 119 deletions(-) diff --git a/core/object/script_language.h b/core/object/script_language.h index 10d8716c8911..daf21609d9e2 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -305,10 +305,27 @@ class ScriptLanguage : public Object { 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 ddf368ed30af..bfe4b8320afb 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 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa 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 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa 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 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa 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 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa 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 34856482e428..095b56dd0d84 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 581e54f8885d..6cde97118ecc 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;