Tuesday, October 9, 2012

Clean Eclipse Preferences Tree with Multiple DSLs

As it is easy to build new domain specific languages (DSLs) with Xtext for Eclipse, enterprise projects may have multiple languages, which are usually related. Each language comes with its own Eclipse preference page, which are by default mixed with other preference pages. A simple way to keep the Eclipse preferences tree clean is to add a root page which aggregates the preference pages for individual (but possibly related) Xtext languages. This can be done by adding the snippet below to a plugin.xml file, for instance to the one of the base DSL's UI project.

    name="My DSLs">
    <keywordReference id="my.id.root.ui.keyword_root_pref"/>

<!-- add keywords for the search in the preferences page --> 
    label="other keywords"/>

Now, the id of the root page, in this case my.id.root.ui just has to be added to the plugin.xml files of the UI projects of the languages whose preferences should be aggregated. This can also be done in the graphical plugin.xml editor of the DSL's UI projects by navigating to the tab Extensions, selecting the first child node under org.eclipse.ui.preferencePages (which should be the DSL preference page) and pasting my.id.root.ui into the category text box.

Wednesday, July 18, 2012

Custom Syntax Error Messages with Quick Fix

Xtext editors for domain specific languages (DSLs) provide many error messages out of the box, such as syntactical errors, duplicate name errors or unresolvable references. For an improved user experience, some technical error messages from the editor (or, more specifically, from the Antlr parser that is used by the editor) may be customized. In many DSLs, identifiers (for DSL concepts like packages, entities and so on) are expected to conform to the regular expression of the terminal rule ID:

terminal ID : '^'?('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'_'|'0'..'9')*;

In addition, any keyword that is defined in other rules of the DSL grammar may not be used as an identifier. Keywords may be escaped with the caret (^) symbol, which is certainly arguable. This would be similar to using "class" as a name for a class in Java (if that would be possible). Here are some snippets of a DSL with a package concept:

package package // the second word is a reserved keyword and therefore not be valid as identifier
package ^package // okay, the keyword was escaped
package myPackage // okay unless 'myPackage' is a grammar keyword

The default error message when using a reserved keyword where an identifier is expected looks like this.

mismatched input 'package' expecting RULE_ID

This  message can be customized using Xtext's SyntaxErrorMessageProvider (written in Xtend):

class SyntaxErrorMessageProviderCustom extends SyntaxErrorMessageProvider {
    @Inject IGrammarAccess grammarAccess
     * Customized error message for reserved keywords
    override getSyntaxErrorMessage(IParserErrorContext context) {
        val unexpectedText = context?.recognitionException?.token?.text
        if (GrammarUtil::getAllKeywords(grammarAccess.getGrammar()).contains(unexpectedText)) {
            return new SyntaxErrorMessage('''
            "«unexpectedText»" is a reserved keyword which is not allowed as Identifier.
            Please choose another word or alternatively confuse your co-workers by escaping it with the caret (^) character like this: "^«unexpectedText»".''',

The customized error message provider has to be bound in MyDslRuntimeModule.java like this:
     * custom error messages for syntax errors
    public Classextends ISyntaxErrorMessageProvider> bindISyntaxErrorMessageProvider() {
        return SyntaxErrorMessageProviderCustom.class;

A simple quickfix in MyDslQuickfixProvider could look like this:
     * Provide a fix when reserved keywords are used as identifiers
     def public void reservedKeywordUsed(Issue issue, IssueResolutionAcceptor acceptor) {
        val unexpectedText = issue.data?.get(0)
        acceptor.accept(issue, '''Change '«unexpectedText»' to '«unexpectedText.generateUniqueIdentifier».' ''', '''
                Change '«unexpectedText»' to '«unexpectedText.generateUniqueIdentifier»',
                which is not a reserved keyword.''', 
                [ IModificationContext context |
                    val xtextDocument = context.getXtextDocument
                    xtextDocument.replace(issue.offset, issue.length, unexpectedText.generateUniqueIdentifier)
     def String generateUniqueIdentifier(String it) {
        val candidate = 'my' + it?.toFirstUpper?:'Name'
        var count = 1
        val reserved = GrammarUtil::getAllKeywords(grammarAccess.getGrammar())
        if (reserved.contains(candidate)) {
            while (reserved.contains(candidate + count)) {
                count = count + 1
            return candidate + count
        return candidate       

This kind of customization has been available for a long time now. For more information, see Customizing error messages from Sebastian Zarnekow.