diff --git a/src/VisualStudio/LanguageService/Lookup/XSharpLookup.cs b/src/VisualStudio/LanguageService/Lookup/XSharpLookup.cs index ad67a19403..b1a0db6990 100644 --- a/src/VisualStudio/LanguageService/Lookup/XSharpLookup.cs +++ b/src/VisualStudio/LanguageService/Lookup/XSharpLookup.cs @@ -99,7 +99,8 @@ static public IList FindIdentifier(XSharpSearchLocation location, stri variable.ResolvedType = types.FirstOrDefault(); if (variable.ResolvedType != null && variable.ResolvedType.FullName != KnownTypes.SystemArray) { - variable.TypeName = variable.ResolvedType.FullName; + // keep the type arguments of the declaration: List must not become List + variable.TypeName = WithTypeArguments(variable.ResolvedType.FullName, variable.TypeName); } } } @@ -137,7 +138,11 @@ static public IList FindIdentifier(XSharpSearchLocation location, stri var type = ResolveImpliedType(location, xVar, currentType, visibility); if (type != null) { - xVar.TypeName = type.FullName.GetXSharpTypeName(); + var typeName = type.FullName.GetXSharpTypeName(); + // ResolveImpliedCollection stores the element type with its type arguments, e.g. KeyValuePair + if (xVar.ImpliedKind == ImpliedKind.InCollection && type.IsGeneric) + typeName = WithTypeArguments(typeName, xVar.TypeName); + xVar.TypeName = typeName; } } } @@ -380,14 +385,99 @@ private static IXTypeSymbol ResolveImpliedCollection(XSharpSearchLocation locati var elementType = GetTypeNameFromSymbol(location, element); if (elementType.EndsWith("[]")) { - return GetArrayType(location, elementType.Substring(0, elementType.Length - 2)); + // the loop variable has the type of the array element, not the array type + return SearchType(location, elementType.Substring(0, elementType.Length - 2)).FirstOrDefault(); } var type = GetTypeFromSymbol(location, element); + var enumType = GetEnumerableElementTypeName(type, elementType); + if (enumType != null) + { + var ftype = SearchType(location, enumType).FirstOrDefault(); + if (ftype != null) + { + xVar.TypeName = enumType; + return ftype; + } + } var etype = getElementType(type, location, element); return etype; } + private static string GetEnumerableElementTypeName(IXTypeSymbol type, string collectionTypeName) + { + // For a generic collection the loop variable has the type argument of IEnumerable, with the + // type arguments of the collection filled in: Dictionary -> KeyValuePair + // The indexer is no good here: Dictionary[TKey] returns TValue + if (type is XPETypeSymbol peType && peType.IsGeneric && peType.TypeDef != null) + { + var typeParams = peType.TypeParameters; + var typeArgs = GetTypeArguments(collectionTypeName); + if (typeArgs.Count != typeParams.Count) + return null; + const string enumerable = "System.Collections.Generic.IEnumerable`1"; + if (peType.TypeDef.FullName == enumerable) + return typeArgs[0]; + foreach (var impl in peType.TypeDef.Interfaces) + { + if (impl.InterfaceType is Mono.Cecil.GenericInstanceType git && git.ElementType.FullName == enumerable) + { + var name = git.GenericArguments[0].FullName.GetXSharpTypeName(); + return System.Text.RegularExpressions.Regex.Replace(name, @"\w+", m => + { + var index = typeParams.IndexOf(m.Value); + return index >= 0 ? typeArgs[index] : m.Value; + }); + } + } + } + return null; + } + + private static IList GetTypeArguments(string typeName) + { + // List -> Customer, Dictionary> -> STRING, List + var result = new List(); + var pos = typeName?.IndexOf('<') ?? -1; + if (pos <= 0 || !typeName.EndsWith(">")) + return result; + int level = 0; + int start = pos + 1; + for (int i = start; i < typeName.Length - 1; i++) + { + switch (typeName[i]) + { + case '<': + level += 1; + break; + case '>': + level -= 1; + break; + case ',': + if (level == 0) + { + result.Add(typeName.Substring(start, i - start).Trim()); + start = i + 1; + } + break; + } + } + result.Add(typeName.Substring(start, typeName.Length - 1 - start).Trim()); + return result; + } + + private static string WithTypeArguments(string resolvedName, string declaredName) + { + // System.Collections.Generic.List + List -> System.Collections.Generic.List + var typeArgs = GetTypeArguments(declaredName); + if (typeArgs.Count == 0) + return resolvedName; + var pos = resolvedName.IndexOf('<'); + if (pos > 0) + resolvedName = resolvedName.Substring(0, pos); + return resolvedName + "<" + string.Join(",", typeArgs) + ">"; + } + private static IXTypeSymbol getElementType(IXTypeSymbol type, XSharpSearchLocation location, IXSymbol element) { string elementType; @@ -1166,7 +1256,13 @@ enum PPUDCType : byte if (xmember.ParentType != null && xmember.ParentType.IsGeneric && symbols.Count > 0) { result.Clear(); - result.Add(AdjustGenericMember(xmember, symbols.Peek())); + var adjusted = AdjustGenericMember(xmember, symbols.Peek()); + if (adjusted.TypeName != xmember.TypeName) + { + // the clone still has the type of the type parameter (TValue), completion needs the real type + adjusted.ResolvedType = SearchType(location, adjusted.TypeName).FirstOrDefault(); + } + result.Add(adjusted); } else {