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

A text editor allows you to modify data, typically text, and applies the changes to the underlying model whenever the user saves.

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

  • Extend the generic editor

  • Implement your own editor

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

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

  • add a content type

  • register a PresentationReconciler for 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, you use an ITextViewer as the controller, which uses a StyledText widget for its presentation.

The IDocument interface stores text and provides 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 be handled according to their type, for example, 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.

Syntax highlighting of source code is implemented via a 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 that implements the IPresentationReconciler interface. An IPresentationReconciler can apply IRule implementations for a specified content type. An IRule defines a rule used when scanning text for 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 partitions

  • partitioning is always up-to-date

1.3. API for working with editors

You can open an editor via the currently active page. For this, you need an IEditorInput object and the ID of the editor, which is defined via the org.eclipse.ui.editors extension point.

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

To get the page, you can use:

// in a view
getViewSite().getPage();
// in a command handler
HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
// anywhere else
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();

1.4. Using TextMate Grammars for syntax highlighting

TextMate grammars are JSON- or YAML-based syntax definitions that describe how to tokenize and style text in an editor, commonly used for syntax highlighting. They follow a hierarchical, pattern-matching approach and are widely supported, for example, by Visual Studio Code and, via plug-ins, by the Eclipse IDE.

TM4E provides a Java API to tokenize text documents according to TextMate grammars and an Eclipse IDE integration that uses this tokenization for syntax highlighting. It also supports VS Code language configuration files for bracket matching, auto-closing and on-enter rules.

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 implementation 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 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 the generic editor for a custom file type

Eclipse provides the generic editor as the basis for creating text-based editors.

In this exercise, you associate files with the tasks extension with the generic editor. In the following exercises, you add more and more functionality to this editor. A tasks file contains key-value pairs, for example:

ID: 1
Summary: Eclipse IDE Training

3.1. Create a new plug-in

Create a new simple plug-in project named 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

It should look similar to the following (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;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: VOGELLA
Automatic-Module-Name: com.vogella.ide.editor.tasks
Bundle-RequiredExecutionEnvironment: JavaSE-25
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.3. Define content type

Using the MANIFEST.MF editor, open the Extensions tab and press the Add…​ button. Select the org.eclipse.core.contenttype.contentTypes extension point.

New Extension dialog with the content types extension point

Press Finish to close the dialog and add the extension.

Right-click on your new entry and select New  content-type. Specify a content type for files with the .tasks extension.

Content type element in the manifest editor

This creates an entry in the plugin.xml file, which should look similar to the following listing. You can see it 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.4. 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.

New Extension dialog with the editors extension point

Right-click on your org.eclipse.ui.editors entry and select New  editorContentTypeBinding. Use your content type ID and org.eclipse.ui.genericeditor.GenericEditor as editor ID.

Editor content type binding in the manifest editor

The resulting plugin.xml should now 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>

</plugin>

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

Add your new plug-in to your IDE feature or product.

If you are not using features or products, skip this step.

3.6. Test your development

If you are using a product configuration file, start a new runtime Eclipse via your product.

Starting via the product updates the associated launch configuration. If you start the runtime Eclipse directly via an unmodified launch configuration, your updated product or feature content is not considered.

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

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

Tasks content type in the preferences

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 file in the generic editor

The file uses the icon of the generic editor.

Tasks files with the generic editor icon

3.7. Optional: Define icon for your content type

Download icons with a size of 16x16 px (and 32x32 px for high-resolution displays) from the Internet or use the Plug-in Image Browser view to copy an existing icon into your project.

If you are reading this text online, you can download the following icons via a right-click in your browser.

16x16 icon for tasks files
32x32 icon for tasks files

Create a folder named icons and copy your icons into this folder.

Add an extension for the org.eclipse.ui.genericeditor.icons extension point to your plugin.xml file and configure the generic editor to use your icon for your content type.

Icon element for the content type

The new entry should look similar to the following:

   <extension
         point="org.eclipse.ui.genericeditor.icons">
      <icon
            contentType="com.vogella.ide.contenttype.tasks"
            icon="icons/file_type_taskfile.png">
      </icon>
   </extension>

Ensure that the icons folder is included in the export via the build.properties file.

source.. = src/
output.. = bin/
bin.includes = META-INF/,\
               .,\
               plugin.xml,\
               icons/

Start the runtime Eclipse again and validate that your tasks files show your icon.

Tasks files with the new 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 an 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 is used as the presentation 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_RED));

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

Add an extension for 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 properties, for example:

ID: 1
Summary: Eclipse IDE Training
Description: Learn how to extend the Eclipse IDE
Done: false
Duedate: 2026-12-01
Dependent:

The keys are shown in red.

Tasks file with highlighted keys

4.3. Optional: Show the keys in bold

TextAttribute also accepts a background color and a font style. To make the keys stand out more, change the tagAttribute field of PropertiesReconciler to also use a bold font.

private final TextAttribute tagAttribute = new TextAttribute(
        Display.getCurrent().getSystemColor(SWT.COLOR_RED), null, SWT.BOLD);

Reopen your .tasks file to see the bold keys.

5. Exercise: Implementing syntax highlighting using TextMate

In this exercise, you implement syntax highlighting for shell files using a TextMate grammar.

This exercise uses TM4E, the TextMate support for Eclipse.

5.1. Add TM4E to target file

Open your target definition file and add the following location to it.

<location includeAllPlatforms="false" includeConfigurePhase="true" includeMode="planner" includeSource="true" type="InstallableUnit">
    <repository location="https://download.eclipse.org/tm4e/releases/latest/"/>
    <unit id="org.eclipse.tm4e.feature.feature.group"/>
</location>

5.2. Add TM4E feature to product

To ensure that the TM4E plug-ins are present at runtime, add the org.eclipse.tm4e.feature feature to your product and start the runtime Eclipse via the product.

5.3. Implement plug-in

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

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

  • org.eclipse.tm4e.registry

5.4. Create syntax description

Create a new folder called syntaxes and in it a file named Shell-Unix-Bash.json with the following content.

{
  "scopeName": "source.shell",
  "name": "Shell Script",
  "fileTypes": ["sh", "bash"],
  "patterns": [
    {
      "name": "comment.line.number-sign.shell",
      "match": "#.*$"
    },
    {
      "name": "string.quoted.double.shell",
      "begin": "\"",
      "end": "\"",
      "patterns": [
        {
          "name": "variable.other.shell",
          "match": "\\$[a-zA-Z_][a-zA-Z0-9_]*"
        }
      ]
    },
    {
      "name": "string.quoted.single.shell",
      "begin": "'",
      "end": "'"
    },
    {
      "name": "variable.other.shell",
      "match": "\\$[a-zA-Z_][a-zA-Z0-9_]*"
    },
    {
      "name": "keyword.control.shell",
      "match": "\\b(if|then|else|fi|for|while|do|done|case|esac)\\b"
    },
    {
      "name": "keyword.operator.shell",
      "match": "\\b(and|or|not)\\b"
    },
    {
      "name": "support.function.builtin.shell",
      "match": "\\b(echo|cd|exit|pwd|read)\\b"
    }
  ],
  "repository": {},
  "uuid": "eae8d676-1b23-11e0-ac64-0800200c9a66"
}

5.5. Create plugin.xml file

Your plug-in has no plugin.xml file yet. Select the Overview tab in the manifest editor and click the Extensions link to create it.

5.6. Create content type and associate it with the generic editor

Create the following extensions to define a new content type and associate it with the generic editor.

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

</plugin>

5.7. Register your grammar and content type with TM4E

Now register your grammar with TM4E and bind it to your content type. Add the following extension in addition to the existing plugin.xml entries.

<extension
      point="org.eclipse.tm4e.registry.grammars">
   <grammar
         path="syntaxes/Shell-Unix-Bash.json"
         scopeName="source.shell">
   </grammar>
   <scopeNameContentTypeBinding
         contentTypeId="com.vogella.ide.editor.shell.content-type"
         scopeName="source.shell">
   </scopeNameContentTypeBinding>
</extension>

5.8. Test

Include your new plug-in in your launch configuration, for example, via your feature, and start the runtime Eclipse via your product. Create a new general project and in it an example.shell file with the following content. The editor should show syntax highlighting for this file.

#!/bin/bash

# A simple script to demonstrate folding

#region Setup
echo "Setting up environment..."

# Declare variables
var1="Hello"
var2="World"
#endregion

#region Main logic
if [[ $var1 == "Hello" && $var2 == "World" ]]; then
    echo "Both variables are correctly set."
else
    echo "Variables are not set correctly."
fi

# Loop example
for i in {1..5}; do
    echo "Loop iteration: $i"
done
#endregion

#region Cleanup
echo "Cleaning up..."
#endregion

The editor shows comments, strings, variables and keywords in different colors.

Shell file with TextMate syntax highlighting

5.9. Adding more language configuration

A VS Code language configuration defines, for example, bracket pairs, auto-closing pairs and folding markers for a language. TM4E can bind such a language configuration to a content type.

Create a folder named language-configurations in your plug-in. Create a file named shell.language-configuration.json in this folder with the following content. It defines # as the line comment and the #region and #endregion comments as folding markers.

{
  "comments": {
    "lineComment": "#"
  },
  "brackets": [
    ["{", "}"],
    ["[", "]"],
    ["(", ")"]
  ],
  "autoClosingPairs": [
    ["{", "}"],
    ["[", "]"],
    ["(", ")"],
    ["\"", "\""],
    ["'", "'"]
  ],
  "folding": {
    "markers": {
      "start": "^\\s*#\\s*region\\b",
      "end": "^\\s*#\\s*endregion\\b"
    }
  }
}

Register this language configuration via the following extension in your plugin.xml file.

<extension
      point="org.eclipse.tm4e.languageconfiguration.languageConfigurations">
   <languageConfiguration
         contentTypeId="com.vogella.ide.editor.shell.content-type"
         path="language-configurations/shell.language-configuration.json">
   </languageConfiguration>
</extension>

Add the syntaxes and language-configurations folders to the bin.includes of your build.properties file, otherwise they are missing in the exported plug-in.

bin.includes = META-INF/,\
               .,\
               plugin.xml,\
               syntaxes/,\
               language-configurations/

Restart your runtime IDE and open your file. You should see folding icons for the #region blocks.

Folding icons for the region blocks

5.10. Checking the dark theme

TM4E supports the standard Eclipse light and dark themes. Switch your runtime IDE to the dark theme via the Window  Preferences  General  Appearance preference page and restart it.

Your editor automatically uses colors that fit the selected theme.

Shell file in the dark theme

5.11. Optional: Switch theme at runtime

TM4E provides multiple themes. Right-click in the editor ruler and select another one via the Switch to Theme…​ entry.

Switch to Theme menu in the ruler

5.12. Optional: Add syntax highlighting for an additional language

Now that you know how to add syntax highlighting for a language, add syntax highlighting for *.ini files. You can use the following grammar for this purpose: https://github.com/textmate/ini.tmbundle/blob/master/Syntaxes/Ini.plist The file to which you copy this content must have the .tmLanguage extension.

6. Exercise: Allow the user to customize the colors

In the previous exercise, you implemented syntax highlighting for a tasks file editor using a hard-coded color. This is not optimal, as it prevents 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.

6.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, select New  themeElementCategory and use:

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

  • label: Tasks settings

Theme element category in the manifest editor

Right-click on the org.eclipse.ui.themes entry, select New  colorDefinition and use:

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

  • label: Task key color

  • value: 255,0,0

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

Color definition in the manifest editor

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>

6.2. Validate

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

Task key color in the Colors and Fonts preferences

6.3. Use colors for your syntax highlighting

Read the color from the color registry of the current theme and use it in your PropertiesReconciler.

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.graphics.Color;
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(rule);
        DefaultDamagerRepairer dr = new DefaultDamagerRepairer(scanner);
        this.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);
        this.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);
    }
}

6.4. Test your changes

Open Window  Preferences  General  Appearance  Colors and Fonts. Search for your color and change it.

Close your editor and reopen it. Validate that your new color is used.

6.5. Use a preference listener to update the color

Preferences are persisted user settings. A preference listener is notified about changes in a preference node.

In your PropertiesReconciler, override the install method and listen to changes in the org.eclipse.ui.workbench node, which persists the color. Store the listener in a field and remove it again in the uninstall method, otherwise every closed editor leaves a listener behind.

private IPreferenceChangeListener listener;

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

    listener = event -> {
        // TODO update your rules with a rule using the new color
        viewer.invalidateTextPresentation();
    };
    InstanceScope.INSTANCE.getNode("org.eclipse.ui.workbench").addPreferenceChangeListener(listener);
}

@Override
public void uninstall() {
    if (listener != null) {
        InstanceScope.INSTANCE.getNode("org.eclipse.ui.workbench").removePreferenceChangeListener(listener);
        listener = null;
    }
    super.uninstall();
}

Solve the TODO and check that the color is updated without reopening the editor.

To find the preference node for a preference, you can use the preference spy. Open the view via Window  Spies  Preference Spy. Toggle the trace for preferences. Afterwards, change a color and see its data.

preference spy colors10

The following listing shows a possible solution.

package com.vogella.ide.editor.tasks;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
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;
    private IPreferenceChangeListener listener;

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

        listener = event -> {
            updateRule();
            viewer.invalidateTextPresentation();
        };
        getWorkbenchPreferences().addPreferenceChangeListener(listener);
    }

    @Override
    public void uninstall() {
        if (listener != null) {
            getWorkbenchPreferences().removePreferenceChangeListener(listener);
            listener = null;
        }
        super.uninstall();
    }

    private IEclipsePreferences getWorkbenchPreferences() {
        return InstanceScope.INSTANCE.getNode("org.eclipse.ui.workbench");
    }

    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);
    }
}

7. Exercise: Implementing content assist for task properties

In this exercise, you implement content assist (code completion) for your .tasks files. As discussed earlier, these files contain key-value pairs, separated by a colon. Properties are proposed if the cursor is located at the beginning of a line.

7.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);

            // only propose properties at the beginning of a line
            if (offset != lineOffset) {
                return new ICompletionProposal[0];
            }
        } catch (BadLocationException e) {
            // ignore here and just continue
        }

        String text = document.get();
        return PROPOSALS.stream()
                .filter(proposal -> !text.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;
    }

}

7.2. Test your implementation

Start the IDE, create an empty testing.tasks file and press Ctrl+Space to activate content assist. The proposals list all keys that the file does not contain yet.

Content assist in an empty tasks file

7.3. Optional exercise: Implement a slow content assist processor

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

Test this by adding a delay to the beginning of your computeCompletionProposals method.

try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Ensure that the editor remains usable while content assist is computing its proposals.

Afterwards, remove the delay again.

8. Improved code completion

8.1. Optional exercise: Add prefix completion

The IDocument interface offers a lot of utilities to parse and modify its contents. Extend your TodoPropertiesContentAssistProcessor#computeCompletionProposals method so that it matches on prefixes.

For example, if you type D, you should see all proposals starting with D.

Content assist after typing D
Solution example
@Override
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
    IDocument document = viewer.getDocument();

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

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

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

8.2. Optional exercise: Enable auto-activation of your content assist

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

IContentAssistProcessor allows you to modify this behavior via its getCompletionProposalAutoActivationCharacters method. Implement it so that any letter activates the content assist.

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

9. 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 assist processor to your editor. This processor allows you to set values for the Dependent: property, which models that a task depends on another task.

9.1. Implement and register a new content assist processor

Create the DependentTodoContentAssistProcessor class implementing the IContentAssistProcessor interface.

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

Solution example
 <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.DependentTodoContentAssistProcessor"
          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);
            if (adapter == null) {
                // file outside the workspace
                return new ICompletionProposal[0];
            }
            IContainer parent = adapter.getParent();
            try {
                int lineOfOffset = document.getLineOfOffset(offset);
                int lineOffset = document.getLineOffset(lineOfOffset);

                String lineProperty = document.get(lineOffset, offset - lineOffset);
                // content assist is only offered in the dependent line
                if (lineProperty.startsWith("Dependent:")) {
                    IResource[] members = parent.members();
                    
                    // only take resources with the "tasks" file extension and skip the current resource
                    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) {
                // no proposals in this case
            }
        }
        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;
    }

}

9.2. Validate

Start the IDE, ensure that at least two .tasks files are available and open one of these files. Then press Ctrl+Space right after the Dependent: property to activate content assist.

10. Exercise: Reacting to document changes

An IDocumentSetupParticipant is called when a document is set up, which allows you to react to changes of this document. You can register an implementation via the org.eclipse.core.filebuffers.documentSetup extension point for a content type.

In this exercise, you use it to add markers to the Problems view for missing keys in a .tasks file.

10.1. Implement and register your IDocumentSetupParticipant

The TodoMarkerDocumentSetup registers an IDocumentListener, which applies markers to the file of the changed document.

package com.vogella.ide.editor.tasks;

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

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.IDocumentSetupParticipant;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.resources.IFile;
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;

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) {
                IFile file = getFile(event.getDocument()); (1)

                if (file != null) {
                    String text = event.getDocument().get(); // snapshot, the job runs later
                    if (markerJob != null) {
                        markerJob.cancel();
                    }
                    markerJob = Job.create("Adding Marker", (ICoreRunnable) monitor -> createMarker(text, file)); (2)
                    markerJob.setUser(false);
                    markerJob.setPriority(Job.DECORATE);
                    // set a delay before reacting to user action to handle continuous typing
                    markerJob.schedule(500); (3)
                }
            }


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

    // the document belongs to a file buffer, the active editor may show another file
    private IFile getFile(IDocument document) {
        ITextFileBuffer buffer = ITextFileBufferManager.DEFAULT.getTextFileBuffer(document);
        if (buffer == null || buffer.getLocation() == null) {
            return null;
        }
        return FileBuffers.getWorkspaceFileAtLocation(buffer.getLocation());
    }

    private void createMarker(String docText, IResource adapter) throws CoreException {
        List<IMarker> markers = Arrays.asList(adapter.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE));
        for (String todoProperty : TodoPropertiesContentAssistProcessor.PROPOSALS) {
            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);
                marker.setAttribute(IMarker.LINE_NUMBER, 1);
            }
        }
    }

}
1 The file is determined via the file buffer of the document, as the active editor might show a different file.
2 The markers are created in a Job, so that the UI thread is not blocked.
3 The job starts with a delay, so that another document change can cancel it and unnecessary marker updates are avoided.

Add the following extension to your plugin.xml file.

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

10.2. Validate

Start the IDE, create a testing.tasks file and enter only the ID and Summary keys. After a short delay, the Problems view shows an info marker for each missing key.

Info markers for missing keys

11. Exercise: Implement hyperlinking for your tasks

Eclipse editors support navigation between references to other files. Source editors typically open the referenced file if you hold Ctrl and click on the reference.

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

11.1. Add manifest dependencies

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

11.2. Implement hyperlinking for dependent task files

Create the following IHyperlink implementation in the com.vogella.ide.editor.tasks.hyperlink package.

package com.vogella.ide.editor.tasks.hyperlink;

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 final IRegion region;
    private final String hyperlinkText;
    private final 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.

package com.vogella.ide.editor.tasks.hyperlink;

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

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;

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();

        IEditorPart activeEditor = getActiveEditor();
        if (activeEditor != null) {
            IEditorInput editorInput = activeEditor.getEditorInput();
            IResource adapter = editorInput.getAdapter(IResource.class);
            if (adapter == null) {
                // file outside the workspace
                return null;
            }
            IContainer parent = adapter.getParent();
            try {
                int offset = region.getOffset();

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

                // hyperlinks are only offered 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();

                    // link to the sibling file named in the dependent line
                    IHyperlink[] hyperlinks = Arrays.stream(members)
                            .filter(res -> res instanceof IFile && dependentResourceName.equals(res.getName()))
                            .map(res -> new ResourceHyperlink(targetRegion, res.getName(), (IFile) res))
                            .toArray(IHyperlink[]::new);
                    if (hyperlinks.length > 0) {
                        return hyperlinks;
                    }
                }
            } catch (CoreException | BadLocationException e) {
                e.printStackTrace();
            }
        }
        // the contract allows only null or a non-empty array
        return null;
    }
}

Register the DependentTodoHyperlinkDetector class in the plugin.xml file like this:

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

11.3. Validate

Create at least two *.tasks files and use the Dependent: property to point from one to the other.

Dependent: Training.tasks

Hold Ctrl and move the mouse over the file name. The file name is shown as a link. Click it to open the linked file.

Dependent file shown as a link

In this exercise, you create a hyperlink detector for the vogella keyword, which works in all text editors.

12.1. Create project and add dependencies

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

Add the following entries as plug-in dependencies:

  • org.eclipse.ui

  • org.eclipse.jface

  • org.eclipse.jface.text

  • org.eclipse.ui.workbench.texteditor

Add the org.eclipse.ui.workbench.texteditor.hyperlinkDetectors extension with a unique ID, a descriptive name and org.eclipse.ui.DefaultTextEditor as the target ID. This target covers all editors based on the default text editor configuration, including the Java editor.

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 {

    @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) {
                return new IHyperlink[] { new VogellaHyperlink(targetRegion) };
            }
        }

        return null;
    }
}

12.3. Validate

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

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

Eclipse text editors can show additional information, called code minings, inline in the text. This information is not saved in the file but helps the user to better understand the content. You can register actions on these minings, so that the user can trigger actions directly from the text editor.

In this exercise, you implement code minings for your task editor to show additional information and to allow the user to perform an action via the minings.

13.1. Implement and register code mining

Implement the following class, which creates a line header annotation.

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(() -> setLabel("This is additional information about the tasks"));
    }
}

Create the following implementation of an 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 {

    @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 file.

<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>

13.2. Validate

Ensure that your editor shows the additional information above the first line.

Code mining above the first line of a tasks file

14. Optional exercise: Add an action to the code mining

Override getAction in TaskCodeMining to allow the user to trigger an action by clicking on the mining.

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)
            throws BadLocationException {
        super(beforeLineNumber, document, provider);
    }

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

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

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

15. Optional exercise: Extend your code mining

Show live information in the header instead of a static text. For example, show how many of the task properties are already set. For this, add a private final IDocument document; field to TaskCodeMining, assign the document passed to the constructor to it, and replace the setLabel call in doResolve with the following code. TodoPropertiesContentAssistProcessor.PROPOSALS is the list of property names from your content assist processor, so make it accessible from the code mining class.

String text = document.get();
long filled = TodoPropertiesContentAssistProcessor.PROPOSALS.stream()
        .filter(text::contains)
        .count();
setLabel(filled + " of " + TodoPropertiesContentAssistProcessor.PROPOSALS.size() + " task properties set");

As a further extension, add a code mining to each line in your editor, which shows the number of characters of the property value.

16. Optional exercise: Review the JDT code minings

The Java editor of the Eclipse IDE provides code minings, for example, for the number of references and implementations of a type or method. Activate them via Window  Preferences  Java  Editor  Code Minings and open a Java file.

Review their implementation in the org.eclipse.jdt.ui plug-in in the eclipse.jdt.ui repository.

17. 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.

17.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);
    }

}

17.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.

17.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);
Home Tutorials Training Consulting Books Company Contact us


Get more...