Eclipse, Xtext, Software Development

Xtext Usability: Hovers on keywords

Xtext has become a framework not only for domain specific languages spoken, authored and read by programmers, but also by real (business-) domain experts themselves. Sometimes these people have reservations such as “a (textual) editor can never provide as much guidance as a form can”. The weirdos among them even want intuitive and fail-safe ready-to-go solutions instead of one week training bootcamps. In this post I will introduce Xtext features that meet such requirements.

The first one is about hovers on keywords. Christian here has posted a nice tutorial about how to costumize Xtext’s standard hovers, e.g.hello-hover.png

Out of the box, Xtext supports hovers only for identifying features of model artifacts, i.e. the name of an object or crosslinks to other objects. The hover in the example above pops up when the cursor is over “Hover” (the name of a Greeting), but not when it is over “Hello” (which is a keyword belonging to a greeting).

I’m going to show how to adjust Xtext’s Domain-Model Example to show hovers on keywords, such as
keywordhover.jpg

We adjust the HoverProvider and the EObjectHover

package org.eclipse.xtext.example.domainmodel.ui.hover;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.internal.text.html.HTMLPrinter;
import org.eclipse.jface.text.IRegion;
import org.eclipse.xtext.Keyword;
import org.eclipse.xtext.ui.editor.hover.html.XtextBrowserInformationControlInput;
import org.eclipse.xtext.xbase.ui.hover.XbaseHoverProvider;
import com.google.inject.Inject;
 
public class MyXbaseHoverProvider extends XbaseHoverProvider {
    /** Utility mapping keywords and hovertext. */
    @Inject MyKeywordHovers keywordHovers;
 
    @Override
    protected XtextBrowserInformationControlInput getHoverInfo(EObject obj, IRegion region, XtextBrowserInformationControlInput prev) {
        if (obj instanceof Keyword) {
            String html = getHoverInfoAsHtml(obj);
            if (html != null) {
                StringBuffer buffer = new StringBuffer(html);
                HTMLPrinter.insertPageProlog(buffer, 0, getStyleSheet());
                HTMLPrinter.addPageEpilog(buffer);
                return new XtextBrowserInformationControlInput(prev, obj, buffer.toString(), labelProvider);
            }
        }
        return super.getHoverInfo(obj, region, prev);
    }
 
    @Override
    protected String getHoverInfoAsHtml(EObject o){
        if (o instanceof Keyword)
            return keywordHovers.hoverText((Keyword) o);
        return super.getHoverInfoAsHtml(o);
    }
}
 
package org.eclipse.xtext.example.domainmodel.ui.hover;
 
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.xtext.Keyword;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.hover.IEObjectHoverProvider;
import org.eclipse.xtext.ui.editor.hover.IEObjectHoverProvider.IInformationControlCreatorProvider;
import org.eclipse.xtext.util.Pair;
import org.eclipse.xtext.xbase.ui.hover.XbaseDispatchingEObjectTextHover;
 
import com.google.inject.Inject;
 
public class MyXbaseDispatchingEObjectTextHover extends XbaseDispatchingEObjectTextHover {
 
    @Inject
    MyKeywordAtOffsetHelper keywordAtOffsetHelper;
 
    @Inject
    IEObjectHoverProvider hoverProvider;
 
    IInformationControlCreatorProvider lastCreatorProvider = null;
 
    @Override
    public Object getHoverInfo(EObject first, ITextViewer textViewer, IRegion hoverRegion) {
        if (first instanceof Keyword) {
            lastCreatorProvider = hoverProvider.getHoverInfo(first, textViewer, hoverRegion);
            return lastCreatorProvider == null ? null : lastCreatorProvider.getInfo();
        }
        lastCreatorProvider = null;
        return super.getHoverInfo(first, textViewer, hoverRegion);
    }
 
    @Override
    public IInformationControlCreator getHoverControlCreator() {
        return this.lastCreatorProvider == null ? super.getHoverControlCreator() : lastCreatorProvider.getHoverControlCreator();
    }
 
    @Override
    protected Pair<EObject, IRegion> getXtextElementAt(XtextResource resource, final int offset) {
        Pair<EObject, IRegion> result = super.getXtextElementAt(resource, offset);
        if (result == null) {
            result = keywordAtOffsetHelper.resolveKeywordAt(resource, offset);
        }
        return result;
    }
}
 
We also need to calculate the offsets of keywords and to specifiy the actual hover texts.
 
package org.eclipse.xtext.example.domainmodel.ui.hover;
 
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.xtext.Keyword;
import org.eclipse.xtext.nodemodel.ILeafNode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.parser.IParseResult;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.util.Pair;
import org.eclipse.xtext.util.Tuples;
/** Inspired by {@link org.eclipse.xtext.resource.EObjectAtOffsetHelper} */
public class MyKeywordAtOffsetHelper {
  public Pair resolveKeywordAt(XtextResource resource, int offset) {
    IParseResult parseResult = resource.getParseResult();
    if (parseResult != null) {
      ILeafNode leaf = NodeModelUtils.findLeafNodeAtOffset(parseResult.getRootNode(), offset);
      if (leaf != null && leaf.isHidden() && leaf.getOffset() == offset) {
        leaf = NodeModelUtils.findLeafNodeAtOffset(parseResult.getRootNode(), offset - 1);
      }
      if (leaf != null && leaf.getGrammarElement() instanceof Keyword) {
        Keyword keyword = (Keyword) leaf.getGrammarElement();
        return Tuples.create((EObject) keyword, (IRegion)new Region(leaf.getOffset(), leaf.getLength()));
      }
    }
    return null;
  }
}
 
package org.eclipse.xtext.example.domainmodel.ui.hover
 
import org.eclipse.xtext.example.domainmodel.services.DomainmodelGrammarAccess
import com.google.inject.Inject
import org.eclipse.xtext.Keyword
// This is Xtend , not Java
class MyKeywordHovers {
    @Inject DomainmodelGrammarAccess ga;
    def hoverText(Keyword k) {
        val result = switch (k) {
            case ga.entityAccess.entityKeyword_0: '''
                An entity represents real business objects. It <ul>
                <li>can <code>extend</code> another entity,i.e. inherit the features of another entity.</li>
                <li>has attributes, specification syntax <code><name> : <type></code></li>
                <li>has operations, specification syntax <code>op <name> (<List of Parameters>)) : <Returntype></code></li>
                </ul>
                '''
        }
        result.toString;
    }
}
 
Let's not forget to bind our implementations for Xtext’s dependency injection.
 
public class DomainmodelUiModule extends AbstractDomainmodelUiModule {
 
    // ...
         
    @Override
    public Class<? extends org.eclipse.xtext.ui.editor.hover.IEObjectHover> bindIEObjectHover() {
        return MyXbaseDispatchingEObjectTextHover.class;
    }
 
    @Override
    public Class<? extends org.eclipse.xtext.ui.editor.hover.IEObjectHoverProvider> bindIEObjectHoverProvider() {
        return MyXbaseHoverProvider.class;
    }
}
    
About Boris Holzer

I've been working as (technical) project manager in several business domains. I like the power of language engineering and every other application that makes development easier and improves quality. Since 2014, I'm in charge of YAKINDU Traceability.