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 EclipseDeveloppez : Eclipse RCP. 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
/**
* Use this class to register components to be used within the IDE.
*/
public
class
AirUiModule extends
com.abernard.xtext.air.ui.AbstractAirUiModule {
public
AirUiModule
(
AbstractUIPlugin plugin) {
super
(
plugin);
}
/**
* Permet d'indiquer a Xtext quelle gestion des imports utiliser.Claude Leloup
2013-02-20T11:39:34.40« à » ?
pour les accents, il faudrait rester cohérent.
*
@return
*/
public
Class<
? extends
IGlobalScopeProvider>
bindIGlobalScopeProvider
(
) {
return
AirImportUriGlobalScopeProvider.class
;
}
}
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 :
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
/*
* generated by Xtext
*/
package
com.abernard.xtext.air.ui.labeling;
import
org.eclipse.emf.edit.ui.provider.AdapterFactoryLabelProvider;
import
org.eclipse.xtext.ui.label.DefaultEObjectLabelProvider;
import
com.abernard.xtext.air.air.Airline;
import
com.abernard.xtext.air.air.Airport;
import
com.abernard.xtext.air.air.Includes;
import
com.abernard.xtext.air.air.Plane;
import
com.google.inject.Inject;
/**
* Provides labels for a EObjects.
*
* see http://www.eclipse.org/Xtext/documentation/latest/xtext.html#labelProvider
*/
public
class
AirLabelProvider extends
DefaultEObjectLabelProvider {
@Inject
public
AirLabelProvider
(
AdapterFactoryLabelProvider delegate) {
super
(
delegate);
}
String text
(
Airline airline) {
return
airline.getName
(
);
}
String text
(
Airport airport) {
return
airport.getTitle
(
);
}
String text
(
Plane plane) {
return
plane.getName
(
);
}
String image
(
Plane plane) {
return
"full/obj16/plane.jpg"
;
}
String image
(
Airport airport) {
return
"full/obj16/airport.jpg"
;
}
String image
(
Airline airline) {
return
"full/obj16/airline.png"
;
}
String image
(
Includes include) {
return
"full/obj16/include.gif"
;
}
}
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 :
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 :
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
/*
* generated by Xtext
*/
package
com.abernard.xtext.air.ui.outline;
import
org.eclipse.emf.ecore.EObject;
import
org.eclipse.jface.viewers.StyledString;
import
org.eclipse.xtext.ui.editor.outline.IOutlineNode;
import
org.eclipse.xtext.ui.editor.outline.impl.DefaultOutlineTreeProvider;
import
com.abernard.xtext.air.air.Airline;
import
com.abernard.xtext.air.air.Airport;
import
com.abernard.xtext.air.air.Model;
/**
* customization of the default outline structure
*
*/
public
class
AirOutlineTreeProvider extends
DefaultOutlineTreeProvider {
@Override
protected
boolean
_isLeaf
(
EObject modelElement) {
if
(
modelElement instanceof
Airline ||
modelElement instanceof
Model) {
return
false
;
}
else
{
return
true
;
}
}
@Override
protected
void
_createChildren
(
IOutlineNode parentNode, EObject modelElement) {
if
(
modelElement instanceof
Airline) {
createNode
(
parentNode, ((
Airline)modelElement).getArrival
(
).eContainer
(
));
createNode
(
parentNode, ((
Airline)modelElement).getDeparture
(
).eContainer
(
));
createNode
(
parentNode, ((
Airline)modelElement).getPlane
(
));
}
else
{
super
._createChildren
(
parentNode, modelElement);
}
}
protected
Object _text
(
Airport airport) {
StyledString res =
new
StyledString
(
);
res.append
(
airport.getTitle
(
));
res.append
(
" ("
+
airport.getCountry
(
) +
")"
,
StyledString.DECORATIONS_STYLER);
return
res;
}
}
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 :
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 » :
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 :
Pour obtenir ce résultat, le nouveau contenu de la classe « AirDescriptionLabelProvider.java » est le suivant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
package
com.abernard.xtext.air.ui.labeling;
import
org.eclipse.xtext.resource.IEObjectDescription;
import
org.eclipse.xtext.ui.label.DefaultDescriptionLabelProvider;
/**
* Provides labels for a IEObjectDescriptions and IResourceDescriptions.
*
* see http://www.eclipse.org/Xtext/documentation/latest/xtext.html#labelProvider
*/
public
class
AirDescriptionLabelProvider extends
DefaultDescriptionLabelProvider {
/**
* Ajoute une image a la description des elements suivant le type d'element
* decrit
*/
public
String image
(
IEObjectDescription ele) {
if
(
"Airline"
.equals
(
ele.getEClass
(
).getName
(
))) {
return
"full/obj16/airline.png"
;
}
if
(
"Airport"
.equals
(
ele.getEClass
(
).getName
(
))) {
return
"full/obj16/airport.jpg"
;
}
if
(
"Plane"
.equals
(
ele.getEClass
(
).getName
(
))) {
return
"full/obj16/plane.jpg"
;
}
return
(
String) super
.image
(
ele);
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
package
com.abernard.xtext.air.ui;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.graphics.RGB;
import
org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightingConfiguration;
import
org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightingConfigurationAcceptor;
import
org.eclipse.xtext.ui.editor.utils.TextStyle;
public
class
AirHighlightingConfiguration implements
IHighlightingConfiguration {
/**
* the ID of keywords highlighting configuration
*/
public
static
final
String KEYWORD_ID =
"keyword"
;
@Override
public
void
configure
(
IHighlightingConfigurationAcceptor acceptor) {
acceptor.acceptDefaultHighlighting
(
KEYWORD_ID, "Keyword"
,
keywordTextStyle
(
));
}
/**
* Create the TextStyle for keywords
*
@return
the style for keywords
*/
public
TextStyle keywordTextStyle
(
) {
TextStyle textStyle =
new
TextStyle
(
);
textStyle.setColor
(
new
RGB
(
255
, 0
, 0
));
textStyle.setStyle
(
SWT.ITALIC);
return
textStyle;
}
}
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 :
La page de préférences devient :
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 :
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 :
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 » :
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 :
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
package
com.abernard.xtext.air.ui.contentassist;
import
java.net.URL;
import
org.eclipse.core.runtime.FileLocator;
import
org.eclipse.core.runtime.Path;
import
org.eclipse.emf.ecore.EObject;
import
org.eclipse.jface.resource.ImageDescriptor;
import
org.eclipse.jface.viewers.StyledString;
import
org.eclipse.swt.graphics.Image;
import
org.eclipse.xtext.RuleCall;
import
org.eclipse.xtext.ui.editor.contentassist.ContentAssistContext;
import
org.eclipse.xtext.ui.editor.contentassist.ICompletionProposalAcceptor;
import
org.osgi.framework.Bundle;
import
org.osgi.framework.FrameworkUtil;
/**
* see http://www.eclipse.org/Xtext/documentation/latest/xtext.html#contentAssist on how to
customize content assistant
*/
public
class
AirProposalProvider extends
AbstractAirProposalProvider {
@Override
public
void
complete_Airport
(
EObject model, RuleCall ruleCall,
ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
String proposal =
"Aeroport
\"\"
:
\n
OACI: ;
\n
Pays:
\"\"
;
\n
Pistes: 0;
\n
End."
;
StyledString displayString =
new
StyledString
(
);
displayString.append
(
"Aeroport "
);
displayString.append
(
"(Bloc)"
, StyledString.DECORATIONS_STYLER);
Bundle bundle =
FrameworkUtil.getBundle
(
this
.getClass
(
));
URL url =
FileLocator.find
(
bundle,
new
Path
(
"icons/full/obj16/airport.jpg"
), null
);
Image image =
ImageDescriptor.createFromURL
(
url).createImage
(
);
acceptor.accept
(
createCompletionProposal
(
proposal, displayString, image, context));
}
}
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 :
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▲
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.