Tuesday, February 8, 2011

Extract Xtext Project Wizard

Besides a nice parser and a powerful editor, Xtext can also generate a project wizard and a generator plugin. In this little posting I will explain how to extract the project wizard related code from the generated UI plugin -- and explain why you would want to to that in the first place. First of all, what does the project wizard? The project wizard is useful to set up an initial project for your DSL, e.g., for creating initial templates, workflow files and to configure the project and its dependencies. The nice thing about the Xtext generated wizard is, that the template files are simply created using Xpand. It is very easy to provide custom templates and other stuff by simply modifying the Xpand file. You will find that Xpand template in the UI plugin, if you have enabled the wizard fragment in your DSL generator workflow:
// project wizard (optional)
fragment = projectWizard.SimpleProjectWizardFragment {
 generatorProjectName = "${projectName}.generator"
 modelFileExtension = file.extensions
}
This code is uncommented by default. So, Xtext generates a nice project wizard. This project wizard is found in the generated ui project. Let's say, my language is called sample.mydsl, then the wizard is created in sample.mydsl.ui, together with the powerful editor. Now, why would I want to extract this editor from the ui plugin. Actually, because I want to use the wizard during development of my language. Sounds silly... OK, here is the longer explanation: I find the wizard extremely useful especially if I have a project which uses code generation. Although you can use any kind of code generation technologies, you probably will often use Xpand and Xtend, as it is nicely integrated with Xtext. That is, Xtext can also generate a generator plugin with some template files and most notably a workflow file (MWE2). Actually, the generated generator plugin looks almost similar to a plugin project created by the generated project wizard. That is, both, the generator plugin and a project wizard created DSL project, contain a folder src/model with a sample model, and also a sample MWE2 generator file. Also, both projects contain a src-gen folder, and the plugin-dependencies are set accordingly. This is no coincidence: If you use code generation for your DSL, and if you use Xpand to perform the generation, using an MWE2 workflow to trigger the generation is a very easy solution. Alternatively, you will have to write a new action or something, but the MWE2 workflow is much easier to set up. The model provided in the generator plugin is usually only used for testing purposes. That is, you write your Xpand (and Xtend) templates in the generator plugin and you can quickly test them by applying them on the models provided in the generator model folder. Later, when the generator plugin is installed, the user of the generator won't see the Xpand templates in the workspace, however they can be accessed by the MWE2 workflow of the DSL project. During development, you probably run into the same situation as I did: You write the templates, then some things are changed in your DSL and you have do adjust the templates, or new features should be implemented, or you have forgotten some weird constellations, or you haven't written templates for all model elements. Most important: You probably will have several different models and you do not want to get the generated code to be generated in your generator plugin (as it should only define the templates, and should not contain some weird generated Java or whatever files). In my case, other guys on the project write the DSLs and I have to maintain the templates. I rarely use the generated DSL editor myself, but I often have to use the generator (and adjust the templates). Since I needed an extra project for each different case, I found myself copying the generator plugin (without the templates, but with my nicely configured MWE2 workflow) over and over again. I always had to adjust the plugin dependencies and so on. Well, a wizard would be nice in that situation. Now, you see my point? The Xtext generated project wizard is exactly what I needed -- but I need it in an Eclipse instance in which I do not have my DSL editor (and neither the DSL parser) installed, as this is the very instance I develop these things. But the wizard would come quite comfortably. So, the idea is as follows: I extracted the wizard into a separate plugin. The wizard plugin has no dependencies to my DSL, so it can be installed without my DSL plugins installed. However, the generator, i.e. the MWE2 workflow, requires all my DSL stuff. But this is no problem, as the generator plugin is an opened project in my development workspace -- thus the MWE2 workflow (not the plugin code, but the workflow) can access the project. Here is how to extract the project wizard (which is not too complicated, however it may save you some minutes):
assumption
You have an existing Xtext project, I will call it "sample.mydsl" in the following, and, of course, a generator plugin "sample.mydsl.generator" created for your DSL. Inside the generator, you have configure the MWE2 workflow.
generate project wizard
Enable Project Wizard Fragment in your DSL workflow, e.g. in "GenerateMyDSL.mwe2" of your DSL project:
// project wizard (optional) 
fragment = projectWizard.SimpleProjectWizardFragment {
 generatorProjectName = "${projectName}.generator" 
 modelFileExtension = file.extensions
}
Now run the workflow "GenerateMyDSL.mwe2", and disable the project wizard fragment (as we do not want to have two wizards in the end).
create wizard plugin
Create new plugin project (e.g., sample.mydsl.ui.wizard) with and Activator, check "This plugin-in will make contributions to the UI".
Important: Use "sample.ui.wizard" as package for the Activator (without mydsl), in order to retrieve the same package names as in the Xtext generated ui project.
move wizard code to new plugin
Move the following classes from the generated ui plugin into the wizard -- this is the actual "extraction" of the wizard:
from src-gen:
  • sample.ui.wizard.MyDSLNewProjectWizard
  • sample.ui.wizard.MyDSLProjectCreator
from src:
  • sample.ui.wizard.MyDSLProjectInfo
  • sample.ui.wizard.MyDSLNewProject.xpt
and copy the following classes from the generated ui plugin into the wizard plugin, put them into the wizard package:
  • sample.ui.MyDSLUiModule
from src-gen:
  • sample.ui.MyDSLExecutableExtensionFactory
Also copy the plugin from the ui plugin into the wizard plugin and remove everything except the last extension with point "org.eclipse.ui.newWizards", adjust class name of extension factory according to the class in your wizard project (you will have to add a ".wizard" to the fully qualified name).
configure wizard project
In Manifest, set singleton directive to true (if not already set) and add missing plug dependencies, e.g.
  
 org.eclipse.xtext.ui,
 org.eclipse.ui.editors;bundle-version="3.5.0",
 org.eclipse.ui.ide;bundle-version="3.5.0",
 org.eclipse.xtext.ui.shared,
 org.eclipse.ui,
 org.eclipse.xtext.builder,
 org.antlr.runtime,
 org.eclipse.core.runtime,
 org.eclipse.core.resources,
 org.eclipse.xtend,
 org.eclipse.xpand
Depending on your project, you may have to add other dependencies as well, e.g. de.itemis.xtext.typesystem if you use the great Xtext type system by Markus Völter.
adjust the copied and moved java files
  • MyDSLUiModule is to be replaced completely:
     
     public class MyDSLUiModule extends AbstractGenericModule {
     
     private AbstractUIPlugin plugin;
    
    
     public MyDSLUiModule(AbstractUIPlugin plugin) {
      this.plugin = plugin;
     }
     
     public void configureLanguageName(Binder binder) {
      binder.bind(String.class).annotatedWith(Names.named(Constants.LANGUAGE_NAME)).toInstance("sample.MyDSL");
     }
     
     public void configureFileExtensions(Binder binder) {
      binder.bind(String.class).annotatedWith(Names.named(Constants.FILE_EXTENSIONS)).toInstance("MyDSL");
     }
     
     @Override
     public void configure(Binder binder) {
      super.configure(binder);
      binder.bind(AbstractUIPlugin.class).toInstance(plugin);
      binder.bind(IDialogSettings.class).toInstance(plugin.getDialogSettings());
     }
     
     
     // contributed by org.eclipse.xtext.ui.generator.projectWizard.SimpleProjectWizardFragment
     public Class<? extends org.eclipse.xtext.ui.wizard.IProjectCreator> bindIProjectCreator() {
      return MyDSLProjectCreator.class;
     }
    }
    
  • MyDSLProjectCreator can be reused, however you may want to add some dependencies to the getRequiredBundles method, depending on the dependencies of your generated classes:
      
    @Override
    protected List<String> getRequiredBundles() {
     List<String> result = Lists.newArrayList(super.getRequiredBundles());
     result.add(DSL_GENERATOR_PROJECT_NAME);
     result.add("org.eclipse.jface.text");
     result.add("org.eclipse.jdt.core");
     result.add("org.eclipse.equinox.common");
     result.add("org.eclipse.core.runtime");
     return result;
    }    
    
    Actually, this list is the list of required bundles of your generated code. That is, if you generate Java code which requires a plugin "my.super.plugin", you have to add the dependency here.
  • MyDSLNewProjectWizard: you probably have to fix a problem in getProjectInfo, simply change the fully qualified name MyDSLProjectInfo to a simple name, as we have moved the info into the same package.
  • MyDSLExecutableExtensionFactor: fix class name of plugin activator, change getInstance().getInjector("..") to getDefault().getInjector()
  • Activator: Add initialization of Guice injector and add an injector attribute to the wizard's activator:
      
    public class Activator extends AbstractUIPlugin {
        ..
        Injector injector;
    
     public Injector getInjector() {
      return injector;
     }
    
     @Override
     public void start(BundleContext context) throws Exception {
      super.start(context);
      plugin = this;
    
      injector = Guice.createInjector(
      // Wizard:
       Modules.override(new MyDSLUiModule(this))
       // Workspace etc.:
        .with(new org.eclipse.xtext.ui.shared.SharedStateModule()));
    
     }
     ..
    }
    
remove obsolte code from ui plugin
  • remove method bindIProjectCreator in AbstractMyDSLUiModule
  • remove the wizard extension point definition from the ui plugin.xml, as you probably do not want to have two wizards.
You can now run the wizard in the Eclipse runtime (i.e. the Eclipse started from within your initial, vanilla, installation). Of course, you can modify the Xpand template, e.g. I have simply copied and pasted the workflow of my generator plugin into that Xpand file. You may also add other adjustments as well. Now, you can export the wizard as a plugin and install it to the dropins folder of your Eclipse installation. After restarting that instance, the wizard is available in the workspace in which you develop your DSL. As it has no dependencies to the DSL, you can create new projects, which are configure as you specified above. The workflow is working, although it probably has dependencies to your DSL (e.g., it uses the generated DSL parser), at the DSL project is an opened project in your workspace. Side effect: Actually, you have extracted the Guice infrastructure as it is used by Xtext generated editors. So, even if you do not use your wizard as often as expected, you may have learned some stuff about this better-then-factory-pattern technology.