This tutorial describes how to create a custom text editor for the Eclipse IDE, with code completion, syntax highlighting, hover and code mining support.

1. Eclipse Editors

An text editor allows to modify data, typically text and applies the changes to the underlying model whenever the save action is triggered.

To provide editor functionality for a certain file extension or content type, you can:

  • Extent the generic editor

  • Implement your own editor

Extending the generic editor is the preferred choice for new files as this accelerates and simplifies the implementation.

Add support a new content type in the generic editor you need to:

  • add a content type

  • register a PresentationReconsiler to it via the org.eclipse.ui.genericeditor.presentationReconcilers extension point

1.1. JFace text framework

In JFace text a text document is modeled as an IDocument. To view or edit an IDocument document you use an ITextViewer as controller, which use a StyledText widget for its presentation.

The IDocument interface stores text and provide support for:

  • line information

  • text manipulation

  • document change listeners

  • customizable position management

  • search

  • customizable partition management

  • document partition change listeners

A document can be partitioned into different partitions via a document partitioner. These partitions can to manipulated according to their type, e.g. they can have different foreground colors.

1.2. Introduction to presentation reconciler

Every time the user changes the document, the presentation reconciler determines which region of the visual presentation should be invalidated and how to repair it.

The highlighting of source code can be archived by using an presentation reconciler. Such a presentation reconciler can be defined via an org.eclipse.ui.genericeditor.presentationReconcilers extension. It requires the specification of a contentType and a class, which implements the IPresentationReconciler interface. When using an IPresentationReconciler certain IRules can be applied for a specified content type. An IRule defines a rule used in the scanning of text for the purpose of document partitioning or text styling.

The partition is a semantic view onto the document:

  • each partition has a content type

  • each character of a document belongs to a partition

  • documents support multiple partitionings

  • partitioning is always up-to-date

1.3. API for working with editors

You can open an Editor via the current active page. For this you need the EditorInput object and the ID for the editor which is defined in the "org.eclipse.ui.editors" extension point.

page.openEditor(new YourEditorInput(), ID_OF_THE_EDITOR);

To get the page you can use:

// If you are in a view
getViewSite().getPage();
// If you are in an command
HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
// Somewhere else
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();

If you hold down the Ctrl and click on an element in the Java editor you can navigate to it.

This functionality is provided via extensions for the org.eclipse.ui.workbench.texteditor.hyperlinkDetectors extension point. The specified name is visible in the preferences under General  Editors  Text Editors  Hyperlinking. The targetId points to the type of editor you want to support. If you want to use in all text editors use org.eclipse.ui.DefaultTextEditor. To target the generic editor use the org.eclipse.ui.genericeditor.GenericEditor target id.

An IHyperlinkDetector is supposed to return an array of IHyperlink objects. The IHyperlink implemention performs the hyperlink action.

2.2. Adding colors and fonts preferences

Eclipse provides a page for the customizations of colors and fonts by the user under General  Appearance  Colors and Fonts.

To define an entry for this page, you need to define an extension for the org.eclipse.ui.themes extension point.

For example, you can provide a category, font and color with the following entry in the plugin.xml file or your plug-in.

 <extension point="org.eclipse.ui.themes">
    <themeElementCategory
        id="com.vogella.eclipse.preferences.mythemeElementCategory"
        label="vogella category">
        <description>
            An example theme category
        </description>
    </themeElementCategory>
    <colorDefinition
        categoryId="com.vogella.eclipse.preferences.mythemeElementCategory"
        id="com.vogella.eclipse.preferences.myFirstColorDefinition"
        label="vogella color"
        value="COLOR_DARK_BLUE">
        <description>
            Your description for the color
        </description>
    </colorDefinition>
    <fontDefinition
        categoryId="com.vogella.eclipse.preferences.mythemeElementCategory"
        id="com.vogella.eclipse.preferences.myFirstFontDefinition"
        label="vogella Font"
        value="Lucida Sans-italic-18">
        <description>
            Your description for the font
        </description>
    </fontDefinition>
</extension>

The value for the color can be a COLOR_* constants defined in the SWT class. You can also specify RGB values like 255,0,0. The value for the font is defined via the following pattern:`fontname-style-height`

The preference can now be changed via the user or via the CSS engine. To get the current value you can use the IThemeManager.

// Eclipse 4 API
@Inject IThemeManager themeManager;

// Eclipse 3 API
IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager();

ITheme currentTheme = themeManager.getCurrentTheme();

ColorRegistry colorRegistry = currentTheme.getColorRegistry();
Color color = colorRegistry.get("com.vogella.eclipse.preferences.myFirstColorDefinition");

FontRegistry fontRegistry = currentTheme.getFontRegistry();
Font font = fontRegistry.get("com.vogella.eclipse.preferences.myFirstFontDefinition");

2.3. Custom spelling engine

The org.eclipse.ui.workbench.texteditor plug-in provides the option to register a custom spelling engine via the org.eclipse.ui.workbench.texteditor.spellingEngine extension point.

3. Exercise : Use generic editor for a custom file type

In this exercise you associate files with the tasks extension with the generic text editor. Within this file, you want to support editing property files as for example.

test:Hello
Helper:stuff

3.1. Create a new plug-in

Create a new simple plug-in project called com.vogella.ide.editor.tasks.

3.2. Add Manifest dependencies

Open the editor for the MANIFEST.MF file and add the following dependencies via the Dependencies tab using the Add button.

  • org.eclipse.text

  • org.eclipse.ui

  • org.eclipse.ui.editors

  • org.eclipse.ui.genericeditor

  • org.eclipse.ui.workbench.texteditor

  • org.eclipse.jface.text

  • org.eclipse.core.runtime

  • org.eclipse.core.resources

3.3. Review manifest as text

If you select the MANIFEST.MF tab, you can see this file as plain text. It should look like the following solution (version numbers have been removed because they change with every release).

Show Solution

It should look similar to the following (again version numbers have been removed as they change frequently).

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Tasks
Bundle-SymbolicName: com.vogella.ide.editor.tasks
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: VOGELLA
Automatic-Module-Name: com.vogella.ide.editor.tasks
Bundle-RequiredExecutionEnvironment: JavaSE-11
Require-Bundle: org.eclipse.text,
 org.eclipse.ui,
 org.eclipse.ui.editors,
 org.eclipse.ui.genericeditor,
 org.eclipse.ui.workbench.texteditor,
 org.eclipse.jface.text,
 org.eclipse.core.runtime,
 org.eclipse.core.resources

3.4. Define content type

Using the MANIFEST.MF editor, open the Extensions tab and press the Add…​ button.

contentTypes extension09

Select the org.eclipse.core.contenttype.contentTypes extension point.

contentTypes extension10

Press Finish to close the dialog and add the extension.

Right-click on your new entry and select New  content-type.

contentTypes extension12

Specify a content type for files with the .tasks extension similar to the following screenshot.

contentTypes extension14

This creates an entry in the plugin.xml file.

Your plugin.xml file should look similar to the following listing. You can see this by clicking the plugin.xml tab of the manifest editor.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
<extension
      point="org.eclipse.core.contenttype.contentTypes">
   <content-type
         file-extensions="tasks"
         id="com.vogella.ide.contenttype.tasks"
         name="Tasks"
         priority="high">
   </content-type>
</extension>
</plugin>

3.5. Associate content type with editor

This content type can be associated with a certain editor. The org.eclipse.ui.editors extension point can be used for this.

Add the org.eclipse.ui.editors extension via the Add button on the Extensions tab.

editor association10

Right-click on your org.eclipse.ui.editors, and select New  editorContentTypeBinding to define this.

editor association20
editor association30

The resulting plugin.xml should now looks similar to the following.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
<extension
      point="org.eclipse.core.contenttype.contentTypes">
   <content-type
         file-extensions="tasks"
         id="com.vogella.ide.contenttype.tasks"
         name="Tasks"
         priority="high">
   </content-type>
</extension>
<extension
      point="org.eclipse.ui.editors">
   <editorContentTypeBinding
         contentTypeId="com.vogella.ide.contenttype.tasks"
         editorId="org.eclipse.ui.genericeditor.GenericEditor">
   </editorContentTypeBinding>
</extension>

</plugin>

3.6. Add your plug-in to your product via the feature

Add your new plug-in to your IDE feature. In case you are not using features, skip this step

3.7. Test your development

Start a new runtime Eclipse via the your product if you are using features and a product.

The start via the product will update your launch configuration. If you start the runtime Eclipse directly via an unmodified launch configuration, your new plug-in will not be included.

If you are not using features and a product, update the launch configuration directly to ensure you new plug-in is included in the start.

In your runtime Eclipse ensure that you content type is visible in Window  Preferences  General  Content Types.

content type task result

Create a new project (either General or Java project).

In this new project create a new file with the .tasks extension. If you open the file, it should open in the generic text editor.

tasks in genericeditor10

The icon should be the icon of the generic editor.

generic editor icon

4. Exercise: Implementing syntax highlighting

In this exercise you implement syntax highlighting for your tasks file editor. You continue to work on the com.vogella.ide.editor.tasks plug-in.

4.1. Implement syntax highlighting

Implement the following class to define a IRule.

package com.vogella.ide.editor.tasks;

import org.eclipse.jface.text.rules.ICharacterScanner;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;

public class PropertyNameRule implements IRule {

    private final Token token;

    public PropertyNameRule(Token token) {
        this.token = token;
    }

    @Override
    public IToken evaluate(ICharacterScanner scanner) {
        int c = scanner.read();
        int count = 1;

        while (c != ICharacterScanner.EOF) {

            if (c == ':') {
                return token;
            }

            if ('\n' == c || '\r' == c) {
                break;
            }

            count++;
            c = scanner.read();
        }

        // put the scanner back to the original position if no match
        for (int i = 0; i < count; i++) {
            scanner.unread();
        }

        return Token.UNDEFINED;
    }
}

Implement the following class which will be used as the reconciler for your editor.

package com.vogella.ide.editor.tasks;

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.presentation.PresentationReconciler;
import org.eclipse.jface.text.rules.DefaultDamagerRepairer;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.RuleBasedScanner;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;

public class PropertiesReconciler extends PresentationReconciler {

    private final TextAttribute tagAttribute = new TextAttribute(
            Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GREEN));

    public PropertiesReconciler() {
        RuleBasedScanner scanner = new RuleBasedScanner();
        IRule rule = new PropertyNameRule(new Token(tagAttribute));
        scanner.setRules(new IRule[] { rule });
        DefaultDamagerRepairer dr = new DefaultDamagerRepairer(scanner);
        this.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);
        this.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);
    }
}

Add the an extension to the org.eclipse.ui.genericeditor.presentationReconcilers extension point to the plugin.xml file of the com.vogella.ide.editor.tasks plug-in.

<extension
      point="org.eclipse.ui.genericeditor.presentationReconcilers">
    <presentationReconciler
         class="com.vogella.ide.editor.tasks.PropertiesReconciler"
         contentType="com.vogella.ide.contenttype.tasks">
   </presentationReconciler>
</extension>

4.2. Test your implementation

Restart your runtime Eclipse.

Open a .tasks file and enter a few property values into the file, as for example:

ID: 1
Summary: Eclipse IDE Training
Description:
Done:
Duedate:
Dependent:

The result should look similar to this:

editor syntax highlight

5. Exercise: Allow user to customize the colors

In the precious exercises you implement syntax highlighting for a tasks file editor using a hard-code color. This is not optimal, as it prevent the user from customizing this color.

In this exercise you extend the com.vogella.ide.editor.tasks plug-in to allow the user to customize this color.

5.1. Define colors

Use the Extensions tab on the manifest editor to add an extension for the org.eclipse.ui.themes extension point. Right-click on the generated entry and select themeElementCategory and use:

  • id: com.vogella.ide.editor.tasks.settings

  • label: Tasks settings

tasks editor color 20

Right-click on the generated entry and select colorDefinition.

Use:

  • id: com.vogella.ide.editor.tasks.key

  • label: Task key color

  • value: 255,0,0

  • categoryId: com.vogella.ide.editor.tasks.settings

tasks editor color 30
Show Solution

The plugin.xml content should look similar to the following:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.core.contenttype.contentTypes">
      <content-type
            file-extensions="tasks"
            id="com.vogella.ide.contenttype.tasks"
            name="Tasks"
            priority="high">
      </content-type>
   </extension>
   <extension
         point="org.eclipse.ui.editors">
      <editorContentTypeBinding
            contentTypeId="com.vogella.ide.contenttype.tasks"
            editorId="org.eclipse.ui.genericeditor.GenericEditor">
      </editorContentTypeBinding>
   </extension>
   <extension
         point="org.eclipse.ui.genericeditor.presentationReconcilers">
      <presentationReconciler
            class="com.vogella.ide.editor.tasks.PropertiesReconciler"
            contentType="com.vogella.ide.contenttype.tasks">
      </presentationReconciler>
   </extension>
   <extension
         point="org.eclipse.ui.themes">
      <colorDefinition
            categoryId="com.vogella.ide.editor.tasks.settings"
            id="com.vogella.ide.editor.tasks.key"
            label="Task key color"
            value="255,0,0">
      </colorDefinition>
      <themeElementCategory
            id="com.vogella.ide.editor.tasks.settings"
            label="Tasks settings">
      </themeElementCategory>
   </extension>

</plugin>

5.2. Validate

If you start your runtime Eclipse, you should be able to see your category and color in the Window  Preferences  General  Appearance  Colors and Fonts setting.

tasks editor color

5.3. Use colors for your syntax highlighting

Access the color registry and use it in your editor. The following code snippet should help you.

IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager();
ITheme currentTheme = themeManager.getCurrentTheme();
ColorRegistry colorRegistry = currentTheme.getColorRegistry();
Color color = colorRegistry.get("com.vogella.ide.editor.tasks.key");
Show Solution
package com.vogella.ide.editor.tasks;

import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.presentation.PresentationReconciler;
import org.eclipse.jface.text.rules.DefaultDamagerRepairer;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.RuleBasedScanner;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.themes.ITheme;
import org.eclipse.ui.themes.IThemeManager;

public class PropertiesReconciler extends PresentationReconciler {

    public PropertiesReconciler() {

        IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager();
        ITheme currentTheme = themeManager.getCurrentTheme();
        ColorRegistry colorRegistry = currentTheme.getColorRegistry();
        Color color = colorRegistry.get("com.vogella.ide.editor.tasks.key");

        TextAttribute tagAttribute = new TextAttribute(color);

        RuleBasedScanner scanner = new RuleBasedScanner();
        IRule rule = new PropertyNameRule(new Token(tagAttribute));
        scanner.setRules(new IRule[] { rule });
        DefaultDamagerRepairer dr = new DefaultDamagerRepairer(scanner);
        this.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);
        this.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);
    }
}

5.4. Test your changes

Open Window  Preferences  General  Appearance  Colors and Fonts. Search for your colors and change them.

Close your editor and reopen it. Validate that your new colors are used.

5.5. Use a preference listener to update the color

Preferences are persisted user settings. It is possible to register changes in a node via a preference listener.

In your ` PropertiesReconciler` override the install method can listen to change in the "org.eclipse.ui.workbench" node used to persist the color.

public void install(ITextViewer viewer) {
        super.install(viewer);

        IEclipsePreferences node = InstanceScope.INSTANCE.getNode("org.eclipse.ui.workbench");

        node.addPreferenceChangeListener(event -> {
            // TODO UPDATE YOUR RULES WITH AN UPDATED RULE WITH THE NEW COLOR
            viewer.invalidateTextPresentation();
        });
    }

Solve the TODO and check that the color is updated.

To find the preference node for a preference you can use the preference spy. Open the view, and toogle the trace for preferences. Afterwards change a color and see its data.

preference spy colors10
Show Solution
package com.vogella.ide.editor.tasks;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.presentation.PresentationReconciler;
import org.eclipse.jface.text.rules.DefaultDamagerRepairer;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.RuleBasedScanner;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.swt.graphics.Color;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.themes.ITheme;
import org.eclipse.ui.themes.IThemeManager;

public class PropertiesReconciler extends PresentationReconciler {

    private ColorRegistry colorRegistry;
    private RuleBasedScanner scanner;
    private IRule rule;

    @Override
    public void install(ITextViewer viewer) {
        super.install(viewer);

        IEclipsePreferences node = InstanceScope.INSTANCE.getNode("org.eclipse.ui.workbench");

        node.addPreferenceChangeListener(event -> {
            updateRule();
            viewer.invalidateTextPresentation();
        });
    }

    private void updateRule() {
        Color color = colorRegistry.get("com.vogella.ide.editor.tasks.key");
        TextAttribute tagAttribute = new TextAttribute(color);
        rule = new PropertyNameRule(new Token(tagAttribute));
        scanner.setRules(new IRule[] { rule });

    }

    public PropertiesReconciler() {

        IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager();
        ITheme currentTheme = themeManager.getCurrentTheme();
        colorRegistry = currentTheme.getColorRegistry();

        scanner = new RuleBasedScanner();
        updateRule();

        DefaultDamagerRepairer dr = new DefaultDamagerRepairer(scanner);
        this.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);
        this.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);
    }
}

6. Exercise: Implementing content assist for todo properties

In this exercise you implement content assist (code completion) for your .tasks files. As discussed earlier, these files should contain key/value pairs, separated by a column. Properties will be proposed in case the cursor is located at the beginning of a line.

6.1. Add the content assists extension

Add an extension for the org.eclipse.ui.genericeditor.contentAssistProcessors extension point in the manifest file of your com.vogella.ide.editor.tasks plug-in.

<extension
      point="org.eclipse.ui.genericeditor.contentAssistProcessors">
   <contentAssistProcessor
         class="com.vogella.ide.editor.tasks.TodoPropertiesContentAssistProcessor"
         contentType="com.vogella.ide.contenttype.tasks">
   </contentAssistProcessor>
</extension>

The implementation of the TodoPropertiesContentAssistProcessor should look similar to the following code.

package com.vogella.ide.editor.tasks;

import java.util.Arrays;
import java.util.List;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;

public class TodoPropertiesContentAssistProcessor implements IContentAssistProcessor {

    // public as used later by other code
    public static final List<String> PROPOSALS = Arrays.asList( "ID:", "Summary:", "Description:", "Done:", "Duedate:", "Dependent:");

    @Override
    public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {

        IDocument document = viewer.getDocument();

        try {
            int lineOfOffset = document.getLineOfOffset(offset);
            int lineOffset = document.getLineOffset(lineOfOffset);

            // do not show any content assist in case the offset is not at the
            // beginning of a line
            if (offset != lineOffset) {
                return new ICompletionProposal[0];
            }
        } catch (BadLocationException e) {
            // ignore here and just continue
        }

        return PROPOSALS.stream().filter(proposal -> !viewer.getDocument().get().contains(proposal))
                .map(proposal -> new CompletionProposal(proposal, offset, 0, proposal.length()))
                .toArray(ICompletionProposal[]::new);
    }

    @Override
    public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
        return null;
    }

    @Override
    public char[] getCompletionProposalAutoActivationCharacters() {
        return null;
    }

    @Override
    public char[] getContextInformationAutoActivationCharacters() {
        return null;
    }

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

    @Override
    public IContextInformationValidator getContextInformationValidator() {
        return null;
    }

}

6.2. Test your implementation

Start the IDE and open a .tasks file and then press CTRL+Space in order to activate content assist. This should result in the following:

editor content assist properties

6.3. Optional Exercise - Implement a slow content assists processor

The generic editor uses asynchronous code completion by default, e.g., the user interface does not block even if one of your proposal computers is slow.

Test this by adding a delay to your completion processor. Ensure the editor remains usable, even if you trigger content assists.

Afterwards remove the delay again.

7. Improved code completion

7.1. Optional Exercise - Add prefix completion

The IDocument class offers a lot of utilities to parse and modify its contents. Extend your TodoPropertiesContentAssistProcessor#computeCompletionProposals method, so that you can also match on prefixes.

For example, if you type "D" you should see all proposal matching D.

editor content assist prefix matching
Show Solution
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {

    IDocument document = viewer.getDocument();

    try {
        int lineOfOffset = document.getLineOfOffset(offset);
        int lineOffset = document.getLineOffset(lineOfOffset);

        int lineTextLenght = offset - lineOffset;
        String lineStartToOffsetValue = document.get(lineOffset, lineTextLenght).toLowerCase();

        return PROPOSALS.stream()
                .filter(proposal -> !viewer.getDocument().get().contains(proposal)
                        && proposal.toLowerCase().startsWith(lineStartToOffsetValue))
                .map(proposal -> new CompletionProposal(proposal, lineOffset, lineTextLenght, proposal.length()))
                .toArray(ICompletionProposal[]::new);
    } catch (BadLocationException e) {
        e.printStackTrace();
    }
    return new ICompletionProposal[0];
}

7.2. Optional Exercise - Enable auto-activation of your content assists

Currently your content assists only shows its proposals if the user presses CTRL+Space. This might not be obvious for the user.

IContentAssistProcessor allows to modify this behavior via its getCompletionProposalAutoActivationCharacters method.

Pressing CTRL+Space may not be obvious for your user. Implement that any letter activate the content assists.

Show Solution
@Override
public char[] getCompletionProposalAutoActivationCharacters() {
    String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return str.toCharArray();
}

8. Exercise : Implementing another content assist using other information

It is possible to register multiple content assist processors for the same content type.

In this exercise you add another content assists to your editor. This processor will allow to set values for the _ property. This allows to model that a task is dependent on another task.

8.1. Implement and register a new content assists processor

Create the DependentTodoContentAssistProcessor class implementing also the IContentAssistProcessor interface.

Register this class via the plugin.xml file as extension to the org.eclipse.ui.genericeditor.contentAssistProcessors extension point.

Show Solution
 <extension
       point="org.eclipse.ui.genericeditor.contentAssistProcessors">
    <contentAssistProcessor
          class="com.vogella.ide.editor.tasks.TodoPropertiesContentAssistProcessor"
          contentType="com.vogella.ide.contenttype.tasks">
    </contentAssistProcessor>
 </extension>
 <extension
       point="org.eclipse.ui.genericeditor.contentAssistProcessors">
    <contentAssistProcessor
          class="com.vogella.ide.editor.tasks.DependentTodoContentProcessor"
          contentType="com.vogella.ide.contenttype.tasks">
    </contentAssistProcessor>
 </extension>

For accessing the active editor, create the following Util class.

package com.vogella.ide.editor.tasks;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;

public class Util {

    private Util() {
        // only helper
    }

    public static IEditorPart getActiveEditor() {
        IWorkbench workbench = PlatformUI.getWorkbench();
        IWorkbenchWindow activeWorkbenchWindow = workbench.getActiveWorkbenchWindow();
        if (null == activeWorkbenchWindow) {
            activeWorkbenchWindow = workbench.getWorkbenchWindows()[0];
        }
        IWorkbenchPage activePage = activeWorkbenchWindow.getActivePage();
        if (activePage == null) {
            return null;
        }
        return activePage.getActiveEditor();
    }
}

The implementation of the DependentTodoContentAssistProcessor should look similar to this:

package com.vogella.ide.editor.tasks;
import static com.vogella.ide.editor.tasks.Util.getActiveEditor;

import java.util.Arrays;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;

public class DependentTodoContentAssistProcessor implements IContentAssistProcessor {

    @Override
    public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
        IDocument document = viewer.getDocument();
        IEditorPart activeEditor = getActiveEditor();

        if (activeEditor != null) {
            IEditorInput editorInput = activeEditor.getEditorInput();
            IResource adapter = editorInput.getAdapter(IResource.class);
            IContainer parent = adapter.getParent();
            try {
                int lineOfOffset = document.getLineOfOffset(offset);
                int lineOffset = document.getLineOffset(lineOfOffset);

                String lineProperty = document.get(lineOffset, offset - lineOffset);
                // Content assist should only be used in the dependent line
                if (lineProperty.startWith("Dependent:")) {
                    IResource[] members = parent.members();

                    // Only take resources, which have the "tasks" file extension and skip the current resource itself
                    return Arrays.asList(members).stream().filter(
                            res -> !adapter.getName().equals(res.getName()) && "tasks".equals(res.getFileExtension()))
                            .map(res -> new CompletionProposal(res.getName(), offset, 0, res.getName().length()))
                            .toArray(ICompletionProposal[]::new);
                }
            } catch (CoreException | BadLocationException e) {
                // ignore here and just continue
            }
        }
        return new ICompletionProposal[0];
    }

    @Override
    public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
        return null;
    }

    @Override
    public char[] getCompletionProposalAutoActivationCharacters() {
        return null;
    }

    @Override
    public char[] getContextInformationAutoActivationCharacters() {
        return null;
    }

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

    @Override
    public IContextInformationValidator getContextInformationValidator() {
        return null;
    }

}

8.2. Validate

Start the IDE and ensure that at least 2 .tasks files are available and open one of these files. Then press CTRL+Space right after the Dependent: property to activate content assist. This should result in the following:

editor content assist dependent

9. Exercise: Reacting to document changes

A IDocumentSetupParticipant allows to be notified during setup and changes of a document to trigger functionality. You can register an implementation via the org.eclipse.core.filebuffers.documentSetup extension point for a content type

In our example we use this to add markers to the Problems view for missing key in the .tasks file.

9.1. Implement and register your IDocumentSetupParticipant

The TodoMarkerDocumentSetup registers an IDocumentListener, which applies markers for the IResource, which is currently changed.

package com.vogella.ide.editor.tasks;
import static com.vogella.ide.editor.tasks.Util.getActiveEditor;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.eclipse.core.filebuffers.IDocumentSetupParticipant;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ICoreRunnable;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.PlatformUI;

public class TodoMarkerDocumentSetup implements IDocumentSetupParticipant {

    private static final String TODO_PROPERTY = "todoProperty";

    @Override
    public void setup(IDocument document) {
        document.addDocumentListener(new IDocumentListener() {

            private Job markerJob;

            @Override
            public void documentChanged(DocumentEvent event) {
                IEditorPart activeEditor = getActiveEditor();

                if (activeEditor != null) {
                    IEditorInput editorInput = activeEditor.getEditorInput();
                    IResource adapter = editorInput.getAdapter(IResource.class);
                    if (markerJob != null) {
                        markerJob.cancel();
                    }
                    markerJob = Job.create("Adding Marker", (ICoreRunnable) monitor -> createMarker(event, adapter)); (1)
                    markerJob.setUser(false);
                    markerJob.setPriority(Job.DECORATE);
                    // set a delay before reacting to user action to handle continuous typing
                    markerJob.schedule(500); (2)
                }
            }


            @Override
            public void documentAboutToBeChanged(DocumentEvent event) {
                // not needed
            }
        });
    }

    private void createMarker(DocumentEvent event, IResource adapter) throws CoreException {
        String docText = event.getDocument().get();

        for (String todoProperty : TodoPropertiesContentAssistProcessor.PROPOSALS) {
            List<IMarker> markers = Arrays
                    .asList(adapter.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE));
            Optional<IMarker> findAny = markers.stream()
                    .filter(m -> todoProperty.equals(m.getAttribute(TODO_PROPERTY, ""))).findAny();
            if (docText.contains(todoProperty) && findAny.isPresent()) {
                findAny.get().delete();
            } else if (!docText.contains(todoProperty) && !findAny.isPresent()) {
                IMarker marker = adapter.createMarker(IMarker.PROBLEM);
                marker.setAttribute(IMarker.MESSAGE, todoProperty + " property is not set");
                marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_INFO);
                marker.setAttribute(IMarker.LOCATION, "Missing line");
                marker.setAttribute(TODO_PROPERTY, todoProperty);
            }
        }
    }

}
1 The implementation uses a Job implementation, so that the UI thread is not blocked during this process.
2 Since the job is started on document change, it starts with a delay so that it can be canceled on another document change so that unnecessary marker creations can be omitted.

Add the following extension to your plugin.xml.

<extension
       point="org.eclipse.core.filebuffers.documentSetup">
    <participant
          class="com.vogella.ide.editor.tasks.TodoMarkerDocumentSetup"
          contentTypeId="com.vogella.ide.contenttype.tasks">
    </participant>
 </extension>

9.2. Implement and register your IDocumentSetupParticipant

The result should look similar to this:

editor document setup marker

10. Exercise : Implement hyperlinking for your tasks

In Eclipse support navigation between references to other files. Source editors typically support CTRL + left mouse click on the reference to open files.

The *.tasks files can point via the Dependent: property to another *.task file inside the same project. In this exercise our editor should allow the user to navigate between dependent task files.

editor todo hyperlinking

10.1. Add manifest dependencies

Add org.eclipse.ui.ide as plug-in dependency to com.vogella.ide.editor.tasks.

10.2. Implement hyperlinking for dependent task files

Create the following IHyperlink implementation.

import org.eclipse.core.resources.IFile;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;

public class ResourceHyperlink implements IHyperlink {

    private IRegion region;
    private String hyperlinkText;
    private IFile resource;

    public ResourceHyperlink(IRegion region, String hyperlinkText, IFile resource) {
        this.region = region;
        this.hyperlinkText = hyperlinkText;
        this.resource = resource;
    }

    @Override
    public IRegion getHyperlinkRegion() {
        return region;
    }

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

    @Override
    public String getHyperlinkText() {
        return hyperlinkText;
    }

    @Override
    public void open() {
        IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
        try {
            IDE.openEditor(activePage, resource);
        } catch (PartInitException e) {
            e.printStackTrace();
        }
    }
}

Create the following IHyperlinkDetector implementation.

import java.util.Arrays;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;

public class DependentTodoHyperlinkDetector extends AbstractHyperlinkDetector {

    private static final String DEPENDENT_PROPERTY = "Dependent:";

    @Override
    public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
        IDocument document = textViewer.getDocument();

        IWorkbench workbench = PlatformUI.getWorkbench();
        IWorkbenchWindow activeWorkbenchWindow = workbench.getActiveWorkbenchWindow();
        if (null == activeWorkbenchWindow) {
            activeWorkbenchWindow = workbench.getWorkbenchWindows()[0];
        }
        IWorkbenchPage activePage = activeWorkbenchWindow.getActivePage();

        IEditorPart activeEditor = activePage.getActiveEditor();
        if (activeEditor != null) {
            IEditorInput editorInput = activeEditor.getEditorInput();
            IResource adapter = editorInput.getAdapter(IResource.class);
            IContainer parent = adapter.getParent();
            try {
                int offset = region.getOffset();

                IRegion lineInformationOfOffset = document.getLineInformationOfOffset(offset);
                String lineContent = document.get(lineInformationOfOffset.getOffset(),
                        lineInformationOfOffset.getLength());

                // Content assist should only be used in the dependent line
                if (lineContent.startsWith(DEPENDENT_PROPERTY)) {
                    String dependentResourceName = lineContent.substring(DEPENDENT_PROPERTY.length()).trim();

                    Region targetRegion = new Region(lineInformationOfOffset.getOffset() + DEPENDENT_PROPERTY.length(),
                            lineInformationOfOffset.getLength() - DEPENDENT_PROPERTY.length());

                    IResource[] members = parent.members();

                    // Only take resources, which have the "todo" file extension and skip the
                    // current resource itself
                    return Arrays.asList(members).stream()
                            .filter(res -> res instanceof IFile && dependentResourceName.equals(res.getName()))
                            .map(res -> new ResourceHyperlink(targetRegion, res.getName(), (IFile) res))
                            .toArray(IHyperlink[]::new);
                }
            } catch (CoreException | BadLocationException e) {
                e.printStackTrace();
            }
        }
        // do not return new IHyperlink[0] because the array may only be null or not
        // empty
        return null;
    }

}

The DependentTodoHyperlinkDetector class can be registered in the plugin.xml like this:

 <extension
       point="org.eclipse.ui.workbench.texteditor.hyperlinkDetectors">
    <hyperlinkDetector
          activate="true"
          class="com.vogella.ide.editor.tasks.DependentTodoHyperlinkDetector"
          id="com.vogella.ide.editor.tasks.hyperlinkDetector"
          name="Hyperlink to other tasks files"
          targetId="org.eclipse.ui.genericeditor.GenericEditor">
    </hyperlinkDetector>
 </extension>

10.3. Validate

To validate this create at least two *.tasks files and use the dependent attribute to point to one of them. Validate that you can use CTRL + left mouse click to navigate to the linked file.

Dependent:Training2.tasks
editor todo hyperlinking

In this exercise you create a hyperlink detector for the vogella keyword in the generic editor.

11.1. Create project and add dependencies

Create a new plug-in project called com.vogella.ide.editor.companylink.

Add the following plug-ins as dependencies:

  • org.eclipse.ui

  • org.eclipse.jface

  • org.eclipse.ui.workbench.texteditor

Add the org.eclipse.ui.workbench.texteditor.hyperlinkDetectors extension with a unique id and a descriptive name.

Create the following two classes to determine the hyperlink and to react to it.

package com.vogella.ide.editor.companylink;

import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.swt.program.Program;

public class VogellaHyperlink implements IHyperlink {

    private final IRegion fUrlRegion;

    public VogellaHyperlink(IRegion urlRegion) {
        fUrlRegion = urlRegion;
    }

    @Override
    public IRegion getHyperlinkRegion() {
        return fUrlRegion;
    }

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

    @Override
    public String getHyperlinkText() {
        return "Open vogella website";
    }

    @Override
    public void open() {
        Program.launch("https://www.vogella.com/");
    }
}
package com.vogella.ide.editor.companylink;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;

public class VogellaHyperlinkDetector extends AbstractHyperlinkDetector {

    public VogellaHyperlinkDetector() {
    }

    @Override
    public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {

        IDocument document = textViewer.getDocument();
        int offset = region.getOffset();

        // extract relevant characters
        IRegion lineRegion;
        String candidate;
        try {
            lineRegion = document.getLineInformationOfOffset(offset);
            candidate = document.get(lineRegion.getOffset(), lineRegion.getLength());
        } catch (BadLocationException ex) {
            return null;
        }

        // look for keyword
        int index = candidate.indexOf("vogella");
        if (index != -1) {

            // detect region containing keyword
            IRegion targetRegion = new Region(lineRegion.getOffset() + index, "vogella".length());
            if ((targetRegion.getOffset() <= offset)
                    && ((targetRegion.getOffset() + targetRegion.getLength()) > offset))
                // create link
                return new IHyperlink[] { new VogellaHyperlink(targetRegion) };
        }

        return null;
    }

}

11.3. Validate

Start your plug-in and add vogella to one of the text editor, for example the Java editor. Press Ctrl and click on vogella. This should open the https://www.vogella.com/ website in an external browser.

12. Exercise : Add code mining information to your task files

Eclipse support showing additional information into the text editor to enhance it. This information is not saved in the file but helps the user to better understand the content. You can also register actions on these minings, so that the user can perform actions directory out of the text editor.

In this exercise we will implement code minings for our task editor to show additional information and to allow the user to perform certain actions via the minings.

12.1. Implement and register code mining

Implement the following class which creates line header annotations.

package com.vogella.ide.editor.tasks;

import java.util.concurrent.CompletableFuture;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.codemining.ICodeMiningProvider;
import org.eclipse.jface.text.codemining.LineHeaderCodeMining;

public class TaskCodeMining extends LineHeaderCodeMining {


    public TaskCodeMining(int beforeLineNumber, IDocument document, ICodeMiningProvider provider)
            throws BadLocationException {
        super(beforeLineNumber, document, provider);
    }

    @Override
    protected CompletableFuture<Void> doResolve(ITextViewer viewer, IProgressMonitor monitor) {
        return CompletableFuture.runAsync(() -> {
            super.setLabel("This is additional information about the tasks");
        });
    }
}

Create the following implementation of a ICodeMiningProvider.

package com.vogella.ide.editor.tasks;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.codemining.ICodeMining;
import org.eclipse.jface.text.codemining.ICodeMiningProvider;

public class TaskCodeMiningProvider implements ICodeMiningProvider {

    public TaskCodeMiningProvider() {
    }

    @Override
    public CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(ITextViewer viewer,
            IProgressMonitor monitor) {
        return CompletableFuture.supplyAsync(() -> {
            List<ICodeMining> minings = new ArrayList<>();
            IDocument document = viewer.getDocument();
            try {
                minings.add(new TaskCodeMining(0, document, this));
            } catch (BadLocationException e) {
                e.printStackTrace();
            }
            return minings;
        });
    }

    @Override
    public void dispose() {
    }
}

Register the default CodeMiningReconciler for your content type via the org.eclipse.ui.genericeditor.reconcilers extension.

<extension
       point="org.eclipse.ui.genericeditor.reconcilers">
    <reconciler
          class="org.eclipse.jface.text.codemining.CodeMiningReconciler"
          contentType="com.vogella.ide.contenttype.tasks">
    </reconciler>
 </extension>

Now add the following extension to your plugin.xml.

<extension
       point="org.eclipse.ui.workbench.texteditor.codeMiningProviders">
    <codeMiningProvider
          class="com.vogella.ide.editor.tasks.TaskCodeMiningProvider"
          id="com.vogella.ide.editor.tasks.codeMiningProvider"
          label="Show additional task info">
       <enabledWhen>
          <with
               variable="editorInput">
                <adapt type="org.eclipse.core.resources.IFile">
                   <test property="org.eclipse.core.resources.contentTypeId" value="com.vogella.ide.contenttype.tasks" />
                </adapt>
          </with></enabledWhen>
    </codeMiningProvider>
 </extension>

12.2. Validate

Ensure that your editor shows the additional information in its header.

code mining tasks

13. Optional exercise - Add action to code mining

Override getAction in TaskCodeMining to allow the user to trigger some actions.

package com.vogella.ide.editor.tasks;

import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.codemining.ICodeMiningProvider;
import org.eclipse.jface.text.codemining.LineHeaderCodeMining;
import org.eclipse.swt.events.MouseEvent;

public class TaskCodeMining extends LineHeaderCodeMining {


    private ITextViewer viewer;

    public TaskCodeMining(int beforeLineNumber, IDocument document, ICodeMiningProvider provider, boolean isValid)
            throws BadLocationException {
        super(beforeLineNumber, document, provider);
    }

    @Override
    protected CompletableFuture<Void> doResolve(ITextViewer viewer, IProgressMonitor monitor) {
        this.viewer = viewer;
        return CompletableFuture.runAsync(() -> {
            super.setLabel("This is additional information about the tasks");
        });
    }

    @Override
    public Consumer<MouseEvent> getAction() {
        return r->showMessageDialog();
    }

    private void showMessageDialog() {
        MessageDialog.openInformation(viewer.getTextWidget().getShell(), "Clicked", "You clicked on the code mining annotation");
    }
}

14. Optional exercise - Extend your code mining

Clone the Git repository located at https://github.com/angelozerr/EclipseConFrance2018. Import the included project and test them.

Use the information to extend your code mining, e.g., add code minings to each line in your editor.

15. Optional exercise - Review JDT coding mining

Clone the Git repository located at https://github.com/angelozerr/jdt-codemining

Clone the code and import the projects. Include them into your runtime Eclipse. Review the code.

16. Excurse: Implementing your custom editor

While it is recommended to reuse the generic editor instead defining a new one, this is still supported. This chapter gives an explanation how this can be done.

16.1. IEditorPart and EditorPart

To define a new editor for the Eclipse IDE, you typically: * Create an IEditorInput class * Define an extension for the org.eclipse.ui.editors extension point * Implement a class extending IEditorPart

IEditorInput serves as the model for the editor. Eclipse will buffer IEditorInput objects therefore this object should be relatively small.

It is supposed to be a light-weight representation of the model. For example the Eclipse IDE uses IEditorInput objects to identify files without handling with the complete file.

The equals() of IEditorInput define the identity of the editor, e.g., it will be used to determine if an editor is already open or not.

The editor receives the IEditorSite and the IEditorInput in the init() method. It must set the input via the setInput() method and the side via the setSite() method.

init() is called before createPartControl() (which creates the user interface). Therefore you can use the input during your UI creation.

If you define your own perspective, you can enable the editor area via the following code in your perspective implementation.

import org.eclipse.ui.IPageLayout;

public class Perspective implements IPerspectiveFactory {

    public void createInitialLayout(IPageLayout layout) {
        //layout.setEditorAreaVisible(false);
        layout.setFixed(true);
    }

}

16.2. Setting the editor title and tooltip

By default, the editor will use the tooltip and title from the IEditorInput. Therefore you may want to change the title and tooltip in your Editor. Use setPartName() to set the title of the Editor and getTitleToolTip() for setting the tooltip. See Bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=107772 for details on the tooltip.

16.3. Saving the editor content

The method isDirty() determines if the editor contains modified data. For inform the workbench about changes in the dirty state you fired an event.

firePropertyChange(IEditorPart.PROP.DIRTY);