samedi 8 décembre 2012

Développez un module de completion pour NetBeans


Mise à jour : j'ai créé une nouvelle API NetBeans pour faciliter fortement les développements pour ce type de complétion.

NetBeans est un environnement de développement Open Source écrit en Java et édité par Oracle.

Son but est d'être le meilleur EDI/IDE pour le développement Java/JEE notamment pour les applications webs (un aperçu du support pour JSF par exemple).

NetBeans est construit à partir du framework NetBeans Platform qui fourni l'ensemble des services pour bâtir des applications RCP.

NetBeans Platform vs Eclipse RCP ?


J'ai  réalisé une étude autour de ces deux frameworks il y a quelques années.

L'utilisation d'outils pour analyser le code des deux frameworks avait démontré que ces frameworks sont comparables à mon sens d'un point de vue de la qualité.

Cependant, la philosophie qui sous tend les IDE bâtis sur ces frameworks est bien différente.

NetBeans IDE est développé par un éditeur unique dont le but est de fournir un outil productif pour Java/JEE : l'IDE colle au plus près des normes Java et des standards associés (CSS 3, HTML 5, ...).

Malheureusement, au fil du temps, Eclipse IDE est devenu une usine à gaz avec pléthore de plugin dont la maturité est parfois douteuse notamment sur des points pourtant cruciaux.

A mon sens , l'écosystème Eclipse a connu une croissance importante au cours des dernières années mais au détriment de la qualité.

De plus, il semble que la priorité de la fondation Eclipse ne soit pas Java/JEE, la persistence de bugs vraiment pénibles sur WTP même dans les versions récentes par exemple en témoigne.

D'autres éléments me gênaient également : le support médiocre de certains standards comme JSF, CSS 3 ou Maven m'auront décidé à tester NetBeans IDE.

Après presque 10 ans d'utilisation intensive d'Eclipse, je suis passé à NetBeans et je ne le regrette absolument pas.

C'est donc tout naturellement que je me suis intéressé au développement de module NetBeans.

Un petit tour de NetBeans Platform


Comme nous l'avons dit plus haut NetBeans Platform est un framework RCP.

Il fournit une API pour construire des modules, des services techniques, des composants d'IHM basé sur Swing, ...

Le framework est plutôt bien documenté notamment pour débuter très rapidement dans le développement de module.

Les modules sont construits par Maven ou le système natif de NetBeans basé sur Ant en fonction de votre préférence.

Le site pivot de la documentation est ici.

Spécifications du module


Le module de complétion permettra de fournir la liste des langages ISO 639-1 lorsque le développeur saisira la valeur d'un attribut lang dans une page XHTML.

L'attribut lang est un point important pour l'accessibilité des sites webs, voici un article qui détaille son mode d'emploi.

La documentation NetBeans propose une liste d'API pour bâtir des éditeurs de  code : c'est en effet le but de notre module.

Chaque API est documentée sous la forme d'un didacticiel et de références à la Javadoc de la plateforme.

Dans notre cas, il s'agit de l'API Editor Code Completion.

Vous noterez que le didacticiel présente un exemple de complétion proche de notre objectif.

Nous baserons pour plus de simplicité notre module sur le système de build natif de NetBeans.

La version de NetBeans retenue est la 7.2.1.

Création du module


Lancez NetBeans 7.2.1.

Créez un nouveau projet (File > New project) puis dans l'assistant choisissez "Module" dans la catégorie "NetBeans Modules".

Ensuite tapez IS06391 pour le nom du module puis suivant.

Dans le code base name ou package principal du module, tapez :

 jee.architect.cookbook.netbeans.iso6391

Terminer l'assistant en laissant les options par défaut.

NetBeans créé pour vous les éléments nécessaire au démarrage du projet.

Une première complétion


Le but du module est de fournir une complétion pour l'attribut lang d'un fichier XHTML : nous allons écrire la classe qui fournira ce service.

Pour NetBeans il s'agit d'implémenter l'interface CompletionProvider. Il faut également indiquer à NetBeans pour quel type de fichier cette complétion doit s'appliquer.

Créez la classe ISO6391CompletionProvider comme suit dans le package principal :


@MimeRegistration(mimeType = "text/xhtml", service = CompletionProvider.class)
public class ISO6391CompletionProvider implements CompletionProvider {

    @Override
    public CompletionTask createTask(int queryType, JTextComponent jTextComponent) {
       
        return new AsyncCompletionTask(new AsyncCompletionQuery() {
            @Override
            protected void query(CompletionResultSet completionResultSet,
                        Document document, int caretOffset) {
                completionResultSet.finish();
            }
        });
    }

    @Override
    public int getAutoQueryTypes(JTextComponent jTextComponent, String string) {
        return 0;
    }
}

Il faut maintenant régler le problème des dépendances manquantes que NetBeans vous signale par des erreurs de compilation.

Pour ce faire cliquez droit sur le noeud "Libraries" du projet et lancer la commande "Add Module Dependency".

Tapez "Editor Code" dans le filtre puis choisissez le module "Editor Code Completion".

Recommencez l'opération avec "mime", choisissez le module "MIME Lookup API".

L'annotation MimeRegistration permet d'enregistrer notre classe comme fournisseur de completion pour le type mime précisé.

Avec cette annotation, NetBeans activera notre module et cette classe pour chaque action de complétion sur un fichier XHTML.

Vous pouvez d'ors et déjà tester cela en lancer le module en mode debug. Pour cela cliquez droit sur le module et exécutez "Debug".

Positionnez un point d'arrêt dans la méthode query puis ouvrez un projet quelconque dans l'éditeur lancé en mode debug.

Ouvrez/Créez un fichier XHTML puis lancer une action de complétion : touche Control+Space sous Windows/Linux. Le débogueur devrait s'arrêter sur votre point d'arrêt.

Il faut maintenant fournir des données valables !

Liste des langages 


Nous allons dans un premier temps fournir la liste des langages ISO639-1 chaque fois que l'utilisateur tentera une complétion dans un fichier XHTML. Nous restreindrons plus tard cette complétion à l'attribut lang courant.

Pour peupler la liste des items de complétion il suffit de peupler l'objet CompletionResultSet fourni en paramètre par la plateforme.

NetBeans fourni le concept CompletionItem qui modélise 1 item présenté dans la liste associée à l'action de complétion de l'utilisateur.

Voici l'item de complétion pour notre cas :


public class ISO6391CompletionItem implements CompletionItem {

      private static ImageIcon ICON =
            new ImageIcon(ImageUtilities.loadImage("jee/architect/cookbook/netbeans/iso6391/bubble.png"));
  
    private String text;

    public ISO6391CompletionItem(String language) {
        this.text = language;
    }

    @Override
    public void defaultAction(JTextComponent jtc) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void processKeyEvent(KeyEvent ke) {
    }

    @Override
    public int getPreferredWidth(Graphics graphics, Font font) {
        return CompletionUtilities.getPreferredWidth(getText(), null, graphics, font);
    }

    @Override
    public void render(Graphics graphics, Font defaultFont, Color defaultColor,
            Color backgroundColor, int width, int height, boolean selected) {
        CompletionUtilities.renderHtml(ICON, getText(), null, graphics, defaultFont,
                (selected ? Color.white : getColor()), width, height, selected);
    }

    @Override
    public CompletionTask createDocumentationTask() {
        return null;
    }

    protected Color getColor() {
        return Color.decode("0x0000B2");
    }

    @Override
    public CompletionTask createToolTipTask() {
        return null;
    }

    @Override
    public boolean instantSubstitution(JTextComponent jtc) {
        return false;
    }

    @Override
    public int getSortPriority() {
        return 0;
    }

    @Override
    public CharSequence getSortText() {
        return getText();
    }

    @Override
    public CharSequence getInsertPrefix() {
        return getText();
    }

    private String getText() {
        return this.text;
    }

}


 La plupart des méthodes de la classe ISO6391CompletionItem sont fournies par l'interface CompletionItem. Vous devrez ajouter l'API Utilities API comme librairie supplémentaire.

Ensuite, il suffit d'ajouter des items à l'objet completionResultSet :


   @Override
    public CompletionTask createTask(int queryType, JTextComponent jTextComponent) {
        
        return new AsyncCompletionTask(new AsyncCompletionQuery() {
            @Override
            protected void query(CompletionResultSet completionResultSet, 
                        Document document, int caretOffset) {
                for(String code:ISO6391CompletionProvider.codes){
                    completionResultSet.addItem(new ISO6391CompletionItem(code));
                }
                completionResultSet.finish();
            }
        });
    }



La complétion est désormais effective mais s'affiche n'importe où :





Maîtriser l'affichage de la complétion


Il nous faut donc retravailler la classe ISO6391CompletionProvider :



    @Override
    public CompletionTask createTask(int queryType, JTextComponent jTextComponent) {

        int position = jTextComponent.getCaretPosition();
        String text = jTextComponent.getText();
        StyledDocument styledDocument = (StyledDocument) jTextComponent.getDocument();
        int lineNumber = NbDocument.findLineNumber(styledDocument, position);
        Element lineElement = styledDocument.getDefaultRootElement().getElement(lineNumber);
        int startOffset = lineElement.getStartOffset();
        int endOffset = lineElement.getEndOffset();
        String lineOfText = text.substring(startOffset, endOffset);
        int column = NbDocument.findLineColumn(styledDocument, position);

        if (LangMatcher.containsRef(lineOfText)) {

            final LangAttribute langAttribute = LangMatcher.getValue(lineOfText, column);
            if (langAttribute == null) {
                return null;
            } else {
                langAttribute.setLineOffset(startOffset);
                return new AsyncCompletionTask(new AsyncCompletionQuery() {
                    @Override
                    protected void query(CompletionResultSet completionResultSet,
                            Document document, int caretOffset) {

                        for (String code : ISO6391CompletionProvider.codes) {
                            if (code.startsWith(langAttribute.getValue())) {
                                completionResultSet.addItem(new ISO6391CompletionItem(langAttribute, code));
                            }
                        }
                        completionResultSet.finish();
                    }
                });
            }

        } else {
            return null;
        }
    }


Le code surligné permet de déterminer la zone, précisément où l'utilisateur interagit.

Nous avons besoin d'obtenir :
- la position du curseur : JTextComponent nous la donne
- la ligne de texte concernée : NbDocument est une classe utilitaire très pratique pour ce genre de problèmes

Ensuite, il s'agit de faire quelques calculs d'offset et d'utiliser un scanner et une regex pour trouver les occurrences de texte qui nous intéressent.

Ce sont les classes LangMatcher et LangAttribute qui vont remplir cette tâche essentielle.

L'idée est de savoir si le curseur de l'utilisateur est dans la zone de saisie d'un attribut lang.

Enfin, il faut maintenant insérer le texte lorsque l'utilisateur choisit un item dans la boîte de complétion.

C'est la responsabilité de la classe  ISO6391CompletionItem :



    @Override
    public void defaultAction(JTextComponent jtc) {
        try {
            StyledDocument doc = (StyledDocument) jtc.getDocument();
            int start = this.langAttribute.getLineOffset() + langAttribute.getStart();
            doc.remove(start, langAttribute.getValue().length());
            doc.insertString(start, getText().substring(0,2), null);
            // Ferme la boite de completion
            Completion.get().hideAll();
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
    }



Notez que cette approche est réutilisable pour implémenter d'autres complétions pour d'autres attributs.
Il faudra bien sûr adapter un peu le code notamment la regex et le scanner.

Ajout : cet article a été commenté par Geertjan Wielenga qui s'est fendu d'une proposition générique dans la foulée.

Conclusion

Nous avons vu qu'il était assez facile de créer un plugin de complétion basique pour NetBeans.

Cet IDE propose bien d'autres points d'extensions intéressants tout aussi utile : hyperliens, recherche rapide, palette, ....


Vous trouverez le code complet sur mon espace github.

Mise à jour importante : j'ai créé une sur-couche à l'API de complétion de NetBeans pour augmenter fortement la productivité.



Contrat Creative Commons
the jee architect cookbook by Olivier SCHMITT est mis à disposition selon les termes de la licence Creative Commons Paternité - Pas d'Utilisation Commerciale - Pas de Modification 3.0 Unported.


Aucun commentaire:

Enregistrer un commentaire