vrijdag 22 juli 2011

Create your own simple intellisense for .NET

I was playing around with the source code of STFUandCode to add a little intellisense and I thought it might be usefull to post some of the code here.

So for this simple example I’ll just create an intellisense that gives you information about the mscorlib assembly.  If you want you can adjust the code to include other assemblies as well.  The first thing we need to do is get the assembly with reflection:

Assembly assembly = Assembly.GetAssembly(typeof(object));

You see I do this by getting the type of object.  The reason why I do this by type and not by name is so that you can use this code for any .NET version you want, because it will resolve the core lib by the type so you’re not bound by any version.  The next step is to get all the types out of this assembly like so:

private static Type[] GetTypes()
{
    Assembly assembly = Assembly.GetAssembly(typeof(object));
    Type[] types = assembly.GetTypes();
    return types;
}

Pretty easy.  Then the only thing we need to do is to filter out the types we want.  Imagine the user typing in:

System.Cons

Then it’s our job to disassemble the string and compare it to the types in the assembly:

private static IEnumerable<string> GetSuggestionsForClasses(string statement)
{
    // Get the last chain in the statement which is in most cases a part of the type name
    var lastStatementChain = (statement.Split('.').LastOrDefault() ?? "").Trim();

    var typeNamespace = GetNamespace(statement);
    var types = GetTypes();

    // Filter by type
    var allTypes = types.Where(x => x.Namespace.Equals(typeNamespace) && x.Name.Contains(lastStatementChain))
                        .Select(x => x.Name);
  
    // filter by namespace
    var allNameSpaces = types.Where(x => !String.IsNullOrEmpty(x.Namespace) &&
                                        x.Namespace.Contains(typeNamespace))
                             .Select(x => x.Namespace);

    // Join types and namespaces
    return allTypes.Union(allNameSpaces).OrderBy(x => x);
}

private static string GetNamespace(string statement)
{
    var typeNamespace = "System";
    var hasNameSpace = statement.Split('.').Count() > 1; // If the statement is chained by a dot

    if (hasNameSpace)
    {
        typeNamespace = statement.Substring(0, statement.LastIndexOf('.')).Trim(); // Extract the namespace
    }
    return typeNamespace;
}

This way we get intellisense with type and namespaces.  We can take it a step further and detect if what the user typed in was already a type and we should show its members:

if (types.Any(x => TypeEqualsStatement(statementTrimmed, x)))
{
    return GetSuggestionsForType(statementTrimmed);
}

Then to see whether the statement equals a type we use the following method:

private static bool TypeEqualsStatement(string statementTrimmed, Type x)
{
    // if we have an empty statement then it's impossible to have a type
    if (string.IsNullOrWhiteSpace(statementTrimmed))
        return false;

    // Get the last chain
    var lastSeperator = statementTrimmed.LastIndexOf('.');
    lastSeperator = lastSeperator == -1 ? 0 : lastSeperator;
    var statementedSubtrimmed = statementTrimmed.Substring(0, lastSeperator);

    // Compare to the statement minus the last chain and the last chain because the last chain can be a part of the type member
    return x.FullName.Equals(statementTrimmed) || (!string.IsNullOrEmpty(statementedSubtrimmed) && x.FullName.Equals(statementedSubtrimmed));
}

We always have to be careful that we compare with and without the last chain of the method.  The reason for this is that the user could have already typed in a part of a member of the type.  For example if he typed in: ‘System.Console.Wr’   then the Wr is a piece of the member and we must ignore this when searching for the type.  The last step is then to give a summary of all the members in the type:

private static IEnumerable<string> GetSuggestionsForType(string statement)
{
    var types = GetTypes();
    var type = types.FirstOrDefault(x => TypeEqualsStatement(statement, x));
    var lastChain = statement.Substring(statement.LastIndexOf('.')).Trim('.', ' ');

    if (type == null)
        return new List<string>();

    // if the last chain is the type itself then we give a full list of it's members
    if (type.FullName.EndsWith(lastChain))
        return type.GetMembers().Select(x => x.Name)
                                .Distinct();

    // if the last chain isn't the type itself then we filter the members
    return type.GetMembers().Where(x => x.Name.Contains(lastChain))
                            .Select(x => x.Name)
                            .Distinct();
}

We can then poor this Enumerable of strings inside the listbox of a popup and show it under the text.  If you're curious in how to align this popup under the desired text then look at this post.

That’s how you can build a simple intellisense!

Until next time.

1 opmerking:

  1. "I invented it, Bill made it famous."
    David Bradley (wrote the code for Ctrl-Alt-Delete on the IBM PC)

    BeantwoordenVerwijderen