Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Improving Code Auto Completion in C#

0.00/5 (No votes)
16 Sep 2009 1  
An article on improving code completion in C#.

Sample Image

Introduction

Code snippets are an excellent feature of Visual Studio, that can greatly improve productivity. However, I feel that in C#, they are slightly lacking in two areas:

  • They do not auto complete open brackets with close brackets.
  • For the most generally used snippets, the typing required to get the statement to appear is unnatural (having to press Tab).

For example, for an if statement's code snippet to fire, you have to type "i", then "f", then "tab", then "tab". It is surely better to type "i", then "f", then "space", and have the code snippet appear. It is more natural and has one less key press. I wonder at the percentage of C# coders who do not even know code snippets exist because of this!

Fortunately, Microsoft has made it easy to override this behaviour by using extensibility to check what text the user has just entered, and then manipulating it.

The add-in does three things:

  1. Check the entered text for code statements such as if, switch, for, foreach, and run the corresponding code snippet after a space or newline is entered.
  2. Check for an open bracket such as (, [, and then automatically add the corresponding close bracket.
  3. After an opening brace {, a corresponding closing brace } will be added.

These options can be configured by the user.

Background

A great background to Visual Studio Extensibility is the excellent LineCounter project on this site.

Installation

Unzip the demo files to a folder somewhere. Move the "Jonno C# AutoComplete.AddIn" file to your "your path\My Documents\Visual Studio 2008\Addins" folder. Open the file up and change the following line to point to the location of the files:

<Assembly>C:\Jonno\Jonno.AddIns.CSharp.AutoComplete\bin\
          Jonno.AddIns.CSharp.AutoComplete.dll</Assembly>

If you are running the source, then the "Jonno C# AutoComplete.AddIn" file will probably be missing. Add it to the add-in project, making sure you link it to the version in your add-ins folder, rather than adding it.

If you wish to run the Unit Tests, you will need NUnit 2.5 and Rhino Mocks 3.6.

I have only tested this on Visual Studio 2008.

Options

The options form can be loaded from the menu entry under Tools->Jonno C# Auto Complete Options.

The options are saved as XML files in the same folder as the add-in assembly.

There are three tabs on the form: Snippets, Brackets, and Braces. Let's examine each one in turn.

Snippets

Sample Image

This is simply a grid view of the snippets that will be checked. If you don't like an entry, then simply delete it. If you feel something is missing, then add it, or edit an existing entry.

The text in the grid view is the text that will be checked, the last character of the string is the key press that will fire the code snippet.

For example, the first entry " if " means that when the user presses space (the last character), it will check to see if the previous text was " if". If it was, then the keypress will be cancelled and tab sent to the window to fire the code snippet.

The entry " if/r" will do the same thing except that it fires the code snippet on a newline rather than a space.

So, if you want the code snippet for a do statement to fire after typing "do ", add the string " do " to the list. If you also want it fired on a newline, add the string " do\r", or if you don't want it to fire, delete it from the list.

Obviously, for this to work, your code snippet must have a shortcut that matches the text in the list; i.e., adding " xxx " to the list will not do much if you don't have a code snippet with a shortcut of xxx!

Brackets

Sample Image

Again, this is a grid view. The first column is the opening bracket to search for, the second column is the closing bracket that will be automatically added.

So, putting ( as the first character and ) as the second means that a ) will automatically be added when the user presses (.

The first and second characters can be the same for quotes, e.g., ' and ', or " and ".

If the characters are different, then a closing bracket will only be added if there are less closing brackets than opening brackets on that line. If the characters are the same, then the closing quote will only be added if the number of quotes is odd on that line.

Once again, you can add, edit, or delete to suit your tastes.

Braces

Sample Image

The first option Matching works as follows (where pipe | represents the cursor):

public string Myproperty |

pressing { will return:

public string Myproperty { | }

Whereas given:

public void myMethod()
    |

pressing { will return:

public void myMethod()
{
    |
}

The One True Brace differs from this in that opening braces where there is already text on the line produces the following:

public void myMethod() |

Pressing { will return:

public void myMethod() {
    |
}

The One True Brace Newline option works as the previous option, with the difference that the closing brace is only added after the user presses Enter after the original brace.

None turns off the option.

Using the Code

Most of the logic is handled in the VSKeyPressHelper class, so we need a property for this:

private VSKeyPressHelper KeyPressHelper { get; set; }

This is initialised with the application object in the OnConnection method in the Connect class, as so:

this.ApplicationObject = (DTE2)application;
this.AddInInstance = (AddIn)addInInst;

this.KeyPressHelper = new VSKeyPressHelper(this.ApplicationObject);

To hook into the key press events, we need a property of type TextDocumentKeyPressEvents. We also need to hook into the window events with a property of WindowEvents, like so:

private TextDocumentKeyPressEvents TextDocKeyEvents { get; set; }

private WindowEvents WindowEvents { get; set; }

Then, in the OnConnection method in the Connect class, we hook up the Windows events we need, which are the WindowActivated and WindowCreated events.

Events2 events = (Events2)this.ApplicationObject.Events;

this.WindowEvents = (WindowEvents)events.get_WindowEvents(null);
this.WindowEvents.WindowActivated += 
   new _dispWindowEvents_WindowActivatedEventHandler(this.WindowActivated);
this.WindowEvents.WindowCreated += 
   new _dispWindowEvents_WindowCreatedEventHandler(this.WindowCreated);

Of course, we need to unhook this event in the OnDisconnection method of the Connect class:

if (this.WindowEvents != null)
{
    this.WindowEvents.WindowActivated -= 
      new _dispWindowEvents_WindowActivatedEventHandler(this.WindowActivated);
    this.WindowEvents.WindowCreated -= 
      new _dispWindowEvents_WindowCreatedEventHandler(this.WindowCreated);
}

In the handler for the WindowActivated and WindowCreated events, we then check whether we are in a C# file or not by checking the last three characters of the window that has been activated:

private void WindowCreated(Window created)
{
    this.AddKeyboardEventsIfFileIsaCSharpFile(created.Caption);            
}

private void WindowActivated(Window gotFocus, Window lostFocus)
{
    this.AddKeyboardEventsIfFileIsaCSharpFile(gotFocus.Caption);
}

private void AddKeyboardEventsIfFileIsaCSharpFile(string fileName)
{
    this.RemoveKeyboardEvents();

    if (fileName.EndsWith(".cs") || fileName.Contains(".cs "))
    {
        this.SetUpKeyboardEventsHandler();
    }
}

If it is a C# file, we then set up the handling of the keyboard events. This means that the add-in only works with C# files and not VB.NET where it is unwanted. We are interested in the BeforeKeyPress and AfterKeypress events. They first get unhooked in the RemoveKeyboardEvents method, and then added in the SetUpKeyboardEventsHandler method.

Events2 events = (Events2)this.ApplicationObject.Events;
this.TextDocKeyEvents = (TextDocumentKeyPressEvents)
                           events.get_TextDocumentKeyPressEvents(null);

this.TextDocKeyEvents.BeforeKeyPress += 
  new _dispTextDocumentKeyPressEvents_BeforeKeyPressEventHandler(this.BeforeKeyPress);
this.TextDocKeyEvents.AfterKeyPress += 
  new _dispTextDocumentKeyPressEvents_AfterKeyPressEventHandler(this.AfterKeyPress);

Let's delve into the BeforeKeyPress method:

// This handles the code snippets checking
if (this.KeyPressHelper.CheckForCodeSnippet(selection, keypress))
{
    cancelKeypress = true;
    
    // sends escape first to exit out of intellisense if it is open
    // if it is not open it does not matter.
    SendKeys.Send("{esc}");
    SendKeys.Send("{tab}");
}

Using the keypress and current selection, the CheckForCodeSnippet determines if a phrase we are interested in has just been entered. If it is, then we send Escape to the active window. This cancels the intellisense if it is open. We then send Tab to the active window, which fires the code snippet (if it exists).

The CheckForCodeSnippetStatement method works by moving backwards from the selection point to get what was entered, then moving the selection point back to where it was, as so:

private string GetPreviousTextFromSelectionPoint(EditPoint ep, EditPoint sp, int length)
{
    sp.CharLeft(length);
    var text = sp.GetText(ep);
    sp.CharRight(length);
    return text;
}

It then compares the text with " if" to see if an "if" statement was entered.

The AfterKeyPress method works in a similar way of manipulating the text around the selection.

switch (keypress)
{
    case "{":   
            // handles normal terse brackets
            this.KeyPressHelper.AddEndBraceAfterOpenBrace(selection);
            break;

    default:
            // handles all other brackets
            this.KeyPressHelper.AddEndBracketAfterOpenBracket(selection, keypress);
            break;
}

The main difference is how the text is manipulated. Instead of sending keys to the active window, we can directly insert text, using the selection and edit points, such as the code that inserts an end bracket:

sp.Insert(reverse);
selection.CharLeft(false, 1);

The first line inserts the bracket, then the selection is moved to the left. Most of the rest of the code in the VSKeyPressHelper class works on similar principles.

A lot of the code in the Connect class deals with creation of tool windows and menus, which I will not detail. Other classes of note are the XMLHelper class which saves and loads the settings to and from XML files, and the OptionsView which is the tool window used to set the options.

Points of Interest

The first annoyance of this type of project is that automated unit testing is extremely hard! Moving the selection point around and inserting text thus involves a bit of trial and error before you get it right. I have added Unit Tests where possible, I feel it was not worth the effort to test the Connect class or the VSKeyPressHelper class.

The most bone headed thing I did was not account for commenting! I added the LineContainsCSharpComments method after using this for a while, which checks for a "//" before the entered text, and turns off the behaviour if it is there.

History

  • 7th September, 2009: Initial version.
  • 14th September, 2009: Added configuration of snippets, brackets, and brace style. Units Tests added to source where practical. Refactored the code base.
  • 15th September, 2009: Added handling of the WindowCreated event.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here