Extending the content assist capabilities of the Eclipse XML editor

It is often desirable to not only have content assistance for the XML structure, but also for text content, when editing XML documents. To get the first one you have just to provide a XML Schema, for the latter you need to create an Eclipse Plugin. Which is fortunately not too complicated.

Let’s say we have a XML document which can contain a list of fruits. Wouldn’t it be nice to get a list of possible fruits when pressing CTRL-Shift? Just like on this screenshot:

completion_proposal_3

Here a step by step guide to implement such a completion proposal plugin for the Eclipse XML editor.

Step 1: Create a plugin project

Open a new Eclipse workspace, goto File -> New -> Project… and select Plug-in Development -> Plug-in Project. Enter an ID and a Name and make sure to check “This plug-in will make contributions to the UI”.

xml_contentproposal_1

Step 2: Add the Required Plug-ins

Double click the generated META-INF/MANIFEST.MF and switch to the Dependencies tab in the wizard. Or, alternatively, to the MANIFEST.MF tab if you want to enter the dependencies manually.

The required Plug-ins are:

  • org.eclipse.core.runtime
  • org.eclipse.wst.sse.ui
  • org.eclipse.wst.sse.core
  • org.eclipse.wst.xml.core
  • org.eclipse.jface.text

Step 3: Implement ICompletionProposalComputer

Create an class which implements ICompletionProposalComputer:

public class FruitsCompletionProposalComputer implements ICompletionProposalComputer {

  @Override
  public List<ICompletionProposal> computeCompletionProposals(CompletionProposalInvocationContext context, IProgressMonitor monitor) {
    //TODO
    return null;
  }

  @Override
  public List<ICompletionProposal> computeContextInformation(CompletionProposalInvocationContext context, IProgressMonitor monitor) {
    return Collections.emptyList();
  }

  @Override
  public String getErrorMessage() {
    return null;
  }

  @Override
  public void sessionEnded() {
  }

  @Override
  public void sessionStarted() {
  }
}

We are going to implement computeCompletionProposals() in Step 5.

Step 4: Define the extension

We need to define now our new proposal computer as extension to the extension point org.eclipse.wst.sse.ui.completionProposal. Return to the plugin wizard from Step 2, switch to tab Extensions and add a new one:
xml_contentproposal_2

Save it and open the newly created plugin.xml to enter the extension details, which is far easier than using the wizard. The extension should look like this (you might of course change the IDs and the name):

<extension
     id="at.nonblocking.xml.completionproposal.demo.fruits"
     point="org.eclipse.wst.sse.ui.completionProposal">         
     
	<proposalCategory 
           id="at.nonblocking.xml.completionproposal.demo.fruits.category"
           name="Fruits completion proposals">
	</proposalCategory>

	<proposalComputer
       activate="true"
       categoryId="at.nonblocking.xml.completionproposal.demo.fruits.category"
       class="at.nonblocking.xml.completionproposal.demo.FruitsCompletionProposalComputer"
       id="at.nonblocking.xml.completionproposal.demo.fruits.proposalcomputer">			
		<contentType id="org.eclipse.core.runtime.xml"/>			
	</proposalComputer>         
 </extension>

Very important is the correct contentType in line 15. And of course the extension point id.

Step 5: Implement the completion proposal computer

In the actual implementation of the proposal computer we need to to the following:

  1. Determine the currently edited XML tag. Abort if the tag is not <fruit>.
  2. Determine the current text node and the currently edited text.
  3. Calculate the absolute document offset of the current text node and the cursor offset within the text.
  4. Determine all fruit names that start with the string between text begin and cursor position.
  5. Create content proposals from the found fruit names.

An example implementation below:

private String[] FRUIT_NAMES = { 
  "Apple", "Apricot", 
  "Banana", "Breadfruit", "Blackberry", "Blackcurrant", "Blueberry", 
  "Currant", "Cherry", "Cloudberry", "Coconut", 
  "Date", "Dragonfruit", "Durian", 
  "Fig", 
  "Gooseberry", "Grape", "Grapefruit", "Guava" 
};

@Override
public List<ICompletionProposal> computeCompletionProposals(
  CompletionProposalInvocationContext context, IProgressMonitor monitor) {

  Node selectedNode = (Node) ContentAssistUtils
    .getNodeAt(context.getViewer(), context.getInvocationOffset());

  Element tag = null;
  if (selectedNode instanceof Element) {
    tag = (Element) selectedNode;
  } else if (selectedNode instanceof Text 
    && selectedNode.getParentNode() instanceof Element) {
    tag = (Element) selectedNode.getParentNode();
  }

  // Process only <fruit> tags
  if (tag == null || !"fruit".equals(tag.getLocalName())) {
    return Collections.EMPTY_LIST;
  }

  // Determine text node
  IDOMNode textNode = null;
  if (selectedNode instanceof Text) {
    textNode = (IDOMNode) selectedNode;
  } else {
    if (selectedNode.getChildNodes().getLength() == 1 
      && selectedNode.getChildNodes().item(0) instanceof Text) {
      // Cursor at the end of a text node
      textNode = (IDOMNode) selectedNode.getChildNodes().item(0);
    } else {
      // Cursor between two tags, no text node yet
    }
  }

  // Determine selected text and offsets
  String selectedText = null;
  int textNodeOffset = -1;
  int cursorOffsetWithinTextNode = -1;

  if (textNode != null) {
    selectedText = textNode.getStartStructuredDocumentRegion().getText();
    textNodeOffset = textNode.getStartOffset();
    cursorOffsetWithinTextNode = context.getInvocationOffset() - textNodeOffset;
  } else {
    selectedText = "";
    textNodeOffset = context.getInvocationOffset();
    cursorOffsetWithinTextNode = 0;
  }

  // Gather proposals
  List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
  String searchPrefix = selectedText.substring(0, cursorOffsetWithinTextNode);

  for (String searchResult : findFruits(searchPrefix)) {
    proposals.add(new CompletionProposal(
     searchResult, // replacement text
     textNodeOffset, 
     selectedText.length(), // replace the full text
     searchResult.length()));
  }

  return proposals;
}

private List<String> findFruits(String searchPrefix) {
  List<String> result = new ArrayList<String>();

  for (String fruitName : FRUIT_NAMES) {
    if (fruitName.startsWith(searchPrefix)) {
      result.add(fruitName);
    }
  }

  return result;
}

Step 6: Launch a new Eclipse instance

Right click on your project and select Run as -> Eclipse Application. Create a new sample project an a XML file. After creating a <fruit> tag you should now have content assistance for possible fruit names.

You should also be able to see your plugin in the XML Content Assist preferences (Window -> Preferences -> XML -> XML Files -> Editor -> Content Assist):
xml_contentproposal_3

Move your category up in the list in order to make your entries the first ones in the completion list.

You can find this example plugin on GitHub.

Eclipse Juno. Performance. Finally.

The Eclipse Foundation just released the first release candidate for the second service release (SR2) of Eclipse Juno. As promised, a lot work concerning the performance has been done and the severe degradation compared to Indigo seems to be fixed.

I’m using this first release candidate now for a week, and besides of a few minor bugs, it feels like a huge improvement. And my frustration level dropped noticeable.

A big thank you to all contributors!