Equipping SWT applications with content assistants

Boost end-user convenience and productivity with intelligent context-sensitive content completion proposals

For users of the Eclipse Java editor, content assistants are a well-known feature. You press Ctrl + spacebar, and a window with a set of completion proposals pops up. Selection of a specific proposal opens another window showing a preview of the insertion of the selected proposal. Committing a proposal with the Enter key or a double-click inserts the proposal into the current document. This article shows how you can easily add this feature to any SWT-based application, either a stand-alone application or a plug-in to the Eclipse workbench.

Share:

Berthold Daum, Consultant, bdaum industrial communications

Berthold Daum is a consultant and writer based in Luetzelbach, Germany. An English edition of his best-selling book Eclipse 2 for Java Developers was recently published by John Wiley & Sons. More details are available at Berthold's Web site. You can contact him at berthold.daum at bdaum.de.



19 November 2003

Also available in Japanese

Creating an HTML editor

The concept of content assistants is related to a specific implementation of JFace text viewers, the class org.eclipse.jface.text.source.SourceViewer. Instances of this class are used throughout the Eclipse workbench to implement the various editors. However, SourceViewers are not restricted to the Eclipse workbench but may be used in any application that builds upon the SWT and JFace JARs. This article demonstrates the implementation of content assistants in the context of an Eclipse editor plug-in, and gives some hints on how to use content assistants with "naked" SourceViewers.

Let's implement a simple HTML editor. Here a content assistant can be very helpful. For example, a content assistant could generate typical HTML structures such as tables or links, or could wrap selected text areas into style tags.

To save time, we will implement this editor by using one of the New Plug-in Project wizards to generate an appropriate editor plug-in. Because this generated editor is an XML editor, and HTML is an XML-based mark-up language, we only need to make some minor modifications to convert this generated editor into an HTML editor. Let's begin.

After invoking the New wizard, select Plug-in Development and Plug-in Project. On the following screen, enter the project name "Sample HTML Editor". In the next screen, define a suitable plug-in ID, such as "com.bdaum.SampleHTMLEditor". The following screen allows you to select an appropriate code generation wizard. Choose Plug-in with an editor, as shown in Figure 1.

Figure 1. Plug-in with an editor
Plug-in with an editor

On the next screen, modify the proposed plug-in name (if desired) and plug-in class name and specify a provider name. Leave everything as is.

Continue to the next screen, and modify the proposed Editor Class Name to "HTMLEditor", the Editor Name to "Sample HTML Editor", and the File Extension to "html, htm", as shown in Figure 2. This latter entry will associate the new editor with all files with a file extension of .html or .htm.

Figure 2. Editor options
Editor options

Click the Finish button to generate the new editor. Now launch a new workbench via Run > Run as ... > Run-time workbench. After creating a new file with a .htm or .html file extension (or importing such a file), open it with the new editor.


Adding a content assistant

As you'll soon find, this editor does not feature a content assistant; pressing Ctrl + spacebar has no effect. SourceViewers by default are not equipped with content assistants. We need to configure the SourceViewer used in our HTML editor appropriately.

The configuration of the HTML editor's SourceViewer is represented by the generated class XMLConfiguration, a subclass of SourceViewerConfiguration (if you wish, you can rename this class to HTMLConfiguration, but this is not essential). To add a content assistant to the source viewer, we need to override the SourceViewerConfiguration method getContentAssistant(). This is best done with the Java editor's context function Source > Override/Implement Methods..., which will create a stub for this method. We now need to implement this method and return an appropriate instance of type IContentAssistant.

Content assistants consist of one or more content processors, one for each content type that we want to support. Documents processed by source viewers can be segmented into several partitions of different content type. Such partitions are determined by partition scanners and, in fact, we find a class XMLPartitionScanner in the package com.bdaum.HTMLEditor.editors. This class defines three different content types for our document type: XML_DEFAULT, XML_COMMENT, and XML_TAG. In addition, documents may contain partitions of type IDocument.DEFAULT_CONTENT_TYPE.

In the new method getContentAssistant(), we first create a new instance of the default implementation of IContentAssistant and equip it with one and the same content assist processor for the content types XML_DEFAULT, XML_TAG, and IDocument.DEFAULT_CONTENT_TYPE. Since we do not plan to offer assistance within HTML comments, we do not create a content assist processor for content type XML_COMMENT. Listing 1 shows the code.

Listing 1. getContentAssistant
public IContentAssistant getContentAssistant(SourceViewer sourceViewer) {

   // Create content assistant
   ContentAssistant assistant = new ContentAssistant();
   
   // Create content assistant processor
   IContentAssistProcessor processor = new HtmlContentAssistProcessor();
   
   // Set this processor for each supported content type
   assistant.setContentAssistProcessor(processor, XMLPartitionScanner.XML_TAG);
   assistant.setContentAssistProcessor(processor, XMLPartitionScanner.XML_DEFAULT);
   assistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);
         
   // Return the content assistant   
   return assistant;
}

Implementing a content assist processor

The class HtmlContentAssistProcessor does not yet exist. Create it by clicking the yellow QuickFix bulb. In this new class, we only need to complete the pre-generated methods that were inherited from interface IContentAssistProcessor. The method that interests us most at the moment is computeCompletionProposals(). This method returns an array of CompletionProposal instances, one for each proposal we have to offer. For example, we could offer a collection of all HTML tags for selection. However, we want it a bit more sophisticated. When a text range is selected in the editor, we want to offer a collection of style tags with which this text can be wrapped. Otherwise, we offer tags for creating new HTML structures. Figures 3 and 4 show what we want to achieve.

Figure 3. structProposal
structProposal
Figure 4. styleProposal
styleProposal

So, first retrieve the current selection from the editor's SourceViewer instance (see Listing 2).

Listing 2. computeCompletionProposals
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, 
   int documentOffset) {

   // Retrieve current document
   IDocument doc = viewer.getDocument();

   // Retrieve current selection range
   Point selectedRange = viewer.getSelectedRange();

Then create an ArrayList instance for collecting the generated ICompletionProposal instance, as shown in Listing 3.

Listing 3. computeCompletionProposals (continued)
   List propList = new ArrayList();

If a text range was selected, retrieve the selected text and compute the proposals for style tags, as shown in Listing 4.

Listing 4. computeCompletionProposals (continued)
   if (selectedRange.y > 0) {
     try {

       // Retrieve selected text
       String text = doc.get(selectedRange.x, selectedRange.y);

       // Compute completion proposals
       computeStyleProposals(text, selectedRange, propList);

     } catch (BadLocationException e) {

     }
   } else {

Otherwise, try to retrieve a qualifier from the document, as shown in Listing 5. Such a qualifier consists of all characters of a partially entered HTML tag and is used to restrict the set of possible proposals.

Listing 5. computeCompletionProposals (continued)
     // Retrieve qualifier
     String qualifier = getQualifier(doc, documentOffset);

     // Compute completion proposals
     computeStructureProposals(qualifier, documentOffset, propList);
   }

Finally, convert the list of completion proposals to an array and return this array as the result, as shown in Listing 6.

Listing 6. computeCompletionProposals (continued)
   // Create completion proposal array
   ICompletionProposal[] proposals = new ICompletionProposal[propList.size()];

   // and fill with list elements
   propList.toArray(proposals);

   // Return the proposals
   return proposals; 
}

Constructing a qualifier

Now, let's see how we can retrieve a qualifier from the current document. We need to implement the method getQualifier(), as shown in Listing 7.

Listing 7. getQualifier
private String getQualifier(IDocument doc, int documentOffset) {

   // Use string buffer to collect characters
   StringBuffer buf = new StringBuffer();
   while (true) {
     try {

       // Read character backwards
       char c = doc.getChar(--documentOffset);

       // This was not the start of a tag
       if (c == '>' || Character.isWhitespace(c))
          return "";

       // Collect character
       buf.append(c);

       // Start of tag. Return qualifier
       if (c == '<')
           return buf.reverse().toString();

     } catch (BadLocationException e) {

       // Document start reached, no tag found
       return "";
     }
   }
}

This is pretty simple. Starting at the current document offset, we read document characters backwards. When we detect an open bracket, we have found the start of a tag and return the collected characters after we have reversed their sequence. In all other cases where we cannot find the beginning of a tag, we return the empty string. In this case, the set of proposals is not restricted.


Compiling completion proposals

Now, let's compile a collection of proposals. Listing 8 shows the set of relevant tags constituting these proposals. If you like, you may add some more.

Listing 8. Collection of proposals
// Proposal part before cursor
private final static String[] STRUCTTAGS1 =
   new String[] { "<P>", "<A SRC=\"", "<TABLE>",  "<TR>",  "<TD>" };

// Proposal part after cursor
private final static String[] STRUCTTAGS2 =
   new String[] { "",    "\"></A>",   "</TABLE>", "</TR>", "</TD>" }

As you can see, we have separated each tag proposal into two parts: in a part before the planned cursor position and in a part after the planned cursor position. Listing 9 shows the method computeStructureProposals() that compiles these proposals.

Listing 9. computeStructureProposals
private void computeStructureProposals(String qualifier, int documentOffset, List propList) { 
   int qlen = qualifier.length();

   // Loop through all proposals
   for (int i = 0; i < STRUCTTAGS1.length; i++) {
      String startTag = STRUCTTAGS1[i];

      // Check if proposal matches qualifier
      if (startTag.startsWith(qualifier)) {

         // Yes -- compute whole proposal text
         String text = startTag + STRUCTTAGS2[i];

         // Derive cursor position
         int cursor = startTag.length();

         // Construct proposal
         CompletionProposal proposal =
            new CompletionProposal(text, documentOffset - qlen, qlen, cursor);

         // and add to result list
         propList.add(proposal);
      }
   }
}

We loop through the tag arrays and select all tags that start with the specified qualifier. For each selected tag, we create a new CompletionProposal instance. For parameters, we pass the complete tag text, the position where this text should be inserted, the length of the text in the document that should be replaced (in other words, the qualifier length), and the planned cursor position relative to the start of the inserted text.

This method will supply us with WYSIWYG ("what you see is what you get") completion proposals. The pop-up window of the content assistant will list the proposals in exactly the same form as they are inserted into the document when a proposal is selected.


Dealing with complex proposals

The previous approach is not suitable for the method computeStyleProposals() that we still have to implement. Here, we need to wrap the selected text into the selected style tags and replace the selected text in the document with this new string. As such a replacement can be of any length, it would not make sense to show it in the content assistants proposal selection window. Instead, it would be better to display a short but meaningful label, and to display a preview window containing the full replacement text, as soon as a specific style proposal is selected. We can achieve such behavior by using an extended form of the CompletionProposal() constructor.

Listing 10 shows the style tags that we want to support and the associated labels. Again, you may want to add some more.

Listing 10. Collection of style tags
private final static String[] STYLETAGS = new String[] { 
      "b", "i", "code", "strong" 
};
private final static String[] STYLELABELS = new String[] { 
      "bold", "italic", "code", "strong" 
};

Listing 11 shows the method computeStyleProposals().

Listing 11. computeStyleProposals
private void computeStyleProposals(String selectedText, Point selectedRange, List propList) {

   // Loop through all styles
   for (int i = 0; i < STYLETAGS.length; i++) {
      String tag = STYLETAGS[i];

      // Compute replacement text
      String replacement = "<" + tag + ">" + selectedText + "</" + tag + ">";

      // Derive cursor position
      int cursor = tag.length()+2;

      // Compute a suitable context information
      IContextInformation contextInfo = 
         new ContextInformation(null, STYLELABELS[i]+" Style");

      // Construct proposal
      CompletionProposal proposal = new CompletionProposal(replacement, 
         selectedRange.x, selectedRange.y, cursor, null, STYLELABELS[i], 
         contextInfo, replacement);

      // and add to result list
      propList.add(proposal);
   }
}

For each supported style tag, we construct a replacement string and create a new completion proposal. Of course, this solution is rather simplistic. A proper implementation would take a closer look at the replacement string. If this string contained tags, we would segment the string accordingly and enclose each single segment separately between the new style tags.


Displaying extra information

The first four parameters in the CompletionProposal() constructor have the same meaning as in the method computeStructureProposals() (replacement string, insertion point, length of replaced text, and cursor position relative to the insertion point). The fifth parameter -- to which we pass null in this example -- accepts an image instance. This image would be shown on the left side of the respective entry in the pop-up window. The sixth parameter accepts the display label that is shown in the proposal selection window. Parameter seven is used for IContextInformation instances, which we will discuss shortly. Finally, parameter eight accepts the text that should be shown in an additional information window when a proposal is selected. However, just supplying a value for this parameter is not sufficient to actually obtain such an information window. We must configure the content assistant accordingly. This is, again, done in the class XMLConfiguration. We simply add the line shown in Listing 12 to the method getContentAssistant().

Listing 12. Add to getContentAssistant
assistant.setInformationControlCreator(getInformationControlCreator(sourceViewer));

What happens here? First, we obtain an instance of type IInformationControlCreator from the current source viewer configuration. This instance is a factory responsible for creating instances of class DefaultInformationControl, which will be responsible for managing the information window. We then tell the content assistant about this factory. The content assistant will eventually use this factory to create a new information control instance when a completion proposal is selected.


Formatting information texts

By default, this information control instance will present the additional information text as plain text. However, it is possible to add some fancy text presentation. For example, we may wish to print all tags in bold. To do this, we need to configure the DefaultInformationControl instance created by the IInformationControlCreator accordingly. The only way to do this is to use a different IInformationControlCreator, and this can be done by overriding the XMLConfiguration method getInformationControlCreator().

The standard implementation of this method in class SourceViewerConfiguration is shown in Listing 13.

Listing 13. getInformationControlCreator
public IInformationControlCreator getInformationControlCreator
      (ISourceViewer sourceViewer) {
   return new IInformationControlCreator() {
      public IInformationControl createInformationControl(Shell parent) {
         return new DefaultInformationControl(parent);
      }
   };
}

We modify the creation of a DefaultInformationControl instance by adding a text presenter of type DefaultInformationControl.IInformationPresenter to the DefaultInformationControl() constructor, as shown in Listing 14.

Listing 14. Add text presenter
   return new DefaultInformationControl(parent, presenter);

What remains to do is to implement this text presenter, as shown in Listing 15.

Listing 15. Text presenter
private static final DefaultInformationControl.IInformationPresenter
   presenter = new DefaultInformationControl.IInformationPresenter() {
      public String updatePresentation(Display display, String infoText,
         TextPresentation presentation, int maxWidth, int maxHeight) {
         int start = -1;

         // Loop over all characters of information text
         for (int i = 0; i < infoText.length(); i++) {
            switch (infoText.charAt(i)) {
               case '<' :

                  // Remember start of tag
                  start = i;
                  break;
               case '>' :
                  if (start >= 0) {

                    // We have found a tag and create a new style range
                    StyleRange range = 
                       new StyleRange(start, i - start + 1, null, null, SWT.BOLD)

                    // Add this style range to the presentation
                    presentation.addStyleRange(range);

                    // Reset tag start indicator
                    start = -1;
                  }
                  break;
         }
      }

      // Return the information text

      return infoText;
   }
};

The processing is done in method updatePresentation(). This method receives the text to be presented and a default TextPresentation instance. We just loop over the characters of the text and add a new style range to this text presentation instance for each XML tag found in the text. In these new style ranges, we leave the foreground and background colors unchanged but set the font style to bold.


Context information

Now let's look at the ContextInformation instance that we had created in method computeStyleProposals(). This context information is shown after a proposal has been inserted into the document. It can be used to inform the user about the successful application of a completion proposal. However, it is not sufficient to just pass a ContextInformation instance to the CompletionProposal() constructor. We must also provide a validator for this context information by completing the method getContextInformationValidator(). Listing 16 shows how it's done.

Listing 16. getContextInformationValidator
public IContextInformationValidator getContextInformationValidator() {
   return new ContextInformationValidator(this);
}

Here, we have used the default implementation ContextInformationValidator. This validator will check if the context information to be shown is contained in the array of context information items returned by method computeContextInformation(). If not, the information will not be shown. We must therefore also complete the method computeContextInformation(), as shown in Listing 17.

Listing 17. computeContextInformation
public IContextInformation[] computeContextInformation(ITextViewer viewer, 
      int documentOffset) {

   // Retrieve selected range
   Point selectedRange = viewer.getSelectedRange();
   if (selectedRange.y > 0) {

      // Text is selected. Create a context information array.
      ContextInformation[] contextInfos = new ContextInformation[STYLELABELS.length];

      // Create one context information item for each style
      for (int i = 0; i < STYLELABELS.length; i++)
         contextInfos[i] = new ContextInformation(null, STYLELABELS[i]+" Style");
      return contextInfos;
   }
   return new ContextInformation[0];
}

Here, we just create an IContextInformation item for each style tag. Of course, this solution is rather simplistic. More advanced implementations would look at the surroundings of the selected text and determine which style tags actually apply to the selected text.

If we don't want to implement this method, we still have the option to implement our own IContextInformationValidator that always returns true.


Activating the assistant

By now, we have completed the main logic of our new content assistant. But when we test the plug-in we will find that still nothing happens when we press Ctrl + spacebar. Of course, why should it? The source viewer still does not know that we want a list of completion proposals when this key combination is pressed.

In a stand-alone SWT/JFace application, we would add a verify listener (see Listing 18) to the source viewer and check for this key combination. Pressing Ctrl + spacebar would trigger the content assist operation and would veto the key event so that it is not processed any further by the source viewer.

Listing 18. VerifyKeyListener
sourceViewer.appendVerifyKeyListener(
   new VerifyKeyListener() {
      public void verifyKey(VerifyEvent event) {

      // Check for Ctrl+Spacebar
      if (event.stateMask == SWT.CTRL && event.character == ' ') {

        // Check if source viewer is able to perform operation
        if (sourceViewer.canDoOperation(SourceViewer.CONTENTASSIST_PROPOSALS))

          // Perform operation
          sourceViewer.doOperation(SourceViewer.CONTENTASSIST_PROPOSALS);

        // Veto this key press to avoid further processing
        event.doit = false;
      }
   }
});

Within a workbench editor setting, however -- as is the case with our HTML editor plug-in -- we don't need to dig into the details of event processing. Here we would rather create an appropriate TextOperationAction (see Listing 19) that invokes the content assist operation. We do this by extending the method createActions() in class HTMLEditor. Just make sure to create a file SampleHTMLEditorPluginResources.properties in package SampleHTMLEditor to satisfy the request for the plug-in resource bundle!

Listing 19. TextOperationAction
private static final String CONTENTASSIST_PROPOSAL_ID = 
   "com.bdaum.HTMLeditor.ContentAssistProposal"; 
protected void createActions() {
   super.createActions();

   // This action will fire a CONTENTASSIST_PROPOSALS operation
   // when executed
   IAction action = 
      new TextOperationAction(SampleHTMLEditorPlugin.getDefault().getResourceBundle(),
      "ContentAssistProposal", this, SourceViewer.CONTENTASSIST_PROPOSALS);
   action.setActionDefinitionId(CONTENTASSIST_PROPOSAL_ID);

   // Tell the editor about this new action
   setAction(CONTENTASSIST_PROPOSAL_ID, action);

   // Tell the editor to execute this action 
   // when Ctrl+Spacebar is pressed
   setActionActivationCode(CONTENTASSIST_PROPOSAL_ID,' ', -1, SWT.CTRL);
}

Now we can test our plug-in again. We should now be able to invoke the content assistant by pressing Ctrl + spacebar. You may want to try the different behavior of the assistant depending on whether text has been selected or not.

Still, we could add a bit more code. For example, the content assistant could auto-activate itself when a '<' character is typed. This can be done by specifying this auto-activation character to the content assistant processor (as we can have specific processors for each document content type, we could also use different auto-activation characters for each content type). We do this by completing the definition of method getCompletionProposalAutoActivationCharacters in class HtmlContentAssistProcessor, as shown in Listing 20.

Listing 20. getCompletionProposalAutoActivationCharacters
public char[] getCompletionProposalAutoActivationCharacters() {
   return new char[] { '<' };
}

In addition, we have to enable auto-activation and to set an auto-activation delay. This is done in class XMLConfiguration. We add the following lines shown in Listing 21 to method getContentAssistant().

Listing 21. Add to getContentAssistant
   assistant.enableAutoActivation(true);
   assistant.setAutoActivationDelay(500);

Finally, we may wish to change the background color of the content assistant pop-up window to differentiate it from the additional information window. So, we add two more lines to method getContentAssistant(), as shown in Listing 22.

Listing 22. Add to getContentAssistant
   Color bgColor = colorManager.getColor(new RGB(230,255,230));
   assistant.setProposalSelectorBackground(bgColor);

Note, that we use the color manager of the XMLConfiguration instance to create a new color. This saves us from having to dispose of the color when it is no longer needed, as the color manager will care about disposal.


Advanced concepts

Now, after we have successfully implemented a content assistant for our HTML manager, you probably want to know how a template-based content assistant works and how it can be implemented. These content assistants -- as we know them from the Java source editor -- have one special feature: their completion proposals can be parameterized. Specific names within such a proposal can be modified by the user with the effect that all occurrences of that name are synchronously updated throughout the whole proposal.

The bad news is that this functionality is part of the Eclipse Java Development Toolkit (JDT) plug-in, and thus is not available in applications where this plug-in is absent -- either stand-alone SWT/JFace-based applications, or minimal Eclipse platforms. The good news is that the source code of this functionality is available and is not too difficult to adapt to other environments. In particular, the classes ExperimentalProposal from package org.eclipse.jdt.internal.ui.text.java and the types ILinkedPositionListener, LinkedPositionUI, and LinkedPositionManager from package org.eclipse.jdt.internal.ui.text.link implement this functionality.


Download

DescriptionNameSize
Code sampleos-ecca/samplehtmleditor.jar---

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source, Java technology
ArticleID=10890
ArticleTitle=Equipping SWT applications with content assistants
publish-date=11192003