From c8912ad100f697718dfa75f184a3af08e0890122 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 13 May 2026 10:47:21 +0200 Subject: [PATCH] Add help support to Debug Commands via XML docs parsing --- .../SourceCodeEditing/CodeDocsModule.cs | 281 +-------------- Source/Engine/Debug/Debug.Build.cs | 8 + Source/Engine/Debug/DebugCommands.cpp | 97 ++++-- Source/Engine/Debug/DebugCommands.cs | 328 ++++++++++++++++++ Source/Engine/Debug/DebugCommands.h | 9 + 5 files changed, 429 insertions(+), 294 deletions(-) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index fe50e022f..26032152a 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -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 _typeCache = new Dictionary(); private Dictionary _memberCache = new Dictionary(); - private Dictionary> _xmlCache = new Dictionary>(); 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(); - var methodGenericMap = new Dictionary(); - 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(); - var methodGenericMap = new Dictionary(); - 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 typeGenericMap, Dictionary 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 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(); - 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")); // - if (nodeRef == null) - nodeRef = memberReader.GetAttribute("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(); } /// diff --git a/Source/Engine/Debug/Debug.Build.cs b/Source/Engine/Debug/Debug.Build.cs index 76dd20137..2208d80bd 100644 --- a/Source/Engine/Debug/Debug.Build.cs +++ b/Source/Engine/Debug/Debug.Build.cs @@ -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"); + } } /// diff --git a/Source/Engine/Debug/DebugCommands.cpp b/Source/Engine/Debug/DebugCommands.cpp index 58cf2894b..feb985cd4 100644 --- a/Source/Engine/Debug/DebugCommands.cpp +++ b/Source/Engine/Debug/DebugCommands.cpp @@ -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(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 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& 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(); diff --git a/Source/Engine/Debug/DebugCommands.cs b/Source/Engine/Debug/DebugCommands.cs index 93381bd0f..3680c6a97 100644 --- a/Source/Engine/Debug/DebugCommands.cs +++ b/Source/Engine/Debug/DebugCommands.cs @@ -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> _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]); + } + + /// + /// Gets the XML docs text for the type. + /// + /// The type. + /// The documentation help. + 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; + } + + /// + /// Gets the XML docs text for the type member. + /// + /// The type member. + /// The documentation help. + 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(); + var methodGenericMap = new Dictionary(); + 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(); + var methodGenericMap = new Dictionary(); + 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 typeGenericMap, Dictionary 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 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(); + 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")); // + if (nodeRef == null) + nodeRef = memberReader.GetAttribute("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 + } } diff --git a/Source/Engine/Debug/DebugCommands.h b/Source/Engine/Debug/DebugCommands.h index f25fe0581..71c90f30c 100644 --- a/Source/Engine/Debug/DebugCommands.h +++ b/Source/Engine/Debug/DebugCommands.h @@ -56,8 +56,17 @@ public: /// Returns flags of the command. /// /// The full name of the command. + /// Command flags. API_FUNCTION() static CommandFlags GetCommandFlags(StringView command); + /// + /// Returns help text of the command (from documentation comment). + /// + /// Only available in non-Release builds and Editor. + /// The full name of the command. + /// Command help text or empty if failed to get it. + API_FUNCTION() static StringView GetCommandHelp(StringView command); + public: static bool Iterate(const StringView& searchText, int32& index); static StringView GetCommandName(int32 index);