Writing a ReSharper Plugin: Auto-completion

.NET Tech Lead at ELEKS

There is no doubt that auto-completion makes every developer more productive. And smart auto-completion makes developers even happy. So, I decided to contribute to the overall happiness in the world by dedicating this article to extending ReSharper auto-completion in the context of the ReReflection plugin.

Writing_a_ReSharper_Plugin

A good example of context-dependent completion that is really helpful is the ReSharper completion inside string literals for formatting specifiers. For me it was always hard to remember all the magic letters and each time I used Google to find the exact specifier needed. Thanks to the ReSharper team, now I can spend more time on code structure and logic. I am extremely happy!

ELEKS_Writing_ReSharper_Plugin3_1

Implementing Items Provider

As everything in the ReSharper infrastructure, things are much simpler than it seems at first sight. To provide custom completion items, you only need to implement a fairly simple abstract class ItemsProviderOfSpecificContext<> with language-specific context specified as the argument. For example, for C# the corresponding context would be CSharpCodeCompletionContext. There are only two methods that require implementation:

  • IsAvailable. Usually quite simple logic to verify the type of completion (either basic or smart, etc.)
  • AddLookupItems. Actual completion logic

The implemented items provider needs to be registered with LanguageAttribute for a specific language. Language is specified as Type of any class inherited from KnownLanguage.

The “Context” parameter of AddLookupItems provides every part of information usually needed for auto-completion. For example, to get the exact PSI node in the file where the cursor is located, you may use the context.NodeInFile code. You can find more information in the official SDK documentation.

ReReflection Completion

Usually, when you need to get any method (a property or a field) using Reflection, it requires a lot of typing. Besides, very often your code doesn’t work at the first attempt. For example, you may need to get a Min method from the Math class. You may not remember that there is a bunch of methods with the name Min available there. So, your first attempt will look like this:

var m = typeof(Math).GetMethod("Min");

And you know what? The code will fail at runtime with AmbiguousMatchException. What I wanted to achieve in this plugin was completion for Type members that provides correct arguments to get the selected member.

You can see how it works in this video:

Implementation reuses a part of logic that was implemented for static verification – the IsReflectionTypeMethod and ResolveReflectedType methods. Completion will work only if the reflected type is exactly known at the time of compilation.

protected override bool AddLookupItems(CSharpCodeCompletionContext context, GroupedItemsCollector collector)
{
var node = context.NodeInFile;
if (node.GetTokenType() == CSharpTokenType.LPARENTH && node.Parent is IInvocationExpression)
{
var invocationExpression = (IInvocationExpression) node.Parent;
IMethod method;
if (ReflectedTypeHelper.IsReflectionTypeMethod(invocationExpression, false, out method))
{
MethodSpecificCompletion methodSpecificCompletion;
if (IsCompletionRegisteredForMethod(method, out methodSpecificCompletion))
{
var reflectedType = ReflectedTypeHelper.ResolveReflectedType(invocationExpression);
if (reflectedType.ResolvedAs == ReflectedTypeResolution.Exact 
|| reflectedType.ResolvedAs == ReflectedTypeResolution.ExactMakeGeneric)
{
methodSpecificCompletion.ProcessMembers(context, collector, reflectedType.TypeElement.GetMembers());
collector.AddFilter(new ReflectionMembersPreference());
}
}
}
}

return base.AddLookupItems(context, collector);
}

ReflectionMemberLookupItem is inherited from CSharpDeclaredElementLookupItem to reuse the already existing logic for formatting and icon selection.

public class ReflectionMemberLookupItem : CSharpDeclaredElementLookupItem
{
private readonly string _code;

public ReflectionMemberLookupItem(string name, string code, DeclaredElementInstance instance, IElementPointerFactory elementPointerFactory, ILookupItemsOwner owner)
: base(name, instance, elementPointerFactory, owner)
{
_code = code;
}

protected override string GetText()
{
return _code;
}
}

Items Selection

You may have noticed that ReSharper auto-completion is pretty smart and scrolls the list box to the position with items that are more frequently accessed in the current context. A good example is auto-completion inside the method body with locals:

ELEKS_Writing_ReSharper_Plugin3_2

I wanted the same logic to be applied for ReflectionMemberLookupItem. Required functionality is achieved by implementing the ILookupItemsPreference interface and applying it as a filter to GroupedItemsCollector.

private class ReflectionMembersPreference : ILookupItemsPreference
{
public IEnumerable<ILookupItem> FilterItems(ICollection<ILookupItem> items)
{
return items.OfType<ReflectionMemberLookupItem>();
}

public int Order
{
get
{
return 100;
}
}
}

I believe that implementation doesn’t require much explanation. And voilà:

ELEKS_Writing_ReSharper_Plugin3_3

Conclusion

Although there is still a lot of ideas to implement, the current version of the plugin is already in the state when it can be presented to the public. So, at last, you can download the plugin. You may use either the ReSharper extensions manager or the direct link to install the ReReflection plugin. Please note that this is not a stable version yet, so, use it on your own risk. And, of course, any feedback is appreciated.

tags

Comments