diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp new file mode 100644 index 000000000..21f9ce01d --- /dev/null +++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp @@ -0,0 +1,796 @@ +/**************************************************************************/ +/* gdscript_extend_parser.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_extend_parser.h" + +#include "../gdscript.h" +#include "core/io/json.h" +#include "gdscript_language_protocol.h" +#include "gdscript_workspace.h" + +void ExtendGDScriptParser::update_diagnostics() { + diagnostics.clear(); + + if (has_error()) { + lsp::Diagnostic diagnostic; + diagnostic.severity = lsp::DiagnosticSeverity::Error; + diagnostic.message = get_error(); + diagnostic.source = "gdscript"; + diagnostic.code = -1; + lsp::Range range; + lsp::Position pos; + int line = LINE_NUMBER_TO_INDEX(get_error_line()); + const String &line_text = get_lines()[line]; + pos.line = line; + pos.character = line_text.length() - line_text.strip_edges(true, false).length(); + range.start = pos; + range.end = range.start; + range.end.character = line_text.strip_edges(false).length(); + diagnostic.range = range; + diagnostics.push_back(diagnostic); + } + + const List &warnings = get_warnings(); + for (const List::Element *E = warnings.front(); E; E = E->next()) { + const GDScriptWarning &warning = E->get(); + lsp::Diagnostic diagnostic; + diagnostic.severity = lsp::DiagnosticSeverity::Warning; + diagnostic.message = "(" + warning.get_name() + "): " + warning.get_message(); + diagnostic.source = "gdscript"; + diagnostic.code = warning.code; + lsp::Range range; + lsp::Position pos; + int line = LINE_NUMBER_TO_INDEX(warning.line); + const String &line_text = get_lines()[line]; + pos.line = line; + pos.character = line_text.length() - line_text.strip_edges(true, false).length(); + range.start = pos; + range.end = pos; + range.end.character = line_text.strip_edges(false).length(); + diagnostic.range = range; + diagnostics.push_back(diagnostic); + } +} + +void ExtendGDScriptParser::update_symbols() { + members.clear(); + + const GDScriptParser::Node *head = get_parse_tree(); + if (const GDScriptParser::ClassNode *gdclass = dynamic_cast(head)) { + parse_class_symbol(gdclass, class_symbol); + + for (int i = 0; i < class_symbol.children.size(); i++) { + const lsp::DocumentSymbol &symbol = class_symbol.children[i]; + members.set(symbol.name, &symbol); + + // cache level one inner classes + if (symbol.kind == lsp::SymbolKind::Class) { + ClassMembers inner_class; + for (int j = 0; j < symbol.children.size(); j++) { + const lsp::DocumentSymbol &s = symbol.children[j]; + inner_class.set(s.name, &s); + } + inner_classes.set(symbol.name, inner_class); + } + } + } +} + +void ExtendGDScriptParser::update_document_links(const String &p_code) { + document_links.clear(); + + GDScriptTokenizerText tokenizer; + FileAccessRef fs = FileAccess::create(FileAccess::ACCESS_RESOURCES); + tokenizer.set_code(p_code); + while (true) { + GDScriptTokenizerText::Token token = tokenizer.get_token(); + if (token == GDScriptTokenizer::TK_EOF || token == GDScriptTokenizer::TK_ERROR) { + break; + } else if (token == GDScriptTokenizer::TK_CONSTANT) { + const Variant &const_val = tokenizer.get_token_constant(); + if (const_val.get_type() == Variant::STRING) { + String path = const_val; + bool exists = fs->file_exists(path); + if (!exists) { + path = get_path().get_base_dir() + "/" + path; + exists = fs->file_exists(path); + } + if (exists) { + String value = const_val; + lsp::DocumentLink link; + link.target = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_uri(path); + link.range.start.line = LINE_NUMBER_TO_INDEX(tokenizer.get_token_line()); + link.range.end.line = link.range.start.line; + link.range.end.character = LINE_NUMBER_TO_INDEX(tokenizer.get_token_column()); + link.range.start.character = link.range.end.character - value.length(); + document_links.push_back(link); + } + } + } + tokenizer.advance(); + } +} + +void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p_class, lsp::DocumentSymbol &r_symbol) { + const String uri = get_uri(); + + r_symbol.uri = uri; + r_symbol.script_path = path; + r_symbol.children.clear(); + r_symbol.name = p_class->name; + if (r_symbol.name.empty()) { + r_symbol.name = path.get_file(); + } + r_symbol.kind = lsp::SymbolKind::Class; + r_symbol.deprecated = false; + r_symbol.range.start.line = LINE_NUMBER_TO_INDEX(p_class->line); + r_symbol.range.start.character = p_class->column; + r_symbol.range.end.line = LINE_NUMBER_TO_INDEX(p_class->end_line); + r_symbol.selectionRange.start.line = r_symbol.range.start.line; + r_symbol.detail = "class " + r_symbol.name; + bool is_root_class = &r_symbol == &class_symbol; + r_symbol.documentation = parse_documentation(is_root_class ? 0 : LINE_NUMBER_TO_INDEX(p_class->line), is_root_class); + + for (int i = 0; i < p_class->variables.size(); ++i) { + const GDScriptParser::ClassNode::Member &m = p_class->variables[i]; + + lsp::DocumentSymbol symbol; + symbol.name = m.identifier; + symbol.kind = m.setter == "" && m.getter == "" ? lsp::SymbolKind::Variable : lsp::SymbolKind::Property; + symbol.deprecated = false; + const int line = LINE_NUMBER_TO_INDEX(m.line); + symbol.range.start.line = line; + symbol.range.start.character = lines[line].length() - lines[line].strip_edges(true, false).length(); + symbol.range.end.line = line; + symbol.range.end.character = lines[line].length(); + symbol.selectionRange.start.line = symbol.range.start.line; + if (m._export.type != Variant::NIL) { + symbol.detail += "export "; + } + symbol.detail += "var " + m.identifier; + if (m.data_type.kind != GDScriptParser::DataType::UNRESOLVED) { + symbol.detail += ": " + m.data_type.to_string(); + } + if (m.default_value.get_type() != Variant::NIL) { + symbol.detail += " = " + JSON::print(m.default_value); + } + + symbol.documentation = parse_documentation(line); + symbol.uri = uri; + symbol.script_path = path; + + r_symbol.children.push_back(symbol); + } + + for (int i = 0; i < p_class->_signals.size(); ++i) { + const GDScriptParser::ClassNode::Signal &signal = p_class->_signals[i]; + + lsp::DocumentSymbol symbol; + symbol.name = signal.name; + symbol.kind = lsp::SymbolKind::Event; + symbol.deprecated = false; + const int line = LINE_NUMBER_TO_INDEX(signal.line); + symbol.range.start.line = line; + symbol.range.start.character = lines[line].length() - lines[line].strip_edges(true, false).length(); + symbol.range.end.line = symbol.range.start.line; + symbol.range.end.character = lines[line].length(); + symbol.selectionRange.start.line = symbol.range.start.line; + symbol.documentation = parse_documentation(line); + symbol.uri = uri; + symbol.script_path = path; + symbol.detail = "signal " + signal.name + "("; + for (int j = 0; j < signal.arguments.size(); j++) { + if (j > 0) { + symbol.detail += ", "; + } + symbol.detail += signal.arguments[j]; + } + symbol.detail += ")"; + + r_symbol.children.push_back(symbol); + } + + for (Map::Element *E = p_class->constant_expressions.front(); E; E = E->next()) { + lsp::DocumentSymbol symbol; + const GDScriptParser::ClassNode::Constant &c = E->value(); + const GDScriptParser::ConstantNode *node = dynamic_cast(c.expression); + ERR_FAIL_COND(!node); + symbol.name = E->key(); + symbol.kind = lsp::SymbolKind::Constant; + symbol.deprecated = false; + const int line = LINE_NUMBER_TO_INDEX(E->get().expression->line); + symbol.range.start.line = line; + symbol.range.start.character = E->get().expression->column; + symbol.range.end.line = symbol.range.start.line; + symbol.range.end.character = lines[line].length(); + symbol.selectionRange.start.line = symbol.range.start.line; + symbol.documentation = parse_documentation(line); + symbol.uri = uri; + symbol.script_path = path; + + symbol.detail = "const " + symbol.name; + if (c.type.kind != GDScriptParser::DataType::UNRESOLVED) { + symbol.detail += ": " + c.type.to_string(); + } + + String value_text; + if (node->value.get_type() == Variant::OBJECT) { + RES res = node->value; + if (res.is_valid() && !res->get_path().empty()) { + value_text = "preload(\"" + res->get_path() + "\")"; + if (symbol.documentation.empty()) { + if (Map::Element *S = GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts.find(res->get_path())) { + symbol.documentation = S->get()->class_symbol.documentation; + } + } + } else { + value_text = JSON::print(node->value); + } + } else { + value_text = JSON::print(node->value); + } + if (!value_text.empty()) { + symbol.detail += " = " + value_text; + } + + r_symbol.children.push_back(symbol); + } + + for (int i = 0; i < p_class->functions.size(); ++i) { + const GDScriptParser::FunctionNode *func = p_class->functions[i]; + lsp::DocumentSymbol symbol; + parse_function_symbol(func, symbol); + r_symbol.children.push_back(symbol); + } + + for (int i = 0; i < p_class->static_functions.size(); ++i) { + const GDScriptParser::FunctionNode *func = p_class->static_functions[i]; + lsp::DocumentSymbol symbol; + parse_function_symbol(func, symbol); + r_symbol.children.push_back(symbol); + } + + for (int i = 0; i < p_class->subclasses.size(); ++i) { + const GDScriptParser::ClassNode *subclass = p_class->subclasses[i]; + lsp::DocumentSymbol symbol; + parse_class_symbol(subclass, symbol); + r_symbol.children.push_back(symbol); + } +} + +void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionNode *p_func, lsp::DocumentSymbol &r_symbol) { + const String uri = get_uri(); + + r_symbol.name = p_func->name; + r_symbol.kind = p_func->_static ? lsp::SymbolKind::Function : lsp::SymbolKind::Method; + r_symbol.detail = "func " + p_func->name + "("; + r_symbol.deprecated = false; + const int line = LINE_NUMBER_TO_INDEX(p_func->line); + r_symbol.range.start.line = line; + r_symbol.range.start.character = p_func->column; + r_symbol.range.end.line = MAX(p_func->body->end_line - 2, r_symbol.range.start.line); + r_symbol.range.end.character = lines[r_symbol.range.end.line].length(); + r_symbol.selectionRange.start.line = r_symbol.range.start.line; + r_symbol.documentation = parse_documentation(line); + r_symbol.uri = uri; + r_symbol.script_path = path; + + String arguments; + for (int i = 0; i < p_func->arguments.size(); i++) { + lsp::DocumentSymbol symbol; + symbol.kind = lsp::SymbolKind::Variable; + symbol.name = p_func->arguments[i]; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(p_func->body->line); + symbol.range.start.character = p_func->body->column; + symbol.range.end = symbol.range.start; + symbol.uri = uri; + symbol.script_path = path; + r_symbol.children.push_back(symbol); + if (i > 0) { + arguments += ", "; + } + arguments += String(p_func->arguments[i]); + if (p_func->argument_types[i].kind != GDScriptParser::DataType::UNRESOLVED) { + arguments += ": " + p_func->argument_types[i].to_string(); + } + int default_value_idx = i - (p_func->arguments.size() - p_func->default_values.size()); + if (default_value_idx >= 0) { + const GDScriptParser::ConstantNode *const_node = dynamic_cast(p_func->default_values[default_value_idx]); + if (const_node == nullptr) { + const GDScriptParser::OperatorNode *operator_node = dynamic_cast(p_func->default_values[default_value_idx]); + if (operator_node) { + const_node = dynamic_cast(operator_node->next); + } + } + + if (const_node) { + String value = JSON::print(const_node->value); + arguments += " = " + value; + } + } + } + r_symbol.detail += arguments + ")"; + if (p_func->return_type.kind != GDScriptParser::DataType::UNRESOLVED) { + r_symbol.detail += " -> " + p_func->return_type.to_string(); + } + + List function_blocks; + List block_stack; + block_stack.push_back(p_func->body); + + while (!block_stack.empty()) { + GDScriptParser::BlockNode *block = block_stack[0]; + block_stack.pop_front(); + + function_blocks.push_back(block); + for (const List::Element *E = block->sub_blocks.front(); E; E = E->next()) { + block_stack.push_back(E->get()); + } + } + + for (const List::Element *B = function_blocks.front(); B; B = B->next()) { + for (const Map::Element *E = B->get()->variables.front(); E; E = E->next()) { + lsp::DocumentSymbol symbol; + const GDScriptParser::LocalVarNode *var = E->value(); + symbol.name = E->key(); + symbol.kind = lsp::SymbolKind::Variable; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(E->get()->line); + symbol.range.start.character = E->get()->column; + symbol.range.end.line = symbol.range.start.line; + symbol.range.end.character = lines[symbol.range.end.line].length(); + symbol.uri = uri; + symbol.script_path = path; + symbol.detail = "var " + symbol.name; + if (var->datatype.kind != GDScriptParser::DataType::UNRESOLVED) { + symbol.detail += ": " + var->datatype.to_string(); + } + symbol.documentation = parse_documentation(line); + r_symbol.children.push_back(symbol); + } + } +} + +String ExtendGDScriptParser::parse_documentation(int p_line, bool p_docs_down) { + ERR_FAIL_INDEX_V(p_line, lines.size(), String()); + + List doc_lines; + + if (!p_docs_down) { // inline comment + String inline_comment = lines[p_line]; + int comment_start = inline_comment.find("#"); + if (comment_start != -1) { + inline_comment = inline_comment.substr(comment_start, inline_comment.length()).strip_edges(); + if (inline_comment.length() > 1) { + doc_lines.push_back(inline_comment.substr(1, inline_comment.length())); + } + } + } + + int step = p_docs_down ? 1 : -1; + int start_line = p_docs_down ? p_line : p_line - 1; + for (int i = start_line; true; i += step) { + if (i < 0 || i >= lines.size()) { + break; + } + + String line_comment = lines[i].strip_edges(true, false); + if (line_comment.begins_with("#")) { + line_comment = line_comment.substr(1, line_comment.length()); + if (p_docs_down) { + doc_lines.push_back(line_comment); + } else { + doc_lines.push_front(line_comment); + } + } else { + if (i > 0 && i < lines.size() - 1) { + String next_line = lines[i + step].strip_edges(true, false); + if (next_line.begins_with("#")) { + continue; + } + } + break; + } + } + + String doc; + for (List::Element *E = doc_lines.front(); E; E = E->next()) { + doc += E->get() + "\n"; + } + return doc; +} + +String ExtendGDScriptParser::get_text_for_completion(const lsp::Position &p_cursor) const { + String longthing; + int len = lines.size(); + for (int i = 0; i < len; i++) { + if (i == p_cursor.line) { + longthing += lines[i].substr(0, p_cursor.character); + longthing += String::chr(0xFFFF); //not unicode, represents the cursor + longthing += lines[i].substr(p_cursor.character, lines[i].size()); + } else { + longthing += lines[i]; + } + + if (i != len - 1) { + longthing += "\n"; + } + } + + return longthing; +} + +String ExtendGDScriptParser::get_text_for_lookup_symbol(const lsp::Position &p_cursor, const String &p_symbol, bool p_func_required) const { + String longthing; + int len = lines.size(); + for (int i = 0; i < len; i++) { + if (i == p_cursor.line) { + String line = lines[i]; + String first_part = line.substr(0, p_cursor.character); + String last_part = line.substr(p_cursor.character + 1, lines[i].length()); + if (!p_symbol.empty()) { + String left_cursor_text; + for (int c = p_cursor.character - 1; c >= 0; c--) { + left_cursor_text = line.substr(c, p_cursor.character - c); + if (p_symbol.begins_with(left_cursor_text)) { + first_part = line.substr(0, c); + first_part += p_symbol; + break; + } + } + } + + longthing += first_part; + longthing += String::chr(0xFFFF); //not unicode, represents the cursor + if (p_func_required) { + longthing += "("; // tell the parser this is a function call + } + longthing += last_part; + } else { + longthing += lines[i]; + } + + if (i != len - 1) { + longthing += "\n"; + } + } + + return longthing; +} + +String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position &p_position, Vector2i &p_offset) const { + ERR_FAIL_INDEX_V(p_position.line, lines.size(), ""); + String line = lines[p_position.line]; + if (line.empty()) { + return ""; + } + ERR_FAIL_INDEX_V(p_position.character, line.size(), ""); + + int start_pos = p_position.character; + for (int c = p_position.character; c >= 0; c--) { + start_pos = c; + CharType ch = line[c]; + bool valid_char = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'; + if (!valid_char) { + break; + } + } + + int end_pos = p_position.character; + for (int c = p_position.character; c < line.length(); c++) { + CharType ch = line[c]; + bool valid_char = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'; + if (!valid_char) { + break; + } + end_pos = c; + } + if (start_pos < end_pos) { + p_offset.x = start_pos - p_position.character; + p_offset.y = end_pos - p_position.character; + return line.substr(start_pos + 1, end_pos - start_pos); + } + + return ""; +} + +String ExtendGDScriptParser::get_uri() const { + return GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_uri(path); +} + +const lsp::DocumentSymbol *ExtendGDScriptParser::search_symbol_defined_at_line(int p_line, const lsp::DocumentSymbol &p_parent) const { + const lsp::DocumentSymbol *ret = nullptr; + if (p_line < p_parent.range.start.line) { + return ret; + } else if (p_parent.range.start.line == p_line) { + return &p_parent; + } else { + for (int i = 0; i < p_parent.children.size(); i++) { + ret = search_symbol_defined_at_line(p_line, p_parent.children[i]); + if (ret) { + break; + } + } + } + return ret; +} + +Error ExtendGDScriptParser::get_left_function_call(const lsp::Position &p_position, lsp::Position &r_func_pos, int &r_arg_index) const { + ERR_FAIL_INDEX_V(p_position.line, lines.size(), ERR_INVALID_PARAMETER); + + int bracket_stack = 0; + int index = 0; + + bool found = false; + for (int l = p_position.line; l >= 0; --l) { + String line = lines[l]; + int c = line.length() - 1; + if (l == p_position.line) { + c = MIN(c, p_position.character - 1); + } + + while (c >= 0) { + const CharType &character = line[c]; + if (character == ')') { + ++bracket_stack; + } else if (character == '(') { + --bracket_stack; + if (bracket_stack < 0) { + found = true; + } + } + if (bracket_stack <= 0 && character == ',') { + ++index; + } + --c; + if (found) { + r_func_pos.character = c; + break; + } + } + + if (found) { + r_func_pos.line = l; + r_arg_index = index; + return OK; + } + } + + return ERR_METHOD_NOT_FOUND; +} + +const lsp::DocumentSymbol *ExtendGDScriptParser::get_symbol_defined_at_line(int p_line) const { + if (p_line <= 0) { + return &class_symbol; + } + return search_symbol_defined_at_line(p_line, class_symbol); +} + +const lsp::DocumentSymbol *ExtendGDScriptParser::get_member_symbol(const String &p_name, const String &p_subclass) const { + if (p_subclass.empty()) { + const lsp::DocumentSymbol *const *ptr = members.getptr(p_name); + if (ptr) { + return *ptr; + } + } else { + if (const ClassMembers *_class = inner_classes.getptr(p_subclass)) { + const lsp::DocumentSymbol *const *ptr = _class->getptr(p_name); + if (ptr) { + return *ptr; + } + } + } + + return nullptr; +} + +const List &ExtendGDScriptParser::get_document_links() const { + return document_links; +} + +const Array &ExtendGDScriptParser::get_member_completions() { + if (member_completions.empty()) { + const String *name = members.next(nullptr); + while (name) { + const lsp::DocumentSymbol *symbol = members.get(*name); + lsp::CompletionItem item = symbol->make_completion_item(); + item.data = JOIN_SYMBOLS(path, *name); + member_completions.push_back(item.to_json()); + + name = members.next(name); + } + + const String *_class = inner_classes.next(nullptr); + while (_class) { + const ClassMembers *inner_class = inner_classes.getptr(*_class); + const String *member_name = inner_class->next(nullptr); + while (member_name) { + const lsp::DocumentSymbol *symbol = inner_class->get(*member_name); + lsp::CompletionItem item = symbol->make_completion_item(); + item.data = JOIN_SYMBOLS(path, JOIN_SYMBOLS(*_class, *member_name)); + member_completions.push_back(item.to_json()); + + member_name = inner_class->next(member_name); + } + + _class = inner_classes.next(_class); + } + } + + return member_completions; +} + +Dictionary ExtendGDScriptParser::dump_function_api(const GDScriptParser::FunctionNode *p_func) const { + Dictionary func; + ERR_FAIL_NULL_V(p_func, func); + func["name"] = p_func->name; + func["return_type"] = p_func->return_type.to_string(); + func["rpc_mode"] = p_func->rpc_mode; + Array arguments; + for (int i = 0; i < p_func->arguments.size(); i++) { + Dictionary arg; + arg["name"] = p_func->arguments[i]; + arg["type"] = p_func->argument_types[i].to_string(); + int default_value_idx = i - (p_func->arguments.size() - p_func->default_values.size()); + if (default_value_idx >= 0) { + const GDScriptParser::ConstantNode *const_node = dynamic_cast(p_func->default_values[default_value_idx]); + if (const_node == nullptr) { + const GDScriptParser::OperatorNode *operator_node = dynamic_cast(p_func->default_values[default_value_idx]); + if (operator_node) { + const_node = dynamic_cast(operator_node->next); + } + } + if (const_node) { + arg["default_value"] = const_node->value; + } + } + arguments.push_back(arg); + } + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(p_func->line))) { + func["signature"] = symbol->detail; + func["description"] = symbol->documentation; + } + func["arguments"] = arguments; + return func; +} + +Dictionary ExtendGDScriptParser::dump_class_api(const GDScriptParser::ClassNode *p_class) const { + Dictionary class_api; + + ERR_FAIL_NULL_V(p_class, class_api); + + class_api["name"] = String(p_class->name); + class_api["path"] = path; + Array extends_class; + for (int i = 0; i < p_class->extends_class.size(); i++) { + extends_class.append(String(p_class->extends_class[i])); + } + class_api["extends_class"] = extends_class; + class_api["extends_file"] = String(p_class->extends_file); + class_api["icon"] = String(p_class->icon_path); + + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(p_class->line))) { + class_api["signature"] = symbol->detail; + class_api["description"] = symbol->documentation; + } + + Array subclasses; + for (int i = 0; i < p_class->subclasses.size(); i++) { + subclasses.push_back(dump_class_api(p_class->subclasses[i])); + } + class_api["sub_classes"] = subclasses; + + Array constants; + for (Map::Element *E = p_class->constant_expressions.front(); E; E = E->next()) { + const GDScriptParser::ClassNode::Constant &c = E->value(); + const GDScriptParser::ConstantNode *node = dynamic_cast(c.expression); + ERR_FAIL_COND_V(!node, class_api); + + Dictionary api; + api["name"] = E->key(); + api["value"] = node->value; + api["data_type"] = node->datatype.to_string(); + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(node->line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + constants.push_back(api); + } + class_api["constants"] = constants; + + Array members; + for (int i = 0; i < p_class->variables.size(); ++i) { + const GDScriptParser::ClassNode::Member &m = p_class->variables[i]; + Dictionary api; + api["name"] = m.identifier; + api["data_type"] = m.data_type.to_string(); + api["default_value"] = m.default_value; + api["setter"] = String(m.setter); + api["getter"] = String(m.getter); + api["export"] = m._export.type != Variant::NIL; + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(m.line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + members.push_back(api); + } + class_api["members"] = members; + + Array signals; + for (int i = 0; i < p_class->_signals.size(); ++i) { + const GDScriptParser::ClassNode::Signal &signal = p_class->_signals[i]; + Dictionary api; + api["name"] = signal.name; + Array args; + for (int j = 0; j < signal.arguments.size(); j++) { + args.append(signal.arguments[j]); + } + api["arguments"] = args; + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(signal.line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + signals.push_back(api); + } + class_api["signals"] = signals; + + Array methods; + for (int i = 0; i < p_class->functions.size(); ++i) { + methods.append(dump_function_api(p_class->functions[i])); + } + class_api["methods"] = methods; + + Array static_functions; + for (int i = 0; i < p_class->static_functions.size(); ++i) { + static_functions.append(dump_function_api(p_class->static_functions[i])); + } + class_api["static_functions"] = static_functions; + + return class_api; +} + +Dictionary ExtendGDScriptParser::generate_api() const { + Dictionary api; + const GDScriptParser::Node *head = get_parse_tree(); + if (const GDScriptParser::ClassNode *gdclass = dynamic_cast(head)) { + api = dump_class_api(gdclass); + } + return api; +} + +Error ExtendGDScriptParser::parse(const String &p_code, const String &p_path) { + path = p_path; + lines = p_code.split("\n"); + + Error err = GDScriptParser::parse(p_code, p_path.get_base_dir(), false, p_path, false, nullptr, false); + update_diagnostics(); + update_symbols(); + update_document_links(p_code); + return err; +} diff --git a/modules/gdscript/language_server/gdscript_extend_parser.h b/modules/gdscript/language_server/gdscript_extend_parser.h new file mode 100644 index 000000000..e360fdcf1 --- /dev/null +++ b/modules/gdscript/language_server/gdscript_extend_parser.h @@ -0,0 +1,102 @@ +/**************************************************************************/ +/* gdscript_extend_parser.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDSCRIPT_EXTEND_PARSER_H +#define GDSCRIPT_EXTEND_PARSER_H + +#include "../gdscript_parser.h" +#include "core/variant.h" +#include "lsp.hpp" + +#ifndef LINE_NUMBER_TO_INDEX +#define LINE_NUMBER_TO_INDEX(p_line) ((p_line)-1) +#endif + +#ifndef SYMBOL_SEPERATOR +#define SYMBOL_SEPERATOR "::" +#endif + +#ifndef JOIN_SYMBOLS +#define JOIN_SYMBOLS(p_path, name) ((p_path) + SYMBOL_SEPERATOR + (name)) +#endif + +typedef HashMap ClassMembers; + +class ExtendGDScriptParser : public GDScriptParser { + String path; + Vector lines; + + lsp::DocumentSymbol class_symbol; + Vector diagnostics; + List document_links; + ClassMembers members; + HashMap inner_classes; + + void update_diagnostics(); + + void update_symbols(); + void update_document_links(const String &p_code); + void parse_class_symbol(const GDScriptParser::ClassNode *p_class, lsp::DocumentSymbol &r_symbol); + void parse_function_symbol(const GDScriptParser::FunctionNode *p_func, lsp::DocumentSymbol &r_symbol); + + Dictionary dump_function_api(const GDScriptParser::FunctionNode *p_func) const; + Dictionary dump_class_api(const GDScriptParser::ClassNode *p_class) const; + + String parse_documentation(int p_line, bool p_docs_down = false); + const lsp::DocumentSymbol *search_symbol_defined_at_line(int p_line, const lsp::DocumentSymbol &p_parent) const; + + Array member_completions; + +public: + _FORCE_INLINE_ const String &get_path() const { return path; } + _FORCE_INLINE_ const Vector &get_lines() const { return lines; } + _FORCE_INLINE_ const lsp::DocumentSymbol &get_symbols() const { return class_symbol; } + _FORCE_INLINE_ const Vector &get_diagnostics() const { return diagnostics; } + _FORCE_INLINE_ const ClassMembers &get_members() const { return members; } + _FORCE_INLINE_ const HashMap &get_inner_classes() const { return inner_classes; } + + Error get_left_function_call(const lsp::Position &p_position, lsp::Position &r_func_pos, int &r_arg_index) const; + + String get_text_for_completion(const lsp::Position &p_cursor) const; + String get_text_for_lookup_symbol(const lsp::Position &p_cursor, const String &p_symbol = "", bool p_func_required = false) const; + String get_identifier_under_position(const lsp::Position &p_position, Vector2i &p_offset) const; + String get_uri() const; + + const lsp::DocumentSymbol *get_symbol_defined_at_line(int p_line) const; + const lsp::DocumentSymbol *get_member_symbol(const String &p_name, const String &p_subclass = "") const; + const List &get_document_links() const; + + const Array &get_member_completions(); + Dictionary generate_api() const; + + Error parse(const String &p_code, const String &p_path); +}; + +#endif // GDSCRIPT_EXTEND_PARSER_H diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp new file mode 100644 index 000000000..f4f7b5b30 --- /dev/null +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -0,0 +1,323 @@ +/**************************************************************************/ +/* gdscript_language_protocol.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_language_protocol.h" + +#include "core/io/json.h" +#include "core/project_settings.h" +#include "editor/editor_log.h" +#include "editor/editor_node.h" + +GDScriptLanguageProtocol *GDScriptLanguageProtocol::singleton = nullptr; + +Error GDScriptLanguageProtocol::LSPeer::handle_data() { + int read = 0; + // Read headers + if (!has_header) { + while (true) { + if (req_pos >= LSP_MAX_BUFFER_SIZE) { + req_pos = 0; + ERR_FAIL_COND_V_MSG(true, ERR_OUT_OF_MEMORY, "Response header too big"); + } + Error err = connection->get_partial_data(&req_buf[req_pos], 1, read); + if (err != OK) { + return FAILED; + } else if (read != 1) { // Busy, wait until next poll + return ERR_BUSY; + } + char *r = (char *)req_buf; + int l = req_pos; + + // End of headers + if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { + r[l - 3] = '\0'; // Null terminate to read string + String header; + header.parse_utf8(r); + content_length = header.substr(16).to_int(); + has_header = true; + req_pos = 0; + break; + } + req_pos++; + } + } + if (has_header) { + while (req_pos < content_length) { + if (req_pos >= LSP_MAX_BUFFER_SIZE) { + req_pos = 0; + has_header = false; + ERR_FAIL_COND_V_MSG(req_pos >= LSP_MAX_BUFFER_SIZE, ERR_OUT_OF_MEMORY, "Response content too big"); + } + Error err = connection->get_partial_data(&req_buf[req_pos], 1, read); + if (err != OK) { + return FAILED; + } else if (read != 1) { + return ERR_BUSY; + } + req_pos++; + } + + // Parse data + String msg; + msg.parse_utf8((const char *)req_buf, req_pos); + + // Reset to read again + req_pos = 0; + has_header = false; + + // Response + String output = GDScriptLanguageProtocol::get_singleton()->process_message(msg); + if (!output.empty()) { + res_queue.push_back(output.utf8()); + } + } + return OK; +} + +Error GDScriptLanguageProtocol::LSPeer::send_data() { + int sent = 0; + if (!res_queue.empty()) { + CharString c_res = res_queue[0]; + if (res_sent < c_res.size()) { + Error err = connection->put_partial_data((const uint8_t *)c_res.get_data() + res_sent, c_res.size() - res_sent - 1, sent); + if (err != OK) { + return err; + } + res_sent += sent; + } + // Response sent + if (res_sent >= c_res.size() - 1) { + res_sent = 0; + res_queue.remove(0); + } + } + return OK; +} + +Error GDScriptLanguageProtocol::on_client_connected() { + Ref tcp_peer = server->take_connection(); + ERR_FAIL_COND_V_MSG(clients.size() >= LSP_MAX_CLIENTS, FAILED, "Max client limits reached"); + Ref peer = memnew(LSPeer); + peer->connection = tcp_peer; + clients.set(next_client_id, peer); + next_client_id++; + EditorNode::get_log()->add_message("Connection Taken", EditorLog::MSG_TYPE_EDITOR); + return OK; +} + +void GDScriptLanguageProtocol::on_client_disconnected(const int &p_client_id) { + clients.erase(p_client_id); + EditorNode::get_log()->add_message("Disconnected", EditorLog::MSG_TYPE_EDITOR); +} + +String GDScriptLanguageProtocol::process_message(const String &p_text) { + String ret = process_string(p_text); + if (ret.empty()) { + return ret; + } else { + return format_output(ret); + } +} + +String GDScriptLanguageProtocol::format_output(const String &p_text) { + String header = "Content-Length: "; + CharString charstr = p_text.utf8(); + size_t len = charstr.length(); + header += itos(len); + header += "\r\n\r\n"; + + return header + p_text; +} + +void GDScriptLanguageProtocol::_bind_methods() { + ClassDB::bind_method(D_METHOD("initialize", "params"), &GDScriptLanguageProtocol::initialize); + ClassDB::bind_method(D_METHOD("initialized", "params"), &GDScriptLanguageProtocol::initialized); + ClassDB::bind_method(D_METHOD("on_client_connected"), &GDScriptLanguageProtocol::on_client_connected); + ClassDB::bind_method(D_METHOD("on_client_disconnected"), &GDScriptLanguageProtocol::on_client_disconnected); + ClassDB::bind_method(D_METHOD("notify_client", "method", "params", "client_id"), &GDScriptLanguageProtocol::notify_client, DEFVAL(Variant()), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("is_smart_resolve_enabled"), &GDScriptLanguageProtocol::is_smart_resolve_enabled); + ClassDB::bind_method(D_METHOD("get_text_document"), &GDScriptLanguageProtocol::get_text_document); + ClassDB::bind_method(D_METHOD("get_workspace"), &GDScriptLanguageProtocol::get_workspace); + ClassDB::bind_method(D_METHOD("is_initialized"), &GDScriptLanguageProtocol::is_initialized); +} + +Dictionary GDScriptLanguageProtocol::initialize(const Dictionary &p_params) { + lsp::InitializeResult ret; + + String root_uri = p_params["rootUri"]; + String root = p_params["rootPath"]; + bool is_same_workspace; +#ifndef WINDOWS_ENABLED + is_same_workspace = root.to_lower() == workspace->root.to_lower(); +#else + is_same_workspace = root.replace("\\", "/").to_lower() == workspace->root.to_lower(); +#endif + + if (root_uri.length() && is_same_workspace) { + workspace->root_uri = root_uri; + } else { + String r_root = workspace->root; + r_root = r_root.lstrip("/"); + workspace->root_uri = "file:///" + r_root; + + Dictionary params; + params["path"] = workspace->root; + Dictionary request = make_notification("gdscript_client/changeWorkspace", params); + + ERR_FAIL_COND_V_MSG(!clients.has(latest_client_id), ret.to_json(), + vformat("GDScriptLanguageProtocol: Can't initialize invalid peer '%d'.", latest_client_id)); + Ref peer = clients.get(latest_client_id); + if (peer != nullptr) { + String msg = JSON::print(request); + msg = format_output(msg); + (*peer)->res_queue.push_back(msg.utf8()); + } + } + + if (!_initialized) { + workspace->initialize(); + text_document->initialize(); + _initialized = true; + } + + return ret.to_json(); +} + +void GDScriptLanguageProtocol::initialized(const Variant &p_params) { + lsp::GodotCapabilities capabilities; + + DocData *doc = EditorHelp::get_doc_data(); + for (Map::Element *E = doc->class_list.front(); E; E = E->next()) { + lsp::GodotNativeClassInfo gdclass; + gdclass.name = E->get().name; + gdclass.class_doc = &(E->get()); + if (ClassDB::ClassInfo *ptr = ClassDB::classes.getptr(StringName(E->get().name))) { + gdclass.class_info = ptr; + } + capabilities.native_classes.push_back(gdclass); + } + + notify_client("gdscript/capabilities", capabilities.to_json()); +} + +void GDScriptLanguageProtocol::poll() { + if (server->is_connection_available()) { + on_client_connected(); + } + const int *id = nullptr; + while ((id = clients.next(id))) { + Ref peer = clients.get(*id); + StreamPeerTCP::Status status = peer->connection->get_status(); + if (status == StreamPeerTCP::STATUS_NONE || status == StreamPeerTCP::STATUS_ERROR) { + on_client_disconnected(*id); + id = nullptr; + } else { + if (peer->connection->get_available_bytes() > 0) { + latest_client_id = *id; + Error err = peer->handle_data(); + if (err != OK && err != ERR_BUSY) { + on_client_disconnected(*id); + id = nullptr; + } + } + Error err = peer->send_data(); + if (err != OK && err != ERR_BUSY) { + on_client_disconnected(*id); + id = nullptr; + } + } + } +} + +Error GDScriptLanguageProtocol::start(int p_port, const IP_Address &p_bind_ip) { + return server->listen(p_port, p_bind_ip); +} + +void GDScriptLanguageProtocol::stop() { + const int *id = nullptr; + while ((id = clients.next(id))) { + Ref peer = clients.get(*id); + peer->connection->disconnect_from_host(); + } + + server->stop(); +} + +void GDScriptLanguageProtocol::notify_client(const String &p_method, const Variant &p_params, int p_client_id) { + if (p_client_id == -1) { + ERR_FAIL_COND_MSG(latest_client_id == -1, + "GDScript LSP: Can't notify client as none was connected."); + p_client_id = latest_client_id; + } + ERR_FAIL_COND(!clients.has(p_client_id)); + Ref peer = clients.get(p_client_id); + ERR_FAIL_COND(peer == nullptr); + + Dictionary message = make_notification(p_method, p_params); + String msg = JSON::print(message); + msg = format_output(msg); + peer->res_queue.push_back(msg.utf8()); +} + +void GDScriptLanguageProtocol::request_client(const String &p_method, const Variant &p_params, int p_client_id) { + if (p_client_id == -1) { + ERR_FAIL_COND_MSG(latest_client_id == -1, + "GDScript LSP: Can't notify client as none was connected."); + p_client_id = latest_client_id; + } + ERR_FAIL_COND(!clients.has(p_client_id)); + Ref peer = clients.get(p_client_id); + ERR_FAIL_COND(peer == nullptr); + + Dictionary message = make_request(p_method, p_params, next_server_id); + next_server_id++; + String msg = JSON::print(message); + msg = format_output(msg); + peer->res_queue.push_back(msg.utf8()); +} + +bool GDScriptLanguageProtocol::is_smart_resolve_enabled() const { + return bool(_EDITOR_GET("network/language_server/enable_smart_resolve")); +} + +bool GDScriptLanguageProtocol::is_goto_native_symbols_enabled() const { + return bool(_EDITOR_GET("network/language_server/show_native_symbols_in_editor")); +} + +GDScriptLanguageProtocol::GDScriptLanguageProtocol() { + server.instance(); + singleton = this; + workspace.instance(); + text_document.instance(); + set_scope("textDocument", text_document.ptr()); + set_scope("completionItem", text_document.ptr()); + set_scope("workspace", workspace.ptr()); + workspace->root = ProjectSettings::get_singleton()->get_resource_path(); +} diff --git a/modules/gdscript/language_server/gdscript_language_protocol.h b/modules/gdscript/language_server/gdscript_language_protocol.h new file mode 100644 index 000000000..8c06ade5b --- /dev/null +++ b/modules/gdscript/language_server/gdscript_language_protocol.h @@ -0,0 +1,113 @@ +/**************************************************************************/ +/* gdscript_language_protocol.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDSCRIPT_LANGUAGE_PROTOCOL_H +#define GDSCRIPT_LANGUAGE_PROTOCOL_H + +#include "core/io/stream_peer.h" +#include "core/io/stream_peer_tcp.h" +#include "core/io/tcp_server.h" +#include "gdscript_text_document.h" +#include "gdscript_workspace.h" +#include "lsp.hpp" +#include "modules/jsonrpc/jsonrpc.h" + +#define LSP_MAX_BUFFER_SIZE 4194304 +#define LSP_MAX_CLIENTS 8 + +class GDScriptLanguageProtocol : public JSONRPC { + GDCLASS(GDScriptLanguageProtocol, JSONRPC) + +private: + struct LSPeer : Reference { + Ref connection; + + uint8_t req_buf[LSP_MAX_BUFFER_SIZE]; + int req_pos = 0; + bool has_header = false; + bool has_content = false; + int content_length = 0; + Vector res_queue; + int res_sent = 0; + + Error handle_data(); + Error send_data(); + }; + + enum LSPErrorCode { + RequestCancelled = -32800, + ContentModified = -32801, + }; + + static GDScriptLanguageProtocol *singleton; + + HashMap> clients; + Ref server; + int latest_client_id = 0; + int next_client_id = 0; + int next_server_id = 0; + + Ref text_document; + Ref workspace; + + Error on_client_connected(); + void on_client_disconnected(const int &p_client_id); + + String process_message(const String &p_text); + String format_output(const String &p_text); + + bool _initialized = false; + +protected: + static void _bind_methods(); + + Dictionary initialize(const Dictionary &p_params); + void initialized(const Variant &p_params); + +public: + _FORCE_INLINE_ static GDScriptLanguageProtocol *get_singleton() { return singleton; } + _FORCE_INLINE_ Ref get_workspace() { return workspace; } + _FORCE_INLINE_ Ref get_text_document() { return text_document; } + _FORCE_INLINE_ bool is_initialized() const { return _initialized; } + + void poll(); + Error start(int p_port, const IP_Address &p_bind_ip); + void stop(); + + void notify_client(const String &p_method, const Variant &p_params = Variant(), int p_client_id = -1); + void request_client(const String &p_method, const Variant &p_params = Variant(), int p_client_id = -1); + + bool is_smart_resolve_enabled() const; + bool is_goto_native_symbols_enabled() const; + + GDScriptLanguageProtocol(); +}; + +#endif // GDSCRIPT_LANGUAGE_PROTOCOL_H diff --git a/modules/gdscript/language_server/gdscript_language_server.cpp b/modules/gdscript/language_server/gdscript_language_server.cpp new file mode 100644 index 000000000..9e4245b22 --- /dev/null +++ b/modules/gdscript/language_server/gdscript_language_server.cpp @@ -0,0 +1,118 @@ +/**************************************************************************/ +/* gdscript_language_server.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_language_server.h" + +#include "core/os/file_access.h" +#include "core/os/os.h" +#include "editor/editor_log.h" +#include "editor/editor_node.h" + +int GDScriptLanguageServer::port_override = -1; + +GDScriptLanguageServer::GDScriptLanguageServer() { + thread_running = false; + started = false; + use_thread = false; + host = "127.0.0.1"; + port = 6008; + + _EDITOR_DEF("network/language_server/remote_host", host); + _EDITOR_DEF("network/language_server/remote_port", port); + _EDITOR_DEF("network/language_server/enable_smart_resolve", true); + _EDITOR_DEF("network/language_server/show_native_symbols_in_editor", false); + _EDITOR_DEF("network/language_server/use_thread", use_thread); +} + +void GDScriptLanguageServer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + start(); + break; + case NOTIFICATION_EXIT_TREE: + stop(); + break; + case NOTIFICATION_INTERNAL_PROCESS: { + if (started && !use_thread) { + protocol.poll(); + } + } break; + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + String host = String(_EDITOR_GET("network/language_server/remote_host")); + int port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); + bool use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); + if (host != this->host || port != this->port || use_thread != this->use_thread) { + this->stop(); + this->start(); + } + } break; + } +} + +void GDScriptLanguageServer::thread_main(void *p_userdata) { + GDScriptLanguageServer *self = static_cast(p_userdata); + while (self->thread_running) { + // Poll 20 times per second + self->protocol.poll(); + OS::get_singleton()->delay_usec(50000); + } +} + +void GDScriptLanguageServer::start() { + host = String(_EDITOR_GET("network/language_server/remote_host")); + port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); + use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); + if (protocol.start(port, IP_Address(host)) == OK) { + EditorNode::get_log()->add_message("--- GDScript language server started on port " + itos(port) + " ---", EditorLog::MSG_TYPE_EDITOR); + if (use_thread) { + thread_running = true; + thread.start(GDScriptLanguageServer::thread_main, this); + } + set_process_internal(!use_thread); + started = true; + } +} + +void GDScriptLanguageServer::stop() { + if (use_thread) { + ERR_FAIL_COND(!thread.is_started()); + thread_running = false; + thread.wait_to_finish(); + } + protocol.stop(); + started = false; + EditorNode::get_log()->add_message("--- GDScript language server stopped ---", EditorLog::MSG_TYPE_EDITOR); +} + +void register_lsp_types() { + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); +} diff --git a/modules/gdscript/language_server/gdscript_language_server.h b/modules/gdscript/language_server/gdscript_language_server.h new file mode 100644 index 000000000..ad9e6e97b --- /dev/null +++ b/modules/gdscript/language_server/gdscript_language_server.h @@ -0,0 +1,63 @@ +/**************************************************************************/ +/* gdscript_language_server.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDSCRIPT_LANGUAGE_SERVER_H +#define GDSCRIPT_LANGUAGE_SERVER_H + +#include "../gdscript_parser.h" +#include "editor/editor_plugin.h" +#include "gdscript_language_protocol.h" + +class GDScriptLanguageServer : public EditorPlugin { + GDCLASS(GDScriptLanguageServer, EditorPlugin); + + GDScriptLanguageProtocol protocol; + + Thread thread; + bool thread_running; + bool started; + bool use_thread; + String host; + int port; + static void thread_main(void *p_userdata); + +private: + void _notification(int p_what); + +public: + static int port_override; + GDScriptLanguageServer(); + void start(); + void stop(); +}; + +void register_lsp_types(); + +#endif // GDSCRIPT_LANGUAGE_SERVER_H diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp new file mode 100644 index 000000000..e48531ea2 --- /dev/null +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -0,0 +1,466 @@ +/**************************************************************************/ +/* gdscript_text_document.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_text_document.h" + +#include "../gdscript.h" +#include "core/os/os.h" +#include "editor/editor_settings.h" +#include "editor/plugins/script_text_editor.h" +#include "gdscript_extend_parser.h" +#include "gdscript_language_protocol.h" + +void GDScriptTextDocument::_bind_methods() { + ClassDB::bind_method(D_METHOD("didOpen"), &GDScriptTextDocument::didOpen); + ClassDB::bind_method(D_METHOD("didClose"), &GDScriptTextDocument::didClose); + ClassDB::bind_method(D_METHOD("didChange"), &GDScriptTextDocument::didChange); + ClassDB::bind_method(D_METHOD("didSave"), &GDScriptTextDocument::didSave); + ClassDB::bind_method(D_METHOD("nativeSymbol"), &GDScriptTextDocument::nativeSymbol); + ClassDB::bind_method(D_METHOD("documentSymbol"), &GDScriptTextDocument::documentSymbol); + ClassDB::bind_method(D_METHOD("completion"), &GDScriptTextDocument::completion); + ClassDB::bind_method(D_METHOD("resolve"), &GDScriptTextDocument::resolve); + ClassDB::bind_method(D_METHOD("rename"), &GDScriptTextDocument::rename); + ClassDB::bind_method(D_METHOD("foldingRange"), &GDScriptTextDocument::foldingRange); + ClassDB::bind_method(D_METHOD("codeLens"), &GDScriptTextDocument::codeLens); + ClassDB::bind_method(D_METHOD("documentLink"), &GDScriptTextDocument::documentLink); + ClassDB::bind_method(D_METHOD("colorPresentation"), &GDScriptTextDocument::colorPresentation); + ClassDB::bind_method(D_METHOD("hover"), &GDScriptTextDocument::hover); + ClassDB::bind_method(D_METHOD("definition"), &GDScriptTextDocument::definition); + ClassDB::bind_method(D_METHOD("declaration"), &GDScriptTextDocument::declaration); + ClassDB::bind_method(D_METHOD("signatureHelp"), &GDScriptTextDocument::signatureHelp); + ClassDB::bind_method(D_METHOD("show_native_symbol_in_editor"), &GDScriptTextDocument::show_native_symbol_in_editor); +} + +void GDScriptTextDocument::didOpen(const Variant &p_param) { + lsp::TextDocumentItem doc = load_document_item(p_param); + sync_script_content(doc.uri, doc.text); +} + +void GDScriptTextDocument::didClose(const Variant &p_param) { + // Left empty on purpose. Godot does nothing special on closing a document, + // but it satisfies LSP clients that require didClose be implemented. +} + +void GDScriptTextDocument::didChange(const Variant &p_param) { + lsp::TextDocumentItem doc = load_document_item(p_param); + Dictionary dict = p_param; + Array contentChanges = dict["contentChanges"]; + for (int i = 0; i < contentChanges.size(); ++i) { + lsp::TextDocumentContentChangeEvent evt; + evt.load(contentChanges[i]); + doc.text = evt.text; + } + sync_script_content(doc.uri, doc.text); +} + +void GDScriptTextDocument::didSave(const Variant &p_param) { + lsp::TextDocumentItem doc = load_document_item(p_param); + Dictionary dict = p_param; + String text = dict["text"]; + + sync_script_content(doc.uri, text); +} + +lsp::TextDocumentItem GDScriptTextDocument::load_document_item(const Variant &p_param) { + lsp::TextDocumentItem doc; + Dictionary params = p_param; + doc.load(params["textDocument"]); + return doc; +} + +void GDScriptTextDocument::notify_client_show_symbol(const lsp::DocumentSymbol *symbol) { + ERR_FAIL_NULL(symbol); + GDScriptLanguageProtocol::get_singleton()->notify_client("gdscript/show_native_symbol", symbol->to_json(true)); +} + +void GDScriptTextDocument::initialize() { + if (GDScriptLanguageProtocol::get_singleton()->is_smart_resolve_enabled()) { + const HashMap &native_members = GDScriptLanguageProtocol::get_singleton()->get_workspace()->native_members; + + const StringName *class_ptr = native_members.next(nullptr); + while (class_ptr) { + const ClassMembers &members = native_members.get(*class_ptr); + + const String *name = members.next(nullptr); + while (name) { + const lsp::DocumentSymbol *symbol = members.get(*name); + lsp::CompletionItem item = symbol->make_completion_item(); + item.data = JOIN_SYMBOLS(String(*class_ptr), *name); + native_member_completions.push_back(item.to_json()); + + name = members.next(name); + } + + class_ptr = native_members.next(class_ptr); + } + } +} + +Variant GDScriptTextDocument::nativeSymbol(const Dictionary &p_params) { + Variant ret; + + lsp::NativeSymbolInspectParams params; + params.load(p_params); + + if (const lsp::DocumentSymbol *symbol = GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_native_symbol(params)) { + ret = symbol->to_json(true); + notify_client_show_symbol(symbol); + } + + return ret; +} + +Array GDScriptTextDocument::documentSymbol(const Dictionary &p_params) { + Dictionary params = p_params["textDocument"]; + String uri = params["uri"]; + String path = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_path(uri); + Array arr; + if (const Map::Element *parser = GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts.find(path)) { + Vector list; + parser->get()->get_symbols().symbol_tree_as_list(uri, list); + for (int i = 0; i < list.size(); i++) { + arr.push_back(list[i].to_json()); + } + } + return arr; +} + +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.empty()) { + int i = 0; + arr.resize(options.size()); + + for (const List::Element *E = options.front(); E; E = E->next()) { + const ScriptCodeCompletionOption &option = E->get(); + lsp::CompletionItem item; + item.label = option.display; + item.data = request_data; + item.insertText = option.insert_text; + + switch (option.kind) { + case ScriptCodeCompletionOption::KIND_ENUM: + item.kind = lsp::CompletionItemKind::Enum; + break; + case ScriptCodeCompletionOption::KIND_CLASS: + item.kind = lsp::CompletionItemKind::Class; + break; + case ScriptCodeCompletionOption::KIND_MEMBER: + item.kind = lsp::CompletionItemKind::Property; + break; + case ScriptCodeCompletionOption::KIND_FUNCTION: + item.kind = lsp::CompletionItemKind::Method; + break; + case ScriptCodeCompletionOption::KIND_SIGNAL: + item.kind = lsp::CompletionItemKind::Event; + break; + case ScriptCodeCompletionOption::KIND_CONSTANT: + item.kind = lsp::CompletionItemKind::Constant; + break; + case ScriptCodeCompletionOption::KIND_VARIABLE: + item.kind = lsp::CompletionItemKind::Variable; + break; + case ScriptCodeCompletionOption::KIND_FILE_PATH: + item.kind = lsp::CompletionItemKind::File; + break; + case ScriptCodeCompletionOption::KIND_NODE_PATH: + item.kind = lsp::CompletionItemKind::Snippet; + break; + case ScriptCodeCompletionOption::KIND_PLAIN_TEXT: + item.kind = lsp::CompletionItemKind::Text; + break; + } + + arr[i] = item.to_json(); + i++; + } + } else if (GDScriptLanguageProtocol::get_singleton()->is_smart_resolve_enabled()) { + arr = native_member_completions.duplicate(); + + for (Map::Element *E = GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts.front(); E; E = E->next()) { + ExtendGDScriptParser *script = E->get(); + const Array &items = script->get_member_completions(); + + const int start_size = arr.size(); + arr.resize(start_size + items.size()); + for (int i = start_size; i < arr.size(); i++) { + arr[i] = items[i - start_size]; + } + } + } + return arr; +} + +Dictionary GDScriptTextDocument::rename(const Dictionary &p_params) { + lsp::TextDocumentPositionParams params; + params.load(p_params); + String new_name = p_params["newName"]; + + return GDScriptLanguageProtocol::get_singleton()->get_workspace()->rename(params, new_name); +} + +Dictionary GDScriptTextDocument::resolve(const Dictionary &p_params) { + lsp::CompletionItem item; + item.load(p_params); + + lsp::CompletionParams params; + Variant data = p_params["data"]; + + const lsp::DocumentSymbol *symbol = nullptr; + + if (data.get_type() == Variant::DICTIONARY) { + params.load(p_params["data"]); + symbol = GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_symbol(params, item.label, item.kind == lsp::CompletionItemKind::Method || item.kind == lsp::CompletionItemKind::Function); + + } else if (data.get_type() == Variant::STRING) { + String query = data; + + Vector param_symbols = query.split(SYMBOL_SEPERATOR, false); + + if (param_symbols.size() >= 2) { + String class_ = param_symbols[0]; + StringName class_name = class_; + String member_name = param_symbols[param_symbols.size() - 1]; + String inner_class_name; + if (param_symbols.size() >= 3) { + inner_class_name = param_symbols[1]; + } + + if (const ClassMembers *members = GDScriptLanguageProtocol::get_singleton()->get_workspace()->native_members.getptr(class_name)) { + if (const lsp::DocumentSymbol *const *member = members->getptr(member_name)) { + symbol = *member; + } + } + + if (!symbol) { + if (const Map::Element *E = GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts.find(class_name)) { + symbol = E->get()->get_member_symbol(member_name, inner_class_name); + } + } + } + } + + if (symbol) { + item.documentation = symbol->render(); + } + + if (item.kind == lsp::CompletionItemKind::Event) { + if (params.context.triggerKind == lsp::CompletionTriggerKind::TriggerCharacter && (params.context.triggerCharacter == "(")) { + const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; + item.insertText = quote_style + item.label + quote_style; + } + } + + return item.to_json(true); +} + +Array GDScriptTextDocument::foldingRange(const Dictionary &p_params) { + Array arr; + return arr; +} + +Array GDScriptTextDocument::codeLens(const Dictionary &p_params) { + Array arr; + return arr; +} + +Array GDScriptTextDocument::documentLink(const Dictionary &p_params) { + Array ret; + + lsp::DocumentLinkParams params; + params.load(p_params); + + List links; + GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_document_links(params.textDocument.uri, links); + for (const List::Element *E = links.front(); E; E = E->next()) { + ret.push_back(E->get().to_json()); + } + return ret; +} + +Array GDScriptTextDocument::colorPresentation(const Dictionary &p_params) { + Array arr; + return arr; +} + +Variant GDScriptTextDocument::hover(const Dictionary &p_params) { + lsp::TextDocumentPositionParams params; + params.load(p_params); + + const lsp::DocumentSymbol *symbol = GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_symbol(params); + if (symbol) { + lsp::Hover hover; + hover.contents = symbol->render(); + hover.range.start = params.position; + hover.range.end = params.position; + return hover.to_json(); + + } else if (GDScriptLanguageProtocol::get_singleton()->is_smart_resolve_enabled()) { + Dictionary ret; + Array contents; + List list; + GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_related_symbols(params, list); + for (List::Element *E = list.front(); E; E = E->next()) { + if (const lsp::DocumentSymbol *s = E->get()) { + contents.push_back(s->render().value); + } + } + ret["contents"] = contents; + return ret; + } + + return Variant(); +} + +Array GDScriptTextDocument::definition(const Dictionary &p_params) { + lsp::TextDocumentPositionParams params; + params.load(p_params); + List symbols; + Array arr = this->find_symbols(params, symbols); + return arr; +} + +Variant GDScriptTextDocument::declaration(const Dictionary &p_params) { + lsp::TextDocumentPositionParams params; + params.load(p_params); + List symbols; + Array arr = this->find_symbols(params, symbols); + if (arr.empty() && !symbols.empty() && !symbols.front()->get()->native_class.empty()) { // Find a native symbol + const lsp::DocumentSymbol *symbol = symbols.front()->get(); + if (GDScriptLanguageProtocol::get_singleton()->is_goto_native_symbols_enabled()) { + String id; + switch (symbol->kind) { + case lsp::SymbolKind::Class: + id = "class_name:" + symbol->name; + break; + case lsp::SymbolKind::Constant: + id = "class_constant:" + symbol->native_class + ":" + symbol->name; + break; + case lsp::SymbolKind::Property: + case lsp::SymbolKind::Variable: + id = "class_property:" + symbol->native_class + ":" + symbol->name; + break; + case lsp::SymbolKind::Enum: + id = "class_enum:" + symbol->native_class + ":" + symbol->name; + break; + case lsp::SymbolKind::Method: + case lsp::SymbolKind::Function: + id = "class_method:" + symbol->native_class + ":" + symbol->name; + break; + default: + id = "class_global:" + symbol->native_class + ":" + symbol->name; + break; + } + call_deferred("show_native_symbol_in_editor", id); + } else { + notify_client_show_symbol(symbol); + } + } + return arr; +} + +Variant GDScriptTextDocument::signatureHelp(const Dictionary &p_params) { + Variant ret; + + lsp::TextDocumentPositionParams params; + params.load(p_params); + + lsp::SignatureHelp s; + if (OK == GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_signature(params, s)) { + ret = s.to_json(); + } + + return ret; +} + +GDScriptTextDocument::GDScriptTextDocument() { + file_checker = FileAccess::create(FileAccess::ACCESS_RESOURCES); +} + +GDScriptTextDocument::~GDScriptTextDocument() { + memdelete(file_checker); +} + +void GDScriptTextDocument::sync_script_content(const String &p_path, const String &p_content) { + String path = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_path(p_path); + GDScriptLanguageProtocol::get_singleton()->get_workspace()->parse_script(path, p_content); + + EditorFileSystem::get_singleton()->update_file(path); + Error error; + Ref script = ResourceLoader::load(path, "", false, &error); + if (error == OK) { + if (script->load_source_code(path) == OK) { + script->reload(true); + } + } +} + +void GDScriptTextDocument::show_native_symbol_in_editor(const String &p_symbol_id) { + ScriptEditor::get_singleton()->call_deferred("_help_class_goto", p_symbol_id); + OS::get_singleton()->move_window_to_foreground(); +} + +Array GDScriptTextDocument::find_symbols(const lsp::TextDocumentPositionParams &p_location, List &r_list) { + Array arr; + const lsp::DocumentSymbol *symbol = GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_symbol(p_location); + if (symbol) { + lsp::Location location; + location.uri = symbol->uri; + location.range = symbol->range; + const String &path = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_path(symbol->uri); + if (file_checker->file_exists(path)) { + arr.push_back(location.to_json()); + } + r_list.push_back(symbol); + } else if (GDScriptLanguageProtocol::get_singleton()->is_smart_resolve_enabled()) { + List list; + GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_related_symbols(p_location, list); + for (List::Element *E = list.front(); E; E = E->next()) { + if (const lsp::DocumentSymbol *s = E->get()) { + if (!s->uri.empty()) { + lsp::Location location; + location.uri = s->uri; + location.range = s->range; + arr.push_back(location.to_json()); + r_list.push_back(s); + } + } + } + } + return arr; +} diff --git a/modules/gdscript/language_server/gdscript_text_document.h b/modules/gdscript/language_server/gdscript_text_document.h new file mode 100644 index 000000000..4d49b327b --- /dev/null +++ b/modules/gdscript/language_server/gdscript_text_document.h @@ -0,0 +1,81 @@ +/**************************************************************************/ +/* gdscript_text_document.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDSCRIPT_TEXT_DOCUMENT_H +#define GDSCRIPT_TEXT_DOCUMENT_H + +#include "core/os/file_access.h" +#include "core/reference.h" +#include "lsp.hpp" + +class GDScriptTextDocument : public Reference { + GDCLASS(GDScriptTextDocument, Reference) +protected: + static void _bind_methods(); + + FileAccess *file_checker; + + void didOpen(const Variant &p_param); + void didClose(const Variant &p_param); + void didChange(const Variant &p_param); + void didSave(const Variant &p_param); + + void sync_script_content(const String &p_path, const String &p_content); + void show_native_symbol_in_editor(const String &p_symbol_id); + + Array native_member_completions; + +private: + Array find_symbols(const lsp::TextDocumentPositionParams &p_location, List &r_list); + lsp::TextDocumentItem load_document_item(const Variant &p_param); + void notify_client_show_symbol(const lsp::DocumentSymbol *symbol); + +public: + Variant nativeSymbol(const Dictionary &p_params); + Array documentSymbol(const Dictionary &p_params); + Array completion(const Dictionary &p_params); + Dictionary resolve(const Dictionary &p_params); + Dictionary rename(const Dictionary &p_params); + Array foldingRange(const Dictionary &p_params); + Array codeLens(const Dictionary &p_params); + Array documentLink(const Dictionary &p_params); + Array colorPresentation(const Dictionary &p_params); + Variant hover(const Dictionary &p_params); + Array definition(const Dictionary &p_params); + Variant declaration(const Dictionary &p_params); + Variant signatureHelp(const Dictionary &p_params); + + void initialize(); + + GDScriptTextDocument(); + virtual ~GDScriptTextDocument(); +}; + +#endif // GDSCRIPT_TEXT_DOCUMENT_H diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp new file mode 100644 index 000000000..ed4ba21c3 --- /dev/null +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -0,0 +1,803 @@ +/**************************************************************************/ +/* gdscript_workspace.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_workspace.h" + +#include "../gdscript.h" +#include "../gdscript_parser.h" +#include "core/project_settings.h" +#include "core/script_language.h" +#include "editor/editor_file_system.h" +#include "editor/editor_help.h" +#include "editor/editor_node.h" +#include "gdscript_language_protocol.h" +#include "scene/resources/packed_scene.h" + +void GDScriptWorkspace::_bind_methods() { + ClassDB::bind_method(D_METHOD("didDeleteFiles"), &GDScriptWorkspace::did_delete_files); + ClassDB::bind_method(D_METHOD("symbol"), &GDScriptWorkspace::symbol); + ClassDB::bind_method(D_METHOD("parse_script", "path", "content"), &GDScriptWorkspace::parse_script); + ClassDB::bind_method(D_METHOD("parse_local_script", "path"), &GDScriptWorkspace::parse_local_script); + ClassDB::bind_method(D_METHOD("get_file_path", "uri"), &GDScriptWorkspace::get_file_path); + ClassDB::bind_method(D_METHOD("get_file_uri", "path"), &GDScriptWorkspace::get_file_uri); + ClassDB::bind_method(D_METHOD("publish_diagnostics", "path"), &GDScriptWorkspace::publish_diagnostics); + ClassDB::bind_method(D_METHOD("generate_script_api", "path"), &GDScriptWorkspace::generate_script_api); + ClassDB::bind_method(D_METHOD("apply_new_signal", "obj", "function", "args"), &GDScriptWorkspace::apply_new_signal); +} + +void GDScriptWorkspace::apply_new_signal(Object *obj, String function, PoolStringArray args) { + Ref