2
0
mirror of https://github.com/godotengine/godot.git synced 2025-04-25 01:48:08 +08:00

Merge pull request from anvilfolk/cache-gd-docs

Scripting: Fix script docs not being searchable without manually recompiling scripts
This commit is contained in:
Thaddeus Crews 2025-03-13 08:57:40 -05:00
commit 0cc7e2c27a
No known key found for this signature in database
GPG Key ID: 62181B86FE9E5D84
8 changed files with 358 additions and 148 deletions

@ -368,6 +368,15 @@ void DocTools::remove_doc(const String &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) {
if (p_class_name.is_empty()) {
return false;

@ -44,6 +44,7 @@ public:
void merge_from(const DocTools &p_data);
void add_doc(const DocData::ClassDoc &p_class_doc);
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);
enum GenerateFlags {
GENERATE_FLAG_SKIP_BASIC_TYPES = (1 << 0),

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

@ -40,6 +40,7 @@
#include "core/string/string_builder.h"
#include "core/version_generated.gen.h"
#include "editor/doc_data_compressed.gen.h"
#include "editor/editor_file_system.h"
#include "editor/editor_main_screen.h"
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
@ -192,37 +193,6 @@ static String _contextualize_class_specifier(const String &p_class_specifier, co
/// 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() {
VBoxContainer::_update_theme_item_cache();
@ -705,8 +675,7 @@ void EditorHelp::_pop_code_font() {
}
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;
}
@ -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) {
const DocTools *doc = EditorHelp::get_doc_data();
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;
}
}
@ -2622,7 +2589,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const C
p_rt->pop(); // font
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].
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);
}
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));
}
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) {
if (!ext_doc) {
ext_doc = memnew(DocTools);
@ -2935,23 +2948,26 @@ void EditorHelp::remove_class(const String &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) {
bool use_script_cache = (bool)p_udata;
Ref<Resource> cache_res = ResourceLoader::load(get_cache_full_path());
if (cache_res.is_valid() && cache_res->get_meta("version_hash", "") == doc_version_hash) {
Array classes = cache_res->get_meta("classes", Array());
for (int i = 0; i < classes.size(); 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).
callable_mp_static(&EditorHelp::_gen_extensions_docs).call_deferred();
} else {
// 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));
@ -2981,6 +2997,12 @@ void EditorHelp::_gen_doc_thread(void *p_udata) {
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));
}
@ -2992,8 +3014,166 @@ void EditorHelp::_gen_extensions_docs() {
doc->merge_from(*ext_doc);
}
}
static void _load_script_doc_cache(bool p_changes) {
EditorHelp::load_script_doc_cache();
}
void EditorHelp::generate_doc(bool p_use_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::_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++;
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())) {
worker_thread.start(_load_doc_thread, nullptr);
worker_thread.start(_load_doc_thread, (void *)p_use_script_cache);
} else {
print_verbose("Regenerating editor help cache");
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() {
}
DocTools *EditorHelp::get_doc_data() {
_wait_for_thread();
return doc;
}
/// EditorHelpBit ///
#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;
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.
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()) {
result.description += "[b]" + brief_description + "[/b]";
@ -3223,18 +3398,18 @@ EditorHelpBit::HelpData EditorHelpBit::_get_class_help_data(const StringName &p_
}
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.");
} 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.");
} 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;
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.
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 DocData::EnumDoc &enum_doc = kv.value;
@ -3304,13 +3478,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_constant_help_data(const StringName
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.
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;
current.description = HANDLE_DOC(constant.description);
if (constant.is_deprecated) {
@ -3356,13 +3529,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_property_help_data(const StringName
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.
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;
current.description = HANDLE_DOC(property.description);
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()) {
// 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) {
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.
if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) {
// 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;
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.
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;
current.description = HANDLE_DOC(theme_item.description);
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;
}
// Check for inherited theme items.
E = dd->class_list.find(E->value.inherits);
class_doc = EditorHelp::get_doc(class_doc->inherits);
}
return result;
@ -3499,12 +3670,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_method_help_data(const StringName &p
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.
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;
current.description = HANDLE_DOC(method.description);
if (method.is_deprecated) {
@ -3552,12 +3723,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_signal_help_data(const StringName &p
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.
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;
current.description = HANDLE_DOC(signal.description);
if (signal.is_deprecated) {
@ -3604,12 +3775,12 @@ EditorHelpBit::HelpData EditorHelpBit::_get_annotation_help_data(const StringNam
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.
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;
current.description = HANDLE_DOC(annotation.description);
if (annotation.is_deprecated) {
@ -3699,8 +3870,7 @@ void EditorHelpBit::_update_labels() {
// Nothing to do.
} break;
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();
if (!inherits.is_empty()) {
@ -3714,7 +3884,7 @@ void EditorHelpBit::_update_labels() {
_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();
}

@ -80,6 +80,8 @@ public:
FindBar();
};
class EditorFileSystemDirectory;
class EditorHelp : public VBoxContainer {
GDCLASS(EditorHelp, VBoxContainer);
@ -109,8 +111,8 @@ class EditorHelp : public VBoxContainer {
RichTextLabel *class_desc = 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;
LineEdit *search = nullptr;
@ -185,14 +187,26 @@ class EditorHelp : public VBoxContainer {
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.
static void _wait_for_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(Thread &p_thread = worker_thread);
static void _load_doc_thread(void *p_udata);
static void _gen_doc_thread(void *p_udata);
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();
struct PropertyCompare {
@ -212,10 +226,23 @@ protected:
static void _bind_methods();
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 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_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 remove_class(const String &p_class);

@ -507,8 +507,9 @@ void EditorNode::_gdextensions_reloaded() {
// Reload script editor to revalidate GDScript if classes are added or removed.
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) {
@ -713,6 +714,7 @@ void EditorNode::_notification(int p_what) {
if (save_accept) {
save_accept->queue_free();
}
EditorHelp::save_script_doc_cache();
editor_data.save_editor_external_data();
FileAccess::set_file_close_fail_notify_callback(nullptr);
log->deinit(); // Do not get messages anymore.

@ -2286,39 +2286,9 @@ void ScriptEditor::_update_script_names() {
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));
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"));
String tooltip = vformat(TTR("%s Class Reference"), name);
@ -2336,6 +2306,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()) {
sedata.sort();
@ -3704,7 +3704,7 @@ bool ScriptEditor::_help_tab_goto(const String &p_name, const String &p_desc) {
}
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++) {
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));

@ -198,8 +198,8 @@ void Shader::get_shader_uniform_list(List<PropertyInfo> *p_params, bool p_get_gr
}
}
#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
}