1/**************************************************************************/
2/* gdscript_language_protocol.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "gdscript_language_protocol.h"
32
33#include "core/config/project_settings.h"
34#include "editor/doc_tools.h"
35#include "editor/editor_help.h"
36#include "editor/editor_log.h"
37#include "editor/editor_node.h"
38#include "editor/editor_settings.h"
39
40GDScriptLanguageProtocol *GDScriptLanguageProtocol::singleton = nullptr;
41
42Error GDScriptLanguageProtocol::LSPeer::handle_data() {
43 int read = 0;
44 // Read headers
45 if (!has_header) {
46 while (true) {
47 if (req_pos >= LSP_MAX_BUFFER_SIZE) {
48 req_pos = 0;
49 ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Response header too big");
50 }
51 Error err = connection->get_partial_data(&req_buf[req_pos], 1, read);
52 if (err != OK) {
53 return FAILED;
54 } else if (read != 1) { // Busy, wait until next poll
55 return ERR_BUSY;
56 }
57 char *r = (char *)req_buf;
58 int l = req_pos;
59
60 // End of headers
61 if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') {
62 r[l - 3] = '\0'; // Null terminate to read string
63 String header;
64 header.parse_utf8(r);
65 content_length = header.substr(16).to_int();
66 has_header = true;
67 req_pos = 0;
68 break;
69 }
70 req_pos++;
71 }
72 }
73 if (has_header) {
74 while (req_pos < content_length) {
75 if (req_pos >= LSP_MAX_BUFFER_SIZE) {
76 req_pos = 0;
77 has_header = false;
78 ERR_FAIL_COND_V_MSG(req_pos >= LSP_MAX_BUFFER_SIZE, ERR_OUT_OF_MEMORY, "Response content too big");
79 }
80 Error err = connection->get_partial_data(&req_buf[req_pos], 1, read);
81 if (err != OK) {
82 return FAILED;
83 } else if (read != 1) {
84 return ERR_BUSY;
85 }
86 req_pos++;
87 }
88
89 // Parse data
90 String msg;
91 msg.parse_utf8((const char *)req_buf, req_pos);
92
93 // Reset to read again
94 req_pos = 0;
95 has_header = false;
96
97 // Response
98 String output = GDScriptLanguageProtocol::get_singleton()->process_message(msg);
99 if (!output.is_empty()) {
100 res_queue.push_back(output.utf8());
101 }
102 }
103 return OK;
104}
105
106Error GDScriptLanguageProtocol::LSPeer::send_data() {
107 int sent = 0;
108 if (!res_queue.is_empty()) {
109 CharString c_res = res_queue[0];
110 if (res_sent < c_res.size()) {
111 Error err = connection->put_partial_data((const uint8_t *)c_res.get_data() + res_sent, c_res.size() - res_sent - 1, sent);
112 if (err != OK) {
113 return err;
114 }
115 res_sent += sent;
116 }
117 // Response sent
118 if (res_sent >= c_res.size() - 1) {
119 res_sent = 0;
120 res_queue.remove_at(0);
121 }
122 }
123 return OK;
124}
125
126Error GDScriptLanguageProtocol::on_client_connected() {
127 Ref<StreamPeerTCP> tcp_peer = server->take_connection();
128 ERR_FAIL_COND_V_MSG(clients.size() >= LSP_MAX_CLIENTS, FAILED, "Max client limits reached");
129 Ref<LSPeer> peer = memnew(LSPeer);
130 peer->connection = tcp_peer;
131 clients.insert(next_client_id, peer);
132 next_client_id++;
133 EditorNode::get_log()->add_message("[LSP] Connection Taken", EditorLog::MSG_TYPE_EDITOR);
134 return OK;
135}
136
137void GDScriptLanguageProtocol::on_client_disconnected(const int &p_client_id) {
138 clients.erase(p_client_id);
139 EditorNode::get_log()->add_message("[LSP] Disconnected", EditorLog::MSG_TYPE_EDITOR);
140}
141
142String GDScriptLanguageProtocol::process_message(const String &p_text) {
143 String ret = process_string(p_text);
144 if (ret.is_empty()) {
145 return ret;
146 } else {
147 return format_output(ret);
148 }
149}
150
151String GDScriptLanguageProtocol::format_output(const String &p_text) {
152 String header = "Content-Length: ";
153 CharString charstr = p_text.utf8();
154 size_t len = charstr.length();
155 header += itos(len);
156 header += "\r\n\r\n";
157
158 return header + p_text;
159}
160
161void GDScriptLanguageProtocol::_bind_methods() {
162 ClassDB::bind_method(D_METHOD("initialize", "params"), &GDScriptLanguageProtocol::initialize);
163 ClassDB::bind_method(D_METHOD("initialized", "params"), &GDScriptLanguageProtocol::initialized);
164 ClassDB::bind_method(D_METHOD("on_client_connected"), &GDScriptLanguageProtocol::on_client_connected);
165 ClassDB::bind_method(D_METHOD("on_client_disconnected"), &GDScriptLanguageProtocol::on_client_disconnected);
166 ClassDB::bind_method(D_METHOD("notify_client", "method", "params", "client_id"), &GDScriptLanguageProtocol::notify_client, DEFVAL(Variant()), DEFVAL(-1));
167 ClassDB::bind_method(D_METHOD("is_smart_resolve_enabled"), &GDScriptLanguageProtocol::is_smart_resolve_enabled);
168 ClassDB::bind_method(D_METHOD("get_text_document"), &GDScriptLanguageProtocol::get_text_document);
169 ClassDB::bind_method(D_METHOD("get_workspace"), &GDScriptLanguageProtocol::get_workspace);
170 ClassDB::bind_method(D_METHOD("is_initialized"), &GDScriptLanguageProtocol::is_initialized);
171}
172
173Dictionary GDScriptLanguageProtocol::initialize(const Dictionary &p_params) {
174 lsp::InitializeResult ret;
175
176 String root_uri = p_params["rootUri"];
177 String root = p_params["rootPath"];
178 bool is_same_workspace;
179#ifndef WINDOWS_ENABLED
180 is_same_workspace = root.to_lower() == workspace->root.to_lower();
181#else
182 is_same_workspace = root.replace("\\", "/").to_lower() == workspace->root.to_lower();
183#endif
184
185 if (root_uri.length() && is_same_workspace) {
186 workspace->root_uri = root_uri;
187 } else {
188 String r_root = workspace->root;
189 r_root = r_root.lstrip("/");
190 workspace->root_uri = "file:///" + r_root;
191
192 Dictionary params;
193 params["path"] = workspace->root;
194 Dictionary request = make_notification("gdscript_client/changeWorkspace", params);
195
196 ERR_FAIL_COND_V_MSG(!clients.has(latest_client_id), ret.to_json(),
197 vformat("GDScriptLanguageProtocol: Can't initialize invalid peer '%d'.", latest_client_id));
198 Ref<LSPeer> peer = clients.get(latest_client_id);
199 if (peer != nullptr) {
200 String msg = Variant(request).to_json_string();
201 msg = format_output(msg);
202 (*peer)->res_queue.push_back(msg.utf8());
203 }
204 }
205
206 if (!_initialized) {
207 workspace->initialize();
208 text_document->initialize();
209 _initialized = true;
210 }
211
212 return ret.to_json();
213}
214
215void GDScriptLanguageProtocol::initialized(const Variant &p_params) {
216 lsp::GodotCapabilities capabilities;
217
218 DocTools *doc = EditorHelp::get_doc_data();
219 for (const KeyValue<String, DocData::ClassDoc> &E : doc->class_list) {
220 lsp::GodotNativeClassInfo gdclass;
221 gdclass.name = E.value.name;
222 gdclass.class_doc = &(E.value);
223 if (ClassDB::ClassInfo *ptr = ClassDB::classes.getptr(StringName(E.value.name))) {
224 gdclass.class_info = ptr;
225 }
226 capabilities.native_classes.push_back(gdclass);
227 }
228
229 notify_client("gdscript/capabilities", capabilities.to_json());
230}
231
232void GDScriptLanguageProtocol::poll() {
233 if (server->is_connection_available()) {
234 on_client_connected();
235 }
236
237 HashMap<int, Ref<LSPeer>>::Iterator E = clients.begin();
238 while (E != clients.end()) {
239 Ref<LSPeer> peer = E->value;
240 peer->connection->poll();
241 StreamPeerTCP::Status status = peer->connection->get_status();
242 if (status == StreamPeerTCP::STATUS_NONE || status == StreamPeerTCP::STATUS_ERROR) {
243 on_client_disconnected(E->key);
244 E = clients.begin();
245 continue;
246 } else {
247 if (peer->connection->get_available_bytes() > 0) {
248 latest_client_id = E->key;
249 Error err = peer->handle_data();
250 if (err != OK && err != ERR_BUSY) {
251 on_client_disconnected(E->key);
252 E = clients.begin();
253 continue;
254 }
255 }
256 Error err = peer->send_data();
257 if (err != OK && err != ERR_BUSY) {
258 on_client_disconnected(E->key);
259 E = clients.begin();
260 continue;
261 }
262 }
263 ++E;
264 }
265}
266
267Error GDScriptLanguageProtocol::start(int p_port, const IPAddress &p_bind_ip) {
268 return server->listen(p_port, p_bind_ip);
269}
270
271void GDScriptLanguageProtocol::stop() {
272 for (const KeyValue<int, Ref<LSPeer>> &E : clients) {
273 Ref<LSPeer> peer = clients.get(E.key);
274 peer->connection->disconnect_from_host();
275 }
276
277 server->stop();
278}
279
280void GDScriptLanguageProtocol::notify_client(const String &p_method, const Variant &p_params, int p_client_id) {
281#ifdef TESTS_ENABLED
282 if (clients.is_empty()) {
283 return;
284 }
285#endif
286 if (p_client_id == -1) {
287 ERR_FAIL_COND_MSG(latest_client_id == -1,
288 "GDScript LSP: Can't notify client as none was connected.");
289 p_client_id = latest_client_id;
290 }
291 ERR_FAIL_COND(!clients.has(p_client_id));
292 Ref<LSPeer> peer = clients.get(p_client_id);
293 ERR_FAIL_COND(peer == nullptr);
294
295 Dictionary message = make_notification(p_method, p_params);
296 String msg = Variant(message).to_json_string();
297 msg = format_output(msg);
298 peer->res_queue.push_back(msg.utf8());
299}
300
301void GDScriptLanguageProtocol::request_client(const String &p_method, const Variant &p_params, int p_client_id) {
302#ifdef TESTS_ENABLED
303 if (clients.is_empty()) {
304 return;
305 }
306#endif
307 if (p_client_id == -1) {
308 ERR_FAIL_COND_MSG(latest_client_id == -1,
309 "GDScript LSP: Can't notify client as none was connected.");
310 p_client_id = latest_client_id;
311 }
312 ERR_FAIL_COND(!clients.has(p_client_id));
313 Ref<LSPeer> peer = clients.get(p_client_id);
314 ERR_FAIL_COND(peer == nullptr);
315
316 Dictionary message = make_request(p_method, p_params, next_server_id);
317 next_server_id++;
318 String msg = Variant(message).to_json_string();
319 msg = format_output(msg);
320 peer->res_queue.push_back(msg.utf8());
321}
322
323bool GDScriptLanguageProtocol::is_smart_resolve_enabled() const {
324 return bool(_EDITOR_GET("network/language_server/enable_smart_resolve"));
325}
326
327bool GDScriptLanguageProtocol::is_goto_native_symbols_enabled() const {
328 return bool(_EDITOR_GET("network/language_server/show_native_symbols_in_editor"));
329}
330
331GDScriptLanguageProtocol::GDScriptLanguageProtocol() {
332 server.instantiate();
333 singleton = this;
334 workspace.instantiate();
335 text_document.instantiate();
336 set_scope("textDocument", text_document.ptr());
337 set_scope("completionItem", text_document.ptr());
338 set_scope("workspace", workspace.ptr());
339 workspace->root = ProjectSettings::get_singleton()->get_resource_path();
340}
341