Tutoriel sur l'autocomplétion dans une application Eclipse RCP

Alain BERNARD

Le mécanisme d'autocomplétion occupe une place centrale dans l'IDE Eclipse : il procure une aide de tous les instants au développeur en plus de permettre un gain de temps considérable lors du développement. Il est possible de mettre en place un tel système personnalisé et adapté aux besoins spécifiques dans une application Eclipse RCP. Cela peut se faire de deux manières différentes que nous allons étudier dans cet article : soit dans un éditeur de texte, soit sur des composants SWT tels que des champs texte par exemple. 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 : Tutoriel sur l'autocomplétion dans une application Eclipse RCP

- Auteur : Alain BERNARD

- Parution : 10 juin 2013

- Public visé :

- 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 ® 2013 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. L'autocomplétion sur des composants SWT

- II-A. Un premier exemple simple

- II-B. Un exemple plus complexe

- III. Autocomplétion dans les éditeurs

- III-A. Éditeurs de texte classiques

- III-A-1. Mise en place de l'éditeur

- III-A-2. Mise en place de l'autocomplétion

- III-A-3. Mis en place des templates

- III-B. Utilisation de Xtext

- IV. Conclusion

- V. Liens utiles

- VI. Remerciements

I - Introduction

Dans cet article, nous étudierons comment mettre en place un système d'autocomplétion similaire à ce qui existe de base dans la plateforme Eclipse pour correspondre aux besoins spécifiques d'une application. Le principe d'autocomplétion dans Eclipse peut se découper en deux principales catégories : l'autocomplétion dans les éditeurs ou l'autocomplétion dans les composants SWT/JFace tels que les champs texte, comme le montrent les captures d'écran ci-dessous :

Autocomplétion dans un éditeur Java

Autocomplétion sur un champ texte

Cet article nécessite de posséder des connaissances sur :

- le développement de plugins avec Eclipse ;

- la création d'éditeurs de texte.

II - L'autocomplétion sur des composants SWT

Dans cette partie nous détaillerons la mise en place de l'autocomplétion sur des champs texte SWT. Ce mécanisme est le plus simple à mettre en place tout en permettant de fournir rapidement aux utilisateurs de l'application une liste de propositions. Ce type d'assistant peut se mettre en place soit sur des champs texte soit sur des listes déroulantes. Cet assistant est déclenché soit par une combinaison de touches (par exemple le célèbre Ctrl+Espace), soit par une liste de caractères prédéfinis. Tout d'abord, nous allons mettre en œuvre ce mécanisme sur un exemple très simple.

II-A - Un premier exemple simple

Créez un nouveau plugin, avec une unique vue contenant un champ texte. Le contenu de la vue est donné ci-dessous. L'intégralité du comportement de l'assistant de contenu est défini dans la méthode « constructFieldAssist » :

ContentAssistView.java

1. package com.abernard.contentassist.swt;

3. import org.eclipse.core.runtime.Status;

4. import org.eclipse.jface.bindings.keys.KeyStroke;

5. import org.eclipse.jface.bindings.keys.ParseException;

6. import org.eclipse.jface.fieldassist.ContentProposalAdapter;

7. import org.eclipse.jface.fieldassist.ControlDecoration;

8. import org.eclipse.jface.fieldassist.FieldDecorationRegistry;

9. import org.eclipse.jface.fieldassist.SimpleContentProposalProvider;

10. import org.eclipse.jface.fieldassist.TextContentAdapter;

11. import org.eclipse.swt.widgets.Composite;

12. import org.eclipse.ui.part.ViewPart;

13. import org.eclipse.ui.statushandlers.StatusManager;

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

15. import org.eclipse.swt.layout.GridLayout;

16. import org.eclipse.swt.widgets.Label;

17. import org.eclipse.swt.SWT;

18. import org.eclipse.swt.widgets.Text;

19. import org.eclipse.swt.layout.GridData;

21. import com.abernard.contentassist.Activator;

23. /**

24. * Cette vue met en oeuvre un assistant de contenu basique sur un composant SWT.

25. * @author A. BERNARD

26. *

27. */

28. public class ContentAssistView extends ViewPart {

30. private Text textAssist;

32. @Override

33. public void createPartControl(Composite parent) {

34. parent.setLayout(new GridLayout(2, false));

36. Label label = new Label(parent, SWT.NONE);

37. label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));

38. label.setText("Enter your text here:");

40. // Definition du champ texte ou l'assistant de contenu sera propose

41. textAssist = new Text(parent, SWT.BORDER);

42. textAssist.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

44. // Creation d'un indicateur sur le champ texte

45. ControlDecoration deco = new ControlDecoration(textAssist, SWT.TOP

46. | SWT.LEFT);

47. // L'image utilisee est l'image standard pour les decorateurs informatifs

48. Image image = FieldDecorationRegistry.getDefault()

49. .getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION)

50. .getImage();

51. deco.setDescriptionText("Utilisez CTRL + ESPACE pour voir la liste des mots-cles.");

52. deco.setImage(image);

53. deco.setShowOnlyOnFocus(true); // Ne montrer l'indicateur que lorsque le champ texte a le

54. focus

56. // Construction de l'assistant de contenu

57. constructFieldAssist();

59. }

61. /**

62. * Construit l'assistant de contenu sur le champ texte

63. */

64. private void constructFieldAssist() {

65. // Definition des caracteres d'activation automatique

66. char[] autoActivationCharacters = new char[] { '_' };

67. try {

68. // Definition du raccourci clavier d'activation

69. KeyStroke keyStroke = KeyStroke.getInstance("Ctrl+Space");

70. // Creation de l'assistant de contenu

71. SimpleContentProposalProvider provider = new SimpleContentProposalProvider(new String[]

72. {

73. "Item_1", "Deuxieme_Item", "Item_n3" });

74. // Filtre les propositions suivant le contenu du champ texte

75. provider.setFiltering(true);

76. // Construit l'assistant de contenu

77. ContentProposalAdapter adapter = new ContentProposalAdapter(textAssist,

78. new TextContentAdapter(), provider, keyStroke, autoActivationCharacters);

79. // Avec cette commande, le champ texte sera integralement remplace par le contenu

80. choisi

81. adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);

82. } catch (ParseException e1) {

83. StatusManager.getManager().handle(new Status(Status.ERROR, Activator.PLUGIN_ID,

84. "Unable to create key stroke for content assist", e1), StatusManager.SHOW);

85. }

87. }

89. @Override

90. public void setFocus() {

91. textAssist.setFocus();

92. }

94. }

Nous pouvons visualiser le résultat en lançant notre plugin dans une application Eclipse :

Assistant basique

Nous pouvons aussi constater que lorsqu'on entre le caractère '_' les éléments apparaissent, filtrés :

Assistant basique avec filtre

Détaillons ensemble le contenu de la méthode « constructFieldAssist ».
En premier lieu, le tableau autoActivationChars nous permet de définir les caractères d'activation automatique. Nous définissons aussi un objet de type « KeyStroke » qui permet de définir le raccourci de déclenchement de l'assistant de contenu.
Nous instancions ensuite un « SimpleContentProposalProvider » qui permet de définir très simplement une liste de contenus à partir d'une liste de mots. Nous verrons par la suite comment définir un provider plus complexe.
Après cela nous créons le « ContentProposalAdapter » qui va mettre en place l'assistant de contenu proprement dit.
Enfin, nous devons indiquer par quel moyen le contenu validé sera inséré dans le champ texte : soit par le remplacement du contenu existant (ContentProposalAdapter.PROPOSAL_REPLACE), soit par insertion (ContentProposalAdapter.PROPOSAL_INSERT), soit pas de modification du champ texte (ContentProposalAdapter.PROPOSAL_IGNORE). Ce dernier cas est réservé aux champs dont le traitement de l'insertion est plus complexe et nécessite des actions spécifiques. On pourra être notifié de l'acceptation de l'assistant un ajoutant un « IContentProposalListener ». Ceci sera détaillé dans le prochain exemple.
En effet, le comportement basique que nous venons de décrire manque de raffinement pour des interfaces plus complexes, où l'information proposée nécessite de donner plus d'informations.

II-B - Un exemple plus complexe

Dans ce deuxième exemple, nous allons insérer des éléments de manière plus complexe. Mettons par exemple que nous voulons proposer une liste d'avions qui possèdent tous un certain nombre de propriétés : le nom de l'avion, l'avionneur, le nombre de membres d'équipage navigant, ainsi que le nombre de moteurs. À la manière d'Eclipse, nous souhaitons qu'un cadre affiche les informations détaillées sur les différents éléments disponibles à côté de la liste affichée.
La première étape consiste à définir des objets qui implémentent l'interface « IContentProposal ». C'est cette interface qui est utilisée par le mécanisme d'autocomplétion pour afficher les éléments dans la liste. Nous définissons par exemple notre propre implémentation, donnée ci-dessous, de cette interface pour nos avions :

PlaneProposal.java

1. package com.abernard.contentassist;

3. import org.eclipse.jface.fieldassist.IContentProposal;

5. /**

6. * Cette classe donne une description des elements a afficher dans

7. * l'assistant de contenu.

8. * @author A. BERNARD

9. *

10. */

11. public class PlaneProposal implements IContentProposal {

13. private String name;

14. private int crew;

15. private int engines;

16. private String manufacturer;

18. /** Constructeur

19. * @param name le nom de l'avion

20. * @param manufacturer l'avionneur

21. * @param crew le nombre de membres d'équipage

22. * @param engines le nombre de moteurs

23. */

24. public PlaneProposal(String name, String manufacturer, int crew, int engines) {

25. this.name = name;

26. this.manufacturer = manufacturer;

27. this.crew = crew;

28. this.engines = engines;

30. }

32. @Override

33. public String getContent() {

34. // Cette methode doit retourner le contenu a placer dans le champ texte

35. return name;

36. }

38. @Override

39. public int getCursorPosition() {

40. // Cette methode retourne la position que doit avoir le curseur apres insertion

41. return name.length();

42. }

44. @Override

45. public String getLabel() {

46. // Le texte a afficher dans la liste des elements disponibles

47. return name + " (" + manufacturer + ")";

48. }

50. @Override

51. public String getDescription() {

52. // Cette description est affichee dans l'encart a cote de l'assistant

53. return manufacturer + " " + name + " : \nEquipage : " + crew

54. + "\nNombre de moteurs: " + engines;

55. }

57. /**

58. * Donne l'avionneur

59. * @return

60. */

61. public String getManufacturer() {

62. return manufacturer;

63. }

65. /**

66. * Donne le nombre de membres d'equipage

67. * @return

68. */

69. public int getCrew() {

70. return crew;

71. }

73. /**

74. * Donne le nombre de moteurs

75. * @return

76. */

77. public int getEngines() {

78. return engines;

79. }

81. }

Nous pouvons ensuite définir l'interface graphique où nous mettrons notre assistant de contenu en place. Cette interface comporte simplement quatre champs texte destinés à afficher les différentes informations sur l'avion qui aura été sélectionné. Seul le premier champ pour le nom de l'avion proposera l'autocomplétion. En tout premier lieu, nous créons la liste des avions disponibles en instanciant pour chaque avion l'objet de type « PlaneProposal » idoine.

ContentAssistAdvanced.java

1. package com.abernard.contentassist.swt;

3. import java.util.ArrayList;

4. import java.util.List;

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

7. import org.eclipse.jface.bindings.keys.KeyStroke;

8. import org.eclipse.jface.bindings.keys.ParseException;

9. import org.eclipse.jface.fieldassist.ContentProposalAdapter;

10. import org.eclipse.jface.fieldassist.ControlDecoration;

11. import org.eclipse.jface.fieldassist.FieldDecorationRegistry;

12. import org.eclipse.jface.fieldassist.IContentProposal;

13. import org.eclipse.jface.fieldassist.IContentProposalListener;

14. import org.eclipse.jface.fieldassist.IContentProposalProvider;

15. import org.eclipse.jface.fieldassist.TextContentAdapter;

16. import org.eclipse.swt.SWT;

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

18. import org.eclipse.swt.layout.GridData;

19. import org.eclipse.swt.layout.GridLayout;

20. import org.eclipse.swt.widgets.Composite;

21. import org.eclipse.swt.widgets.Label;

22. import org.eclipse.swt.widgets.Text;

23. import org.eclipse.ui.part.ViewPart;

24. import org.eclipse.ui.statushandlers.StatusManager;

26. import com.abernard.contentassist.Activator;

27. import com.abernard.contentassist.PlaneProposal;

29. /**

30. * Cette vue met en oeuvre un assistant de contenu plus avance sur un composant

31. * SWT.

32. * @author A. BERNARD

33. *

34. */

35. public class ContentAssistAdvanced extends ViewPart {

37. private Text textAssist;

38. private List<PlaneProposal> elements;

39. private Text textManufacturer;

40. private Text textCrew;

41. private Text textEngines;

43. /**

44. * Constructeur. Initialise la liste des elements disponibles

45. */

46. public ContentAssistAdvanced() {

47. elements = new ArrayList<PlaneProposal>();

48. elements.add(new PlaneProposal("A320", "Airbus", 2, 2));

49. elements.add(new PlaneProposal("Rafale", "Dassault", 1, 1));

50. elements.add(new PlaneProposal("Falcon 7x", "Dassault", 3, 3));

51. elements.add(new PlaneProposal("B777", "Boeing", 4, 2));

52. elements.add(new PlaneProposal("A380", "Airbus", 2, 4));

53. }

55. @Override

56. public void createPartControl(Composite parent) {

57. parent.setLayout(new GridLayout(4, false));

59. Label labelPlane = new Label(parent, SWT.NONE);

60. labelPlane.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));

61. labelPlane.setText("Plane name:");

63. // Definition du champ texte ou l'assistant de contenu sera propose

64. textAssist = new Text(parent, SWT.BORDER);

65. textAssist.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

66. // Contruction de l'assistant de contenu

67. constructFieldAssist();

69. // L'image utilisee est l'image standard pour les decorateurs informatifs

70. Image image = FieldDecorationRegistry.getDefault()

71. .getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION)

72. .getImage();

75. // Creation d'un indicateur sur le champ texte

76. ControlDecoration deco = new ControlDecoration(textAssist, SWT.TOP

77. | SWT.LEFT);

78. deco.setDescriptionText("Utilisez CTRL + ESPACE pour voir la liste des mots-cles.");

79. deco.setImage(image);

80. deco.setShowOnlyOnFocus(true);

82. // Creation des autres champs d'affichage

83. Label labelManufacturer = new Label(parent, SWT.NONE);

84. labelManufacturer.setText("Manufacturer:");

86. textManufacturer = new Text(parent, SWT.READ_ONLY);

87. textManufacturer.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

89. Label labelCrew = new Label(parent, SWT.NONE);

90. labelCrew.setText("Crew:");

92. textCrew = new Text(parent, SWT.READ_ONLY);

93. textCrew.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

95. Label labelEngines = new Label(parent, SWT.NONE);

96. labelEngines.setText("Engines:");

98. textEngines = new Text(parent, SWT.READ_ONLY);

99. textEngines.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

102. }

104. /**

105. * Construit l'assistant de contenu sur le champ texte

106. */

107. private void constructFieldAssist() {

108. // Definition des caracteres d'activation automatique

109. char[] autoActivationCharacters = new char[] { 'A', 'B' };

110. try {

111. // Definition du raccourci clavier d'activation

112. KeyStroke keyStroke = KeyStroke.getInstance("Ctrl+Space");

113. // Creation de l'assistant de contenu

114. ContentProposalAdapter adapter = new ContentProposalAdapter(textAssist,

115. new TextContentAdapter(), new PlaneContentProposalProvider(true),

116. keyStroke, autoActivationCharacters);

117. // Avec cette commande, le champ texte sera integralement remplace par le contenu

118. choisi

119. adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);

120. adapter.addContentProposalListener(new IContentProposalListener() {

121. @Override

122. public void proposalAccepted(IContentProposal proposal) {

123. // On peut dans cette methode effectuer des traitements supplementaires

124. //    apres que la validation du contenu.

125. if (proposal instanceof PlaneProposal) {

126. PlaneProposal planeProposal  = (PlaneProposal) proposal;

127. textManufacturer.setText(planeProposal.getManufacturer());

128. textCrew.setText(Integer.toString(planeProposal.getCrew()));

129. textEngines.setText(Integer.toString(planeProposal.getEngines()));

130. }

131. }

132. });

133. } catch (ParseException e1) {

134. StatusManager.getManager().handle(new Status(Status.ERROR, Activator.PLUGIN_ID,

135. "Unable to create key stroke for content assist", e1), StatusManager.SHOW);

136. }

138. }

140. @Override

141. public void setFocus() {

142. textAssist.setFocus();

144. }

147. /**

148. * Cette classe fournit les elements a afficher a l'assistant de contenu

149. * @author A. BERNARD

150. *

151. */

152. public class PlaneContentProposalProvider implements IContentProposalProvider {

154. private boolean filterProposals = false;

156. /**

157. * Constructeur

158. * @param filter <code>true</code> si les elements doivent etre filtres

159. *

160. */

161. public PlaneContentProposalProvider(boolean filter) {

162. this.filterProposals = filter;

163. }

165. @Override

166. public IContentProposal[] getProposals(String contents, int position) {

167. if (filterProposals) {

168. // Effectuer un filtrage s'il est active: les elements a afficher

169. //   sont ceux qui correspondent au texte deja entre

170. ArrayList<IContentProposal> list = new ArrayList<IContentProposal>();

171. for (IContentProposal proposal : elements) {

172. if (proposal.getContent().startsWith(contents)) {

173. list.add(proposal);

174. }

175. }

176. return list.toArray(new IContentProposal[list.size()]);

177. }

178. return elements.toArray(new IContentProposal[elements.size()]);

179. }

180. }

182. }

Afin de construire correctement la liste des avions à proposer à l'utilisateur, nous effectuons un filtrage sur les éléments à afficher. Ce filtrage est fait au travers de la classe « PlaneContentProposalProvider ». Grâce à la définition de la méthode « getDescription » de la classe « PlaneProposal », le cadre jaune affiché à côté de la liste affiche bien la description de nos éléments :

Description du contenu

De plus, afin de pouvoir remplir les champs texte lorsque l'utilisateur valide une proposition, nous ajoutons un écouteur grâce à la méthode « addContentProposalListener(IContentProposalListener) » :

adapter.addContentProposalListener(new IContentProposalListener() {
@Override
public void proposalAccepted(IContentProposal proposal) {
// Ici le traitement particulier.
}
});

Remplissage des champs texte

Ces deux exemples nous ont permis de voir rapidement comment mettre en place un assistant de contenu sur des champs texte ou des listes déroulantes. Notons qu'il est possible de construire ses assistants sur n'importe quel type de composant SWT, en redéfinissant soi-même une implémentation de l'interface « IControlContentAdapter » et en l'utilisant en lieu et place du « TextContentAdapter ».
La mise en place de l'assistant de contenu sur des champs texte reste très simple. Celle pour des éditeurs est plus complexe, mais offre bien davantage de possibilités.

III - Autocomplétion dans les éditeurs

III-A - Éditeurs de texte classiques

De la même manière que l'autocomplétion pour les fichiers de code Java, il est possible de mettre en place un assistant de contenu dans un éditeur personnalisé. Nous expliquons ici comment mettre en place un tel assistant, toujours en se basant sur le même exemple que dans le paragraphe précédent : une liste d'avions. Dans cet exemple, nous nous baserons sur un éditeur de texte très classique qui hérite directement de la classe « AbstractDecoratedTextEditor ».

III-A-1 - Mise en place de l'éditeur

Pour commencer à mettre en place l'autocomplétion, nous devons créer notre éditeur « squelette ». Pour cela, vérifier que vous avez dans les dépendances de votre plugin, les plugins suivants :

- org.eclipse.jface.text ;

- org.eclipse.ui.editors ;

- org.eclipse.core.resources ;

- org.eclipse.ui.ide.

Il faut ensuite créer l'éditeur. Il dérive directement de la classe « AbstractDecoratedTextEditor ». Dans cet éditeur, nous devons créer un objet de type « TextSourceViewerConfiguration » :

AircraftsEditor.java

1. package com.abernard.contentassist.editor;

3. import org.eclipse.ui.texteditor.AbstractDecoratedTextEditor;

5. /**

6. * Cet editeur affiche du texte brut. Il propose une autocompletion sur une

7. * liste de mots-cles.

8. * @author A. BERNARD

9. *

10. */

11. public class AircraftsEditor extends AbstractDecoratedTextEditor {

13. /**

14. * Constructeur. Definit la configuration de l'editeur.

15. */

16. public AircraftsEditor() {

17. super();

18. this.setSourceViewerConfiguration(new AircraftSourceViewerConfiguration(getSharedColors(),

19. getPreferenceStore()));

20. }

22. }

AircraftSourceViewerConfiguration.java

1. package com.abernard.contentassist.editor;

4. import org.eclipse.jface.preference.IPreferenceStore;

5. import org.eclipse.jface.text.DefaultInformationControl.IInformationPresenter;

6. import org.eclipse.jface.text.source.ISharedTextColors;

7. import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;

9. /**

10. * Cette configuration de l'editeur permet d'activer l'autocompletion sur le

11. * texte en cours de saisie.

12. * @author A. BERNARD

13. *

14. */

15. public class AircraftSourceViewerConfiguration extends

16. TextSourceViewerConfiguration {

18. /**

19. * Constructeur.

20. * @param sharedColors

21. * @param preferenceStore

22. */

23. public AircraftSourceViewerConfiguration(final ISharedTextColors sharedColors,

24. IPreferenceStore preferenceStore) {

25. super(preferenceStore);

26. }

27. }

À ce stade, si vous lancez votre application Eclipse, vous devriez pouvoir ouvrir des fichiers dans l'éditeur défini.

Dans le fichier « plugin.xml », vous devez déclarer à quelle extension de fichier est associé votre éditeur. Nous avons choisi dans cet exemple l'extension totalement arbitraire « *.ac ».

III-A-2 - Mise en place de l'autocomplétion

Nous pouvons maintenant mettre en place l'autocomplétion dans notre éditeur. Cela passe par deux étapes : en premier nous devons définir une classe qui implémente l'interface « IContentAssistProcessor », puis indiquer à notre classe « AircraftSourceViewerConfiguration » d'indiquer cette classe pour la gestion de l'autocomplétion. Créons tout d'abord la classe « AircraftCompletionProcessor » :

AircraftCompletionProcessor.java

1. package com.abernard.contentassist.editor;

3. import java.util.ArrayList;

4. import java.util.List;

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

7. import org.eclipse.jface.text.BadLocationException;

8. import org.eclipse.jface.text.IDocument;

9. import org.eclipse.jface.text.ITextViewer;

10. import org.eclipse.jface.text.contentassist.CompletionProposal;

11. import org.eclipse.jface.text.contentassist.ContextInformation;

12. import org.eclipse.jface.text.contentassist.ContextInformationValidator;

13. import org.eclipse.jface.text.contentassist.ICompletionProposal;

14. import org.eclipse.jface.text.contentassist.IContentAssistProcessor;

15. import org.eclipse.jface.text.contentassist.IContextInformation;

16. import org.eclipse.jface.text.contentassist.IContextInformationValidator;

17. import org.eclipse.ui.statushandlers.StatusManager;

19. import com.abernard.contentassist.Activator;

20. import com.abernard.contentassist.PlaneProposal;

23. /**

24. * Cree la liste des elements a afficher dans l'assistant de contenu de l'editeur.

25. * @author A. BERNARD

26. *

27. */

28. public class AircraftCompletionProcessor implements IContentAssistProcessor {

30. private List<PlaneProposal> elements;

32. /**

33. * Constructeur. Initialise la liste des elements disponibles

34. */

35. public AircraftCompletionProcessor() {

36. elements = new ArrayList<PlaneProposal>();

37. elements.add(new PlaneProposal("A320", "Airbus", 2, 2));

38. elements.add(new PlaneProposal("Rafale", "Dassault", 1, 1));

39. elements.add(new PlaneProposal("Falcon 7x", "Dassault", 3, 3));

40. elements.add(new PlaneProposal("B777", "Boeing", 4, 2));

41. elements.add(new PlaneProposal("A380", "Airbus", 2, 4));

42. }

44. @Override

45. public char[] getCompletionProposalAutoActivationCharacters() {

46. return new char[] {'A', 'B'};

47. }

49. @Override

50. public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {

51. IDocument document = viewer.getDocument();

52. int currOffset = offset-1;

53. String currWord = "";

54. if (currOffset >= 0) {

55. try {

56. char currChar;

57. /*

58. * Retrouver le debut du mot sur lequel on a declenche la completion

59. * On se deplace d'offset en offset jusqu'a rencontrer un caractere

60. * de type 'whitespace' (espace, ou retour a la ligne, ...).

61. * Chaque caractere est ajoute au debut du mot en cours de lecture

62. */

63. while (currOffset >= 0 && !Character.isWhitespace(currChar = document

64. .getChar(currOffset))) {

65. currWord = currChar + currWord;

66. currOffset--;

67. }

68. // Une fois le mot reconstruit, retrouver les elements a proposer

69. List<PlaneProposal> availableSuggests = getAvailableElements(currWord);

70. // A partir des elements disponibles construire la liste des propositions

71. ICompletionProposal[] proposals = null;

72. if (availableSuggests.size() > 0) {

73. proposals = buildProposals(availableSuggests, currWord, offset -

74. currWord.length());

75. }

76. return proposals;

77. } catch (BadLocationException e) {

78. StatusManager.getManager().handle(new Status(Status.ERROR, Activator.PLUGIN_ID,

79. e.getMessage(), e), StatusManager.LOG);

80. return null;

81. }

82. } else {

83. // Pas de texte dans le document, donc pas de completion disponible !

84. return null;

85. }

86. }

88. @Override

89. public IContextInformation[] computeContextInformation(

90. ITextViewer viewer, int offset) {

91. ContextInformation[] contextInfos = new ContextInformation[elements.size()];

92. for (int i = 0; i < contextInfos.length; i++) {

93. contextInfos[i] = new ContextInformation("Avion : " + elements.get(i).getContent(),

94. elements.get(i).getContent());

95. }

97. return contextInfos;

98. }

100. @Override

101. public char[] getContextInformationAutoActivationCharacters() {

102. return null;

103. }

105. @Override

106. public String getErrorMessage() {

107. return "No completions found";

108. }

111. @Override

112. public IContextInformationValidator getContextInformationValidator() {

113. return new ContextInformationValidator(this);

114. }

116. /**

117. * Construit la liste des elements qui, peuvent etre inseres a cet endroit

118. * dans l'editeur.

119. * @param word le mot en cours d'ecriture dans l'editeur

120. * @return la liste des elements disponibles

121. */

122. private List<PlaneProposal> getAvailableElements(String word) {

123. List<PlaneProposal> availableItems = new ArrayList<PlaneProposal>();

124. for (PlaneProposal proposal : elements) {

125. if (proposal.getContent().startsWith(word)) {

126. availableItems.add(proposal);

127. }

128. }

129. return availableItems;

130. }

132. /**

133. * Construit la liste des elements d'autocompletion a partir des elements

134. * disponibles.

135. * @param availableElements la liste des elements disponibles

136. * @param replacedWord le mot a remplacer dans l'editeur

137. * @param offset la position du curseur dans le document

138. * @return la liste des suggestions d'autocompletion

139. */

140. private ICompletionProposal[] buildProposals(List<PlaneProposal> availableElements, String

141. replacedWord, int offset) {

142. ICompletionProposal[] proposals = new ICompletionProposal[availableElements.size()];

143. int index = 0;

144. // Create proposals from model elements.

145. for (PlaneProposal proposal : availableElements) {

146. IContextInformation contextInfo = new ContextInformation(null, proposal.getContent());

147. proposals[index] = new CompletionProposal(proposal.getContent(), offset,

148. replacedWord.length(), proposal.getContent().length(),

149. null, proposal.getContent(),

150. contextInfo, proposal.getDescription());

151. index++;

152. }

153. return proposals;

154. }

156. }

Notez que, par facilité, nous utilisons la classe « PlaneProposal » que nous avons créée précédemment mais que cela n'est pas nécessaire. N'importe quelle classe pourra convenir. Le constructeur ne sert ici qu'à créer la liste des éléments disponibles. Dans un cas réel d'utilisation, les éléments disponibles seront probablement chargés dynamiquement à l'appel de l'autocomplétion. C'est en effet dans la méthode « public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) » que les éléments à afficher dans la popup d'autocomplétion doivent être créés. Cette méthode est appelée à chaque fois que l'utilisateur appuie sur la combinaison de touche « Ctrl+Espace » pour afficher les éléments disponibles.
En premier lieu, notre implémentation tente de retrouver le début de mot que nous sommes en train d'écrire. Pour cela, on lit les caractères un à un jusqu'à tomber sur un caractère d'espacement (espace, tabulation, retour à la ligne, etc.). Si on est au début du fichier, il n'y pas d'éléments disponibles (cas où offset = 0). Une fois reconstitué le mot en cours de saisie (« currWord »), on filtre la liste des éléments disponibles afin de trouver ceux compatibles avec le mot, au sein de la méthode « private List<PlaneProposal> getAvailableElements(String word) ». Les éléments disponibles sont simplement ceux qui commencent par « currWord ».
De là, nous pouvons construire les éléments à afficher dans le popup d'autocomplétion, au sein de la méthode « private ICompletionProposal[] buildProposals(List<PlaneProposal> availableElements, String replacedWord, int offset) ». Pour chaque élément disponible, nous devons instancier un élément de type « CompletionProposal ».
Remarquez que nous créons en plus un élément de type « ContextInformation ». Cet élément sera affiché dans un tooltip lors de la validation de la proposition choisie : . Notre méthode principale retourne la liste de propositions ainsi construite, permettant à la plateforme d'afficher le popup d'autocomplétion.

Ceci fait, nous devons activer le mécanisme d'autocomplétion dans notre classe « AircraftSourceViewerConfiguration » :

AircraftSourceViewerConfiguration.java

1. package com.abernard.contentassist.editor;

3. import org.eclipse.jface.preference.IPreferenceStore;

4. import org.eclipse.jface.text.DefaultInformationControl;

5. import org.eclipse.jface.text.DefaultInformationControl.IInformationPresenter;

6. import org.eclipse.jface.text.IDocument;

7. import org.eclipse.jface.text.IInformationControl;

8. import org.eclipse.jface.text.IInformationControlCreator;

9. import org.eclipse.jface.text.TextPresentation;

10. import org.eclipse.jface.text.contentassist.ContentAssistant;

11. import org.eclipse.jface.text.contentassist.IContentAssistant;

12. import org.eclipse.jface.text.source.ISharedTextColors;

13. import org.eclipse.jface.text.source.ISourceViewer;

14. import org.eclipse.swt.SWT;

15. import org.eclipse.swt.custom.StyleRange;

16. import org.eclipse.swt.widgets.Display;

17. import org.eclipse.swt.widgets.Shell;

18. import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;

20. /**

21. * Cette configuration de l'editeur permet d'activer l'autocompletion sur le

22. * texte en cours de saisie.

23. * @author A. BERNARD

24. *

25. */

26. public class AircraftSourceViewerConfiguration extends

27. TextSourceViewerConfiguration {

29. private IInformationPresenter presenter;

31. /**

32. * Constructeur.

33. * @param sharedColors

34. * @param preferenceStore

35. */

36. public AircraftSourceViewerConfiguration(final ISharedTextColors sharedColors,

37. IPreferenceStore preferenceStore) {

38. super(preferenceStore);

40. presenter = new DefaultInformationControl.IInformationPresenter() {

41. @Override

42. public String updatePresentation(Display display, String hoverInfo,

43. TextPresentation presentation, int maxWidth, int maxHeight) {

44. // Mise en gras du "titre" (reference et avionneur)

45. int firstColon = (hoverInfo.indexOf(':') != -1 ? hoverInfo.indexOf(':') : 0);

46. StyleRange title = new StyleRange(0, firstColon, null, null, SWT.BOLD |

47. SWT.ITALIC);

48. presentation.addStyleRange(title);

49. return hoverInfo;

50. }

51. };

52. }

54. @Override

55. public IContentAssistant getContentAssistant(final ISourceViewer sourceViewer) {

56. ContentAssistant assistant = new ContentAssistant();

57. assistant.setDocumentPartitioning(getConfiguredDocumentPartitioning(sourceViewer));

58. AircraftCompletionProcessor processor = new AircraftCompletionProcessor();

59. assistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);

60. assistant.setInformationControlCreator(getInformationControlCreator(sourceViewer));

61. // Insere automatiquement la seule possibilite si elle est unique

62. assistant.enableAutoInsert(true);

63. // Autorise l'"autoactivation", i.e. le declenchement sur des caracteres particuliers

64. assistant.enableAutoActivation(true);

65. // Affiche la ligne de statut en bas de la popup de l'assistant

66. assistant.setStatusLineVisible(true);

67. assistant.setStatusMessage("Available planes to insert");

68. return assistant;

69. }

72. @Override

73. public IInformationControlCreator getInformationControlCreator(ISourceViewer sourceViewer) {

74. return new IInformationControlCreator() {

75. @Override

76. public IInformationControl createInformationControl(Shell parent) {

77. return new DefaultInformationControl(parent, presenter);

78. }

79. };

80. }

82. }

La déclaration de l'assistant de contenu est faite dans la méthode « public IContentAssistant getContentAssistant(final ISourceViewer sourceViewer) ». Il faut créer un élément de type « ContentAssistant », qui va déclencher les différents assistants disponibles suivant le type de contenu où l'on se trouve dans le document. Ainsi, il est nécessaire de configurer le partitionnement du document grâce à l'instruction « assistant.setDocumentPartitioning(Partioning) » et d'affecter ensuite nos différents éléments « IContentAssistProcessor » suivant le type de contenu grâce à l'instruction « assistant.setContentAssistProcessor(ContentAssistProcessor, ContentType) ». Dans notre cas, nous n'avons pas défini de partitionnement, notre assistant de contenu est donc configuré pour le type de contenu par défaut.
On peut ensuite configurer l'assistant grâce aux quelques méthodes utilisées dans notre exemple ici :

- autoriser l'activation automatique sur certains caractères : « assistant.enableAutoActivation(true) » ;

- insérer automatiquement la seule option disponible le cas échéant : « assistant.enableAutoInsert(true) » ;

- afficher la barre de statut avec le texte idoine : « assistant.setStatusLineVisible(true) » accompagné de la méthode « assistant.setStatusMessage("texte") »;

- permettre un assistant de contenu cyclique : « assistant.setRepeatedInvocationMode(true) ».

Enfin, nous devons aussi configurer la manière dont Eclipse va afficher les détails concernant les différentes propositions affichées (le cadre jaune affiché à côté de la liste). Pour cela, nous définissons notre variable « presenter » de type « IInformationPresenter ». Puis nous indiquons à la plateforme d'utiliser ce mode de présentation dans la méthode « getInformationControlCreator ».

Ce mode de rendu sera utilisé dans tous les tooltips affichés par votre éditeur, notamment par exemple pour les descriptions des erreurs de syntaxe dans les markers.

Nous pouvons lancer notre application et constater le résultat.

Notre assistant de contenu

III-A-3 - Mis en place des templates

Nous pouvons compléter notre assistant de contenu avec des templates. Les templates sont des éléments qui, activés par l'utilisateur, insèrent automatiquement dans l'éditeur un ensemble de contenu. La meilleure manière de définir les templates est de le faire via le fichier « plugin.xml ». Nous allons dans cet exemple créer un template qui insère tous les avions disponibles. La première étape est de définir une nouvelle extension pour le point d'extension « org.eclipse.ui.editors.templates ». Au sein de ce point d'extension, il faut définir un nouvel élément de type « contextType ». Dans notre cas, nous utilisons pour ce contexte celui par défaut de la plateforme.

Définition du contextType

Puis nous définissons le template proprement dit. Le template est associé au context type défini précédemment afin que la plateforme puisse déterminer dans quel cas proposer ce template. Dans ce template, le sous-élément « pattern » définit le contenu du template à proprement parler.

Définition du template

Pour afficher les templates, notre classe « AircraftCompletionProcessor » doit être une sous-classe de « TemplateCompletionProcessor ». Notre classe se trouve donc modifiée comme suit :

AircraftCompletionProcessor.java

1. package com.abernard.contentassist.editor;

3. import java.io.IOException;

5. import org.eclipse.jface.text.templates.Template;

6. import org.eclipse.jface.text.templates.TemplateCompletionProcessor;

7. import org.eclipse.jface.text.templates.TemplateContextType;

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

9. import org.eclipse.ui.editors.text.templates.ContributionContextTypeRegistry;

10. import org.eclipse.ui.editors.text.templates.ContributionTemplateStore;

12. import com.abernard.contentassist.Activator;

13. import com.abernard.contentassist.Constants;

14. // ... autres imports

17. /**

18. * Cree la liste des elements a afficher dans l'assistant de contenu de l'editeur.

19. * @author A. BERNARD

20. *

21. */

22. public class AircraftCompletionProcessor extends TemplateCompletionProcessor {

24. private List<PlaneProposal> elements;

25. private ContributionTemplateStore fTemplateStore;

26. private ContributionContextTypeRegistry fRegistry;

28. /**

29. * Constructeur. Initialise la liste des elements disponibles

30. */

31. public AircraftCompletionProcessor() {

32. // ... construction de la liste des elements comme precedemment

34. fRegistry= new ContributionContextTypeRegistry();

35. fRegistry.addContextType(Constants.TEMPLATE_CONTEXT_TYPE);

37. fTemplateStore= new ContributionTemplateStore(fRegistry,

38. Activator.getDefault().getPreferenceStore(),

39. Constants.PREF_TEMPLATE);

40. try {

41. fTemplateStore.load();

42. } catch (IOException x) {

43. Activator.getDefault().getLog().log(new Status(Status.ERROR,

44. Activator.PLUGIN_ID, x.getMessage(), x));

45. }

46. }

48. @Override

49. public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {

50. ICompletionProposal[] proposals = super.computeCompletionProposals(viewer, offset);

51. IDocument document = viewer.getDocument();

52. int currOffset = offset-1;

53. String currWord = "";

54. if (currOffset >= 0) {

55. try {

56. char currChar;

57. /*

58. * Retrouver le debut du mot sur lequel on a declenche la completion

59. * On se deplace d'offset en offset jusqu'a rencontrer un caractere

60. * de type 'whitespace' (espace, ou retour a la ligne, ...).

61. * Chaque caractere est ajoute au debut du mot en cours de lecture

62. */

63. while (currOffset >= 0 && !Character.isWhitespace(currChar = document

64. .getChar(currOffset))) {

65. currWord = currChar + currWord;

66. currOffset--;

67. }

68. // Une fois le mot reconstruit, retrouver les elements a proposer

69. List<PlaneProposal> availableSuggests = getAvailableElements(currWord);

70. // A partir des elements disponibles construire la liste des propositions

71. List<ICompletionProposal> proposalsList = new ArrayList<ICompletionProposal>();

72. proposalsList.addAll(Arrays.asList(proposals));

73. if (availableSuggests.size() > 0) {

74. proposalsList.addAll(buildProposals(availableSuggests,

75. currWord, offset - currWord.length()));

76. }

77. proposals = proposalsList.toArray(new ICompletionProposal[proposalsList.size()]);

78. return proposals;

79. } catch (BadLocationException e) {

80. StatusManager.getManager().handle(new Status(Status.ERROR, Activator.PLUGIN_ID,

81. e.getMessage(), e), StatusManager.LOG);

82. return null;

83. }

84. } else {

85. // Pas de texte dans le document, donc pas de completion disponible !

86. return proposals;

87. }

88. }

91. @Override

92. protected TemplateContextType getContextType(ITextViewer viewer,

93. IRegion region) {

94. return fRegistry.getContextType(Constants.TEMPLATE_CONTEXT_TYPE);

95. }

97. @Override

98. protected Image getImage(Template template) {

99. return Activator.getDefault().getImageRegistry().get(Constants.IMG_TEMPLATE);

100. }

102. @Override

103. protected Template[] getTemplates(String contextTypeId) {

104. return fTemplateStore.getTemplates(contextTypeId);

105. }

107. // ... autres methodes implementees precedemment

109. }

La première étape consiste à indiquer quels templates Eclipse charger dans le contexte de notre éditeur. C'est pour cela que nous initialisons les attributs fTemplatesStore et fRegistry dans le constructeur de la classe. Le lien est fait entre le type de contexte que nous avons défini pour notre template dans le fichier « plugin.xml » et les préférences d'Eclipse. Il ne reste ensuite qu'à implémenter les trois méthodes « getContextType », « getImage » (optionnelle) et »getTemplates ».
À chaque appel de l'assistant de contenu, les templates idoines sont recherchés au sein des templates existant, en faisant tout simplement un appel à « super.computeCompletionProposals(viewer, offset) ». Nous les ajoutons ensuite à la liste des éléments affichés à l'utilisateur. En lançant notre application, nous pouvons observer la présence de notre template au sein de la liste de propositions, ainsi que l'insertion de ce template au sein de notre document.

Notre template dans l'assistant de contenu

III-B - Utilisation de Xtext

Le système que nous avons décrit dans ce paragraphe utilise les API bas niveau d'Eclipse. Si l'on ajoute à la gestion de l'assistant de contenu la gestion de la coloration syntaxique, de la vérification de syntaxe et de toutes les préférences associées, la mise en place de tous ces éléments peut devenir lourde sur des éditeurs complexes. Certains frameworks, tel Xtext permettent de mettre en place ces éléments de manière plus simple. Vous trouverez dans cet article comment mettre en place l'autocomplétion dans Xtext.

IV - Conclusion

Nous avons vu dans cet article deux méthodes pour mettre en place un assistant de contenu dans une application Eclipse RCP : sur un composant SWT ou au sein d'un éditeur de texte. Bien que différentes par leur complexité et les possibilités qu'elles offrent, ces deux méthodes ont le même but : proposer à l'utilisateur de votre application un moyen simple et intuitif de connaître les différentes possibilités qui s'offrent à lui. Ces systèmes sont très appréciés et apporteront rapidement une réelle plus-value à vos interfaces.

V - Liens utiles

À propos de l'autocomplétion :

- L'autocomplétion dans l'aide officielle ;

- La mise en place de l'autocomplétion avec Xtext.

Sur les éditeurs en général :

- Tutoriel sur les éditeurs de Mickaël BARON ;

- Mettre en place son éditeur en 60 minutes, sur OCTO Talks.

VI - Remerciements

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