diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index 6270b3e6a..27d029940 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -1,7 +1,12 @@ // Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. +using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Xml; using FlaxEditor.Scripting; using FlaxEngine; @@ -15,6 +20,7 @@ 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) @@ -29,15 +35,29 @@ namespace FlaxEditor.Modules.SourceCodeEditing /// The documentation tooltip. public string GetTooltip(ScriptType type, object[] attributes = null) { + // Try to use cache if (_typeCache.TryGetValue(type, out var text)) return text; + // Try to use tooltip attribute if (attributes == null) attributes = type.GetAttributes(false); text = type.TypeName; var tooltip = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute); if (tooltip != null) text += '\n' + tooltip.Text; + 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' + xmlDoc; + } + } + _typeCache.Add(type, text); return text; } @@ -50,18 +70,216 @@ namespace FlaxEditor.Modules.SourceCodeEditing /// The documentation tooltip. public string GetTooltip(ScriptMemberInfo member, object[] attributes = null) { + // Try to use cache if (_memberCache.TryGetValue(member, out var text)) return text; + // Try to use tooltip attribute if (attributes == null) attributes = member.GetAttributes(true); var tooltip = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute); if (tooltip != null) text = tooltip.Text; + 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); + } + } + _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; + return "`" + typeGenericMap[type.Name]; + } + 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 Dictionary GetXmlDocs(Assembly assembly) + { + if (!_xmlCache.TryGetValue(assembly, out var result)) + { + Profiler.BeginEvent("GetXmlDocs"); + + var uri = new UriBuilder(assembly.CodeBase); + var path = Uri.UnescapeDataString(uri.Path); + var name = assembly.GetName().Name; + var xmlFilePath = Path.Combine(Path.GetDirectoryName(path), name + ".xml"); + if (File.Exists(xmlFilePath)) + { + Profiler.BeginEvent(name); + try + { + // Parse xml documentation + using (var xmlReader = XmlReader.Create(new StreamReader(xmlFilePath))) + { + result = new Dictionary(); + 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")) + { + result[rawName] = memberReader.ReadInnerXml().Replace('\n', ' ').Trim(); + } + } + } + } + } + catch + { + // Ignore errors + } + Profiler.EndEvent(); + } + + _xmlCache[assembly] = result; + Profiler.EndEvent(); + } + return result; + } + + private void OnTypesCleared() + { + _typeCache.Clear(); + _memberCache.Clear(); + _xmlCache.Clear(); + } + /// public override void OnInit() { @@ -70,12 +288,6 @@ namespace FlaxEditor.Modules.SourceCodeEditing Editor.CodeEditing.TypesCleared += OnTypesCleared; } - private void OnTypesCleared() - { - _typeCache.Clear(); - _memberCache.Clear(); - } - /// public override void OnExit() {