Add managed assembly xml docs parsing for tooltips in Editor
This commit is contained in:
@@ -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<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)
|
||||
@@ -29,15 +35,29 @@ namespace FlaxEditor.Modules.SourceCodeEditing
|
||||
/// <returns>The documentation tooltip.</returns>
|
||||
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
|
||||
/// <returns>The documentation tooltip.</returns>
|
||||
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<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;
|
||||
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<string, string> 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<string, string>();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnInit()
|
||||
{
|
||||
@@ -70,12 +288,6 @@ namespace FlaxEditor.Modules.SourceCodeEditing
|
||||
Editor.CodeEditing.TypesCleared += OnTypesCleared;
|
||||
}
|
||||
|
||||
private void OnTypesCleared()
|
||||
{
|
||||
_typeCache.Clear();
|
||||
_memberCache.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnExit()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user