Tag Archives: Hover

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