diff --git a/modules/gdscript/doc_classes/GDScriptLanguageProtocol.xml b/modules/gdscript/doc_classes/GDScriptLanguageProtocol.xml index 400154ef4f..18ed0417df 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 fda5f0539f..34856482e4 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 897fe11efb..6ba4fc53ee 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 @@ private: 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 @@ private: 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 @@ public: 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 2db9aba74d..8c07f17b3e 100644 --- a/modules/gdscript/language_server/gdscript_text_document.cpp +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -176,65 +176,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 2c5d58b3b3..542125eac3 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"]; }