Sunday, May 25, 2014

Implementing a custom discovery site

When developing your own components you might end up with some optional features you do not want to install by default. Typically your first option would be to put additional features to an update site so your users can install them on their own.

But sometimes it would be great to use a more polished interface like the discovery mechanism used by maven or subversive. As p2 already provides all the necessary dialogs such a feature can easily be implemented in your own application.

There exist two predefined commands for the discovery wizard. This tutorial will describe both of them.

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.  

Option 1: Display p2 repository content in a wizard

The first option allows to simply display the content of an existing p2 repository in a nicer way. To use it, simply add a new command to a toolbar or menu. Use org.eclipse.equinox.p2.ui.discovery.commands.ShowRepositoryCatalog as commandId and add a parameter to it. Set the parameter name to org.eclipse.equinox.p2.ui.discovery.commands.RepositoryParameter and the value to the URI of the p2 update site to use.

 That's it, nothing else to do. After activating the command you will end up with a dialog like this:

While this solution is extremely simple, it has some drawbacks:
  • only one p2 repository per handler
  • no filtering
  • no extended information (icons, links, ...)

Option 2: Customize dialog with a discovery site

While the first option might be sufficient for small repositories, you might want to have more control over the displayed items when your components get more complex. The second mechanism allows you to exactly define the content of the wizard.

Step 1: Add command

As before we can use a predefined command with using commandId org.eclipse.equinox.p2.ui.discovery.commands.ShowBundleCatalog. Again we need a parameter: set name to org.eclipse.equinox.p2.ui.discovery.commands.DirectoryParameter. For value you need to provide a URL that points to an XML file containing directory information. In the example code we will host this locally. In a real life scenario you would put this on your project website.

Step 2: Populating directory.xml

A directory is a simple list of eclipse plugin files (in jar format) that contain further information.

<?xml version="1.0" encoding="UTF-8"?>
<directory>
 <entry
  url="file:///mnt/data/develop/workspaces/Blog/com.codeandme.discovery/resources/directory.jar"
  permitCategories="true" />
</directory>

Each entry points to an eclipse plugin that contains actual extension listings.

Step 3: Provide extension listings

Create a new Plug-in Project and switch to the Extensions tab of your plugin.xml. Add a new extension of type org.eclipse.mylyn.discovery.core.connectorDiscovery.

The first component to create is a connectorCategory. It will show up as a nice blueish bar in the wizard. The values to provide are pretty much self explanatory. Categories may have icons and an overview. The overview will be denoted as a small info icon on the right hand side of the entry. It will open a popup on mouse hover.

Now we may add dedicated components to a category. Therefore add a new categoryDescriptor to the extension point. Add all the required fields which should be straight forward. Similar to the category description before we may set an icon and overview information. The id provides a feature identifier of the component to be installed. If we want to install multiple features at the same time, we can attach iu (installable units) nodes to the descriptor. By providing such nodes, the original id will not be used anymore, so make sure you add that feature as a iu node, too.

Optional you may add a certification node to the extension point. If present, a categoryDescriptor may link to it, which provides a "certified" link at the end of its description.


Make sure you deploy the plug-in to the correct location as defined in the directory.xml file and give it a try.

Note

When using the example code from svn you have to update directory.xml content to fit your local path. Also update the location of directory.xml in your command parameter.

Thursday, May 15, 2014

Extending JSDT: adding your own content assist

Extending the JSDT content assist is a  topic asked several times on stackoverflow(20738788, 20779899). As I needed it for the Eclipse EASE project, I decided to come up with a short tutorial.

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly. 

Step 1: Preparations

As we will have dependencies to JSDT you either need to check out the source code from gerrit or add JSDT to your target platform.

Step 2: Defining the extension point

Create a new Plug-in Project com.codeandme.jsdt.contentassist and add following dependencies:
  • org.eclipse.wst.jsdt.ui
  • org.eclipse.core.runtime
  • org.eclipse.jface.text
Afterwards create a new Extension for org.eclipse.wst.jsdt.ui.javaCompletionProposalComputer. Provide an ID and Name. The ID will be needed as reference to a category, the name will be displayed in Preferences/JavaScript/Editor/Content Assist/Advanced. Create a proposalCategory node and provide a nice icon for your category.


Create a second extension of the same type for our implementation. Again provide an ID. Create a javaCompletionProposalComputer subnode with a class and activate set to true. The categoryID consists of your plugin name followed by the category ID we provided earlier.


If you use a target definition containing JSDT plugins you might not be able to use the Plug-in Manifest Editor for creating these extensions. In that case you have to edit the xml code directly:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension point="org.eclipse.wst.jsdt.ui.javaCompletionProposalComputer"
   id="codeandme_category"
   name="Code and me proposals">
   <proposalCategory/>
 </extension>

 <extension
       id="codeandme_proposal"
       point="org.eclipse.wst.jsdt.ui.javaCompletionProposalComputer">
   <javaCompletionProposalComputer
      class="com.codeandme.jsdt.contentassist.CustomCompletionProposalComputer"
      categoryId="com.codeandme.jsdt.contentassist.codeandme_category"
      activate="true">
   </javaCompletionProposalComputer>
 </extension>

</plugin>
Step 3: Proposal computer implementation

Now for the easy part:create a new class CustomCompletionProposalComputer with following content:

package com.codeandme.jsdt.contentassist;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.wst.jsdt.ui.text.java.ContentAssistInvocationContext;
import org.eclipse.wst.jsdt.ui.text.java.IJavaCompletionProposalComputer;

public class CustomCompletionProposalComputer implements IJavaCompletionProposalComputer {

 @Override
 public void sessionStarted() {
 }

 @Override
 public List computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) {

  ArrayList<CompletionProposal> proposals = new ArrayList<CompletionProposal>();

  proposals.add(new CompletionProposal("codeandme.blogspot.com", context.getInvocationOffset(), 0, "codeandme.blogspot.com".length()));
  proposals.add(new CompletionProposal("<your proposal here>", context.getInvocationOffset(), 0, "<your proposal here>".length()));

  return proposals;
 }

 @Override
 public List computeContextInformation(ContentAssistInvocationContext context, IProgressMonitor monitor) {
  return null;
 }

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

 @Override
 public void sessionEnded() {
 }
}



CompletionProposals could contain more information like attached documentation or ContextInformation for variables and other dynamic content. See this post for some information on the Contextinformation.

Optional: Helper methods

Typically you would need to evaluate the last characters before the cursor position to filter your proposals. You also might be interested in the content of the current line left of the cursor position. Maybe these helper methods are of some interest:
 private static final Pattern LINE_DATA_PATTERN = Pattern.compile(".*?([^\\p{Alnum}]?)(\\p{Alnum}*)$");

 /**
  * Extract context relevant information from current line. The returned matcher locates the last alphanumeric word in the line and an optional non
  * alphanumeric character right before that word. result.group(1) contains the last non-alphanumeric token (eg a dot, brackets, arithmetic operators, ...),
  * result.group(2) contains the alphanumeric text. This text can be used to filter content assist proposals.
  * 
  * @param context
  *            content assist context
  * @return matcher containing content assist information
  * @throws BadLocationException
  */
 protected Matcher matchLastToken(final ContentAssistInvocationContext context) throws BadLocationException {
  String data = getCurrentLine(context);
  return LINE_DATA_PATTERN.matcher(data);
 }

 /**
  * Extract text from current line up to the cursor position
  * 
  * @param context
  *            content assist context
  * @return current line data
  * @throws BadLocationException
  */
 protected String getCurrentLine(final ContentAssistInvocationContext context) throws BadLocationException {
  IDocument document = context.getDocument();
  int lineNumber = document.getLineOfOffset(context.getInvocationOffset());
  IRegion lineInformation = document.getLineInformation(lineNumber);

  return document.get(lineInformation.getOffset(), context.getInvocationOffset() - lineInformation.getOffset());
 }