Dr. Stefan Winkler
freier Softwareentwickler und IT-Berater

Domain specific languages are a great tool when we want to give our users control over complex aspects in our applications; and in most cases, experienced uses can learn syntax and semantics of a well-designed DSL quickly. But on the other hand, there are also inexperienced users, who usually struggle with DSLs and do not want to deal with textual input. Instead, they are used to graphical user interfaces which help them to grasp the structure of information and to enter new data.

Last week, I had a project, in which I needed to provide both groups of users with a suitable user interface. In other words, I needed to create an editor, which provides both an Xtext source editor for my DSL, and a GUI editor for the model behind the DSL.

I started to search the Internet for some hints on how to do this but I did not find a complete example. Basically, to combine multiple editors into a single one, we have to subclass and implement an org.eclipse.ui.part.MultiPageEditorPart. But I did not find more than hints about how to share the model between an Xtext editor and another editor. So, I started experimenting and after some time I was successful, and I wanted to let you participate in my results. Maybe this helps someone who is looking for an example as well.

For the rest of this post, I will use the state machine example that comes with the Xtext distribution as a starting point. To demonstrate the solution I will add a GUI editor for events to the generated Xtext editor. For brevity, I will only show the relevant parts. The full modified example source code is available on GitHub.

Step 1: Setup

To get started, we create the State Machine Example (File > New… > Xtext > Xtext State-Machine Example) in our workspace. The example code is a bit old; to be able to use Java 8 features, we need to switch the Execution Environment, Java Compiler Settings and Java Library Path to Java 1.8.

Step 2: Create the MultiPageEditor

Inside the UI bundle, I have created a new package org.eclipse.xtext.example.fowlerdsl.ui.editor for my editor code. In there, I have created the class StatemachineMultiPageEditor as follows:

public class StatemachineMultiPageEditor extends MultiPageEditorPart
{
  // get the default XtextEditor injected to use as a page
  @Inject
  private XtextEditor sourceEditor;

  private int sourcePageIndex;

  // create and name our editor page
  // The instance is already injected. We only have to add it to the MultiPageEditorPart
  @Override
  protected void createPages()
  {
    try
    {
      this.sourcePageIndex = addPage(this.sourceEditor, getEditorInput());
      setPageText(this.sourcePageIndex, "Source");
    }
    catch (PartInitException e)
    {
      throw new IllegalStateException("Failed to add Xtext editor", e);
    }
  }

  // there are a few abstract methods, we need to implement. 
  // Because the XtextEditor is the master regarding model management, we just delegate every call there 
  @Override
  public void doSave(final IProgressMonitor monitor)
  {
    this.sourceEditor.doSave(monitor);
  }

  // ... other methods likewise
}

This implementation more or less just wraps the Xtext editor. To enable it in the runtime application, we can edit the plugin.xml and replace the XtextEditor which is configured by Xtext with our own implementation:

<extension
        point="org.eclipse.ui.editors">
    <editor
        class="org.eclipse.xtext.example.fowlerdsl.ui.StatemachineExecutableExtensionFactory:org.eclipse.xtext.example.fowlerdsl.ui.editor.StatemachineMultiPageEditor"
        contributorClass="org.eclipse.ui.editors.text.TextEditorActionContributor"
        default="true"
        extensions="statemachine"
        id="org.eclipse.xtext.example.fowlerdsl.Statemachine"
        name="Statemachine Editor">
    </editor>
</extension>

When we start the runtime application, we should be able to create a state machine file (see README in the example project for instructions) and when opening it, we should see that it opens in our own StatemachineMultiPageEditor, which has a single tab called “Source”.

Step 3: Create the UI editor

Now comes the interesting part. To implement our own editor, we need a suitable editor input which provides our editor with access to the XtextEditor model, which is the XtextDocument. So, I decided to wrap the IXtextDocument in an IEditorInput for our own editor:

public class XtextDocumentBasedEditorInput implements IEditorInput
{
  private IXtextDocument document;
  private IFile file;

  public XtextDocumentBasedEditorInput(final IXtextDocument document)
  {
    this.document = document;
    this.file = document.getAdapter(IFile.class);
  }

  public IXtextDocument getDocument()
  {
    return this.document;
  }

  @Override
  public boolean exists()
  {
    return this.file.exists();
  }

  @Override
  public ImageDescriptor getImageDescriptor()
  {
    return this.file.getAdapter(IWorkbenchAdapter.class).getImageDescriptor(this.file);
  }
  
  // the other methods delegate similarly 
  // ...
}

Based on this, we can implement the GUI editor. For this simple example, we will just have a ListViewer showing the list of Events and three buttons (add, edit, delete).

public class EventEditor extends EditorPart
{
  private XtextDocumentBasedEditorInput xtextEditorInput;
  private ListViewer listViewer;
  
  // <irrelevant methods omitted>

  @Override
  public void createPartControl(final Composite parent)
  {
    // <some GUI code omitted>
    
    // create the list viewer
    this.listViewer = new ListViewer(mainComposite, SWT.BORDER);
    this.listViewer.getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
    this.listViewer.setContentProvider(new ArrayContentProvider());
    this.listViewer.setLabelProvider(new LabelProvider()
    {
      @Override
      public String getText(final Object element)
      {
        if (element instanceof Event)
        {
          Event event = (Event) element;
          return event.getName() + " [" + event.getCode() + "]";
        }
        return super.getText(element);
      }
    });

    // create the buttons
    Composite buttonComposite = new Composite(mainComposite, SWT.NONE);
    buttonComposite.setLayout(new FillLayout(SWT.VERTICAL));
    buttonComposite.setLayoutData(GridDataFactory.swtDefaults().create());
    Button addButton = new Button(buttonComposite, SWT.PUSH);
    addButton.setText("Add");
    addButton.addListener(SWT.Selection, e -> addEvent());

    // <edit and remove button likewise>

    // Synchonize the list viewer input with the Xtext document
    refreshInput();
  }

  /**
   * Recalculates the input to the list viewer, so it is in sync with the source in the Xtext editor.
   * 
   * This is called - when the page becomes visible - after performing an edit operation
   * 
   */
  public void refreshInput()
  {
    IXtextDocument doc = this.xtextEditorInput.getDocument();
    Collection<Event> events = doc.readOnly(res -> EcoreUtil.copyAll(((Statemachine) res.getContents().get(0)).getEvents()));
    this.listViewer.setInput(events.toArray());
  }

  /**
   * Add a new event.
   * 
   * This initializes a new Event object, opens the dialog to let the user specify the event information and if the user closes the dialog by clicking ok, the
   * new event is added to the Xtext document.
   */
  protected void addEvent()
  {
    Event event = StatemachineFactory.eINSTANCE.createEvent();
    event.setName("newEvent");
    event.setCode("CODE");

    boolean result = EditEventDialog.editEvent(getSite().getShell(), event);
    if (result)
    {
      IXtextDocument doc = this.xtextEditorInput.getDocument();
      doc.modify(res -> ((Statemachine) res.getContents().get(0)).getEvents().add(event));
    }

    refreshInput();
  }
  
  // <event handlers for edit and remove button omitted>
}

The interesting methods here are refreshInput() and addEvent(). They use the XtextDocument to access the model in its (dirty) state and/or to manipulate it. It is important to note that the custom code should not hold on to the EMF instances retrieved this way, because when the text is reparsed, Xtext usually throws away old EObject instances and create new ones. Likewise, when modifying the model, make sure that you use the XtextResource and own copies of the information to navigate to the desired model element(s), instead of using previously retrieved EObjects etc.

Step 4: Add the UI editor as a page to the MultiPageEditor

Now we need to add our EventEditor to the StatemachineMultiPageEditor. We let Guice inject our editor just like the XtextEditor. Then we create the editor input and add a second page with our new EventEditor.

Finally, we need to take care of the synchronization between the editors. We could have our editor install a listener on the XtextDocument, so that we get notified on any change. But I decided that it should be enough (and is simpler) to refresh the GUI whenever the user switches the page and the EventEditor becomes visible. To do this, we override the method pageChange().

The resulting changes to the StatemachineMultiPageEditor look like this:

public class StatemachineMultiPageEditor extends MultiPageEditorPart
{
  // ...
  @Inject
  private EventEditor formEditor;
  private int formPageIndex;

  @Override
  protected void createPages()
  {
    // ...
    try
    {
      this.formPageIndex = addPage(this.formEditor, new XtextDocumentBasedEditorInput(this.sourceEditor.getDocument()));
      setPageText(this.formPageIndex, "States");
    }
    catch (PartInitException e)
    {
      throw new IllegalStateException("Failed to add State editor", e);
    }
  }

  @Override
  protected void pageChange(final int newPageIndex)
  {
    if (newPageIndex == this.formPageIndex)
    {
      // when the GUI editor becomes visible, synchronize with the Xtext document
      this.formEditor.refreshInput();
    }
    super.pageChange(newPageIndex);
  }
}

And here is a small demo of the running editor:

 Editor in Action

Odds and Ends

Of course, this example is only a proof of concept and not fully implemented in all detail. For example, validation in the GUI is completely missing. So far, I have not found a good way to include Xtext validation in the GUI (it would be nice to have a name or code checked against the DSL syntax instead of writing an own validator). Also, some additional cases must be handled (for example, what happens if the list of events becomes empty or if there are events with the same name etc.)

Additionally, to have a good interoperability between DSL source and GUI, you have to customize the formatter so that generated model elements are serialized nicely (not like in my example). Also keep in mind that hidden tokens (such as comments) are not represented in the model itself and could be deleted by accident in model modification operations.

{jcomments on}