Hovertext

Warning: this post contains deprecated informations. Hovering is fully implemented in the new Xtext 2.0. I strongly suggest you to upgrade if you haven’t already done it. I write this only as a memento of my efforts, I’ll scrap everything and switch to the new Xtext as soon as possible. Please, read Christoph’s post if you want to know more.

Imagine that we want to display a comment of an element of our grammar in a rectangle, with the element selected through a mouse hover (in other words, like the JDT), how can we implement that? Digging through the Xtext and Eclipse api, we should find everything that we need.
It looks like in Eclipse each TextEditor holds a SourceViewConfiguration which is the main entry point for all the UI customization of the editor. The SourceViewConfiguration is in charge, amongst other things, to provide an implementation of ITextHover which in turn provide the Object that holds the informations we want to display through a mouse hover. The SourceViewConfiguration provides an IInformationControlCreator as well, that is responsible of the creation of an IInformationControl, that displays the ITextHover‘s informations.
There are many resources that explain how the Eclipse hovering mechanism works, for instance Prashant Deva’s article or the Eclipse Wiki.

As Xtext relies on Eclipse, we should find some traces of these classes somewhere. Searching for SourceViewerConfiguration in the UI module lead to org.eclipse.xtext.ui.editor.XtextEditor which has an injected org.eclipse.xtext.ui.editor.XtextSourceViewerConfiguration. So we can bind our version of SourceViewerConfiguration. In the Xtext forum, there are some hints on how to put all these things together.

MyDslUiModule.java

public Class<? extends org.eclipse.xtext.ui.editor.XtextSourceViewerConfiguration> bindSourceViewerConfiguration()
   {
   return org.xtext.example.mydsl.ui.MyDslSourceViewerConfiguration.class;
   }

Create a new class:

MyDslSourceViewerConfiguration.java

public class MyDslSourceViewerConfiguration extends XtextSourceViewerConfiguration
   {
   public IInformationControlCreator getInformationControlCreator(ISourceViewer sourceViewer)
      {
      return new IInformationControlCreator()
         {
         public IInformationControl createInformationControl(Shell parent)
            {
            //How we want to display it.
            return new MyDslInformationControl(parent, (String)null);
            }
         };
      }
	
   @Override
   public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType)
      {
      //What we want to display.
      return new MyDslTextHover(sourceViewer);
      }
   }

MyDslTextHover is where we manipulate the Xtext model to retrieve what we want to display. Remember that when we work with live models, we have to call the readOnly method and never return EObjects or Resources. Again, peeking at the forum, tell us how to get a specific EObject at a given offset.

MyDslTextHover.java

public class MyDslTextHover implements ITextHover, ITextHoverExtension2
   {
   /**
     * We need this to access to the IDocument, hence the model.
     */
   private ISourceViewer source;
   
    /**
     * As pointed out in the forum, we should put problem messages first.
     */
   private ProblemHover problem;
   
   public MyDslTextHover(ISourceViewer sourceViewer)
      {
      source = sourceViewer;
      problem = new ProblemHover(sourceViewer);
      }

   @Override
   public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion)
      {
      //This method is deprecated and replaced by ITextHoverExtension2.getHoverInfo2.
      return getHoverInfo2(textViewer, hoverRegion);
      }

   /**
     * Simply a copy & paste from AbstractHover.getHoverRegion.
     */
   @Override
   public IRegion getHoverRegion(ITextViewer textViewer, int offset)
      {
      Point selection = textViewer.getSelectedRange();
      //x is the offset of the selection, y is the length of the selection.
      //If the given offset falls in between, return the selected range.
      if (selection.x <= offset && offset < selection.x + selection.y)
         {
         return new Region(selection.x, selection.y);
         }
      //Otherwise returns a zero-length region.
      return new Region(offset, 0);
      }

   @Override
   public String getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion)
      {
      String problemString = (String) problem.getHoverInfo2(textViewer, hoverRegion);
      String ourString = getMyDslHover(textViewer, hoverRegion);
      //If there are problems, return them first.
      if(problemString!=null && !problemString.isEmpty())
         return problemString+"\n"+ourString;
      //Otherwise, return our hover string.
      return ourString;
      }
   
    private String getMyDslHover(ITextViewer textViewer, final IRegion hoverRegion)
      {
      IXtextDocument doc = (IXtextDocument)textViewer.getDocument();
      //Call readOnly to safe access the model.
      return doc.readOnly(
         new IUnitOfWork<String, XtextResource>()
            {
            @Override
            public String exec(XtextResource state) throws Exception
               {
               TextLocation textLoc = new TextLocation(hoverRegion.getOffset(),
                                                       hoverRegion.getLength());
               EObject subject = EObjectAtOffsetHelper.resolveContainedElementAt(state,
                                                                                 hoverRegion.getOffset(),
                                                                                 textLoc);
               Iterable<AbstractNode> contents = NodeUtil.getAllContents(NodeUtil.getNode(subject));
               StringBuffer result = new StringBuffer();
               for (AbstractNode abstractNode : contents)
                  {
                  if (abstractNode instanceof LeafNode)
                     {
                     LeafNode leaf = (LeafNode)abstractNode;
                     //Check which nodes to consider.
                     //Consider using a TokenUtil also.
                     if(leaf.isHidden())
                        {
                        if(leaf.getGrammarElement() instanceof TerminalRule)
                           {
                          //TerminalRule terminal = (TerminalRule)leaf.getGrammarElement();
                          //if(terminal.getName().compareTo("SL_COMMENT")==0)
                          //result.append(leaf.getText().substring(2));
                          //else
                             result.append(leaf.getText());
                          }
                        }
                     }
                  }
               return result.toString();
               }
            });
       }
   }

Here getHoverInfo2 returns a String but it can returns any Object, if we need a more complex data structure.
Fow now, lets create a simple MyDslInformationControl, which extends the default implementation.

MyDslInformationControl.java

public class MyDslInformationControl extends DefaultInformationControl
   {
   public MyDslInformationControl(Shell parent, String statusFieldText)
      {
      super(parent, statusFieldText);
      }
   }

Build & run and you should see something like this:

As you can see the default control support some HTML tags but not all of them (in the screenshot we can see that em is ignored).
To overcome this, we can provide a more complex IInformationControl, which holds a Browser control. Please read Dan Breslau’s post for more.

MyDslInformationControl.java

public class MyDslInformationControl extends AbstractInformationControl
   {
   
    private Browser browser;

   public MyDslInformationControl(Shell parentShell, String statusFieldText)
      {
      super(parentShell, statusFieldText);
      create();
      }

   @Override
   public boolean hasContents()
      {
       return browser.getText().length() > 0;
      }

   @Override
   protected void createContent(Composite parent)
      {
       browser = new Browser(parent, SWT.NONE);
      }
  
   public void setInformation(String string)
      {
      browser.setText(string);
      setVisible(true);
      }
  
   public Point computeSizeHint()
      {
       Point size = getShell().computeSize(300, SWT.DEFAULT, true);
      return size;
      }
  
   public Point computeSizeConstraints(int widthInChars, int heightInChars)
      {
        return super.computeSizeConstraints(widthInChars, heightInChars);
       }
  
   public IInformationControlCreator getInformationPresenterControlCreator()
      {
      return new IInformationControlCreator()
         {
         public IInformationControl createInformationControl(Shell parent)
            {
            return new MyDslInformationControl(parent, (String)null);
            }
         };
      }
   }

Now your text should be displayed in a little browser. This is only the beginning. You may want to use a more complex data structure other than String and show it in a more complex, composite, control. In that case your IInformationControl must implement IInformationControlExtension2 also; then the ITextHoverExtension2.getHoverInfo2 must return the same kind of object that IInformationControlExtension2.setInput will take. Look at org.eclipse.jface.internal.text.html.BrowserInformationControl too.

Want a simpler way? Go and get the new Xtext 2.0! šŸ™‚

Advertisements

About Luong
http://www.linkedin.com/in/manhluong

3 Responses to Hovertext

  1. Caner Kilinc says:

    i guess this sample is not compatible with Xtext 2.0
    Is there any other article/tutorial for xtext 2.0?

    • Luong says:

      Hi Caner!
      Mmm I’m sure that someone out there wrote one! Try out the links in my resource page!
      I will write one as soon as I can, stay tuned! ;o)

      By the way as I wrote in this post, Xtext 2.0 has its own hovering system…

  2. hirdesh96 says:

    I am getting the error during the xtext migration, please let me know what is the reason of the following error..
    I followed all following process for migrate the xtext version.
    1. Delete the old plug-ins and update the latest plug-ins in target platform.
    2. Update the Plug-in Dependencies and Import Statements.
    3. Introduction of the Qualified Name.
    4. Changes in the index and in find references.
    5. Rewritten Node Model.
    6. AutoEditStrategy.
    7. Other Noteworthy API Changes
    To consider the above steps, I started the work with Eclipse-4.2, Which has a xtext-2.3.0 dependency. Successfully I completed the all above steps and removed the compilation error.

    Problem: After that I start the testing and getting below error Messages:
    [XtextLinkingDiagnostic: null:6 Couldn’t resolve reference to Material ‘MPS_RECUR’.,
    XtextLinkingDiagnostic: null:9 Couldn’t resolve reference to Cstic ‘NUM_OF_ALLOC’.,
    XtextLinkingDiagnostic: null:15 Couldn’t resolve reference to Cstic ‘INSTANCE_NUM’.,
    XtextLinkingDiagnostic: null:14 Couldn’t resolve reference to Class ‘ALLOC’.]

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