Category Archives: Xtext

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

CFEngine workbench based on Eclipse and Xtext

Here’s a screencast showing a CFEngine-workbench that I built earlier this year.

CFEngine is a tool for system administrators that “has a powerful language and tools to define the desired state of your infrastructure”. The workbench is driven by the idea (some may think it’s crazy) of transfering the usability that software developers enjoy in IDEs into the usage scenarios for SAs .

Visualize Xtext models with PlantUML

In this post I show how (ab)use PlantUML to visualize Xtext models. Warning: This is about diagrams, not about UML.

PlantUML Intro

PlantUML is an “Open Source tool .. to draw UML using a simple and intuitive language”.
Let me just draw an example class diagram to demonstrate how it feels:
This PlantUML specification

@startuml
abstract class Superclass << general >>;
abstract AbstractCreator{
  {abstract} create() : Superclass
}
Superclass <|-- Subclass
note left of Superclass : Instantiation not possible 
ConcreteCreator-up-|> AbstractCreator 
ConcreteCreator : create() : Superclass
ConcreteCreator .> Subclass
@enduml

results in this diagram:
plantumlex1

PlantUML also provides an Eclipse plugin which dynamically visualizes the “current active diagram, i.e. the diagram where the text cursor is located”. Better than that, this plugin provides an extension which lets you contribute a diagramTextProvider for a given editor. This means you can specify custom visualizations (limited by the PlantUML’s “UML”-capabilities) for any editor input. And the best of it: It works dynamically, i.e. while typing.

Prereq: Adjust PlantUML plugin

The above mentioned diagramTextProvider – extension requires the specification of a providerClass, i.e. a class which creates the plantuml diagram spec for a given editor input. This class has to implement net.sourceforge.plantuml.eclipse.utils.DiagramTextProvider. For some reason, the plugin does not export this package, so you may want to add something like

Export-Package: net.sourceforge.plantuml.eclipse.utils

to the plugin’s manifest.

Update: The PlantUML guys adjusted the manifest already, so you can skip this step with the latest PlantUML-Version (see comment below)

Create Xtext Project

Create a new Xtext Project (accept all defaults).  Adjust the grammar to make it look like this

// dont adjust grammar header
Model: greetings+=Greeting*;
Greeting: from=ID 'says' message=STRING 'to' to=ID;

and run the GenerateMyDsl.mwe2-worklow.

Create diagramTextProvider

Navigate to the ui project of your language. Add a dependency to the PlantUML-plugin to the MANIFEST.MF:

Require-Bundle: ...,net.sourceforge.plantuml.eclipse;

In the plugin.xml, add the following extension point:

   <extension point="net.sourceforge.plantuml.eclipse.diagramTextProvider">
      <diagramTextProvider 
      fileExtensions="mydsl" 
           providerClass="org.xtext.example.mydsl.ui.plantuml.MyDiagramTextProvider">
      </diagramTextProvider>
   </extension>   

Implement MyDiagramTextProvider as follows:

class MyDiagramTextProvider extends AbstractDiagramTextProvider {
	
	def DomainmodelDiagramTextProvider() {
		fileExtensions = "mydsl";
		editorType = typeof(XtextEditor)
	}
	
	override String getDiagramText(IEditorPart editorPart, IEditorInput editorInput) {
		// Retrieve the "semantic" EMF from XtextEditor
		val document = (editorPart as XtextEditor).getDocumentProvider().getDocument(editorInput) as XtextDocument;
		val Model model = document.readOnly[
			return contents.head as Model
		]
		
		// Collect names specified in textual model into Set
		val allNames = new HashSet 
		model.greetings.forEach[if(from!=null) allNames.add(from); if(to!=null) allNames.add(to)]
		
		// draw some actors and labelled arrows
		'''
			actor «FOR n:allNames»:«n»: as «n»
			«ENDFOR»
			«FOR it:model.greetings.filter[from!=null && to!=null && message!=null]»
				«from»->«to»:«message»
			«ENDFOR»
		'''
	}
}

Some notes on this implementation:

Try it

Run the Xtext/PlantUML plugins. It looks like (note: the editor is dirty … it really works dynamically):
xtextGraphviz