Saturday Aug 09, 2008

How to Create a Caret Aware Code Completion Box

Casper Bang asked a great question at the end of my previous blog entry about the new code completion tutorial:

We have a desire in our team to get code completion for JPA queries. However, JPA is an embedded DSL inside string annotations. Does the approach described in the tutorial also apply if one wanted to "sub-tokenize" and provide completion inside a String token of a x-java MIME type?

The answer is, of course, "Yes!" To set up a scenario, I recreated my code completion plugin so that it now ONLY works for Java parameters. I.e., as you can see in the screenshot below (more or less), whenever I invoke code completion and ONLY IF the cursor is on a parameter (or in the place where a parameter SHOULD BE defined), will my custom items be displayed in the code completion box:

The three high level clues to understanding how to do this are provided by the following:

Especially the last of the above is extremely relevant (it is from Sandip's great blog), it taught me everything I now know in this case, but the other two should be read first (especially the first, don't worry too much about the second, just be aware that it is there and can be useful when actually implementing a scenario).

In short, starting from the sample that the code completion tutorial provides, you need to do the following:

  1. Change the layer.xml file so that the code completion plugin will apply to Java files instead of HTML files. Do this by simply replacing "text/html" with "text/x-java" in the folder hierarchy. Now, anywhere in a Java file, you will see your code completion entries. That's not what you want, you only want to see it in very specific cases.

  2. So, as discussed in Sandip's blog entry, you need to create a class that extends CaretAwareJavaSourceTaskFactory:

    import org.netbeans.api.java.source.CancellableTask;
    import org.netbeans.api.java.source.CompilationInfo;
    import org.netbeans.api.java.source.JavaSource.Phase;
    import org.netbeans.api.java.source.JavaSource.Priority;
    import org.netbeans.api.java.source.support.CaretAwareJavaSourceTaskFactory;
    import org.openide.filesystems.FileObject;
    
    public class CountriesCompletionJavaSourceTaskFactory extends CaretAwareJavaSourceTaskFactory {
        
        public CountriesCompletionJavaSourceTaskFactory() {
            super(Phase.ELEMENTS_RESOLVED, Priority.LOW);
        }
    
        public CancellableTask<CompilationInfo> createTask(FileObject fileObject) {
            return new CountriesCompletionTask(this, fileObject);
        }
        
    }

  3. Next, register the above class in META-INF/Services, for the "org.netbeans.api.java.source.JavaSourceTaskFactory" interface.

  4. Then create the task that you see referred to above, i.e., "CountriesCompletionTask", which exists for no other reason than to toggle a boolean whenever the required element is found under the cursor:

    import com.sun.source.util.TreePath;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.ElementKind;
    import org.netbeans.api.java.source.CancellableTask;
    import org.netbeans.api.java.source.CompilationInfo;
    import org.openide.awt.StatusDisplayer;
    import org.openide.filesystems.FileObject;
    
    public class CountriesCompletionTask implements CancellableTask<CompilationInfo> {
    
        private CountriesCompletionJavaSourceTaskFactory 
                countriesCompletionJavaSourceTaskFactory;
        private FileObject fileObject;
        private boolean canceled;
    
        //Toggle whether code completion should be shown:
        private static boolean showCC = false;
        
        public static boolean isShowCC() {
            return showCC;
        }
        
        public static void setShowCC(boolean showCC) {
            CountriesCompletionTask.showCC = showCC;
        }
        
        //Constructor:
        CountriesCompletionTask(CountriesCompletionJavaSourceTaskFactory
                countriesCompletionJavaSourceTaskFactory, FileObject fileObject) {
            this.countriesCompletionJavaSourceTaskFactory = 
                    countriesCompletionJavaSourceTaskFactory;
            this.fileObject = fileObject;
            
        }
    
        public void run(CompilationInfo compilationInfo) {
    
            //Find the TreePath for the caret position:
            @SuppressWarnings("static-access")
            TreePath tp =
                    compilationInfo.getTreeUtilities().pathFor(
                    countriesCompletionJavaSourceTaskFactory.getLastPosition(fileObject));
    
            //If cancelled, return:
            if (isCancelled()) {
                return;
            }
    
            //Get Element:
            Element element = compilationInfo.getTrees().getElement(tp);
    
            //If cancelled, return:
            if (isCancelled()) {
                return;
            }
    
            if (element != null) {
                //If the element is a parameter...
                if (element.getKind() == ElementKind.PARAMETER) {
                    //...toggle the boolean to show the code completion:
                    showCC = true;
                    StatusDisplayer.getDefault().setStatusText("" +
                            "You can invoke code completion for \\'" 
                            + element.getSimpleName()+"\\'");
                }
            }
        }
    
        public final synchronized void cancel() {
            canceled = true;
        }
    
        protected final synchronized boolean isCancelled() {
            return canceled;
        }
    
    }

    So, Casper, you'd need to identify the element in the code that you want to deal with. I.e., instead of a parameter, you'd look for a certain kind of string for your JPA queries. Here's something I've been playing with, although it doesn't get into the strings:

    Refer to the documents referenced earlier for details.

  5. Finally, in the CompletionProvider class, which is discussed at length in the code completion tutorial, check whether the boolean has been switched or not and, if so, show the code completion box. Once shown, set the boolean back to false so that it is back in its default state. All that's done in the lines in bold below:

    import java.util.Locale;
    import javax.swing.text.Document;
    import javax.swing.text.JTextComponent;
    import org.netbeans.spi.editor.completion.CompletionProvider;
    import org.netbeans.spi.editor.completion.CompletionResultSet;
    import org.netbeans.spi.editor.completion.CompletionTask;
    import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery;
    import org.netbeans.spi.editor.completion.support.AsyncCompletionTask;
    
    public class CountriesCompletionProvider implements CompletionProvider {
    
        public CompletionTask createTask(int i, final JTextComponent jTextComponent) {
    
            
            if (i != CompletionProvider.COMPLETION_QUERY_TYPE) {
                return null;
            }
    
            return new AsyncCompletionTask(new AsyncCompletionQuery() {
    
                @Override
                protected void query(CompletionResultSet completionResultSet, final Document document, int caretOffset) {
    
                    //Iterate through the available locales
                    //and assign each country display name
                    //to a CompletionResultSet:
                    Locale[] locales = Locale.getAvailableLocales();
                    for (int i = 0; i < locales.length; i++) {
                        final Locale locale = locales[i];
                        final String country = locale.getDisplayCountry();
                        if (!country.equals("") && CountriesCompletionTask.isShowCC()) {
                            completionResultSet.addItem(new CountriesCompletionItem(country, caretOffset));
                        }
                    }
                    completionResultSet.finish();
                    CountriesCompletionTask.setShowCC(false);
                }
            }, jTextComponent);
    
        }
    
        public int getAutoQueryTypes(JTextComponent arg0, String arg1) {
            return 0;
        }
    
    }

And that's all. Now you can have a code completion box for very specific elements within your Java code.

About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« August 2008 »
SunMonTueWedThuFriSat
     
2
4
5
6
7
11
14
17
20
23
27
29
30
31
      
Today