Sunday, March 25, 2012

Developing content assistant for Eclipse plugins (part 1)

This small tutorial is aimed in explaining how to implement the infrastructure for a Content Assistant system in eclipse plugins or, more generically, for JFace applications.
In the following we will distinguish between two cases:
  • Eclipse plugin (or RCP applications) editor. In this case we want to attach Content Assistant to an Editor, which implements org.eclipse.ui.IEditorPart
  • Attaching a Content Assistant to a standard text control (which must anyway be an org.eclipse.jface.text.source.SourceViewer. This case can be found in a standalone application or in any eclipse plugin where content  assistant is not provided to an editor
We will see that Content Assistant is provided in the same way, but the infrastructure providing the proposals is attached to the text control in a different way.
Content Assistant is managed through three different subjects which will be the topic of the next three sections.

IContentAssistantProcessor
The main component of a content assistant is the implementation of IContentAssistantProcessor. Qhen this is attached to an editor or to a SourceViewer SWT component, it is invoked when content assistant is requested, it finds the context and provides the content of the menu.

The method to be implemented to provide the list of suggestions is computeCompletionProposal. What has to be returned is an array of ICompletionProposal objects. ComputeCompletionProposal receives two parameters: a TextViewer which is the SWT control the content assistant is attached to and and integer which represents cursor's position on the control.
The TextViewer is needed to extract the context to provide the list of items. As an example, the context could represent the prefix of a class name which is used to filter the content of the content assistant.
A CompletionProposal object includes 4 elements which can be passed through its constructor:
  • replacementString is the String to be shown and to be written back into the TextViewer
  • replacementOffset is the destination position in the TextViewer where the chosen element will be put
  • replacementLength is the number of already present characters to be substituted (let's say the user wrote java. and that he chooses java.util.List from the content assistant, replacementLength is 5 (java.))
  • cursorPosition is the position of the cursor after writng the content of the proposal.
Example:
public class OQLContentAssistantProcessor implements IContentAssistProcessor
{

    /**
     * provides suggestions given the context
     */
    private SuggestionProvider suggestionProvider;

    /**
     * Extracts the context from TextViewer
     */
    private ContextExtractor extractor;

   .....


    public ICompletionProposal[] computeCompletionProposals(ITextViewer arg0, int arg1)
    {
        String context = extractor.getPrefix(arg0, arg1);
        List suggestions = 
         suggestionProvider.getSuggestions(context);

        return buildResult(suggestions, arg1, context.length());

    }

   
    public char[] getCompletionProposalAutoActivationCharacters()
    {
        return new char[] { '.', '"' };
    }

    
    private ICompletionProposal[] buildResult(List suggestions, int currentCursor,
                    int replaceLength)
    {
        if (suggestions == null)
            throw new IllegalArgumentException("Cannot produce a suggestion. List is null");

        ICompletionProposal[] retProposals = new ICompletionProposal[suggestions.size()];
        Iterator it = suggestions.iterator();

        int c = 0;
        while (it.hasNext())
        {
            ContentAssistElement cp = it.next();
            String classname = cp.getClassName();
            ICompletionProposal completion = new CompletionProposal(classname, currentCursor - replaceLength,
                            replaceLength, currentCursor - replaceLength + classname.length());
            retProposals[c] = completion;
            c++;
        }

        return retProposals;
    }
}
In this example, computeCompletionProposal uses two helpers: the first (ContextExtractor) extract the prefix of the string to be searched, while the second (SuggestionProvider)takes this and computes the list of Strings. Once this is done, the list of suggestions is converted into a list of CompletionProposal objects through buildResult method.

SourceViewerConfiguration
In the last section we saw what provides the content for a content assistant system. Now it is time to attach our ContentAssistantProcessor to a source viewer, which can be an Editor in Eclipse plugins or a SourceViewer SWT components in any jface application.

In both the cases of the Eclipse plugin or the stand alone application, the ContentAssistantProcessor is provided by an instance of SourceViewerConfiguration, which besides the ContentAssistantProcessor provides managers for document formatter, text partitioner and so on. (see here: http://help.eclipse.org/indigo/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fjface%2Ftext%2Fsource%2FSourceViewerConfiguration.html   for details about SourceViewerConfiguration class.

Typically a plugin would subclass it extending only the methods of interest like:
public class OQLTextViewerConfiguration extends SourceViewerConfiguration
{
.....

@Override
    public IContentAssistant getContentAssistant(ISourceViewer sourceViewer)
    {
        ContentAssistant cAssist = new ContentAssistant();

.......
        OQLContentAssistantProcessor fromProcessor = new OQLContentAssistantProcessor(classSuggestions, classNameExtr);
         
        cAssist.setContentAssistProcessor(fromProcessor, IDocument.DEFAULT_CONTENT_TYPE);

        cAssist.enableAutoActivation(true);

        cAssist.setAutoActivationDelay(500);
        cAssist.setProposalPopupOrientation(IContentAssistant.CONTEXT_INFO_BELOW);
        cAssist.setContextInformationPopupOrientation(IContentAssistant.CONTEXT_INFO_BELOW);

        return cAssist;
    }

getContentAssistant provides an instance of ContentAssistant, which wraps the ContentAssistantProcessor discussed above. The interesting reason why a ContentAssistantProcessor is wrapped into a ContentAssistant is that it is possible to associate several ContentAssistantProcessors to the same SWT control providing different information in different contexts (for example in an SQL content assistant, the set of proposals will be different between SELECT clause and FROM clause, thus they may be served by different ContentAssistantProcessors).

This is managed through setContentAssistProcessor which takes a second parameter (here IDocument.DEFAULT_CONTENT_TYPE) which identifies the context where the processor has to be used. 

Defining the contexts will be subject of the second part of this tutorial. 

Now that we have a SourceViewerConfiguration, we have to attach it to the text control we want to provide content assistant. In case we are developing an editor for an eclipse plugin, each Editor class has a method setSourceViewerConfiguration that allows to attach the SourceViewerConfiguration. This method is protected and can be invoked by editor constructor. 
In case of stand alone jface application, or simply when we want to attach content assistant to a control which is not a jface editor, the editor must provide a method to attach a SourceViewerConfiguration (configure(SourceViewerConfiguration) method for org.eclipse.jface.text.source.ISourceViewer).

Key binding
In most of the cases you will want the content assistant not only to be activated by a specific character typed by the user (like the '.' in Java editors) but also by a key combination (like CTRL+space). 


If you are writing an Eclipse plugin and you are attaching a content assistant to an Editor, this comes for free, in that your editor comes with an action that shows the content assistant and this action is bound to a key binding (CTRL+space) that can be modified by the used through Eclipse key bindings management. 


This is not the case in a stand alone JFace application or when adding the content assistant to a JFace control which is not an Editor.

To better understand this concept let's see how this key binding work and how do they trigger content assistant:


Start the Content Assistant: Operation can be requested programmatically invoking doOperation method on the SourceViewer control (org.eclipse.jface.text.source.SourceViewer) passing ISourceViewer.CONTENTASSIST_PROPOSALS as parameter.


Providing an Action that start the Content Assistant: Actions implement a Command pattern allowing to define a behavior of your application independently from the logic that triggers it. In this case we have a simple action that opens the content assistant:
contentAssistAction = new Action()
        {
            @Override
            public void run()
            {
                queryViewer.doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS);
            }
        };


Defining a Command for content assistant: Commands associate an Id to a user behavior. They are kept separated from Actions in order to be able to associate different actions on the same user behavior in different plugins. 

Commands are defined into plugin.xml, they do not define a semantics of the action that has to be performed (it is the action that is then attached to a command). They just define an id for a user behavior:

 
      
Defining a key binding: This allows to attach a key binding to a command. This is performed in plugin.xml as well.


Previous four steps  are not necessary for eclipse plugins editors in that they are already defined. It is only necessary to provide the correct SourceViewerConfiguration.