Introduction à Xtext : personnaliser l'environnement de développement de son DSL

Alain BERNARD

Cet article présente les concepts liés à la personnalisation de l'environnement de développement personnalisé pour votre DSL. Il fait suite au tutoriel qui porte sur la création d'une grammaire pour votre DSL.
Après avoir généré toute l'infrastructure nécessaire au déploiement de votre IDE, Xtext offre la possibilité de le personnaliser grâce par exemple à une Outline, l'autocomplétion, la coloration syntaxique, etc. Ces concepts peuvent être mis en place de manière manuelle (voir ici) mais nécessitent l'apprentissage des API et un grand nombre de manipulations. Xtext permet de s'affranchir de tout ceci et de fournir ces mécanismes de manière plus simple et rapide, ce que nous détaillerons dans cet article. Nous nous baserons ici sur l'exemple créé dans le premier article dont les sources sont disponibles ici : FTP ou HTTP.

Les sources de cet exemple sont disponibles à l'adresse suivante : FTP ou HTTP.

Pour toute remarque ou question sur ce tutoriel, profitez de cette discussion : Commentez

- Titre : Introduction à Xtext : personnaliser l'environnement de développement de son DSL

- Auteur : Alain BERNARD

- Parution : 25 février 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 Alain BERNARD. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. La gestion des imports

- III. Personnalisation de la vue Outline

- III-A. Modification du LabelProvider

- III-B. Modification du AirOutlineTreeProvider

- IV. Personnalisation de la description des éléments

- V. Personnalisation de la coloration syntaxique

- VI. Création de templates

- VII. Autocomplétion

- VIII. Conclusion

- IX. Liens utiles

- X. Remerciements

I - Introduction

Le but de cet article est de présenter les différentes manières de personnaliser l'IDE généré par Xtext pour votre DSL. En effet, la plupart du temps, l'environnement de base créé par Xtext devra être modifié pour être parfaitement adapté à vos besoins. Nous nous efforcerons donc ici de présenter les principaux mécanismes qui vous permettront d'atteindre cet objectif :

- gestion des imports déclarés dans la grammaire ;

- personnalisation de la vue 'Outline' ;

- personnalisation de la vue 'Search' ;

- templates ;

- personnalisation de la coloration syntaxique ;

- gestion de l'autocomplétion.

Notez que toutes les modifications que nous effectuerons se feront dans le plugin « ui » de notre exemple : « com.abernard.xtext.air.ui ».
Pour suivre cet article, il est recommandé d'avoir des connaissances en développement de plugins sous Eclipse. Il est bien évidemment nécessaire d'avoir créé une grammaire Xtext pour son DSL. Cet article est basé sur Eclipse 4.2 (Juno).

II - La gestion des imports

Par défaut, la gestion des fichiers d'inclusion suffit à satisfaire la plupart des cas. Cependant, il peut être utile pour votre DSL de modifier cette gestion afin de satisfaire certaines exigences particulières. Pour cela, il faut indiquer à Xtext de quelle manière gérer les inclusions grâce à une classe qui implémente « IGlobalScopeProvider ». La méthode la plus simple est d'étendre la classe « DefaultGlobalScopeProvider » qui propose déjà l'implémentation « par défaut » et de modifier l'implémentation de la méthode « protected List<IContainer> getVisibleContainers(Resource resource) ». Cette méthode retourne la liste des éléments qui sont « visibles » depuis la ressource donnée en paramètre. On peut par exemple créer la classe « AirImportUriGlobalScopeProvider » qui étend la classe « DefautlGlobalScopeProvider » :

package com.abernard.xtext.air.ui;

import java.util.List;

import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.resource.IContainer;
import org.eclipse.xtext.scoping.impl.DefaultGlobalScopeProvider;

/**
* Cette classe definit quels fichiers peuvent etre inclus dans un fichier de notre grammaireClaude
Leloup 2013-02-20T11:37:43.38je suppose que l'absence d'accents dans les commentaires est
voulue.Alain BERNARD2013-02-21T20:00:12.40Répondre à Claude Leloup  (20/02/2013, 11:37): "..."
En effet, ceci pour garder un certain côté multiplateformes appréciable, surtout en Java et éviter
les erreurs d'encodage.
* @author A. BERNARD
*/
public class AirImportUriGlobalScopeProvider extends DefaultGlobalScopeProvider {

@Override
protected List<IContainer> getVisibleContainers(Resource resource) {
// Ici, l'implementation personnalisee
return super.getVisibleContainers(resource);
}

}

Pour que notre gestion des includes personnalisée soit prise en compte par Xtext, il faut la déclarer dans le fichier AirUiModule.java. Ce fichier permet de définir des implémentations personnalisées pour un certain nombre d'interfaces et d'indiquer au framework d'utiliser ces implémentations plutôt que celles par défaut.

AirUiModule.java

1. /**

2. * Use this class to register components to be used within the IDE.

3. */

4. public class AirUiModule extends com.abernard.xtext.air.ui.AbstractAirUiModule {

5. public AirUiModule(AbstractUIPlugin plugin) {

6. super(plugin);

7. }

9. /**

10. * Permet d'indiquer a Xtext quelle gestion des imports utiliser.Claude Leloup

11. 2013-02-20T11:39:34.40« à » ?

12. pour les accents, il faudrait rester cohérent.

13. * @return

14. */

15. public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() {

16. return AirImportUriGlobalScopeProvider.class;

17. }

19. }

III - Personnalisation de la vue Outline

Par défaut, l'IDE généré par Xtext crée les contributions nécessaires à la vue Outline d'Eclipse. Pour remplir cette vue, Xtext se base sur les attributs 'name' des éléments de la grammaire. Par ailleurs, tous les niveaux de la grammaire sont représentés. Par exemple, si c'était le cas sur la vue Outline Java d'Eclipse, en plus des noms des méthodes, la vue Outline afficherait tout le contenu de la méthode, ce qui rendrait vite l'interface illisible. En plus, si l'attribut 'name' ne suffit pas à décrire vos éléments ou ne correspond pas à ce que vous voulez afficher, il serait judicieux de pouvoir modifier le texte affiché, ainsi que l'icône associée. Nous allons voir dans cette section que Xtext nous permet de modifier facilement cette vue au travers de deux éléments : un LabelProvider et un TreeProvider. Pour l'instant, la vue Outline générée a cette allure :

Outline par défaut

III-A - Modification du LabelProvider

La classe à modifier se trouve dans le package « com.abernard.xtext.air.ui.labeling » : AirLabelProvider.java. Comme indiqué dans le template par défaut généré par Xtext, la définition des labels et des images se fait par les méthodes text(Object) et image(Object). Xtext récupère les valeurs pour chaque objet de votre modèle grâce au polymorphisme avec ces deux méthodes. En effet, pour chaque objet du modèle, on va définir l'implémentation idoine pour ces méthodes. Par exemple, une implémentation possible dans notre cas est la suivante :

AirLabelProvider.java

1. /*

2. * generated by Xtext

3. */

4. package com.abernard.xtext.air.ui.labeling;

6. import org.eclipse.emf.edit.ui.provider.AdapterFactoryLabelProvider;

7. import org.eclipse.xtext.ui.label.DefaultEObjectLabelProvider;

9. import com.abernard.xtext.air.air.Airline;

10. import com.abernard.xtext.air.air.Airport;

11. import com.abernard.xtext.air.air.Includes;

12. import com.abernard.xtext.air.air.Plane;

13. import com.google.inject.Inject;

15. /**

16. * Provides labels for a EObjects.

17. *

18. * see http://www.eclipse.org/Xtext/documentation/latest/xtext.html#labelProvider

19. */

20. public class AirLabelProvider extends DefaultEObjectLabelProvider {

22. @Inject

23. public AirLabelProvider(AdapterFactoryLabelProvider delegate) {

24. super(delegate);

25. }

27. String text(Airline airline) {

28. return airline.getName();

29. }

31. String text(Airport airport) {

32. return airport.getTitle();

33. }

35. String text(Plane plane) {

36. return plane.getName();

37. }

39. String image(Plane plane) {

40. return "full/obj16/plane.jpg";

41. }

43. String image(Airport airport) {

44. return "full/obj16/airport.jpg";

45. }

47. String image(Airline airline) {

48. return "full/obj16/airline.png";

49. }

51. String image(Includes include) {

52. return "full/obj16/include.gif";

53. }

55. }

Pour décrire de manière exhaustive les éléments du modèle, une solution est de se référer aux interfaces présentes dans le package « src-gen/com.abernard.xtext.air.air » du plugin « com.abernard.xtext.air ». D'autre part, les méthodes images(Object) doivent retourner le chemin vers l'icône souhaitée relative au dossier « icons » du plugin. Par convention, ce dossier « icons » doit se trouver à la racine du plugin. Après modifications, le résultat est le suivant :

Vue Outline

Les éléments de notre grammaire ont maintenant une icône personnalisée. Cependant certains éléments sont affichés alors que nous ne le souhaitons pas : le code OACI des aéroports. Par contre nous aimerions avoir plus de détails sur les liaisons aériennes.

III-B - Modification du AirOutlineTreeProvider

Cette classe nous permet de modifier les éléments qui sont affichés dans la vue Outline. Trois méthodes principales sont à redéfinir pour personnaliser la vue Outline :

- _isLeaf : permet d'indiquer si les enfants d'un élément du modèle doivent être affichés ou non ;

- _createChildren : permet de définir les enfants à afficher d'un élément du modèle ;

- createNode : permet de créer des éléments fils pour un élément du modèle.

De plus, il est possible de personnaliser le texte affiché dans la vue Outline et de le mettre en forme, comme ce qui est fait dans la vue Outline d'Eclipse, où les valeurs de retour sont affichées dans une certaine couleur afin de les différencier du nom de la méthode :

Couleurs de la vue Outline d'Eclipse

Pour notre vue Outline, nous pouvions par exemple rajouter le pays derrière le nom de l'aéroport, de la même manière que les types dans la vue Outline d'Eclipse. Cela se fait, comme dans le LabelProvider, par l'appel de la méthode « _text(EObject object) ». Notre classe AirOutlineTreeProvider est donc la suivante :

AirOutlineTreeProvider.java

1. /*

2. * generated by Xtext

3. */

4. package com.abernard.xtext.air.ui.outline;

6. import org.eclipse.emf.ecore.EObject;

7. import org.eclipse.jface.viewers.StyledString;

8. import org.eclipse.xtext.ui.editor.outline.IOutlineNode;

9. import org.eclipse.xtext.ui.editor.outline.impl.DefaultOutlineTreeProvider;

11. import com.abernard.xtext.air.air.Airline;

12. import com.abernard.xtext.air.air.Airport;

13. import com.abernard.xtext.air.air.Model;

15. /**

16. * customization of the default outline structure

17. *

18. */

19. public class AirOutlineTreeProvider extends DefaultOutlineTreeProvider {

21. @Override

22. protected boolean _isLeaf(EObject modelElement) {

23. if(modelElement instanceof Airline || modelElement instanceof Model) {

24. return false;

25. } else {

26. return true;

27. }

28. }

30. @Override

31. protected void _createChildren(IOutlineNode parentNode, EObject modelElement) {

32. if (modelElement instanceof Airline) {

33. createNode(parentNode, ((Airline)modelElement).getArrival().eContainer());

34. createNode(parentNode, ((Airline)modelElement).getDeparture().eContainer());

35. createNode(parentNode, ((Airline)modelElement).getPlane());

36. } else {

37. super._createChildren(parentNode, modelElement);

38. }

39. }

41. protected Object _text(Airport airport) {

42. StyledString res = new StyledString();

43. res.append(airport.getTitle());

44. res.append(" (" + airport.getCountry() + ")",

45. StyledString.DECORATIONS_STYLER);

46. return res;

47. }

49. }

Notez que si vous avez besoin de réutiliser ce que vous avez écrit dans votre LabelProvider, vous pouvez utiliser l'injection de dépendance pour utiliser simplement votre LabelProvider, en l'ajoutant en attribut de la classe AirOutlineTreeProvider :
@Inject private AirLabelProvider labelProvider;

Au final, notre vue Outline sur notre fichier d'exemple ressemble à ceci :

Vue Outline de notre grammaire

IV - Personnalisation de la description des éléments

Lorsque l'utilisateur cherche les références d'un élément de la grammaire, les résultats de la recherche sont affichés dans la vue « Search » d'Eclipse. Par exemple, si l'on recherche les références (clic droit > « Find References ») du code OACI LFBO, Xtext affiche l'unique référence à cet aéroport dans la ligne « Toulouse-Marseille » :

Affichage par défaut des références

Cet affichage peut être personnalisé grâce à la classe « AirDescriptionLabelProvider.java » du package « labeling », en affichant par exemple une image à côté de la description :

Nouvelle description

Pour obtenir ce résultat, le nouveau contenu de la classe « AirDescriptionLabelProvider.java » est le suivant :

AirDescriptionLabelProvider.java

1. package com.abernard.xtext.air.ui.labeling;

3. import org.eclipse.xtext.resource.IEObjectDescription;

4. import org.eclipse.xtext.ui.label.DefaultDescriptionLabelProvider;

6. /**

7. * Provides labels for a IEObjectDescriptions and IResourceDescriptions.

8. *

9. * see http://www.eclipse.org/Xtext/documentation/latest/xtext.html#labelProvider

10. */

11. public class AirDescriptionLabelProvider extends DefaultDescriptionLabelProvider {

14. /**

15. * Ajoute une image a la description des elements suivant le type d'element

16. * decrit

17. */

18. public String image(IEObjectDescription ele) {

19. if ("Airline".equals(ele.getEClass().getName())) {

20. return "full/obj16/airline.png";

21. }

22. if ("Airport".equals(ele.getEClass().getName())) {

23. return "full/obj16/airport.jpg";

24. }

25. if ("Plane".equals(ele.getEClass().getName())) {

26. return "full/obj16/plane.jpg";

27. }

28. return (String) super.image(ele);

29. }

31. }

V - Personnalisation de la coloration syntaxique

Par défaut, Xtext propose une coloration syntaxique par défaut pour notre langage. Le framework crée aussi une page de préférences pour permettre à l'utilisateur de modifier ses préférences. Il peut néanmoins être intéressant de modifier ces valeurs si l'on veut personnaliser l'éditeur de notre DSL. Cela se fait en créant une classe qui implémente l'interface « IHighlightingConfiguration ». Comme pour la gestion des includes, cette classe doit être déclarée dans la classe AirUiModule via la commande :

public Class<? extends IHighlightingConfiguration> bindIHighlightingConfiguration() {
return AirHighlightingConfiguration.class;
}

Dans notre classe, nous pouvons associer, pour chaque type d'élément du langage un style particulier, par exemple nous pouvons afficher les mots-clés en italique rouge :

AirHighlightingConfiguration.java

1. package com.abernard.xtext.air.ui;

3. import org.eclipse.swt.SWT;

4. import org.eclipse.swt.graphics.RGB;

5. import org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightingConfiguration;

6. import org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightingConfigurationAcceptor;

7. import org.eclipse.xtext.ui.editor.utils.TextStyle;

9. public class AirHighlightingConfiguration implements IHighlightingConfiguration {

11. /**

12. * the ID of keywords highlighting configuration

13. */

14. public static final String KEYWORD_ID = "keyword";

16. @Override

17. public void configure(IHighlightingConfigurationAcceptor acceptor) {

18. acceptor.acceptDefaultHighlighting(KEYWORD_ID, "Keyword",

19. keywordTextStyle());

20. }

22. /**

23. * Create the TextStyle for keywords

24. * @return the style for keywords

25. */

26. public TextStyle keywordTextStyle() {

27. TextStyle textStyle = new TextStyle();

28. textStyle.setColor(new RGB(255, 0, 0));

29. textStyle.setStyle(SWT.ITALIC);

30. return textStyle;

31. }

32. }

Dans la méthode « acceptDefaultHighlighting », le premier argument définit l'ID au sein de Xtext de l'élément du langage dont le style est modifié. Le deuxième argument définit le texte à afficher dans la page de préférences pour la modification des styles par l'utilisateur. Le troisième argument donne le style à utiliser proprement dit. L'utilisation de ce style donne le résultat suivant dans l'éditeur :

Coloration syntaxique dans l'éditeur

La page de préférences devient :

Préférences de coloration syntaxique

Comme on peut le constater, il sera nécessaire de redéfinir la coloration syntaxique pour tous les éléments souhaités pour obtenir un résultat satisfaisant. On peut s'inspirer pour cela de la classe « org.eclipse.xtext.ui.editor.syntaxcoloring.DefaultHighlightingConfiguration ».

VI - Création de templates

Xtext permet aussi de générer ses propres templates pour le DSL. Ces templates sont des morceaux de code ou de texte que l'utilisateur peut insérer automatiquement dans le texte, à la manière de ceux proposés dans Eclipse pour les boucles par exemple :

Templates dans Eclipse

La façon la plus simple de créer des templates dans Xtext est de passer directement par la page de préférences associée lorsque l'IDE du DSL est lancé et de créer les templates voulus :

Définition d'un template

Une fois que ce template est défini, on peut insérer le code d'un avion directement dans le fichier dès lors qu'on commence à taper le mot-clé « Avion » :

Insertion d'un template

Chaque template est associé à un « contexte ». Ces contextes définissent quand et à quel endroit l'utilisateur peut insérer le template correspondant. Par ailleurs, au sein du template, on peut définir un certain nombre de variables déjà définies dans Eclipse. Dans notre exemple, nous avons utilisé la variable « ${cursor} ». Elle permet de définir à quel endroit se trouvera le curseur après insertion du template.
Pour que ces templates soient utilisables dès le démarrage de l'IDE personnalisé, il suffit de les exporter au format XML grâce au bouton idoine dans la page de préférences « Export ». Il faut ensuite placer le fichier dans un dossier « templates » à la racine du plugin UI :

Cependant, pour être reconnu au lancement, chaque template doit posséder un attribut de type « id ». Il faut donc modifier le fichier XML de manière à définir un ID pour chacun des templates.

VII - Autocomplétion

Par défaut, Xtext propose une autocomplétion sur les mots-clés du langage, qui peut s'avérer suffisante dans la plupart des cas :

Autocomplétion par défaut

On peut ajouter des éléments à la liste de l'assistant de contenu en utilisant la classe « AirProposalProvider.java ». Dans cette classe, on peut redéfinir les méthodes correspondant à chaque élément du modèle afin d'ajouter des éléments à proposer à l'utilisateur. Par exemple, nous pouvons proposer un bloc « Aeroport » complet à l'utilisateur au lieu de proposer simplement le mot-clé. Notre classe « AirProposalProvider » devient par exemple :

AirProposalProvider.java

1. package com.abernard.xtext.air.ui.contentassist;

3. import java.net.URL;

5. import org.eclipse.core.runtime.FileLocator;

6. import org.eclipse.core.runtime.Path;

7. import org.eclipse.emf.ecore.EObject;

8. import org.eclipse.jface.resource.ImageDescriptor;

9. import org.eclipse.jface.viewers.StyledString;

10. import org.eclipse.swt.graphics.Image;

11. import org.eclipse.xtext.RuleCall;

12. import org.eclipse.xtext.ui.editor.contentassist.ContentAssistContext;

13. import org.eclipse.xtext.ui.editor.contentassist.ICompletionProposalAcceptor;

14. import org.osgi.framework.Bundle;

15. import org.osgi.framework.FrameworkUtil;

17. /**

18. * see http://www.eclipse.org/Xtext/documentation/latest/xtext.html#contentAssist on how to

19. customize content assistant

20. */

21. public class AirProposalProvider extends AbstractAirProposalProvider {

24. @Override

25. public void complete_Airport(EObject model, RuleCall ruleCall,

26. ContentAssistContext context, ICompletionProposalAcceptor acceptor) {

27. String proposal = "Aeroport \"\":\nOACI: ;\nPays: \"\";\nPistes: 0;\nEnd.";

28. StyledString displayString = new StyledString();

29. displayString.append("Aeroport ");

30. displayString.append("(Bloc)", StyledString.DECORATIONS_STYLER);

31. Bundle bundle = FrameworkUtil.getBundle(this.getClass());

32. URL url = FileLocator.find(bundle,

33. new Path("icons/full/obj16/airport.jpg"), null);

34. Image image = ImageDescriptor.createFromURL(url).createImage();

35. acceptor.accept(createCompletionProposal(proposal, displayString, image, context));

36. }

37. }

La chaîne de caractères « proposal » définit ce qui devra être inséré si l'utilisateur valide le contenu proposé. La chaîne « displayString » permet de définir le texte qui sera affiché dans le menu d'autocomplétion, en plus de l'image. Notez que la méthode « createCompletionProposal » peut être appelée aussi sans autre argument que la chaîne « proposal » et le contexte « context », rendant la définition d'une image et d'un texte optionnelle. Le texte affiché est alors le contenu qui sera inséré (« proposal »). On peut alors observer le résultat dans notre IDE :

Autocomplétion améliorée

VIII - Conclusion

Au terme de cet article, nous avons pu aborder les principaux concepts de la personnalisation de l'IDE généré par le framework Xtext. Cette personnalisation, associée à la richesse des concepts de grammaire détaillés dans le premier article permet de créer un environnement riche et complet pour la manipulation des DSL. Certains aspects plus pointus n'ont pas été abordés dans cet article, je vous enjoins en cas de besoin à vous référer à la documentation officielle de Xtext ainsi qu'aux forums dédiés dont les liens sont donnés dans le paragraphe suivant.

IX - Liens utiles

- Personnalisation de l'IDE sur le site officiel de Xtext.

- Xtext sur le forum d'Eclipse.

X - Remerciements

Je tiens à remercier l'équipe de Developpez.com, particulièrement Mickaël et Marc, ainsi que Claude LELOUP pour sa relecture orthographique attentive.