X

Geertjan's Blog

  • August 24, 2007

How to Write a Groovy Editor (Part 2)

Geertjan Wielenga
Product Manager
I've come to believe that by far the most important feature of an editor is its syntax coloring. Forget all the rest, even code completion—the ability to distinguish one piece of syntax from another is the fundamental demand that an editor should fulfil. Yesterday we made some progress in providing this for Groovy. At least, I discussed the main principles and applied them to the import statement and the Groovy keywords. We started with the Java NBS file and began whittling it down to the more relaxed Groovy standard. However, this is not an approach that will ultimately be satisfactory. Really, we need to take the official token and grammar definitions from Groovy (in ANTLR) and work with that. Anything else, such as my "lets start with Java NBS and somehow fumble our way to Groovy NBS" approach, is just too haphazard and random. I have several resources that will make for a better end result, based on ANTLR definitions, and I will look at those in one of the next parts of this series.

For now, even more important at this stage than complete and absolute syntax coloring, is the ability to run Groovy scripts inside the IDE. Not just that, though. We also need to write the Groovy output to the NetBeans Output window. Kind of exactly like this:

So, you should be able to right-click in the Groovy editor, choose 'Run Groovy Script' and then the Output window contains the results, as shown above. (Just for laughs, I started adding a drag-and-drop code snippet palette to the editor, as can be seen above, which takes about 3 surprisingly simple steps to create, as you will see later in this blog entry.)

However, when things go wrong with the Groovy script, we want the error output to appear as hyperlinks in the Output window, via our trusty old friend OutputListener, the tireless Tonto to any lone NetBeans editor. As a result, we should be able to see a hyperlink for each error, in the Output window, as below:

Now we will, step by step, create all the above functionality...

  1. First we need to create the "Run Groovy Script" menu item in the Groovy editor. "Ah," you are now probably thinking (if you are familiar with the NetBeans Platform), "that means, we should use the New Action wizard to create a CookieAction implementation. The CookieAction.enable method could then be used as a filter, if needed." Well, nice try, but wrong. Schliemann lets you declare ACTIONs, which you implement in Java code. So, to see how this works, add this to your Groovy.nbs:

    ACTION:run_script: {
    name:"LBL_Run";
    performer:org.netbeans.modules.mygroovyeditor.Groovy.performRun;
    enabled:org.netbeans.modules.mygroovyeditor.Groovy.enabledRun;
    explorer:"false";
    }

    Then, in the bundle file add this:

    LBL_Run=Run Groovy Script

    At this point, one thing you might be wondering is: "How will the NBS file find the value of the 'LBL_Run' key?" Well, in step 4 of yesterday's blog entry, we created this BUNDLE declaration in the Groovy.nbs file:

    BUNDLE "org.netbeans.modules.mygroovyeditor.Bundle"

    So, via that BUNDLE declaration, we will get our label on the menu item.

  2. The most important parts of the ACTION declaration are the 'performer' and 'enabled' keys. The latter determines when the menu item will be enabled. The former determines what happens when the menu item is clicked. So they're like the methods enable and actionPerformed. We already have a class called Groovy.java, because that's where we put the code completion code yesterday. Let's now add the code that will determine whether the menu item is enabled:

    public static boolean enabledRun(ASTNode node, JTextComponent comp) {
    try {
    ClassLoader cl = Groovy.class.getClassLoader();
    Class managerClass = cl.loadClass("javax.script.ScriptEngineManager");
    return managerClass != null;
    } catch (ClassNotFoundException ex) {
    return false;
    }
    }

  3. Now, in the performRun method, before we do anything else, let's first get to a point where we have the Groovy scripting engine available to us. At the end of this step, we will print the available script engines to the Output window, and we must be able to see that Groovy is included, because by default only the other two are available:

    First, our performRun method:

    public static void performRun(final ASTNode node, final JTextComponent comp) {
    RequestProcessor.getDefault().post(new Runnable() {
    public void run() {
    Groovy gr = new Groovy();
    ScriptEngineManager factory = new ScriptEngineManager();
    List se = factory.getEngineFactories();
    gr.printCollection(se);
    }
    });
    }

    And here is the helper method:

    private void printCollection(List c) {
    @SuppressWarnings(value = "unchecked")
    Iterator i = c.iterator();
    while (i.hasNext()) {
    ScriptEngineFactory item = i.next();
    System.out.println("Engine name: " +
    item.getEngineName() + ",\\nEngine version: " +
    item.getEngineVersion() + ",\\nLanguage name: " +
    item.getLanguageName() + ",\\nLanguage version: " +
    item.getLanguageVersion() + "\\n");
    }
    }

    I got the above code from this helpful blog. (Make sure that you set the enabledRun method to simply return true, at this stage, because if you do not have the Groovy scripting engine available, the menu item itself will not be enabled and so you will not be able to click it to produce the results in the Output window!) When you now install the editor, open a Groovy file, and click the 'Run Groovy Script' menu item, the Output window should show you that you have two script engines available, neither of them being Groovy. You need to create a library wrapper module for groovy-engine.jar, with a dependency on another library wrapper module that contains groovy-all-1.0.jar. These library wrapper modules then need to be declared as dependencies of the MyGroovyEditor module. Once you've done all that, you've put the Groovy scripting engine functionality on the editor's classpath and then, when you click the menu item again, you will see that the information about the Groovy scripting engine will be printed to the Output window. Do not continue with these instructions until you are able to see the info about the Groovy scripting engine in the Output window, because as long as it isn't there, you do not have the Groovy scripting engine available to your editor.

  4. Next we will rewrite the performRun method to run the current Groovy script, and print hyperlinks to the
    Output window on failure. The cool thing about JSR 223 is that, because of the script engine standardization, we can simply reuse code from the same method in the JavaScript.java file, which seeks to do the same thing. So, without further ado, here it is, with comments interspersed, which will hopefully explain things here and there:

    public static void performRun(final ASTNode node, final JTextComponent comp) {
    RequestProcessor.getDefault().post(new Runnable() {
    InputOutput io;
    ClassLoader cl;
    FileObject fo;
    public void run() {
    try {//Get our Groovy script engine:
    ScriptEngineManager factory = new ScriptEngineManager();
    ScriptEngine engine = factory.getEngineByExtension("groovy");//Get our document, from the component
    //thst we received from the ACTION declaration,
    //and the name, for display in the Output window tab:

    Document doc = comp.getDocument();
    DataObject dob = NbEditorUtilities.getDataObject(doc);
    String name = dob.getPrimaryFile().getNameExt();//Load the script engine class:
    cl = Groovy.class.getClassLoader();
    Class engineClass = cl.loadClass("javax.script.ScriptEngine");//Open the Output window,
    //and set the label in the tab
    //to Run + the name of the file:

    io = IOProvider.getDefault().getIO("Run " + name, false);//Get the script context class
    //and use it to write to
    //the Output window:

    Class contextClass = cl.loadClass("javax.script.ScriptContext");
    Method setWriter = contextClass.getMethod("setWriter", new Class[]{Writer.class});
    Method setErrorWriter = contextClass.getMethod("setErrorWriter", new Class[]{Writer.class});
    Method setReader = contextClass.getMethod("setReader", new Class[]{Reader.class});
    Method getContext = engineClass.getMethod("getContext", new Class[]{});
    Object context = getContext.invoke(engine, new Object[]{});
    setWriter.invoke(context, new Object[]{io.getOut()});
    setErrorWriter.invoke(context, new Object[]{io.getErr()});
    setReader.invoke(context, new Object[]{io.getIn()});//Select the Output window:
    io.select();//Get the 'eval' method
    //and then invoke it, using the text
    //from the received component:

    Method eval = engineClass.getMethod("eval", new Class[]{String.class});
    Object o = eval.invoke(engine, new Object[]{doc.getText (0, doc.getLength ())});//If the Groovy script fails...
    } catch (InvocationTargetException ex) {
    try {
    Class scriptExceptionClass = cl.loadClass("javax.script.ScriptException");
    if (ex.getCause() != null && scriptExceptionClass.isAssignableFrom
    (ex.getCause().getClass())) {
    if (io != null) {
    String msg = ex.getCause().getMessage();
    int line = 0;
    if (msg.startsWith("sun.org.mozilla")) {
    msg = msg.substring(msg.indexOf(':') + 1);
    msg = msg.substring(0, msg.lastIndexOf('(')).trim() + " "
    + msg.substring(msg.lastIndexOf(')') + 1).trim();
    try {
    line = Integer.valueOf(msg.substring(msg.
    lastIndexOf("number") + 7)); //NOI18N
    } catch (NumberFormatException nfe) {
    }
    }//Implement OutputListener, for hyperlinks:
    io.getOut().println(msg, new MyOutputListener(fo, line));
    } else {
    Exceptions.printStackTrace(ex);
    }
    }
    } catch (Exception ex2) {
    Exceptions.printStackTrace(ex2);
    }
    } catch (Exception ex) {
    Exceptions.printStackTrace(ex);
    }
    }
    });
    }

  5. Next, create a class called MyOutputListener, let it implement OutputListener and, for now, just let the IDE generate three method stubs that we can fill out some other time. So, for now, when you click a link in the Output window, you'll receive an error message because you haven't implemented the method yet.

  6. And what about the palette? Just download it here, add it to the module suite, and then tweak a few things. For example, the palette was written for text/x-java, so change that to text/x-groovy. You might also want to do some refactoring, to make the class names, and so on, Groovy-oriented rather than Java-oriented. Then change the sample snippet to generate the code that you want to have generated. Now your module suite should be as follows:

    It really is cool that one can now (from 6.0 onwards) create a palette in one module, and (via MIME-type registration in the layer) make it available to an editor defined in a different module. Potentially, your users do not want to have a palette in their Groovy editor. Now they don't have to have it. They simply would not install it. Plus, one engineer could 'own' the palette module, while another engineer could 'own' the editor module, thus allowing for a very clean separation of work areas. Ah, but anyone reading this blog, and who has got this far in this particular blog entry, should be aware of the benefits of module-based development already... :-)

When you now install the editor again, you should see the same things as shown in the two screenshots at the start of this blog entry. Hurray! Thank you, JSR 223 and thank you Schliemann. Now there's scripting support for Groovy in NetBeans IDE.

In other news. My colleague Rohan, from Bangalore, wrote me with this cool news: "We, at Sun India, have launched a unique contest called the CODE FOR FREEDOM CONTEST to encourage students in India to adopt open source technologies and contribute to them. You can find more details here. NetBeans is part of the open source technologies covered by this contest." Hurray!

Join the discussion

Comments ( 8 )
  • Mohamed Monday, August 27, 2007

    Hi Geertjan, Howdy?

    Hope you can help me with this, I'm trying to use Schliemann to add editing support for Torque Scripting Language (Torque is a game engine). Torque Script has a C like syntax, so I'm trying to reuse the javascript.nbs. Only changes I made until now is adding local and global variables, which are similar to the identifier rule, but has % precedes local variable and $ precedes global.

    TOKEN:local_variable:(

    "%"["a"-"z" "A"-"Z" "_"]

    ["a"-"z" "A"-"Z" "0"-"9" "_"]\*

    )

    TOKEN:global_variable:(

    "$"["a"-"z" "A"-"Z" "_"]

    ["a"-"z" "A"-"Z" "0"-"9" "_"]\*

    )

    now the switch block grammar rule is like Java and Javascript but without the break;

    SwitchStatement = "switch" "(" Expression ")" "{" (CaseClause)\* [DefaultClause] (CaseClause)\* "}";

    CaseClause = "case" Expression ":" (Statement)\*;

    DefaultClause = "default" ":" (Statement)\*;

    LabelledStatement = <identifier> ":" Statement;

    PrimaryExpression = PrimaryExpressionInitial | ObjectLiteral;

    PrimaryExpressionInitial = "null" | "true" | "false" | <number> | <string> |

    <local_variable> | <global_variable> | "(" Expression ")" |

    ArrayLiteral | FunctionDeclaration | FunctionCall;

    when I try to test this, the case clause inside the switch block is always highlighted as syntax error.

    not sure if I made this clear, but would you please guide me to find the problem?

    Thanks,

    Mohamed


  • Geertjan Monday, August 27, 2007

    Hi Mohamed, are you sure there's a problem there? I tried pasting the above in my own NBS file, and there's no error marking at all. If you like, you can send the whole NBS file to me at geertjan DOT wielenga AT sun DOT com and I (and maybe others if needed) will look at it. Sounds great, by the way, Torque support -- keep going and don't give up!


  • Mohamed Thursday, August 30, 2007

    Hi Greetjan, thanks a lot for your reply, the error is not in NBS file, it's in the Torque Script test file. I've sent the project including the NBS and the Torque Script sample file, I hope files was arrived to your email.


  • Geertjan Thursday, August 30, 2007

    Sorry, I haven't received it. Make sure it is in a ZIP file, not in a RAR file or something different, otherwise it will get rejected by my mail server. Don't know why.


  • Alejandro T&eacute;llez Saturday, October 25, 2008

    Hi Geertjan, I used to ask this questions to Roumen, but, apparently he's now at opensolaris evangelization, I hope not to bother u, I believe u r quite busy right now.

    I'm a developer and a teacher here in Mexico, now I have been assigned to and Algorithms class, I used to teach something more advanced like Java EE, but I want to use pseudo-code before I introduce my students to the whole wolrd of Java and NetBeans, I found something interesting with this article of yours, and an Idea hit me. So i wrote a Schliemann's module for a pseudo-code "programming", almost everything works fine, but I have some problems with expressions and operators, I took Java.nbs (as u recomended, but now with NB6.5 seems replaced with lexer?). Is there some kind of debuger, or something I could use, 'cause I've been strugling with this problem for several weeks.

    Thnx in advance for your attention


  • Alejandro T&eacute;llez Saturday, October 25, 2008

    I forgot to mention, I used Java.nbs as a template, 'cause I need the exact same logic for operators and expressions, here I post some of the expressions, that are marked as errors,

    x = 25 \* (z+y)

    x = z\*y

    In the first one, NB65 marks with an error underline the \* operator and the ( symbol. It's funny, because the second one also marks error at the \* operator. But with + or - operators everything works fine.

    I've put the nbs file at this url, also I've created a new filetype, with extension pcode, there r three possible options, an empty pcode file, and algorithm file, and finally a class file.

    http://idisk.mac.com/atellezf-Public/pseudo-code

    Thnx again for your time


  • Geertjan Saturday, October 25, 2008
  • bob Monday, August 15, 2011

    scriptExceptionClass.isAssignableFrom(ex.getCause().getClass())

    should be written as

    scriptExceptionClass.isInstance(ex.getCause())


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha
Oracle

Integrated Cloud Applications & Platform Services