Skip to content

Commit dbcb8a4

Browse files
committed
C#: Implement tooltips for Signals and Properties in the inspector.
1 parent e4e024a commit dbcb8a4

File tree

16 files changed

+542
-4
lines changed

16 files changed

+542
-4
lines changed

editor/editor_file_system.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -2016,6 +2016,10 @@ bool EditorFileSystem::_should_reload_script(const String &p_path) {
20162016
return false;
20172017
}
20182018

2019+
if (scr->get_language()->get_name() == "C#") {
2020+
return false;
2021+
}
2022+
20192023
// Scripts are reloaded via the script editor if they are currently opened.
20202024
if (ScriptEditor::get_singleton()->get_open_scripts().has(scr)) {
20212025
return false;

modules/mono/csharp_script.cpp

+70-1
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,10 @@ Error CSharpLanguage::open_in_external_editor(const Ref<Script> &p_script, int p
10241024
bool CSharpLanguage::overrides_external_editor() {
10251025
return get_godotsharp_editor()->call("OverridesExternalEditor");
10261026
}
1027+
1028+
bool CSharpLanguage::supports_documentation() const {
1029+
return true;
1030+
}
10271031
#endif
10281032

10291033
bool CSharpLanguage::debug_break_parse(const String &p_file, int p_line, const String &p_error) {
@@ -2100,6 +2104,56 @@ void GD_CLR_STDCALL CSharpScript::_add_property_default_values_callback(CSharpSc
21002104
p_script->exported_members_defval_cache[name] = value;
21012105
}
21022106
}
2107+
2108+
void CSharpScript::get_docs(Ref<CSharpScript> p_script) {
2109+
Dictionary class_doc_dict;
2110+
class_doc_dict.~Dictionary();
2111+
2112+
GDMonoCache::managed_callbacks.ScriptManagerBridge_GetDocs(p_script.ptr(), &class_doc_dict);
2113+
2114+
p_script->docs.clear();
2115+
2116+
if (class_doc_dict.is_empty()) {
2117+
// Script has no docs.
2118+
return;
2119+
}
2120+
2121+
String inherits;
2122+
Ref<CSharpScript> base = p_script->get_base_script();
2123+
if (base.is_null()) {
2124+
// Must be a native base type.
2125+
inherits = p_script->get_instance_base_type();
2126+
} else {
2127+
inherits = base->get_global_name();
2128+
if (inherits == StringName()) {
2129+
inherits = base->get_path();
2130+
}
2131+
}
2132+
2133+
DocData::ClassDoc class_doc = DocData::ClassDoc();
2134+
class_doc.inherits = inherits;
2135+
class_doc.name = class_doc_dict["name"];
2136+
class_doc.brief_description = class_doc_dict["description"];
2137+
class_doc.is_script_doc = true;
2138+
2139+
Array property_docs_dict = class_doc_dict["properties"];
2140+
for (Dictionary property_doc_dict : property_docs_dict) {
2141+
DocData::PropertyDoc prop_doc;
2142+
prop_doc.name = property_doc_dict["name"];
2143+
prop_doc.description = property_doc_dict["description"];
2144+
class_doc.properties.push_back(prop_doc);
2145+
}
2146+
2147+
Array signals_docs_dict = class_doc_dict["signals"];
2148+
for (Dictionary signals_doc_dict : signals_docs_dict) {
2149+
DocData::MethodDoc signal_doc;
2150+
signal_doc.name = signals_doc_dict["name"];
2151+
signal_doc.description = signals_doc_dict["description"];
2152+
class_doc.signals.push_back(signal_doc);
2153+
}
2154+
2155+
p_script->docs.append(class_doc);
2156+
}
21032157
#endif
21042158

21052159
bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_update) {
@@ -2120,7 +2174,11 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda
21202174
#endif
21212175
{
21222176
#ifdef TOOLS_ENABLED
2123-
exports_invalidated = false;
2177+
if (!get_path().is_empty()) {
2178+
// If the script doesn't have a path we can't properly add the CATEGORY PropertyInfo,
2179+
// so we keep the exports invalidated so the next update can fix them.
2180+
exports_invalidated = false;
2181+
}
21242182
#endif
21252183

21262184
changed = true;
@@ -2223,6 +2281,15 @@ void CSharpScript::reload_registered_script(Ref<CSharpScript> p_script) {
22232281
p_script->_update_exports();
22242282

22252283
#ifdef TOOLS_ENABLED
2284+
get_docs(p_script);
2285+
Vector<DocData::ClassDoc> docs = p_script->docs;
2286+
// At this point, `EditorFileSystem::_update_script_documentation()` cannot be triggered.
2287+
if (EditorHelp::get_doc_data()) {
2288+
for (int i = 0; i < docs.size(); i++) {
2289+
EditorHelp::get_doc_data()->add_doc(docs[i]);
2290+
}
2291+
}
2292+
22262293
// If the EditorFileSystem singleton is available, update the file;
22272294
// otherwise, the file will be updated when the singleton becomes available.
22282295
EditorFileSystem *efs = EditorFileSystem::get_singleton();
@@ -2603,6 +2670,8 @@ Error CSharpScript::reload(bool p_keep_state) {
26032670
_update_exports();
26042671

26052672
#ifdef TOOLS_ENABLED
2673+
get_docs(this);
2674+
26062675
// If the EditorFileSystem singleton is available, update the file;
26072676
// otherwise, the file will be updated when the singleton becomes available.
26082677
EditorFileSystem *efs = EditorFileSystem::get_singleton();

modules/mono/csharp_script.h

+4-2
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ class CSharpScript : public Script {
193193
bool exports_invalidated = true;
194194
void _update_exports_values(HashMap<StringName, Variant> &values, List<PropertyInfo> &propnames);
195195
void _placeholder_erased(PlaceHolderScriptInstance *p_placeholder) override;
196+
197+
Vector<DocData::ClassDoc> docs;
196198
#endif
197199

198200
#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED)
@@ -206,6 +208,7 @@ class CSharpScript : public Script {
206208
static void GD_CLR_STDCALL _add_property_info_list_callback(CSharpScript *p_script, const String *p_current_class_name, void *p_props, int32_t p_count);
207209
#ifdef TOOLS_ENABLED
208210
static void GD_CLR_STDCALL _add_property_default_values_callback(CSharpScript *p_script, void *p_def_vals, int32_t p_count);
211+
static void get_docs(Ref<CSharpScript> p_script);
209212
#endif
210213
bool _update_exports(PlaceHolderScriptInstance *p_instance_to_update = nullptr);
211214

@@ -239,8 +242,6 @@ class CSharpScript : public Script {
239242

240243
#ifdef TOOLS_ENABLED
241244
virtual Vector<DocData::ClassDoc> get_documentation() const override {
242-
// TODO
243-
Vector<DocData::ClassDoc> docs;
244245
return docs;
245246
}
246247
virtual String get_class_icon_path() const override {
@@ -567,6 +568,7 @@ class CSharpLanguage : public ScriptLanguage {
567568
#ifdef TOOLS_ENABLED
568569
Error open_in_external_editor(const Ref<Script> &p_script, int p_line, int p_col) override;
569570
bool overrides_external_editor() override;
571+
virtual bool supports_documentation() const override;
570572
#endif
571573

572574
RBMap<Object *, CSharpScriptBinding>::Element *insert_script_binding(Object *p_object, const CSharpScriptBinding &p_script_binding);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Godot.SourceGenerators.Sample;
2+
3+
/// <summary>
4+
/// class description
5+
/// test
6+
/// </summary>
7+
public partial class ClassDoc : GodotObject
8+
{
9+
/// <summary>
10+
/// field description
11+
/// test
12+
/// </summary>
13+
[Export]
14+
private int _fieldDocTest = 1;
15+
16+
/// <summary>
17+
/// property description
18+
/// test
19+
/// </summary>
20+
[Export]
21+
public int PropertyDocTest { get; set; }
22+
23+
/// <summary>
24+
/// signal description
25+
/// test
26+
/// </summary>
27+
/// <param name="num"></param>
28+
[Signal]
29+
public delegate void SignalDocTestEventHandler(int num);
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Xunit;
2+
3+
namespace Godot.SourceGenerators.Tests;
4+
5+
public class ScriptDocsGeneratorTests
6+
{
7+
[Fact]
8+
public async void Docs()
9+
{
10+
await CSharpSourceGeneratorVerifier<ScriptDocsGenerator>.Verify(
11+
"ClassDoc.cs",
12+
"ClassDoc_ScriptDocs.generated.cs"
13+
);
14+
}
15+
16+
[Fact]
17+
public async void AllDocs()
18+
{
19+
await CSharpSourceGeneratorVerifier<ScriptDocsGenerator>.Verify(
20+
"ClassAllDoc.cs",
21+
"ClassAllDoc_ScriptDocs.generated.cs"
22+
);
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
partial class ClassAllDoc
2+
{
3+
#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword
4+
#if TOOLS
5+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
6+
internal new static global::Godot.Collections.Dictionary GetGodotClassDocs()
7+
{
8+
var docs = new global::Godot.Collections.Dictionary();
9+
docs.Add("name","\"ClassAllDoc.cs\"");
10+
docs.Add("description","class description\ntest");
11+
12+
var propertyDocs = new global::Godot.Collections.Array();
13+
propertyDocs.Add(new global::Godot.Collections.Dictionary { { "name", PropertyName.PropertyDocTest}, { "description", "property description\ntest" } });
14+
propertyDocs.Add(new global::Godot.Collections.Dictionary { { "name", PropertyName._fieldDocTest}, { "description", "field description\ntest" } });
15+
docs.Add("properties", propertyDocs);
16+
17+
var signalDocs = new global::Godot.Collections.Array();
18+
signalDocs.Add(new global::Godot.Collections.Dictionary { { "name", SignalName.SignalDocTest}, { "description", "signal description\ntest" } });
19+
docs.Add("signals", signalDocs);
20+
21+
return docs;
22+
}
23+
24+
#endif // TOOLS
25+
#pragma warning restore CS0109
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
partial class ClassDoc
2+
{
3+
#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword
4+
#if TOOLS
5+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
6+
internal new static global::Godot.Collections.Dictionary GetGodotClassDocs()
7+
{
8+
var docs = new global::Godot.Collections.Dictionary();
9+
docs.Add("name","\"ClassDoc.cs\"");
10+
docs.Add("description","This is the class documentation.");
11+
12+
return docs;
13+
}
14+
15+
#endif // TOOLS
16+
#pragma warning restore CS0109
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Godot;
2+
3+
/// <summary>
4+
/// class description
5+
/// test
6+
/// </summary>
7+
public partial class ClassAllDoc : GodotObject
8+
{
9+
/// <summary>
10+
/// field description
11+
/// test
12+
/// </summary>
13+
[Export]
14+
private int _fieldDocTest = 1;
15+
16+
/// <summary>
17+
/// property description
18+
/// test
19+
/// </summary>
20+
[Export]
21+
public int PropertyDocTest { get; set; }
22+
23+
/// <summary>
24+
/// signal description
25+
/// test
26+
/// </summary>
27+
/// <param name="num"></param>
28+
[Signal]
29+
public delegate void SignalDocTestEventHandler(int num);
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Godot;
2+
3+
/// <summary>
4+
/// This is the class documentation.
5+
/// </summary>
6+
public partial class ClassDoc : GodotObject
7+
{
8+
[Export]
9+
public int MyProperty { get; set; }
10+
}

modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs

+17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Immutable;
44
using System.Linq;
55
using System.Text;
6+
using System.Text.RegularExpressions;
67
using Microsoft.CodeAnalysis;
78
using Microsoft.CodeAnalysis.CSharp;
89
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -48,6 +49,22 @@ public static bool InheritsFrom(this ITypeSymbol? symbol, string assemblyName, s
4849
return false;
4950
}
5051

52+
public static string? GetDocumentationSummaryText(this ISymbol symbol)
53+
{
54+
// TODO: Can't use GetDocumentationCommentXml in source generators: https://github.com/dotnet/roslyn/issues/23673
55+
var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
56+
while (syntax is VariableDeclaratorSyntax vds)
57+
{
58+
syntax = vds.Parent?.Parent;
59+
}
60+
var trivia = syntax?.GetLeadingTrivia();
61+
62+
string summaryContent = Regex.Match(trivia.ToString(), "(?<=<summary>)[.\\s\\S]*?(?=</summary>)", RegexOptions.Singleline).Value;
63+
string cleanedSummary = Regex.Replace(summaryContent, @"^\s*///\s?", "", RegexOptions.Multiline);
64+
65+
return cleanedSummary.Trim().Replace("\n", "\\n").Replace("\r", "\\r");
66+
}
67+
5168
public static INamedTypeSymbol? GetGodotScriptNativeClass(this INamedTypeSymbol classTypeSymbol)
5269
{
5370
var symbol = classTypeSymbol;

0 commit comments

Comments
 (0)