Add help support to Debug Commands via XML docs parsing

This commit is contained in:
2026-05-13 10:47:21 +02:00
parent 40413edbab
commit c8912ad100
5 changed files with 429 additions and 294 deletions
@@ -2,12 +2,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using FlaxEditor.Scripting;
using FlaxEngine;
@@ -21,7 +17,6 @@ namespace FlaxEditor.Modules.SourceCodeEditing
{
private Dictionary<ScriptType, string> _typeCache = new Dictionary<ScriptType, string>();
private Dictionary<ScriptMemberInfo, string> _memberCache = new Dictionary<ScriptMemberInfo, string>();
private Dictionary<Assembly, Dictionary<string, string>> _xmlCache = new Dictionary<Assembly, Dictionary<string, string>>();
internal CodeDocsModule(Editor editor)
: base(editor)
@@ -61,13 +56,9 @@ namespace FlaxEditor.Modules.SourceCodeEditing
else if (type.Type != null)
{
// Try to use xml docs for managed type
var xml = GetXmlDocs(type.Type.Assembly);
if (xml != null)
{
var key = "T:" + GetXmlKey(type.Type.FullName);
if (xml.TryGetValue(key, out var xmlDoc))
text += '\n' + FilterWhitespaces(xmlDoc);
}
var xmlDoc = DebugCommands.GetXml(type.Type);
if (xmlDoc != null)
text += '\n' + xmlDoc;
}
_typeCache.Add(type, text);
@@ -108,278 +99,20 @@ namespace FlaxEditor.Modules.SourceCodeEditing
else if (member.Type != null)
{
// Try to use xml docs for managed member
var memberInfo = member.Type;
var xml = GetXmlDocs(memberInfo.DeclaringType.Assembly);
if (xml != null)
{
// [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"]
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection
var memberType = memberInfo.MemberType;
string key = null;
if (memberType.HasFlag(MemberTypes.Field))
{
var fieldInfo = (FieldInfo)memberInfo;
key = "F:" + GetXmlKey(fieldInfo.DeclaringType.FullName) + "." + fieldInfo.Name;
}
else if (memberType.HasFlag(MemberTypes.Property))
{
var propertyInfo = (PropertyInfo)memberInfo;
key = "P:" + GetXmlKey(propertyInfo.DeclaringType.FullName) + "." + propertyInfo.Name;
}
else if (memberType.HasFlag(MemberTypes.Event))
{
var eventInfo = (EventInfo)memberInfo;
key = "E:" + GetXmlKey(eventInfo.DeclaringType.FullName) + "." + eventInfo.Name;
}
else if (memberType.HasFlag(MemberTypes.Constructor))
{
var constructorInfo = (ConstructorInfo)memberInfo;
key = GetXmlKey(constructorInfo);
}
else if (memberType.HasFlag(MemberTypes.Method))
{
var methodInfo = (MethodInfo)memberInfo;
key = GetXmlKey(methodInfo);
}
else if (memberType.HasFlag(MemberTypes.TypeInfo) || memberType.HasFlag(MemberTypes.NestedType))
{
var typeInfo = (TypeInfo)memberInfo;
key = "T:" + GetXmlKey(typeInfo.FullName);
}
if (key != null)
xml.TryGetValue(key, out text);
// Customize tooltips for properties to be more human-readable in UI
if (text != null && memberType.HasFlag(MemberTypes.Property) && text.StartsWith("Gets or sets ", StringComparison.Ordinal))
{
text = text.Substring(13);
unsafe
{
fixed (char* e = text)
e[0] = char.ToUpper(e[0]);
}
}
}
var xmlDoc = DebugCommands.GetXml(member.Type);
if (xmlDoc != null)
text = xmlDoc;
}
_memberCache.Add(member, text);
return text;
}
// [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"]
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection
private string GetXmlKey(MethodInfo methodInfo)
{
var typeGenericMap = new Dictionary<string, int>();
var methodGenericMap = new Dictionary<string, int>();
ParameterInfo[] parameterInfos = methodInfo.GetParameters();
if (methodInfo.DeclaringType.IsGenericType)
{
var methods = methodInfo.DeclaringType.GetGenericTypeDefinition().GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
methodInfo = methods.First(x => x.MetadataToken == methodInfo.MetadataToken);
}
Type[] typeGenericArguments = methodInfo.DeclaringType.GetGenericArguments();
for (int i = 0; i < typeGenericArguments.Length; i++)
{
Type typeGeneric = typeGenericArguments[i];
typeGenericMap[typeGeneric.Name] = i;
}
Type[] methodGenericArguments = methodInfo.GetGenericArguments();
for (int i = 0; i < methodGenericArguments.Length; i++)
{
Type methodGeneric = methodGenericArguments[i];
methodGenericMap[methodGeneric.Name] = i;
}
string declarationTypeString = GetXmlKey(methodInfo.DeclaringType, false, typeGenericMap, methodGenericMap);
string memberNameString = methodInfo.Name;
string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty;
string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", methodInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty;
string key = "M:" + declarationTypeString + "." + memberNameString + methodGenericArgumentsString + parametersString;
if (methodInfo.Name is "op_Implicit" || methodInfo.Name is "op_Explicit")
{
key += "~" + GetXmlKey(methodInfo.ReturnType, true, typeGenericMap, methodGenericMap);
}
return key;
}
private string GetXmlKey(ConstructorInfo constructorInfo)
{
var typeGenericMap = new Dictionary<string, int>();
var methodGenericMap = new Dictionary<string, int>();
ParameterInfo[] parameterInfos = constructorInfo.GetParameters();
Type[] typeGenericArguments = constructorInfo.DeclaringType.GetGenericArguments();
for (int i = 0; i < typeGenericArguments.Length; i++)
{
Type typeGeneric = typeGenericArguments[i];
typeGenericMap[typeGeneric.Name] = i;
}
string declarationTypeString = GetXmlKey(constructorInfo.DeclaringType, false, typeGenericMap, methodGenericMap);
string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty;
string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", constructorInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty;
return "M:" + declarationTypeString + "." + "#ctor" + methodGenericArgumentsString + parametersString;
}
internal static string GetXmlKey(Type type, bool isMethodParameter, Dictionary<string, int> typeGenericMap, Dictionary<string, int> methodGenericMap)
{
if (type.IsGenericParameter)
{
if (methodGenericMap.TryGetValue(type.Name, out var methodIndex))
return "``" + methodIndex;
if (typeGenericMap.TryGetValue(type.Name, out var typeKey))
return "`" + typeKey;
return "`";
}
if (type.HasElementType)
{
string elementTypeString = GetXmlKey(type.GetElementType(), isMethodParameter, typeGenericMap, methodGenericMap);
if (type.IsPointer)
return elementTypeString + "*";
if (type.IsByRef)
return elementTypeString + "@";
if (type.IsArray)
{
int rank = type.GetArrayRank();
string arrayDimensionsString = rank > 1 ? "[" + string.Join(",", Enumerable.Repeat("0:", rank)) + "]" : "[]";
return elementTypeString + arrayDimensionsString;
}
throw new Exception();
}
string prefaceString = type.IsNested ? GetXmlKey(type.DeclaringType, isMethodParameter, typeGenericMap, methodGenericMap) + "." : type.Namespace + ".";
string typeNameString = isMethodParameter ? Regex.Replace(type.Name, @"`\d+", string.Empty) : type.Name;
string genericArgumentsString = type.IsGenericType && isMethodParameter ? "{" + string.Join(",", type.GetGenericArguments().Select(argument => GetXmlKey(argument, true, typeGenericMap, methodGenericMap))) + "}" : string.Empty;
return prefaceString + typeNameString + genericArgumentsString;
}
private static string GetXmlKey(string typeFullNameString)
{
return Regex.Replace(typeFullNameString, @"\[.*\]", string.Empty).Replace('+', '.');
}
private static string FilterWhitespaces(string str)
{
if (str.Contains(" ", StringComparison.Ordinal))
{
var sb = new StringBuilder();
var prev = str[0];
sb.Append(prev);
for (int i = 1; i < str.Length; i++)
{
var c = str[i];
if (prev != ' ' || c != ' ')
{
sb.Append(c);
}
prev = c;
}
str = sb.ToString();
}
return str;
}
private Dictionary<string, string> GetXmlDocs(Assembly assembly)
{
if (!_xmlCache.TryGetValue(assembly, out var result))
{
Profiler.BeginEvent("GetXmlDocs");
var assemblyPath = Utils.GetAssemblyLocation(assembly);
var assemblyName = assembly.GetName().Name;
var xmlFilePath = Path.ChangeExtension(assemblyPath, ".xml");
if (!File.Exists(assemblyPath) && !string.IsNullOrEmpty(assemblyPath))
{
var uri = new UriBuilder(assemblyPath);
var path = Uri.UnescapeDataString(uri.Path);
xmlFilePath = Path.Combine(Path.GetDirectoryName(path), assemblyName + ".xml");
}
if (File.Exists(xmlFilePath))
{
Profiler.BeginEvent(assemblyName);
try
{
// Parse xml documentation
using (var xmlReader = XmlReader.Create(new StreamReader(xmlFilePath)))
{
result = new Dictionary<string, string>();
StringBuilder content = new StringBuilder(2048);
while (xmlReader.Read())
{
if (xmlReader.NodeType == XmlNodeType.Element && string.Equals(xmlReader.Name, "member", StringComparison.Ordinal))
{
string rawName = xmlReader["name"];
var memberReader = xmlReader.ReadSubtree();
if (memberReader.ReadToDescendant("summary"))
{
content.Clear();
do
{
if (memberReader.NodeType == XmlNodeType.Element && memberReader.Read())
{
while (memberReader.NodeType == XmlNodeType.Text)
{
content.Append(memberReader.Value);
if (memberReader.Read() && memberReader.NodeType == XmlNodeType.Element)
{
var nodeRef = TrimRef(memberReader.GetAttribute("cref")); // <see cref=""/>
if (nodeRef == null)
nodeRef = memberReader.GetAttribute("name"); // <paramref name=""/>
content.Append(nodeRef);
memberReader.Read();
string TrimRef(string str)
{
if (str == null)
return null;
if (str.IndexOf(":FlaxEngine.") == 1)
return str.Substring("T:FlaxEngine.".Length);
return str.Substring("T:".Length);
}
}
}
}
if (memberReader.NodeType == XmlNodeType.EndElement && memberReader.Name == "summary")
break;
} while (memberReader.Read());
result[rawName] = content.ToString().Trim(' ', '\r', '\n');
}
}
}
}
}
catch
{
// Ignore errors
}
Profiler.EndEvent();
}
_xmlCache[assembly] = result;
Profiler.EndEvent();
}
return result;
}
private void OnTypesCleared()
{
_typeCache.Clear();
_memberCache.Clear();
foreach (var asm in _xmlCache.Keys.ToArray())
{
if (asm.IsCollectible)
_xmlCache.Remove(asm);
}
DebugCommands.ClearXml();
}
/// <inheritdoc />
+8
View File
@@ -19,6 +19,14 @@ public class Debug : EngineModule
{
options.PublicDefinitions.Add("COMPILE_WITH_DEBUG_DRAW");
}
if (options.Target.IsEditor || options.Configuration != TargetConfiguration.Release)
{
// Used by DebugCommands to parse Xml documentation
options.ScriptingAPI.SystemReferences.Add("System.Xml");
options.ScriptingAPI.SystemReferences.Add("System.Xml.ReaderWriter");
options.ScriptingAPI.SystemReferences.Add("System.Text.RegularExpressions");
}
}
/// <inheritdoc />
+77 -20
View File
@@ -16,8 +16,11 @@
#include "Engine/Scripting/ManagedCLR/MMethod.h"
#include "Engine/Scripting/ManagedCLR/MField.h"
#include "Engine/Scripting/ManagedCLR/MProperty.h"
#include "Engine/Scripting/ManagedCLR/MUtils.h"
#include "FlaxEngine.Gen.h"
#define WITH_HELP (USE_EDITOR || !BUILD_RELEASE) && USE_CSHARP
struct CommandData
{
String Name;
@@ -26,6 +29,38 @@ struct CommandData
void* MethodGet = nullptr;
void* MethodSet = nullptr;
void* Field = nullptr;
#if WITH_HELP
mutable String Help;
StringView GetHelp() const
{
if (Help.IsEmpty())
{
if (dynamic_cast<ManagedBinaryModule*>(Module))
{
// Get C# type and member name
const MClass* mclass = nullptr;
StringAnsiView name;
if (auto field = (MField*)Field)
{
mclass = field->GetParentClass();
name = field->GetName();
}
else if (auto method = (MMethod*)(Method ? Method : (MethodGet ? MethodGet : MethodSet)))
{
mclass = method->GetParentClass();
name = method->GetName();
}
// Use Xml docs reader used by Editor to get tooltips
auto getXmlInternal = DebugCommands::TypeInitializer.GetClass()->GetMethod("GetXmlInternal", 2);
void* params[2] = { INTERNAL_TYPE_GET_OBJECT(mclass->GetType()), MUtils::ToString(name) };
Help = MUtils::ToString((MString*)getXmlInternal->Invoke(nullptr, params, nullptr));
}
}
return Help;
}
#endif
static void PrettyPrint(StringBuilder& sb, const Variant& value)
{
@@ -122,7 +157,11 @@ struct CommandData
// Parse arguments
if (args == StringView(TEXT("?"), 1))
{
LOG(Warning, "TODO: debug commands help/docs printing"); // TODO: debug commands help/docs printing (use CodeDocsModule that parses XML docs)
#if WITH_HELP
// Print command description
LOG(Info, "> {} ?", Name);
LOG_STR(Info, GetHelp());
#endif
return;
}
Array<Variant> params;
@@ -356,6 +395,22 @@ namespace
InitCommands();
Locker.Unlock();
}
const CommandData* GetCommand(StringView command)
{
if (command.FindLast(' ') != -1)
command = command.Left(command.Find(' '));
// TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush)
String commandCopy = command;
command = commandCopy;
EnsureInited();
for (auto& e : Commands)
{
if (e.Name == command)
return &e;
}
return nullptr;
}
}
class DebugCommandsService : public EngineService
@@ -475,30 +530,32 @@ void DebugCommands::GetAllCommands(Array<StringView>& commands)
DebugCommands::CommandFlags DebugCommands::GetCommandFlags(StringView command)
{
CommandFlags result = CommandFlags::None;
if (command.FindLast(' ') != -1)
command = command.Left(command.Find(' '));
// TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush)
String commandCopy = command;
command = commandCopy;
EnsureInited();
for (auto& e : Commands)
if (auto cmd = GetCommand(command))
{
if (e.Name == command)
{
if (e.Method)
result |= CommandFlags::Exec;
else if (e.Field)
result |= CommandFlags::ReadWrite;
if (e.MethodGet)
result |= CommandFlags::Read;
if (e.MethodSet)
result |= CommandFlags::Write;
break;
}
if (cmd->Method)
result |= CommandFlags::Exec;
else if (cmd->Field)
result |= CommandFlags::ReadWrite;
if (cmd->MethodGet)
result |= CommandFlags::Read;
if (cmd->MethodSet)
result |= CommandFlags::Write;
}
return result;
}
StringView DebugCommands::GetCommandHelp(StringView command)
{
StringView result;
#if WITH_HELP
if (auto cmd = GetCommand(command))
{
result = cmd->GetHelp();
}
#endif
return result;
}
bool DebugCommands::Iterate(const StringView& searchText, int32& index)
{
EnsureInited();
+328
View File
@@ -1,6 +1,17 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#if USE_EDITOR || !BUILD_RELEASE
#define WITH_HELP
#endif
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace FlaxEngine
{
@@ -12,4 +23,321 @@ namespace FlaxEngine
public sealed class DebugCommand : Attribute
{
}
partial class DebugCommands
{
#if WITH_HELP
private static Dictionary<Assembly, Dictionary<string, string>> _xmlCache = new();
internal static void ClearXml()
{
foreach (var asm in _xmlCache.Keys.ToArray())
{
if (asm.IsCollectible)
_xmlCache.Remove(asm);
}
}
internal static string GetXmlInternal(Type type, string memberName)
{
// Redirect into type when no member specified
if (string.IsNullOrEmpty(memberName))
return GetXml(type);
// Redirect property function getter/setter into owning property docs
if (memberName.StartsWith("get_") || memberName.StartsWith("set_"))
memberName = memberName.Substring(4);
// Find member of that name
var members = type.GetMember(memberName, BindingFlags.Static | BindingFlags.Public);
if (members.Length == 0)
return null;
return GetXml(members[0]);
}
/// <summary>
/// Gets the XML docs text for the type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The documentation help.</returns>
public static string GetXml(Type type)
{
var xml = GetXmlDocs(type.Assembly);
if (xml != null)
{
var key = "T:" + GetXmlKey(type.FullName);
if (xml.TryGetValue(key, out var xmlDoc))
return FilterWhitespaces(xmlDoc);
}
return null;
}
/// <summary>
/// Gets the XML docs text for the type member.
/// </summary>
/// <param name="member">The type member.</param>
/// <returns>The documentation help.</returns>
public static string GetXml(MemberInfo member)
{
string text = null;
var xml = GetXmlDocs(member.DeclaringType.Assembly);
if (xml != null)
{
// [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"]
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection
var memberType = member.MemberType;
string key = null;
if (memberType.HasFlag(MemberTypes.Field))
{
var fieldInfo = (FieldInfo)member;
key = "F:" + GetXmlKey(fieldInfo.DeclaringType.FullName) + "." + fieldInfo.Name;
}
else if (memberType.HasFlag(MemberTypes.Property))
{
var propertyInfo = (PropertyInfo)member;
key = "P:" + GetXmlKey(propertyInfo.DeclaringType.FullName) + "." + propertyInfo.Name;
}
else if (memberType.HasFlag(MemberTypes.Event))
{
var eventInfo = (EventInfo)member;
key = "E:" + GetXmlKey(eventInfo.DeclaringType.FullName) + "." + eventInfo.Name;
}
else if (memberType.HasFlag(MemberTypes.Constructor))
{
var constructorInfo = (ConstructorInfo)member;
key = GetXmlKey(constructorInfo);
}
else if (memberType.HasFlag(MemberTypes.Method))
{
var methodInfo = (MethodInfo)member;
key = GetXmlKey(methodInfo);
}
else if (memberType.HasFlag(MemberTypes.TypeInfo) || memberType.HasFlag(MemberTypes.NestedType))
{
var typeInfo = (TypeInfo)member;
key = "T:" + GetXmlKey(typeInfo.FullName);
}
if (key != null)
xml.TryGetValue(key, out text);
// Customize tooltips for properties to be more human-readable in UI
if (text != null && memberType.HasFlag(MemberTypes.Property) && text.StartsWith("Gets or sets ", StringComparison.Ordinal))
{
text = text.Substring(13);
unsafe
{
fixed (char* e = text)
e[0] = char.ToUpper(e[0]);
}
}
}
return text;
}
private static string FilterWhitespaces(string str)
{
if (str.Contains(" ", StringComparison.Ordinal))
{
var sb = new StringBuilder();
var prev = str[0];
sb.Append(prev);
for (int i = 1; i < str.Length; i++)
{
var c = str[i];
if (prev != ' ' || c != ' ')
{
sb.Append(c);
}
prev = c;
}
str = sb.ToString();
}
return str;
}
// [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"]
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection
private static string GetXmlKey(MethodInfo methodInfo)
{
var typeGenericMap = new Dictionary<string, int>();
var methodGenericMap = new Dictionary<string, int>();
ParameterInfo[] parameterInfos = methodInfo.GetParameters();
if (methodInfo.DeclaringType.IsGenericType)
{
var methods = methodInfo.DeclaringType.GetGenericTypeDefinition().GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
methodInfo = methods.First(x => x.MetadataToken == methodInfo.MetadataToken);
}
Type[] typeGenericArguments = methodInfo.DeclaringType.GetGenericArguments();
for (int i = 0; i < typeGenericArguments.Length; i++)
{
Type typeGeneric = typeGenericArguments[i];
typeGenericMap[typeGeneric.Name] = i;
}
Type[] methodGenericArguments = methodInfo.GetGenericArguments();
for (int i = 0; i < methodGenericArguments.Length; i++)
{
Type methodGeneric = methodGenericArguments[i];
methodGenericMap[methodGeneric.Name] = i;
}
string declarationTypeString = GetXmlKey(methodInfo.DeclaringType, false, typeGenericMap, methodGenericMap);
string memberNameString = methodInfo.Name;
string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty;
string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", methodInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty;
string key = "M:" + declarationTypeString + "." + memberNameString + methodGenericArgumentsString + parametersString;
if (methodInfo.Name is "op_Implicit" || methodInfo.Name is "op_Explicit")
{
key += "~" + GetXmlKey(methodInfo.ReturnType, true, typeGenericMap, methodGenericMap);
}
return key;
}
private static string GetXmlKey(ConstructorInfo constructorInfo)
{
var typeGenericMap = new Dictionary<string, int>();
var methodGenericMap = new Dictionary<string, int>();
ParameterInfo[] parameterInfos = constructorInfo.GetParameters();
Type[] typeGenericArguments = constructorInfo.DeclaringType.GetGenericArguments();
for (int i = 0; i < typeGenericArguments.Length; i++)
{
Type typeGeneric = typeGenericArguments[i];
typeGenericMap[typeGeneric.Name] = i;
}
string declarationTypeString = GetXmlKey(constructorInfo.DeclaringType, false, typeGenericMap, methodGenericMap);
string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty;
string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", constructorInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty;
return "M:" + declarationTypeString + "." + "#ctor" + methodGenericArgumentsString + parametersString;
}
internal static string GetXmlKey(Type type, bool isMethodParameter, Dictionary<string, int> typeGenericMap, Dictionary<string, int> methodGenericMap)
{
if (type.IsGenericParameter)
{
if (methodGenericMap.TryGetValue(type.Name, out var methodIndex))
return "``" + methodIndex;
if (typeGenericMap.TryGetValue(type.Name, out var typeKey))
return "`" + typeKey;
return "`";
}
if (type.HasElementType)
{
string elementTypeString = GetXmlKey(type.GetElementType(), isMethodParameter, typeGenericMap, methodGenericMap);
if (type.IsPointer)
return elementTypeString + "*";
if (type.IsByRef)
return elementTypeString + "@";
if (type.IsArray)
{
int rank = type.GetArrayRank();
string arrayDimensionsString = rank > 1 ? "[" + string.Join(",", Enumerable.Repeat("0:", rank)) + "]" : "[]";
return elementTypeString + arrayDimensionsString;
}
throw new Exception();
}
string prefaceString = type.IsNested ? GetXmlKey(type.DeclaringType, isMethodParameter, typeGenericMap, methodGenericMap) + "." : type.Namespace + ".";
string typeNameString = isMethodParameter ? Regex.Replace(type.Name, @"`\d+", string.Empty) : type.Name;
string genericArgumentsString = type.IsGenericType && isMethodParameter ? "{" + string.Join(",", type.GetGenericArguments().Select(argument => GetXmlKey(argument, true, typeGenericMap, methodGenericMap))) + "}" : string.Empty;
return prefaceString + typeNameString + genericArgumentsString;
}
private static string GetXmlKey(string typeFullNameString)
{
return Regex.Replace(typeFullNameString, @"\[.*\]", string.Empty).Replace('+', '.');
}
private static Dictionary<string, string> GetXmlDocs(Assembly assembly)
{
if (!_xmlCache.TryGetValue(assembly, out var result))
{
Profiler.BeginEvent("GetXmlDocs");
// Find XML file path (based on assembly location)
var assemblyPath = Utils.GetAssemblyLocation(assembly);
var assemblyName = assembly.GetName().Name;
var xmlFilePath = Path.ChangeExtension(assemblyPath, ".xml");
if (!File.Exists(assemblyPath) && !string.IsNullOrEmpty(assemblyPath))
{
var uri = new UriBuilder(assemblyPath);
var path = Uri.UnescapeDataString(uri.Path);
xmlFilePath = Path.Combine(Path.GetDirectoryName(path), assemblyName + ".xml");
}
if (File.Exists(xmlFilePath))
{
Profiler.BeginEvent(assemblyName);
try
{
// Parse xml documentation
using (var xmlReader = XmlReader.Create(new StreamReader(xmlFilePath)))
{
result = new Dictionary<string, string>();
StringBuilder content = new StringBuilder(2048);
while (xmlReader.Read())
{
if (xmlReader.NodeType == XmlNodeType.Element && string.Equals(xmlReader.Name, "member", StringComparison.Ordinal))
{
string rawName = xmlReader["name"];
var memberReader = xmlReader.ReadSubtree();
if (memberReader.ReadToDescendant("summary"))
{
content.Clear();
do
{
if (memberReader.NodeType == XmlNodeType.Element && memberReader.Read())
{
while (memberReader.NodeType == XmlNodeType.Text)
{
content.Append(memberReader.Value);
if (memberReader.Read() && memberReader.NodeType == XmlNodeType.Element)
{
var nodeRef = TrimRef(memberReader.GetAttribute("cref")); // <see cref=""/>
if (nodeRef == null)
nodeRef = memberReader.GetAttribute("name"); // <paramref name=""/>
content.Append(nodeRef);
memberReader.Read();
string TrimRef(string str)
{
if (str == null)
return null;
if (str.IndexOf(":FlaxEngine.") == 1)
return str.Substring("T:FlaxEngine.".Length);
return str.Substring("T:".Length);
}
}
}
}
if (memberReader.NodeType == XmlNodeType.EndElement && memberReader.Name == "summary")
break;
} while (memberReader.Read());
result[rawName] = content.ToString().Trim(' ', '\r', '\n');
}
}
}
}
}
catch
{
// Ignore errors
}
Profiler.EndEvent();
}
_xmlCache[assembly] = result;
Profiler.EndEvent();
}
return result;
}
#endif
}
}
+9
View File
@@ -56,8 +56,17 @@ public:
/// Returns flags of the command.
/// </summary>
/// <param name="command">The full name of the command.</param>
/// <returns>Command flags.</returns>
API_FUNCTION() static CommandFlags GetCommandFlags(StringView command);
/// <summary>
/// Returns help text of the command (from documentation comment).
/// </summary>
/// <remarks>Only available in non-Release builds and Editor.</remarks>
/// <param name="command">The full name of the command.</param>
/// <returns>Command help text or empty if failed to get it.</returns>
API_FUNCTION() static StringView GetCommandHelp(StringView command);
public:
static bool Iterate(const StringView& searchText, int32& index);
static StringView GetCommandName(int32 index);