Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions core/object/script_language.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Resource> icon;
Variant default_value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</description>
</method>
<method name="initialize" deprecated="Accessing LSP endpoints directly might lead to unwanted side effects. Connect to the server via TCP, like a regular language server client.">
<return type="Dictionary" />
<return type="Variant" />
<param index="0" name="params" type="Dictionary" />
<description>
</description>
Expand Down
236 changes: 117 additions & 119 deletions modules/gdscript/gdscript_editor.cpp

Large diffs are not rendered by default.

109 changes: 108 additions & 1 deletion modules/gdscript/language_server/gdscript_language_protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,26 @@ void GDScriptLanguageProtocol::_bind_methods() {
#endif // !DISABLE_DEPRECATED
}

Dictionary GDScriptLanguageProtocol::initialize(const Dictionary &p_params) {
template <typename T>
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 <typename T1, typename... T2>
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;

{
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -511,6 +535,89 @@ 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<ScriptLanguage::CodeCompletionOption> options;
get_workspace()->completion(params, &options);

const Vector<String> &lines = get_parse_result(workspace->get_file_path(params.textDocument.uri))->get_lines();

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("(");
}

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;
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<const LSP::DocumentSymbol *> &r_list) {
LSP_CLIENT;

Expand Down
17 changes: 16 additions & 1 deletion modules/gdscript/language_server/gdscript_language_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<StreamPeerTCP> connection;
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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.
*
Expand Down
60 changes: 1 addition & 59 deletions modules/gdscript/language_server/gdscript_text_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScriptLanguage::CodeCompletionOption> 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) {
Expand Down
14 changes: 14 additions & 0 deletions modules/gdscript/language_server/godot_lsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

/**
Expand Down Expand Up @@ -1076,6 +1083,12 @@ struct CompletionItem {
if (!insertText.is_empty()) {
dict["insertText"] = insertText;
}
if (insertTextFormat) {
dict["insertTextFormat"] = insertTextFormat;
}
if (!textEdit.newText.is_empty()) {
dict["textEdit"] = textEdit.to_json();
}
if (resolved) {
if (!detail.is_empty()) {
dict["detail"] = detail;
Expand Down Expand Up @@ -1135,6 +1148,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"];
}
Expand Down
Loading