Xtext Usability: Hovers on keywords


Xtext is making it’s way becoming a framework not only for domain specific languages spoken, authored and read by programmers, but also by real (business-) domain experts themselves. Sometimes these guys 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. This post is the start of a series introducing Xtext features meeting such requirements.

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

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

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 &amp;&amp; leaf.isHidden() &amp;&amp; leaf.getOffset() == offset) {
        leaf = NodeModelUtils.findLeafNodeAtOffset(parseResult.getRootNode(), offset - 1);
      }
      if (leaf != null &amp;&amp; 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 <a href="https://www.eclipse.org/xtend/">Xtend</a> , 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> has attributes, specification syntax <code>&lt;name&gt; : &lt;type&gt;</code>  
				<li> has operations, specification syntax <code>op &lt;name&gt; (&lt;List of Parameters&gt;)) : &lt;Returntype&gt;</code>
				</ul>
			'''
		}
		result.toString;
	}
}


We don’t 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;
	}
}

Advertisements

11 responses

  1. Hi,

    This is exactly what I needed for my project too.
    However, it seems like I am unable to
    extend XbaseHoverProvider
    and
    import org.xtext.example.mydsl.ui.hover.XbaseHoverProvider;

    Is there anything I need to download or miss out??

    Thank you.

    1. Hey Poh,

      let me come back to you tomorrow.

      BR
      Boris

  2. Hey Boris,

    Is it a must to use XBase here??

    Thank you.

    1. No, no need to use XBase.

      Try the following:

      You need to find out which HoverProvider your language is using and the extend / override that class. Since Xtext uses Google’s Guice to inject the HoverProvider, you must find the Guice-Binding for IEObjectHoverProvider.
      – In your Xtext-UI project, find the “UiModule”: This is the class name UiModule in the ..ui project
      – Within that class – probably in the superclass-hierarchy of that class- find the method bindIEObjectHover().
      — If there is such a method anywhere in the superclass hierarchy: Look at the implementation and you’ll see the relevant class to override-
      — If there is no such method, you are using the default-Implementation which is org.eclipse.xtext.ui.editor.hover.html.DefaultEObjectHoverProvider. Note: This is the common case if you are using an Xtext language without XBase and if you haven’t adjusted the HoverProvider.

      Anyway: If you override that class, you need to tell Xtext to use your class instead of the default. The last souce code snippet with the DomainmodelUiModule shows how this can be done.

      Does that help?
      BR,
      Boris

      1. Yes Boris!!!
        It works so well now!!!
        Thank you very much for your explanation!!

        However in order to get it runs, I made two additional changes

        I need to add

        @Inject ILabelProvider labelProvider;
        in the public class MyXbaseHoverProvider extends DefaultEObjectHoverProvider

        I think it is because of the injection of labelProvider in DefaultEObjectHoverProvider is private.

        I need to remove the @Override for this in the UiModule…

        //@Override
        public Class bindIEObjectHoverProvider() {
        return MyXbaseHoverProvider.class;
        }

        I do not know why for this…Can you help explain??
        Thank you very much!!

  3. If I need to make another keyword, then I will only need to add more cases in the switch case there right??

    Others files do not need any changes is it??

    Thank you!

    Best regards,
    Poh

    1. Yep. The idea of the whole thing is that you only need to add more cases 🙂

      Regarding your other question: You mustn’t use “@Override” if you are not overriding a superclass method. In this case, you are not overriding a superclass method, because Xtext is using the default-binding (you may want to look at the sourcecode of org.eclipse.xtext.ui.editor.hover.IEObjectHoverProvider):

      @ImplementedBy(DefaultEObjectHoverProvider.class)
      public interface IEObjectHoverProvider {

      You are welcome !
      Boris

  4. Hi Boris,

    I don’t understand in the public class MyXbaseDispatchingEObjectTextHover

    protected Pair getXtextElementAt(XtextResource resource, final int offset) {
    Pair result = super.getXtextElementAt(resource, offset);
    if (result == null) {
    result = keywordAtOffsetHelper.resolveKeywordAt(resource, offset);
    }
    return result;
    }

    why you need to check null??
    Can you explain me under what condition it will return null, and what will not return null??
    Because I cannot catch the trend when it will return null and when will not.

    Thank you.

    1. **Additional**
      I noticed that the result of null or not is from the AbstractEObjectHover

      else {
      EObject o = eObjectAtOffsetHelper.resolveElementAt(resource, offset);
      if (o != null) {
      ITextRegion region = locationInFileProvider.getSignificantTextRegion(o);
      final IRegion region2 = new Region(region.getOffset(), region.getLength());
      if (TextUtilities.overlaps(region2, new Region(offset, 0)))
      return Tuples.create(o, region2);
      }
      }
      return null;

      It is comparing the region and region2…what is that for?
      How does it calculate it??
      @@

  5. Hey Poh,

    regarding “why you need to check null??” :
    for your the super-impl returns null if it is a keyword !

    regarding: It is comparing …what is that for?
    I can only guess here: Looking at the code, I get the impression that this is a crosscheck: “is the region from locationInFileProvier really the region containing the EObject?” You may want to look e.g. at org.eclipse.xtext.util.ITextRegionWithLineInformation.EMPTY_REGION: If locationInFileProvider return such an Implementation of IRegion, you would not wamt to use it.

    1. Hi Boris!
      It is working good now!!
      Many thanks for your explanation!!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: