Basic intellisense

time to read 7 min | 1316 words

In many cases, intellisense is the killer feature that will make all the difference in using a language. However, it is significantly harder than just defining the syntax rules. The main problem is that we need to deal with the current context. Let us take a look at what we would like our intellisense to do for the Quote Generation DSL.

image

  • On empty line, show "specification"
  • On specification parameter, show all available modules.
  • On empty line inside specification block, show all actions (requires, users_per_machine, same_machine_as)
  • On action parameter, find appropriate value (available modules for the requires and same_machine_as actions, pre-specified user counts for the users_per_machine)

And this is for a scenario when we don't even want to deal with intellisense for the CLR API that we can use…

#Develop will give us the facilities, but it can't give us the context, this is something that we need to provide.Let us see how this works, shall we?

First, we need to decide what will invoke the intellisense. In this case, I decided to use the typical ctrl+space, so I added this:

this.editorControl.ActiveTextAreaControl.TextArea.KeyDown+=delegate(object sender, KeyEventArgs e)
{
    if (e.Control == false)
        return;
    if (e.KeyCode != Keys.Space)
        return;
    e.SuppressKeyPress = true;
    ShowIntellisense((char)e.KeyValue);
};

I don't think that this code requires any explanation, so we will move directly to the ShowIntellisense method:

private void ShowIntellisense(char value)
{
    ICompletionDataProvider completionDataProvider = new CodeCompletionProvider(this.imageList1);

    codeCompletionWindow = CodeCompletionWindow.ShowCompletionWindow(
        this,                // The parent window for the completion window
        editorControl, 	     // The text editor to show the window for
        "",	       	     // Filename - will be passed back to the provider
        completionDataProvider,// Provider to get the list of possible completions
        value		     // Key pressed - will be passed to the provider
        );
    if (this.codeCompletionWindow != null)
    {
        // ShowCompletionWindow can return null when the provider returns an empty list
        this.codeCompletionWindow.Closed += CloseCodeCompletionWindow;
    }
}

We aren't doing much here, simply invoking the facilities that #Develop gives us for intellisense. The interesting bit of work all happen in CodeCompletionProvider. There is a lot of boiler plate code there, so we will scan it shortly, and then arrive to the real interesting part:

public class CodeCompletionProvider : ICompletionDataProvider
{
    private ImageList imageList;

    public CodeCompletionProvider(ImageList imageList)
    {
        this.imageList = imageList;
    }

    public ImageList ImageList
    {
        get
        {
            return imageList;
        }
    }

    public string PreSelection
    {
        get
        {
            return null;
        }
    }

    public int DefaultIndex
    {
        get
        {
            return -1;
        }
    }

    public CompletionDataProviderKeyResult ProcessKey(char key)
    {
        if (char.IsLetterOrDigit(key) || key == '_')
        {
            return CompletionDataProviderKeyResult.NormalKey;
        }
        return CompletionDataProviderKeyResult.InsertionKey;
    }

    /// <summary>
    /// Called when entry should be inserted. Forward to the insertion action of the completion data.
    /// </summary>
    public bool InsertAction(ICompletionData data, TextArea textArea, int insertionOffset, char key)
    {
        textArea.Caret.Position = textArea.Document.OffsetToPosition(
            Math.Min(insertionOffset, textArea.Document.TextLength)
            );
        return data.InsertAction(textArea, key);
    }

    public ICompletionData[] GenerateCompletionData(string fileName, TextArea textArea, char charTyped)
    {
        return new ICompletionData[] {
             new DefaultCompletionData("Text", "Description", 0),
             new DefaultCompletionData("Text2", "Description2", 1)
        };
    }
}

The properties should be self explanatory. ProcessKey allows you decide how to handle the current keypress. Here, you get to see only normal keys (send to the actual text control) and insertion (add the current text to the text control). Another is CompletionDataProviderKeyResult.BeforeStartKey, which tells the control to ignore this key. That is important when you want to narrow the selection choice based on what the user it typing.

InsertAction simply instructs the editor in where to place the newly added text. This is important if the user enabled intellisense in the middle of a term, and you want to fix that term.

GenerateCompletionData is where the real interest lies. Everything else is just user experience, a very important detail, but basically just a detail. GenerateComletionData is where the power lies. (note, this is the appropriate Muhahaha! moment).

The current implementation isn't doing much, just returning a hard coded list of values. Note that even this trivial implementation, without any context whatsoever will give you a lot of value. Just because you can now expose more easily the DSL structure. And let us not forget the marketing side of that, if you have intellsense, even if none too intelligent one, you are already way ahead of the game. And here is out result:

image 

I'll go over providing the actual context for the code in another post.