From a4d029f889f9fbf952c18bd370dbe6acd0df8944 Mon Sep 17 00:00:00 2001 From: Dekara VanHoc Date: Sat, 3 Jan 2026 14:05:02 +0100 Subject: [PATCH] LSP: Fix loading scene for every request on script file for workspace completion Co-authored-by: HolonProduction --- .../gdscript_language_protocol.cpp | 10 + .../gdscript_language_protocol.h | 4 + .../language_server/gdscript_workspace.cpp | 51 +---- .../language_server/gdscript_workspace.h | 4 - modules/gdscript/language_server/godot_lsp.h | 9 + .../gdscript/language_server/scene_cache.cpp | 186 ++++++++++++++++++ .../gdscript/language_server/scene_cache.h | 65 ++++++ modules/gdscript/tests/test_lsp.h | 5 +- 8 files changed, 276 insertions(+), 58 deletions(-) create mode 100644 modules/gdscript/language_server/scene_cache.cpp create mode 100644 modules/gdscript/language_server/scene_cache.h diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp index 2d01c49cf2..209378bffa 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.cpp +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -148,6 +148,9 @@ Error GDScriptLanguageProtocol::on_client_connected() { void GDScriptLanguageProtocol::on_client_disconnected(const int &p_client_id) { clients.erase(p_client_id); + if (clients.is_empty()) { + scene_cache.clear(); + } EditorNode::get_log()->add_message("[LSP] Disconnected", EditorLog::MSG_TYPE_EDITOR); } @@ -270,6 +273,8 @@ void GDScriptLanguageProtocol::poll(int p_limit_usec) { on_client_connected(); } + scene_cache.poll(); + HashMap>::Iterator E = clients.begin(); while (E != clients.end()) { Ref peer = E->value; @@ -316,6 +321,7 @@ void GDScriptLanguageProtocol::stop() { peer->connection->disconnect_from_host(); } + scene_cache.clear(); server->stop(); } @@ -448,6 +454,8 @@ void GDScriptLanguageProtocol::lsp_did_open(const Dictionary &p_params) { client->managed_files[path] = document; client->parse_script(path); + + scene_cache.request_load(path); } void GDScriptLanguageProtocol::lsp_did_change(const Dictionary &p_params) { @@ -493,6 +501,8 @@ void GDScriptLanguageProtocol::lsp_did_close(const Dictionary &p_params) { /// A close notification requires a previous open notification to be sent. ERR_FAIL_COND_MSG(!was_opened, "LSP: Client is closing file without opening it."); + + scene_cache.unload(path); } void GDScriptLanguageProtocol::resolve_related_symbols(const LSP::TextDocumentPositionParams &p_doc_pos, List &r_list) { diff --git a/modules/gdscript/language_server/gdscript_language_protocol.h b/modules/gdscript/language_server/gdscript_language_protocol.h index c0f30cb9c3..4205570c5d 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.h +++ b/modules/gdscript/language_server/gdscript_language_protocol.h @@ -32,6 +32,7 @@ #include "gdscript_text_document.h" #include "gdscript_workspace.h" +#include "scene_cache.h" #include "core/io/stream_peer_tcp.h" #include "core/io/tcp_server.h" @@ -92,6 +93,7 @@ private: static GDScriptLanguageProtocol *singleton; HashMap> clients; + SceneCache scene_cache; Ref server; int latest_client_id = LSP_NO_CLIENT; int next_client_id = 0; @@ -119,6 +121,8 @@ 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_ SceneCache *get_scene_cache() { return &scene_cache; } + _FORCE_INLINE_ bool is_initialized() const { return _initialized; } void poll(int p_limit_usec); diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp index 39fc21ebd4..0d5e0e3d53 100644 --- a/modules/gdscript/language_server/gdscript_workspace.cpp +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -41,7 +41,6 @@ #include "editor/editor_node.h" #include "editor/file_system/editor_file_system.h" #include "editor/settings/editor_settings.h" -#include "scene/resources/packed_scene.h" void GDScriptWorkspace::_bind_methods() { ClassDB::bind_method(D_METHOD("apply_new_signal"), &GDScriptWorkspace::apply_new_signal); @@ -592,51 +591,6 @@ void GDScriptWorkspace::publish_diagnostics(const String &p_path) { GDScriptLanguageProtocol::get_singleton()->notify_client("textDocument/publishDiagnostics", params); } -void GDScriptWorkspace::_get_owners(EditorFileSystemDirectory *efsd, String p_path, List &owners) { - if (!efsd) { - return; - } - - for (int i = 0; i < efsd->get_subdir_count(); i++) { - _get_owners(efsd->get_subdir(i), p_path, owners); - } - - for (int i = 0; i < efsd->get_file_count(); i++) { - Vector deps = efsd->get_file_deps(i); - bool found = false; - for (int j = 0; j < deps.size(); j++) { - if (deps[j] == p_path) { - found = true; - break; - } - } - if (!found) { - continue; - } - - owners.push_back(efsd->get_file_path(i)); - } -} - -Node *GDScriptWorkspace::_get_owner_scene_node(String p_path) { - Node *owner_scene_node = nullptr; - List owners; - - _get_owners(EditorFileSystem::get_singleton()->get_filesystem(), p_path, owners); - - for (const String &owner : owners) { - NodePath owner_path = owner; - Ref owner_res = ResourceLoader::load(String(owner_path)); - if (Object::cast_to(owner_res.ptr())) { - Ref owner_packed_scene = Ref(Object::cast_to(*owner_res)); - owner_scene_node = owner_packed_scene->instantiate(); - break; - } - } - - return owner_scene_node; -} - void GDScriptWorkspace::completion(const LSP::CompletionParams &p_params, List *r_options) { String path = get_file_path(p_params.textDocument.uri); String call_hint; @@ -644,7 +598,7 @@ void GDScriptWorkspace::completion(const LSP::CompletionParams &p_params, Listget_parse_result(path); if (parser) { - Node *owner_scene_node = _get_owner_scene_node(path); + Node *owner_scene_node = GDScriptLanguageProtocol::get_singleton()->get_scene_cache()->get(path); Array stack; Node *current = nullptr; @@ -670,9 +624,6 @@ void GDScriptWorkspace::completion(const LSP::CompletionParams &p_params, Listget_text_for_completion(p_params.position); GDScriptLanguage::get_singleton()->complete_code(code, path, current, r_options, forced, call_hint); - if (owner_scene_node) { - memdelete(owner_scene_node); - } } } diff --git a/modules/gdscript/language_server/gdscript_workspace.h b/modules/gdscript/language_server/gdscript_workspace.h index 7386f45ca8..27960db078 100644 --- a/modules/gdscript/language_server/gdscript_workspace.h +++ b/modules/gdscript/language_server/gdscript_workspace.h @@ -35,15 +35,11 @@ #include "godot_lsp.h" #include "core/variant/variant.h" -#include "editor/file_system/editor_file_system.h" class GDScriptWorkspace : public RefCounted { GDCLASS(GDScriptWorkspace, RefCounted); private: - void _get_owners(EditorFileSystemDirectory *efsd, String p_path, List &owners); - Node *_get_owner_scene_node(String p_path); - #ifndef DISABLE_DEPRECATED void didDeleteFiles() {} Error parse_script(const String &p_path, const String &p_content) { diff --git a/modules/gdscript/language_server/godot_lsp.h b/modules/gdscript/language_server/godot_lsp.h index 75470bfe78..6a36fb4aeb 100644 --- a/modules/gdscript/language_server/godot_lsp.h +++ b/modules/gdscript/language_server/godot_lsp.h @@ -34,6 +34,15 @@ #include "core/object/class_db.h" #include "core/templates/list.h" +// Enable additional LSP related logging. +//#define DEBUG_LSP + +#ifdef DEBUG_LSP +#define LOG_LSP(...) print_line("[ LSP -", __FILE__, ":", __LINE__, "-", __func__, "] -", ##__VA_ARGS__) +#else +#define LOG_LSP(...) +#endif + namespace LSP { typedef String DocumentUri; diff --git a/modules/gdscript/language_server/scene_cache.cpp b/modules/gdscript/language_server/scene_cache.cpp new file mode 100644 index 0000000000..041648303b --- /dev/null +++ b/modules/gdscript/language_server/scene_cache.cpp @@ -0,0 +1,186 @@ +/**************************************************************************/ +/* scene_cache.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 "scene_cache.h" + +#include "godot_lsp.h" + +#include "core/io/resource_loader.h" +#include "editor/file_system/editor_file_system.h" +#include "scene/resources/packed_scene.h" + +void SceneCache::_get_owner_paths(EditorFileSystemDirectory *p_dir, const String &p_script_path, LocalVector &r_owner_paths) { + if (!p_dir) { + return; + } + + for (int i = 0; i < p_dir->get_subdir_count(); i++) { + _get_owner_paths(p_dir->get_subdir(i), p_script_path, r_owner_paths); + } + + for (int i = 0; i < p_dir->get_file_count(); i++) { + if (p_dir->get_file_deps(i).has(p_script_path)) { + r_owner_paths.push_back(p_dir->get_file_path(i)); + } + } +} + +void SceneCache::_finalize_scene_load() { + ERR_FAIL_COND(current_loaded_owner.is_empty() || script_path_queue.is_empty()); + + Ref scene_res = ResourceLoader::load_threaded_get(current_loaded_owner); + + if (scene_res.is_valid()) { + cache[script_path_queue[0]] = scene_res->instantiate(); + } else { + cache[script_path_queue[0]] = nullptr; + } + + LOG_LSP("Scene cached for script:", script_path_queue[0]); + LOG_LSP("pending_script_queue length:", script_path_queue.size() - 1); + + script_path_queue.remove_at(0); + current_loaded_owner = String(); +} + +void SceneCache::poll() { + if (current_loaded_owner.is_empty()) { + // No load ongoing, start the next one. + + if (EditorFileSystem::get_singleton()->is_scanning() || script_path_queue.is_empty()) { + return; + } + + LocalVector owners; + _get_owner_paths(EditorFileSystem::get_singleton()->get_filesystem(), script_path_queue[0], owners); + for (const String &owner : owners) { + if (ResourceLoader::load_threaded_request(owner) == Error::OK) { + current_loaded_owner = owner; + LOG_LSP("Scene load started for:", current_loaded_owner); + break; + } + } + + if (current_loaded_owner.is_empty()) { + cache[script_path_queue[0]] = nullptr; + LOG_LSP("No scene found for script:", script_path_queue[0]); + script_path_queue.remove_at(0); + LOG_LSP("pending_script_queue length:", script_path_queue.size()); + } + } else { + ERR_FAIL_COND(script_path_queue.is_empty()); + + // There is an ongoing load. Check the status. + + ResourceLoader::ThreadLoadStatus status = ResourceLoader::load_threaded_get_status(current_loaded_owner); + + if (status == ResourceLoader::THREAD_LOAD_IN_PROGRESS) { + return; + } + + if (status == ResourceLoader::THREAD_LOAD_LOADED) { + _finalize_scene_load(); + } else { + LOG_LSP("Scene load failure for:", current_loaded_owner); + cache[script_path_queue[0]] = nullptr; + + script_path_queue.remove_at(0); + current_loaded_owner = String(); + } + } +} + +Node *SceneCache::get(const String &p_script_path) { + if (!script_path_queue.is_empty() && script_path_queue[0] == p_script_path && !current_loaded_owner.is_empty()) { + _finalize_scene_load(); + } else { + script_path_queue.erase(p_script_path); + } + + if (Node **entry = cache.getptr(p_script_path)) { + return *entry; + } + + // Fallback to blocking load. This could happen if the open request was only recently sent. + // TODO: This could also happen when multiple clients are connected. + + LocalVector owners; + _get_owner_paths(EditorFileSystem::get_singleton()->get_filesystem(), p_script_path, owners); + for (const String &owner : owners) { + Ref scene = ResourceLoader::load(owner); + if (scene.is_valid()) { + Node *instance = scene->instantiate(); + cache[p_script_path] = instance; + return instance; + } + } + + cache[p_script_path] = nullptr; + return nullptr; +} + +void SceneCache::request_load(const String &p_script_path) { + if (!cache.has(p_script_path) && !script_path_queue.has(p_script_path)) { + script_path_queue.push_back(p_script_path); + LOG_LSP("Scene load requested for:", p_script_path); + LOG_LSP("pending_script_queue length:", script_path_queue.size()); + } +} + +void SceneCache::unload(const String &p_script_path) { + if (!script_path_queue.is_empty() && script_path_queue[0] == p_script_path && !current_loaded_owner.is_empty()) { + _ALLOW_DISCARD_ ResourceLoader::load_threaded_get(current_loaded_owner); + + script_path_queue.remove_at(0); + current_loaded_owner = String(); + } else { + script_path_queue.erase(p_script_path); + } + + if (!cache.has(p_script_path)) { + return; + } + memdelete_notnull(cache[p_script_path]); + cache.erase(p_script_path); + LOG_LSP("Cache cleared for path:", p_script_path); +} + +void SceneCache::clear() { + if (!current_loaded_owner.is_empty()) { + _ALLOW_DISCARD_ ResourceLoader::load_threaded_get(current_loaded_owner); + current_loaded_owner = String(); + } + script_path_queue.clear(); + for (const KeyValue &E : cache) { + memdelete_notnull(E.value); + } + cache.clear(); + LOG_LSP("Cache cleared."); +} diff --git a/modules/gdscript/language_server/scene_cache.h b/modules/gdscript/language_server/scene_cache.h new file mode 100644 index 0000000000..908e4e8fdf --- /dev/null +++ b/modules/gdscript/language_server/scene_cache.h @@ -0,0 +1,65 @@ +/**************************************************************************/ +/* scene_cache.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. */ +/**************************************************************************/ + +#pragma once + +#include "core/string/ustring.h" +#include "core/templates/hash_map.h" +#include "core/templates/local_vector.h" + +class Node; +class EditorFileSystemDirectory; +class PackedScene; + +/** + * Used to load and cache scene instances for LSP autocompletion. + * + * This implementation is not thread safe. + */ +class SceneCache { + // Always contains the path to the scene which is currently loaded via the `ResourceLoader`. + // If this is not empty, `script_path_queue` must have at least one element. + String current_loaded_owner; + LocalVector script_path_queue; + + HashMap cache; + + void _get_owner_paths(EditorFileSystemDirectory *p_dir, const String &p_script_path, LocalVector &r_owner_paths); + void _finalize_scene_load(); + +public: + void poll(); + + void clear(); + void request_load(const String &p_script_path); + void unload(const String &p_script_path); + + Node *get(const String &p_script_path); +}; diff --git a/modules/gdscript/tests/test_lsp.h b/modules/gdscript/tests/test_lsp.h index 0c727afeb0..2fd11f71ad 100644 --- a/modules/gdscript/tests/test_lsp.h +++ b/modules/gdscript/tests/test_lsp.h @@ -44,10 +44,7 @@ #include "../language_server/godot_lsp.h" #include "core/io/dir_access.h" -#include "core/io/file_access_pack.h" -#include "core/os/os.h" -#include "editor/doc/editor_help.h" -#include "editor/editor_node.h" +#include "editor/file_system/editor_file_system.h" #include "modules/gdscript/gdscript_analyzer.h" #include "modules/regex/regex.h"