Răsfoiți Sursa

Scripting: Add script documentation cache to project

This PR adds a script documentation cache in the project folder.
It is loaded at alongside native documentation caches. This makes
scripts fully accessible through Search Help, including their
members, etc, right from project start, without having to compile
every single script.

Co-authored-by: Hilderin <[email protected]>
ocean 1 an în urmă
părinte
comite
72045c8306

+ 9 - 0
editor/doc_tools.cpp

@@ -368,6 +368,15 @@ void DocTools::remove_doc(const String &p_class_name) {
 	class_list.erase(p_class_name);
 	class_list.erase(p_class_name);
 }
 }
 
 
+void DocTools::remove_script_doc_by_path(const String &p_path) {
+	for (KeyValue<String, DocData::ClassDoc> &E : class_list) {
+		if (E.value.is_script_doc && E.value.script_path == p_path) {
+			remove_doc(E.key);
+			return;
+		}
+	}
+}
+
 bool DocTools::has_doc(const String &p_class_name) {
 bool DocTools::has_doc(const String &p_class_name) {
 	if (p_class_name.is_empty()) {
 	if (p_class_name.is_empty()) {
 		return false;
 		return false;

+ 1 - 0
editor/doc_tools.h

@@ -44,6 +44,7 @@ public:
 	void merge_from(const DocTools &p_data);
 	void merge_from(const DocTools &p_data);
 	void add_doc(const DocData::ClassDoc &p_class_doc);
 	void add_doc(const DocData::ClassDoc &p_class_doc);
 	void remove_doc(const String &p_class_name);
 	void remove_doc(const String &p_class_name);
+	void remove_script_doc_by_path(const String &p_path);
 	bool has_doc(const String &p_class_name);
 	bool has_doc(const String &p_class_name);
 	enum GenerateFlags {
 	enum GenerateFlags {
 		GENERATE_FLAG_SKIP_BASIC_TYPES = (1 << 0),
 		GENERATE_FLAG_SKIP_BASIC_TYPES = (1 << 0),

+ 2 - 1
editor/editor_file_system.cpp

@@ -2171,6 +2171,7 @@ void EditorFileSystem::_update_script_documentation() {
 
 
 		if (!efd || index < 0) {
 		if (!efd || index < 0) {
 			// The file was removed
 			// The file was removed
+			EditorHelp::remove_script_doc_by_path(path);
 			continue;
 			continue;
 		}
 		}
 
 
@@ -2188,7 +2189,7 @@ void EditorFileSystem::_update_script_documentation() {
 					scr->reload_from_file();
 					scr->reload_from_file();
 				}
 				}
 				for (const DocData::ClassDoc &cd : scr->get_documentation()) {
 				for (const DocData::ClassDoc &cd : scr->get_documentation()) {
-					EditorHelp::get_doc_data()->add_doc(cd);
+					EditorHelp::add_doc(cd);
 					if (!first_scan) {
 					if (!first_scan) {
 						// Update the documentation in the Script Editor if it is open.
 						// Update the documentation in the Script Editor if it is open.
 						ScriptEditor::get_singleton()->update_doc(cd.name);
 						ScriptEditor::get_singleton()->update_doc(cd.name);

+ 272 - 102
editor/editor_help.cpp

@@ -40,6 +40,7 @@
 #include "core/string/string_builder.h"
 #include "core/string/string_builder.h"
 #include "core/version_generated.gen.h"
 #include "core/version_generated.gen.h"
 #include "editor/doc_data_compressed.gen.h"
 #include "editor/doc_data_compressed.gen.h"
+#include "editor/editor_file_system.h"
 #include "editor/editor_main_screen.h"
 #include "editor/editor_main_screen.h"
 #include "editor/editor_node.h"
 #include "editor/editor_node.h"
 #include "editor/editor_paths.h"
 #include "editor/editor_paths.h"
@@ -192,37 +193,6 @@ static String _contextualize_class_specifier(const String &p_class_specifier, co
 
 
 /// EditorHelp ///
 /// EditorHelp ///
 
 
-// TODO: This is sometimes used directly as `doc->something`, other times as `EditorHelp::get_doc_data()`, which is thread-safe.
-// Might this be a problem?
-DocTools *EditorHelp::doc = nullptr;
-DocTools *EditorHelp::ext_doc = nullptr;
-
-int EditorHelp::doc_generation_count = 0;
-String EditorHelp::doc_version_hash;
-Thread EditorHelp::worker_thread;
-
-static bool _attempt_doc_load(const String &p_class) {
-	// Docgen always happens in the outer-most class: it also generates docs for inner classes.
-	const String outer_class = p_class.get_slicec('.', 0);
-	if (!ScriptServer::is_global_class(outer_class)) {
-		return false;
-	}
-
-	// `ResourceLoader` is used in order to have a script-agnostic way to load scripts.
-	// This forces GDScript to compile the code, which is unnecessary for docgen, but it's a good compromise right now.
-	const Ref<Script> script = ResourceLoader::load(ScriptServer::get_global_class_path(outer_class), outer_class);
-	if (script.is_valid()) {
-		const Vector<DocData::ClassDoc> docs = script->get_documentation();
-		for (int j = 0; j < docs.size(); j++) {
-			const DocData::ClassDoc &doc = docs.get(j);
-			EditorHelp::get_doc_data()->add_doc(doc);
-		}
-		return true;
-	}
-
-	return false;
-}
-
 void EditorHelp::_update_theme_item_cache() {
 void EditorHelp::_update_theme_item_cache() {
 	VBoxContainer::_update_theme_item_cache();
 	VBoxContainer::_update_theme_item_cache();
 
 
@@ -705,8 +675,7 @@ void EditorHelp::_pop_code_font() {
 }
 }
 
 
 Error EditorHelp::_goto_desc(const String &p_class) {
 Error EditorHelp::_goto_desc(const String &p_class) {
-	// If class doesn't have docs listed, attempt on-demand docgen
-	if (!doc->class_list.has(p_class) && !_attempt_doc_load(p_class)) {
+	if (!doc->class_list.has(p_class)) {
 		return ERR_DOES_NOT_EXIST;
 		return ERR_DOES_NOT_EXIST;
 	}
 	}
 
 
@@ -2403,12 +2372,10 @@ void EditorHelp::_help_callback(const String &p_topic) {
 }
 }
 
 
 static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) {
 static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) {
-	const DocTools *doc = EditorHelp::get_doc_data();
-
 	bool is_native = false;
 	bool is_native = false;
 	{
 	{
-		const HashMap<String, DocData::ClassDoc>::ConstIterator E = doc->class_list.find(p_class);
-		if (E && !E->value.is_script_doc) {
+		const DocData::ClassDoc *E = EditorHelp::get_doc(p_class);
+		if (E && !E->is_script_doc) {
 			is_native = true;
 			is_native = true;
 		}
 		}
 	}
 	}
@@ -2622,7 +2589,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const C
 			p_rt->pop(); // font
 			p_rt->pop(); // font
 
 
 			pos = brk_end + 1;
 			pos = brk_end + 1;
-		} else if (doc->class_list.has(tag)) {
+		} else if (EditorHelp::has_doc(tag)) {
 			// Use a monospace font for class reference tags such as [Node2D] or [SceneTree].
 			// Use a monospace font for class reference tags such as [Node2D] or [SceneTree].
 
 
 			p_rt->push_font(doc_code_font);
 			p_rt->push_font(doc_code_font);
@@ -2902,9 +2869,9 @@ void EditorHelp::_add_text(const String &p_bbcode) {
 	_add_text_to_rt(p_bbcode, class_desc, this, edited_class);
 	_add_text_to_rt(p_bbcode, class_desc, this, edited_class);
 }
 }
 
 
-void EditorHelp::_wait_for_thread() {
-	if (worker_thread.is_started()) {
-		worker_thread.wait_to_finish();
+void EditorHelp::_wait_for_thread(Thread &p_thread) {
+	if (p_thread.is_started()) {
+		p_thread.wait_to_finish();
 	}
 	}
 }
 }
 
 
@@ -2917,6 +2884,52 @@ String EditorHelp::get_cache_full_path() {
 	return EditorPaths::get_singleton()->get_cache_dir().path_join(vformat("editor_doc_cache-%d.%d.res", VERSION_MAJOR, VERSION_MINOR));
 	return EditorPaths::get_singleton()->get_cache_dir().path_join(vformat("editor_doc_cache-%d.%d.res", VERSION_MAJOR, VERSION_MINOR));
 }
 }
 
 
+String EditorHelp::get_script_doc_cache_full_path() {
+	return EditorPaths::get_singleton()->get_project_settings_dir().path_join("editor_script_doc_cache.res");
+}
+
+DocTools *EditorHelp::get_doc_data() {
+	_wait_for_thread();
+	return doc;
+}
+
+bool EditorHelp::has_doc(const String &p_class_name) {
+	return get_doc(p_class_name) != nullptr;
+}
+
+DocData::ClassDoc *EditorHelp::get_doc(const String &p_class_name) {
+	return get_doc_data()->class_list.getptr(p_class_name);
+}
+
+void EditorHelp::add_doc(const DocData::ClassDoc &p_class_doc) {
+	if (!_script_docs_loaded.is_set()) {
+		_docs_to_add.push_back(p_class_doc);
+		return;
+	}
+
+	get_doc_data()->add_doc(p_class_doc);
+}
+
+void EditorHelp::remove_doc(const String &p_class_name) {
+	if (!_script_docs_loaded.is_set()) {
+		_docs_to_remove.push_back(p_class_name);
+		return;
+	}
+
+	DocTools *dt = get_doc_data();
+	if (dt->has_doc(p_class_name)) {
+		dt->remove_doc(p_class_name);
+	}
+}
+
+void EditorHelp::remove_script_doc_by_path(const String &p_path) {
+	if (!_script_docs_loaded.is_set()) {
+		_docs_to_remove_by_path.push_back(p_path);
+		return;
+	}
+	get_doc_data()->remove_script_doc_by_path(p_path);
+}
+
 void EditorHelp::load_xml_buffer(const uint8_t *p_buffer, int p_size) {
 void EditorHelp::load_xml_buffer(const uint8_t *p_buffer, int p_size) {
 	if (!ext_doc) {
 	if (!ext_doc) {
 		ext_doc = memnew(DocTools);
 		ext_doc = memnew(DocTools);
@@ -2935,23 +2948,26 @@ void EditorHelp::remove_class(const String &p_class) {
 	}
 	}
 
 
 	if (doc && doc->has_doc(p_class)) {
 	if (doc && doc->has_doc(p_class)) {
-		doc->remove_doc(p_class);
+		remove_doc(p_class);
 	}
 	}
 }
 }
 
 
 void EditorHelp::_load_doc_thread(void *p_udata) {
 void EditorHelp::_load_doc_thread(void *p_udata) {
+	bool use_script_cache = (bool)p_udata;
 	Ref<Resource> cache_res = ResourceLoader::load(get_cache_full_path());
 	Ref<Resource> cache_res = ResourceLoader::load(get_cache_full_path());
 	if (cache_res.is_valid() && cache_res->get_meta("version_hash", "") == doc_version_hash) {
 	if (cache_res.is_valid() && cache_res->get_meta("version_hash", "") == doc_version_hash) {
 		Array classes = cache_res->get_meta("classes", Array());
 		Array classes = cache_res->get_meta("classes", Array());
 		for (int i = 0; i < classes.size(); i++) {
 		for (int i = 0; i < classes.size(); i++) {
 			doc->add_doc(DocData::ClassDoc::from_dict(classes[i]));
 			doc->add_doc(DocData::ClassDoc::from_dict(classes[i]));
 		}
 		}
-
+		if (use_script_cache) {
+			callable_mp_static(&EditorHelp::load_script_doc_cache).call_deferred();
+		}
 		// Extensions' docs are not cached. Generate them now (on the main thread).
 		// Extensions' docs are not cached. Generate them now (on the main thread).
 		callable_mp_static(&EditorHelp::_gen_extensions_docs).call_deferred();
 		callable_mp_static(&EditorHelp::_gen_extensions_docs).call_deferred();
 	} else {
 	} else {
 		// We have to go back to the main thread to start from scratch, bypassing any possibly existing cache.
 		// We have to go back to the main thread to start from scratch, bypassing any possibly existing cache.
-		callable_mp_static(&EditorHelp::generate_doc).call_deferred(false);
+		callable_mp_static(&EditorHelp::generate_doc).call_deferred(false, use_script_cache);
 	}
 	}
 
 
 	OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
 	OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
@@ -2981,6 +2997,12 @@ void EditorHelp::_gen_doc_thread(void *p_udata) {
 		ERR_PRINT("Cannot save editor help cache (" + get_cache_full_path() + ").");
 		ERR_PRINT("Cannot save editor help cache (" + get_cache_full_path() + ").");
 	}
 	}
 
 
+	// Load script docs after native ones are cached so native cache doesn't contain script docs.
+	bool use_script_cache = (bool)p_udata;
+	if (use_script_cache) {
+		callable_mp_static(&EditorHelp::load_script_doc_cache).call_deferred();
+	}
+
 	OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
 	OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
 }
 }
 
 
@@ -2992,8 +3014,166 @@ void EditorHelp::_gen_extensions_docs() {
 		doc->merge_from(*ext_doc);
 		doc->merge_from(*ext_doc);
 	}
 	}
 }
 }
+static void _load_script_doc_cache(bool p_changes) {
+	EditorHelp::load_script_doc_cache();
+}
+
+void EditorHelp::load_script_doc_cache() {
+	if (!ProjectSettings::get_singleton()->is_project_loaded()) {
+		print_verbose("Skipping loading script doc cache since no project is open.");
+		return;
+	}
+
+	_wait_for_thread();
+
+	if (!ResourceLoader::exists(get_script_doc_cache_full_path())) {
+		print_verbose("Script documentation cache not found. Regenerating it may take a while for projects with many scripts.");
+		regenerate_script_doc_cache();
+		return;
+	}
+
+	if (EditorFileSystem::get_singleton()->is_scanning()) {
+		// This is assuming EditorFileSystem is performing first scan. We must wait until it is done.
+		EditorFileSystem::get_singleton()->connect(SNAME("sources_changed"), callable_mp_static(_load_script_doc_cache), CONNECT_ONE_SHOT);
+		return;
+	}
+
+	worker_thread.start(_load_script_doc_cache_thread, nullptr);
+}
+
+void EditorHelp::_process_postponed_docs() {
+	for (const String &class_name : _docs_to_remove) {
+		doc->remove_doc(class_name);
+	}
+	for (const String &path : _docs_to_remove_by_path) {
+		doc->remove_script_doc_by_path(path);
+	}
+	for (const DocData::ClassDoc &cd : _docs_to_add) {
+		doc->add_doc(cd);
+	}
+	_docs_to_add.clear();
+	_docs_to_remove.clear();
+	_docs_to_remove_by_path.clear();
+}
 
 
-void EditorHelp::generate_doc(bool p_use_cache) {
+void EditorHelp::_load_script_doc_cache_thread(void *p_udata) {
+	ERR_FAIL_COND_MSG(!ProjectSettings::get_singleton()->is_project_loaded(), "Error: cannot load script doc cache without a project.");
+	ERR_FAIL_COND_MSG(!ResourceLoader::exists(get_script_doc_cache_full_path()), "Error: cannot load script doc cache from inexistent file.");
+
+	Ref<Resource> script_doc_cache_res = ResourceLoader::load(get_script_doc_cache_full_path(), "", ResourceFormatLoader::CACHE_MODE_IGNORE);
+	if (script_doc_cache_res.is_null()) {
+		print_verbose("Script doc cache is corrupted. Regenerating it instead.");
+		_delete_script_doc_cache();
+		callable_mp_static(EditorHelp::regenerate_script_doc_cache).call_deferred();
+		return;
+	}
+
+	Array classes = script_doc_cache_res->get_meta("classes", Array());
+	for (const Dictionary dict : classes) {
+		doc->add_doc(DocData::ClassDoc::from_dict(dict));
+	}
+
+	// Protect from race condition in other threads reading / this thread writing to _docs_to_add/remove/etc.
+	_script_docs_loaded.set();
+
+	// Deal with docs likely added from EditorFileSystem's scans while the cache was loading in EditorHelp::worker_thread.
+	_process_postponed_docs();
+
+	// Always delete the doc cache after successful load since most uses of editor will change a script, invalidating cache.
+	_delete_script_doc_cache();
+}
+
+// Helper method to deal with "sources_changed" signal having a parameter.
+static void _regenerate_script_doc_cache(bool p_changes) {
+	EditorHelp::regenerate_script_doc_cache();
+}
+
+void EditorHelp::regenerate_script_doc_cache() {
+	if (EditorFileSystem::get_singleton()->is_scanning()) {
+		// Wait until EditorFileSystem scanning is complete to use updated filesystem structure.
+		EditorFileSystem::get_singleton()->connect(SNAME("sources_changed"), callable_mp_static(_regenerate_script_doc_cache), CONNECT_ONE_SHOT);
+		return;
+	}
+
+	_wait_for_thread(worker_thread);
+	_wait_for_thread(loader_thread);
+	loader_thread.start(_regen_script_doc_thread, EditorFileSystem::get_singleton()->get_filesystem());
+}
+
+// Runs on worker_thread since it writes to DocData.
+void EditorHelp::_finish_regen_script_doc_thread(void *p_udata) {
+	loader_thread.wait_to_finish();
+	_process_postponed_docs();
+	_script_docs_loaded.set();
+
+	OS::get_singleton()->benchmark_end_measure("EditorHelp", "Generate Script Documentation");
+}
+
+// Runs on loader_thread since _reload_scripts_documentation calls ResourceLoader::load().
+// Avoids deadlocks of worker_thread needing main thread for load task dispatching, but main thread waiting on worker_thread.
+void EditorHelp::_regen_script_doc_thread(void *p_udata) {
+	OS::get_singleton()->benchmark_begin_measure("EditorHelp", "Generate Script Documentation");
+
+	EditorFileSystemDirectory *dir = static_cast<EditorFileSystemDirectory *>(p_udata);
+	_script_docs_loaded.set_to(false);
+
+	// Ignore changes from filesystem scan since script docs will be now.
+	_docs_to_add.clear();
+	_docs_to_remove.clear();
+	_docs_to_remove_by_path.clear();
+
+	_reload_scripts_documentation(dir);
+
+	// All ResourceLoader::load() calls are done, so we can no longer deadlock with main thread.
+	// Switch to back to worker_thread from loader_thread to resynchronize access to DocData.
+	worker_thread.start(_finish_regen_script_doc_thread, nullptr);
+}
+
+void EditorHelp::_reload_scripts_documentation(EditorFileSystemDirectory *p_dir) {
+	// Recursively force compile all scripts, which should generate their documentation.
+	for (int i = 0; i < p_dir->get_subdir_count(); i++) {
+		_reload_scripts_documentation(p_dir->get_subdir(i));
+	}
+
+	for (int i = 0; i < p_dir->get_file_count(); i++) {
+		if (ClassDB::is_parent_class(p_dir->get_file_type(i), SNAME("Script"))) {
+			Ref<Script> scr = ResourceLoader::load(p_dir->get_file_path(i));
+			if (scr.is_valid()) {
+				for (const DocData::ClassDoc &cd : scr->get_documentation()) {
+					_docs_to_add.push_back(cd);
+				}
+			}
+		}
+	}
+}
+
+void EditorHelp::_delete_script_doc_cache() {
+	if (FileAccess::exists(get_script_doc_cache_full_path())) {
+		DirAccess::remove_file_or_error(ProjectSettings::get_singleton()->globalize_path(get_script_doc_cache_full_path()));
+	}
+}
+
+void EditorHelp::save_script_doc_cache() {
+	if (!_script_docs_loaded.is_set()) {
+		print_verbose("Script docs haven't been properly loaded or regenerated, so don't save them to disk.");
+		return;
+	}
+
+	Ref<Resource> cache_res;
+	cache_res.instantiate();
+	Array classes;
+	for (const KeyValue<String, DocData::ClassDoc> &E : doc->class_list) {
+		if (E.value.is_script_doc) {
+			classes.push_back(DocData::ClassDoc::to_dict(E.value));
+		}
+	}
+
+	cache_res->set_meta("classes", classes);
+	Error err = ResourceSaver::save(cache_res, get_script_doc_cache_full_path(), ResourceSaver::FLAG_COMPRESS);
+	ERR_FAIL_COND_MSG(err != OK, vformat("Cannot save script documentation cache in %s.", get_script_doc_cache_full_path()));
+}
+
+void EditorHelp::generate_doc(bool p_use_cache, bool p_use_script_cache) {
 	doc_generation_count++;
 	doc_generation_count++;
 	OS::get_singleton()->benchmark_begin_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
 	OS::get_singleton()->benchmark_begin_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
 
 
@@ -3009,11 +3189,11 @@ void EditorHelp::generate_doc(bool p_use_cache) {
 	}
 	}
 
 
 	if (p_use_cache && FileAccess::exists(get_cache_full_path())) {
 	if (p_use_cache && FileAccess::exists(get_cache_full_path())) {
-		worker_thread.start(_load_doc_thread, nullptr);
+		worker_thread.start(_load_doc_thread, (void *)p_use_script_cache);
 	} else {
 	} else {
 		print_verbose("Regenerating editor help cache");
 		print_verbose("Regenerating editor help cache");
 		doc->generate();
 		doc->generate();
-		worker_thread.start(_gen_doc_thread, nullptr);
+		worker_thread.start(_gen_doc_thread, (void *)p_use_script_cache);
 	}
 	}
 }
 }
 
 
@@ -3190,11 +3370,6 @@ EditorHelp::EditorHelp() {
 EditorHelp::~EditorHelp() {
 EditorHelp::~EditorHelp() {
 }
 }
 
 
-DocTools *EditorHelp::get_doc_data() {
-	_wait_for_thread();
-	return doc;
-}
-
 /// EditorHelpBit ///
 /// EditorHelpBit ///
 
 
 #define HANDLE_DOC(m_string) ((is_native ? DTR(m_string) : (m_string)).strip_edges())
 #define HANDLE_DOC(m_string) ((is_native ? DTR(m_string) : (m_string)).strip_edges())
@@ -3206,13 +3381,13 @@ EditorHelpBit::HelpData EditorHelpBit::_get_class_help_data(const StringName &p_
 
 
 	HelpData result;
 	HelpData result;
 
 
-	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
-	if (E) {
+	const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	if (class_doc) {
 		// Non-native class shouldn't be cached, nor translated.
 		// Non-native class shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		const String brief_description = HANDLE_DOC(E->value.brief_description);
-		const String long_description = HANDLE_DOC(E->value.description);
+		const String brief_description = HANDLE_DOC(class_doc->brief_description);
+		const String long_description = HANDLE_DOC(class_doc->description);
 
 
 		if (!brief_description.is_empty()) {
 		if (!brief_description.is_empty()) {
 			result.description += "[b]" + brief_description + "[/b]";
 			result.description += "[b]" + brief_description + "[/b]";
@@ -3223,18 +3398,18 @@ EditorHelpBit::HelpData EditorHelpBit::_get_class_help_data(const StringName &p_
 			}
 			}
 			result.description += long_description;
 			result.description += long_description;
 		}
 		}
-		if (E->value.is_deprecated) {
-			if (E->value.deprecated_message.is_empty()) {
+		if (class_doc->is_deprecated) {
+			if (class_doc->deprecated_message.is_empty()) {
 				result.deprecated_message = TTR("This class may be changed or removed in future versions.");
 				result.deprecated_message = TTR("This class may be changed or removed in future versions.");
 			} else {
 			} else {
-				result.deprecated_message = HANDLE_DOC(E->value.deprecated_message);
+				result.deprecated_message = HANDLE_DOC(class_doc->deprecated_message);
 			}
 			}
 		}
 		}
-		if (E->value.is_experimental) {
-			if (E->value.experimental_message.is_empty()) {
+		if (class_doc->is_experimental) {
+			if (class_doc->experimental_message.is_empty()) {
 				result.experimental_message = TTR("This class may be changed or removed in future versions.");
 				result.experimental_message = TTR("This class may be changed or removed in future versions.");
 			} else {
 			} else {
-				result.experimental_message = HANDLE_DOC(E->value.experimental_message);
+				result.experimental_message = HANDLE_DOC(class_doc->experimental_message);
 			}
 			}
 		}
 		}
 
 
@@ -3253,13 +3428,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_enum_help_data(const StringName &p_c
 
 
 	HelpData result;
 	HelpData result;
 
 
-	const DocTools *dd = EditorHelp::get_doc_data();
-	const HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
-	if (E) {
+	const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	if (class_doc) {
 		// Non-native enums shouldn't be cached, nor translated.
 		// Non-native enums shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		for (const KeyValue<String, DocData::EnumDoc> &kv : E->value.enums) {
+		for (const KeyValue<String, DocData::EnumDoc> &kv : class_doc->enums) {
 			const StringName enum_name = kv.key;
 			const StringName enum_name = kv.key;
 			const DocData::EnumDoc &enum_doc = kv.value;
 			const DocData::EnumDoc &enum_doc = kv.value;
 
 
@@ -3304,13 +3478,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_constant_help_data(const StringName
 
 
 	HelpData result;
 	HelpData result;
 
 
-	const DocTools *dd = EditorHelp::get_doc_data();
-	const HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
-	if (E) {
+	const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	if (class_doc) {
 		// Non-native constants shouldn't be cached, nor translated.
 		// Non-native constants shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		for (const DocData::ConstantDoc &constant : E->value.constants) {
+		for (const DocData::ConstantDoc &constant : class_doc->constants) {
 			HelpData current;
 			HelpData current;
 			current.description = HANDLE_DOC(constant.description);
 			current.description = HANDLE_DOC(constant.description);
 			if (constant.is_deprecated) {
 			if (constant.is_deprecated) {
@@ -3356,13 +3529,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_property_help_data(const StringName
 
 
 	HelpData result;
 	HelpData result;
 
 
-	const DocTools *dd = EditorHelp::get_doc_data();
-	const HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
-	if (E) {
+	const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	if (class_doc) {
 		// Non-native properties shouldn't be cached, nor translated.
 		// Non-native properties shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		for (const DocData::PropertyDoc &property : E->value.properties) {
+		for (const DocData::PropertyDoc &property : class_doc->properties) {
 			HelpData current;
 			HelpData current;
 			current.description = HANDLE_DOC(property.description);
 			current.description = HANDLE_DOC(property.description);
 			if (property.is_deprecated) {
 			if (property.is_deprecated) {
@@ -3397,10 +3569,10 @@ EditorHelpBit::HelpData EditorHelpBit::_get_property_help_data(const StringName
 
 
 			if (!enum_class_name.is_empty() && !enum_name.is_empty()) {
 			if (!enum_class_name.is_empty() && !enum_name.is_empty()) {
 				// Classes can use enums from other classes, so check from which it came.
 				// Classes can use enums from other classes, so check from which it came.
-				const HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(enum_class_name);
+				const DocData::ClassDoc *enum_class = EditorHelp::get_doc(enum_class_name);
 				if (enum_class) {
 				if (enum_class) {
 					const String enum_prefix = EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " ";
 					const String enum_prefix = EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " ";
-					for (DocData::ConstantDoc constant : enum_class->value.constants) {
+					for (DocData::ConstantDoc constant : enum_class->constants) {
 						// Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector.
 						// Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector.
 						if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) {
 						if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) {
 							// Prettify the enum value display, so that "<ENUM_NAME>_<ITEM>" becomes "Item".
 							// Prettify the enum value display, so that "<ENUM_NAME>_<ITEM>" becomes "Item".
@@ -3441,13 +3613,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringNam
 	HelpData result;
 	HelpData result;
 
 
 	bool found = false;
 	bool found = false;
-	const DocTools *dd = EditorHelp::get_doc_data();
-	HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
-	while (E) {
+	DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	while (class_doc) {
 		// Non-native theme items shouldn't be cached, nor translated.
 		// Non-native theme items shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) {
+		for (const DocData::ThemeItemDoc &theme_item : class_doc->theme_properties) {
 			HelpData current;
 			HelpData current;
 			current.description = HANDLE_DOC(theme_item.description);
 			current.description = HANDLE_DOC(theme_item.description);
 			if (theme_item.is_deprecated) {
 			if (theme_item.is_deprecated) {
@@ -3481,12 +3652,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringNam
 			}
 			}
 		}
 		}
 
 
-		if (found || E->value.inherits.is_empty()) {
+		if (found || class_doc->inherits.is_empty()) {
 			break;
 			break;
 		}
 		}
 
 
 		// Check for inherited theme items.
 		// Check for inherited theme items.
-		E = dd->class_list.find(E->value.inherits);
+		class_doc = EditorHelp::get_doc(class_doc->inherits);
 	}
 	}
 
 
 	return result;
 	return result;
@@ -3499,12 +3670,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_method_help_data(const StringName &p
 
 
 	HelpData result;
 	HelpData result;
 
 
-	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
-	if (E) {
+	const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	if (class_doc) {
 		// Non-native methods shouldn't be cached, nor translated.
 		// Non-native methods shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		for (const DocData::MethodDoc &method : E->value.methods) {
+		for (const DocData::MethodDoc &method : class_doc->methods) {
 			HelpData current;
 			HelpData current;
 			current.description = HANDLE_DOC(method.description);
 			current.description = HANDLE_DOC(method.description);
 			if (method.is_deprecated) {
 			if (method.is_deprecated) {
@@ -3552,12 +3723,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_signal_help_data(const StringName &p
 
 
 	HelpData result;
 	HelpData result;
 
 
-	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
-	if (E) {
+	const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	if (class_doc) {
 		// Non-native signals shouldn't be cached, nor translated.
 		// Non-native signals shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		for (const DocData::MethodDoc &signal : E->value.signals) {
+		for (const DocData::MethodDoc &signal : class_doc->signals) {
 			HelpData current;
 			HelpData current;
 			current.description = HANDLE_DOC(signal.description);
 			current.description = HANDLE_DOC(signal.description);
 			if (signal.is_deprecated) {
 			if (signal.is_deprecated) {
@@ -3604,12 +3775,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_annotation_help_data(const StringNam
 
 
 	HelpData result;
 	HelpData result;
 
 
-	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
-	if (E) {
+	const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
+	if (class_doc) {
 		// Non-native annotations shouldn't be cached, nor translated.
 		// Non-native annotations shouldn't be cached, nor translated.
-		const bool is_native = !E->value.is_script_doc;
+		const bool is_native = !class_doc->is_script_doc;
 
 
-		for (const DocData::MethodDoc &annotation : E->value.annotations) {
+		for (const DocData::MethodDoc &annotation : class_doc->annotations) {
 			HelpData current;
 			HelpData current;
 			current.description = HANDLE_DOC(annotation.description);
 			current.description = HANDLE_DOC(annotation.description);
 			if (annotation.is_deprecated) {
 			if (annotation.is_deprecated) {
@@ -3699,8 +3870,7 @@ void EditorHelpBit::_update_labels() {
 				// Nothing to do.
 				// Nothing to do.
 			} break;
 			} break;
 			case SYMBOL_HINT_INHERITANCE: {
 			case SYMBOL_HINT_INHERITANCE: {
-				const HashMap<String, DocData::ClassDoc> &class_list = EditorHelp::get_doc_data()->class_list;
-				const DocData::ClassDoc *class_doc = class_list.getptr(symbol_class_name);
+				const DocData::ClassDoc *class_doc = EditorHelp::get_doc(symbol_class_name);
 				String inherits = class_doc ? class_doc->inherits : String();
 				String inherits = class_doc ? class_doc->inherits : String();
 
 
 				if (!inherits.is_empty()) {
 				if (!inherits.is_empty()) {
@@ -3714,7 +3884,7 @@ void EditorHelpBit::_update_labels() {
 
 
 						_add_type_to_title({ inherits, String(), false });
 						_add_type_to_title({ inherits, String(), false });
 
 
-						const DocData::ClassDoc *base_class_doc = class_list.getptr(inherits);
+						const DocData::ClassDoc *base_class_doc = EditorHelp::get_doc(inherits);
 						inherits = base_class_doc ? base_class_doc->inherits : String();
 						inherits = base_class_doc ? base_class_doc->inherits : String();
 					}
 					}
 
 

+ 35 - 8
editor/editor_help.h

@@ -80,6 +80,8 @@ public:
 	FindBar();
 	FindBar();
 };
 };
 
 
+class EditorFileSystemDirectory;
+
 class EditorHelp : public VBoxContainer {
 class EditorHelp : public VBoxContainer {
 	GDCLASS(EditorHelp, VBoxContainer);
 	GDCLASS(EditorHelp, VBoxContainer);
 
 
@@ -109,8 +111,8 @@ class EditorHelp : public VBoxContainer {
 
 
 	RichTextLabel *class_desc = nullptr;
 	RichTextLabel *class_desc = nullptr;
 	HSplitContainer *h_split = nullptr;
 	HSplitContainer *h_split = nullptr;
-	static DocTools *doc;
-	static DocTools *ext_doc;
+	inline static DocTools *doc = nullptr;
+	inline static DocTools *ext_doc = nullptr;
 
 
 	ConfirmationDialog *search_dialog = nullptr;
 	ConfirmationDialog *search_dialog = nullptr;
 	LineEdit *search = nullptr;
 	LineEdit *search = nullptr;
@@ -185,14 +187,26 @@ class EditorHelp : public VBoxContainer {
 
 
 	void _toggle_scripts_pressed();
 	void _toggle_scripts_pressed();
 
 
-	static int doc_generation_count;
-	static String doc_version_hash;
-	static Thread worker_thread;
+	inline static int doc_generation_count = 0;
+	inline static String doc_version_hash;
+	inline static Thread worker_thread;
+	inline static Thread loader_thread; // Only load scripts here to avoid deadlocking with main thread.
+
+	inline static SafeFlag _script_docs_loaded = SafeFlag(false);
+	inline static LocalVector<DocData::ClassDoc> _docs_to_add;
+	inline static LocalVector<String> _docs_to_remove;
+	inline static LocalVector<String> _docs_to_remove_by_path;
 
 
-	static void _wait_for_thread();
+	static void _wait_for_thread(Thread &p_thread = worker_thread);
 	static void _load_doc_thread(void *p_udata);
 	static void _load_doc_thread(void *p_udata);
 	static void _gen_doc_thread(void *p_udata);
 	static void _gen_doc_thread(void *p_udata);
 	static void _gen_extensions_docs();
 	static void _gen_extensions_docs();
+	static void _process_postponed_docs();
+	static void _load_script_doc_cache_thread(void *p_udata);
+	static void _regen_script_doc_thread(void *p_udata);
+	static void _finish_regen_script_doc_thread(void *p_udata);
+	static void _reload_scripts_documentation(EditorFileSystemDirectory *p_dir);
+	static void _delete_script_doc_cache();
 	static void _compute_doc_version_hash();
 	static void _compute_doc_version_hash();
 
 
 	struct PropertyCompare {
 	struct PropertyCompare {
@@ -212,10 +226,23 @@ protected:
 	static void _bind_methods();
 	static void _bind_methods();
 
 
 public:
 public:
-	static void generate_doc(bool p_use_cache = true);
-	static DocTools *get_doc_data();
+	static void generate_doc(bool p_use_cache = true, bool p_use_script_cache = true);
 	static void cleanup_doc();
 	static void cleanup_doc();
+	static void load_script_doc_cache();
+	static void regenerate_script_doc_cache();
+	static void save_script_doc_cache();
 	static String get_cache_full_path();
 	static String get_cache_full_path();
+	static String get_script_doc_cache_full_path();
+
+	// Adding scripts to DocData directly may make script doc cache inconsistent. Use methods below when adding script docs.
+	// Usage during startup can also cause deadlocks.
+	static DocTools *get_doc_data();
+	// Method forwarding to underlying DocTools to keep script doc cache consistent.
+	static DocData::ClassDoc *get_doc(const String &p_class_name);
+	static void add_doc(const DocData::ClassDoc &p_class_doc);
+	static void remove_doc(const String &p_class_name);
+	static void remove_script_doc_by_path(const String &p_path);
+	static bool has_doc(const String &p_class_name);
 
 
 	static void load_xml_buffer(const uint8_t *p_buffer, int p_size);
 	static void load_xml_buffer(const uint8_t *p_buffer, int p_size);
 	static void remove_class(const String &p_class);
 	static void remove_class(const String &p_class);

+ 4 - 2
editor/editor_node.cpp

@@ -507,8 +507,9 @@ void EditorNode::_gdextensions_reloaded() {
 	// Reload script editor to revalidate GDScript if classes are added or removed.
 	// Reload script editor to revalidate GDScript if classes are added or removed.
 	ScriptEditor::get_singleton()->reload_scripts(true);
 	ScriptEditor::get_singleton()->reload_scripts(true);
 
 
-	// Regenerate documentation.
-	EditorHelp::generate_doc();
+	// Regenerate documentation without using script documentation cache since that would
+	// revert doc changes during this session.
+	EditorHelp::generate_doc(true, false);
 }
 }
 
 
 void EditorNode::_update_theme(bool p_skip_creation) {
 void EditorNode::_update_theme(bool p_skip_creation) {
@@ -717,6 +718,7 @@ void EditorNode::_notification(int p_what) {
 			if (save_accept) {
 			if (save_accept) {
 				save_accept->queue_free();
 				save_accept->queue_free();
 			}
 			}
+			EditorHelp::save_script_doc_cache();
 			editor_data.save_editor_external_data();
 			editor_data.save_editor_external_data();
 			FileAccess::set_file_close_fail_notify_callback(nullptr);
 			FileAccess::set_file_close_fail_notify_callback(nullptr);
 			log->deinit(); // Do not get messages anymore.
 			log->deinit(); // Do not get messages anymore.

+ 33 - 33
editor/plugins/script_editor_plugin.cpp

@@ -2297,39 +2297,9 @@ void ScriptEditor::_update_script_names() {
 			sedata.push_back(sd);
 			sedata.push_back(sd);
 		}
 		}
 
 
-		Vector<String> disambiguated_script_names;
-		Vector<String> full_script_paths;
-		for (int j = 0; j < sedata.size(); j++) {
-			String name = sedata[j].name.replace("(*)", "");
-			ScriptListName script_display = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");
-			switch (script_display) {
-				case DISPLAY_NAME: {
-					name = name.get_file();
-				} break;
-				case DISPLAY_DIR_AND_NAME: {
-					name = name.get_base_dir().get_file().path_join(name.get_file());
-				} break;
-				default:
-					break;
-			}
-
-			disambiguated_script_names.append(name);
-			full_script_paths.append(sedata[j].tooltip);
-		}
-
-		EditorNode::disambiguate_filenames(full_script_paths, disambiguated_script_names);
-
-		for (int j = 0; j < sedata.size(); j++) {
-			if (sedata[j].name.ends_with("(*)")) {
-				sedata.write[j].name = disambiguated_script_names[j] + "(*)";
-			} else {
-				sedata.write[j].name = disambiguated_script_names[j];
-			}
-		}
-
 		EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
 		EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
-		if (eh) {
-			String name = eh->get_class();
+		if (eh && !eh->get_class().is_empty()) {
+			String name = eh->get_class().unquote();
 			Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Help"));
 			Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Help"));
 			String tooltip = vformat(TTR("%s Class Reference"), name);
 			String tooltip = vformat(TTR("%s Class Reference"), name);
 
 
@@ -2347,6 +2317,36 @@ void ScriptEditor::_update_script_names() {
 		}
 		}
 	}
 	}
 
 
+	Vector<String> disambiguated_script_names;
+	Vector<String> full_script_paths;
+	for (int j = 0; j < sedata.size(); j++) {
+		String name = sedata[j].name.replace("(*)", "");
+		ScriptListName script_display = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");
+		switch (script_display) {
+			case DISPLAY_NAME: {
+				name = name.get_file();
+			} break;
+			case DISPLAY_DIR_AND_NAME: {
+				name = name.get_base_dir().get_file().path_join(name.get_file());
+			} break;
+			default:
+				break;
+		}
+
+		disambiguated_script_names.append(name);
+		full_script_paths.append(sedata[j].tooltip);
+	}
+
+	EditorNode::disambiguate_filenames(full_script_paths, disambiguated_script_names);
+
+	for (int j = 0; j < sedata.size(); j++) {
+		if (sedata[j].name.ends_with("(*)")) {
+			sedata.write[j].name = disambiguated_script_names[j] + "(*)";
+		} else {
+			sedata.write[j].name = disambiguated_script_names[j];
+		}
+	}
+
 	if (_sort_list_on_update && !sedata.is_empty()) {
 	if (_sort_list_on_update && !sedata.is_empty()) {
 		sedata.sort();
 		sedata.sort();
 
 
@@ -3715,7 +3715,7 @@ bool ScriptEditor::_help_tab_goto(const String &p_name, const String &p_desc) {
 }
 }
 
 
 void ScriptEditor::update_doc(const String &p_name) {
 void ScriptEditor::update_doc(const String &p_name) {
-	ERR_FAIL_COND(!EditorHelp::get_doc_data()->has_doc(p_name));
+	ERR_FAIL_COND(!EditorHelp::has_doc(p_name));
 
 
 	for (int i = 0; i < tab_container->get_tab_count(); i++) {
 	for (int i = 0; i < tab_container->get_tab_count(); i++) {
 		EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
 		EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));

+ 2 - 2
scene/resources/shader.cpp

@@ -198,8 +198,8 @@ void Shader::get_shader_uniform_list(List<PropertyInfo> *p_params, bool p_get_gr
 		}
 		}
 	}
 	}
 #ifdef TOOLS_ENABLED
 #ifdef TOOLS_ENABLED
-	if (EditorHelp::get_doc_data() != nullptr && Engine::get_singleton()->is_editor_hint() && !class_doc.name.is_empty() && p_params) {
-		EditorHelp::get_doc_data()->add_doc(class_doc);
+	if (Engine::get_singleton()->is_editor_hint() && !class_doc.name.is_empty() && p_params) {
+		EditorHelp::add_doc(class_doc);
 	}
 	}
 #endif
 #endif
 }
 }