Intro
Now that Xtext is at 1.0 RC1 I thought it was time to start using more of all the new features for Eclipse b3. One of the features I wanted to add was to support time stamps in a nice way in the editor. Internally, a time stamp is naturally stored as a java.util.Date
so there is never a question about the exact UTC it is representing. When editing however, you may want to use some other format (if not copying an actual timestamp, you may want to use something like 'feb 10, 11:00:00am' .
The issue is that the reference to 'feb 10, 11:00:00am' in the source text has no time zone information, and the name of the month may not be in english etc. In order for the source to be valid everywhere, it would be required to fully specify the date format used, as well as the timezone and store this in the source. I choose a middle ground where the editor understands the more human friendly formats and offers to help to convert it to a format that is always possible to parse.
All of this may not be all that interesting, but it gave me opportunity to try some features of Xtext that I had not used. The rest of this blog is about my first iteration of the implementation, and it shows some Xtext techniques like:
- Using an ecore data type in the grammar
- A Date value converter
- Overriding the SyntaxErrorMessageProvider
- Providing a quick fix for a ValueConverterException
The Grammar
First step is to define the grammar that involves a time stamp
import "http://www.eclipse.org/emf/2002/Ecore" as ecore Entity : "timestamp" '=' TIMESTAMP ; TIMESTAMP returns ecore::EDate : STRING ;This simply declares that a language element 'Entity' has a 'timestamp'. The TIMESTAMP rule declares that it returns an
ecore:EDate
. Luckily we don't have to state more than the import of ecore to make use of it in our language. Also in our favour is that EDate is declared in ecore. If this was for a datatype not in ecore, we would need to create a model containing the definition of the data type. As this was not the case here, we can move on to the data converter.
Date Value Converter
This is almost boiler plate code, but there are some interesting details. Here is the converter method.01 @ValueConverter(rule = "TIMESTAMP") |
The code first tries to convert the string entered by the user using the wanted timestamp format. If this fails, an attempt is made to use the default format. If this works, we know we have source text that (most likely) does not have the correct time zone information in it, and we want to offer a quick fix to convert the format. But how can that be done — the ValueConverterException does not allow us to specify a 'diagnostic code' that allows a quick fix to detect the particular problem. The ValueConverterException
is also final (in the 1.0RC1 release at least), so the only option is to use a marker Exception as the cause (In this case NonUTCTimestampException
).
The final attempt to convert (again using the preferred timestamp format) is there simply to catch the error (it could have been remembered from the first attempt).
As you will see later, the design can be improved further by supplying the actual format that was used to successfully parse the entered timestamp in the marker exception, but I left that for a later iteration.
Note that the error message includes the two valid formats as feedback to the user in case the entered text was unparsable. It would be easy to try several formats.
Overriding the Syntax Error Message Provider
The default SyntaxErrorMessageProvider is a class that hands out SyntaxError
instances describing a problem occuring in a particular context. In my case I just wanted to add handling of the ValueConverterException with my special non-UTC cause Exception.
Here it is
1 public class BeeLangSyntaxErrorMessageProvider extends SyntaxErrorMessageProvider { |
As you can see, this is straight forward, simply return a SyntaxErrorMessage
with a diagnostic code (a static string) that I called IBeeLangDiagnostic.ISSUE_TIMESTAMP__NON_UTC
. At this point, non of the new code (except the data value conversion is in effect, and a bit of magic is needed to make it kick in.
Xtext makes good use of google guice dependency injection. In addition to the standard guice, there is also advanced so called 'polymorphic dispatching'. This means, that even if it is not apparent in the guice module Xtext generates for a DSL that something can be bound to a specialized class, it is still just as easy to bind almost anything by simply adding a method.
Here is the part that was added to the guice module for my DSL
1 public Class<? extends ISyntaxErrorMessageProvider> bindISyntaxErrorMessageProvider() { |
This means that whenever the Xtext runtime wants an implementation of the ISyntaxErrorMessageProvider
, it will now get an instance of the specialized class shown earlier.
The Quick Fix
The final part of the puzzle is to provide the quick fix. There really is not much to say but to show the code:
01 @Fix(IBeeLangDiagnostic.ISSUE_TIMESTAMP__NON_UTC) |
This is pretty much bolier plate code for a quick fix (when generating a DSL with Xtext, there is a sample that shows ow it is done). The code above simply converts the source string using the default format in the value converter, turning it into a timestamp in the correct format. It the replaces the string in the input text.
An improvement would be to pass the date format used in the 'Issue' (it is possible to pass data with a diagnostic code), but I did not look into how to do this with the SyntaxError class yet.
A big thank you to Sebastian Zarnekow at Itemis for pointing me in the right direction