25

I'm using the ACE editor for interactive JavaScript editing. When I set the editor to JavaScript mode, ACE automatically determines if the code is valid or not, with an error message and line number highlighted when it's not.

During the change event handler, I want to detect if ACE thinks the code is valid or not before I attempt to eval() it. The only way I thought that I might do it is:

var jsMode = require("ace/mode/javascript").Mode;
var editor = ace.edit('mycode'), edEl = document.querySelector('#mycode');
editor.getSession().setMode(new jsMode);
editor.getSession().on('change',function(){
  // bail out if ACE thinks there's an error
  if (edEl.querySelector('div.ace_gutter-cell.ace_error')) return;
  try{
    eval(editor.getSession().getValue());
  }catch(e){}
});

However:

  1. Leaning on the presence of an element in the UI with a particular class seems awfully fragile, but more importantly,
  2. The visual update for parsing occurs after the change callback occurs.

Thus, I actually have to wait more than 500ms (the delay before the JavaScript worker kicks in):

editor.getSession().on('change',function(){
  setTimeout(function(){
    // bail out if ACE thinks there's an error
    if (edEl.querySelector('div.ace_gutter-cell.ace_error')) return;
    try{
      eval(editor.getSession().getValue());
    }catch(e){}
  },550); // Must be longer than timeout delay in javascript_worker.js
});

Is there a better way, something in an undocumented API for the JS mode, to ask whether there are any errors or not?

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • I don't really know much about ACE but could you explain why you are using eval? – hradac Feb 29 '12 at 20:51
  • @hradac Better than that, [I'll show you](http://phrogz.net/JS/d3-playground/) the (in progress) work. – Phrogz Feb 29 '12 at 21:38
  • I'm doing something similar and am hoping for an answer too. Eval is too expensive to run every time. – AndrewKS Mar 30 '12 at 02:41

4 Answers4

29

The current session fires onChangeAnnotation event when annotations change.

after that the new set of annotations can be retrieved as follows

var annotations = editor.getSession().getAnnotations();

seems to do the trick. It returns a JSON object which has the row as key and an array as value. The value array may have more than one object, depending on whether there are more than one annotation for each row.

the structure is as follows (copied from firebug –for a test script that I wrote)

// annotations would look like
({

82:[
    {/*annotation*/
        row:82, 
        column:22, 
        text:"Use the array literal notation [].", 
        type:"warning", 
        lint:{/*raw output from jslint*/}
    }
],

rownumber : [ {anotation1}, {annotation2} ],

...

});

so..

editor.getSession().on("changeAnnotation", function(){

    var annot = editor.getSession().getAnnotations();

    for (var key in annot){
        if (annot.hasOwnProperty(key))
            console.log("[" + annot[key][0].row + " , " + annot[key][0].column + "] - \t" + annot[key][0].text);
    }

});

// thanks http://stackoverflow.com/a/684692/1405348 for annot.hasOwnProperty(key) :)

should give you a list of all annotations in the current Ace edit session, when the annotations change!

Hope this helps!

hkrish
  • 1,259
  • 1
  • 10
  • 11
  • This is nice information, but it does not help detect the problem immediately: the annotations (including errors) are not available in the `change` callback for the editor; you have to wait until the asynchronous validation occurs. – Phrogz May 19 '12 at 17:43
  • Edited the answer! Hope it helps now! – hkrish May 20 '12 at 17:41
  • The getAnnotations() format has changed... it is now an array of objects. – DrFriedParts Dec 24 '12 at 01:48
  • 3
    It should be `annot[key].row`, not `annot[key][0].row`. – Onur Yıldırım Mar 03 '13 at 22:58
  • @OnurYILDIRIM the Annotations object has an array "rownumber : [ {anotation1}, {annotation2} ]" for every row ;see above. Since multiple annotations are possible per row. (Or did the API change now?) edit: I didn't see DrFriedParts' comment above. I will check it and correct it. – hkrish Mar 04 '13 at 10:09
  • Hey mate you never edited your post :'), Onur's comment should help you – SamHoque May 31 '21 at 15:00
3

I found a solution that is probably faster than traversing the DOM. The editor's session has a getAnnotations method you can use. Each annotation has a type that shows whether they are an error or not.

Here is how I set my callback for the on 'change'

function callback() {
    var annotation_lists = window.aceEditor.getSession().getAnnotations();
    var has_error = false;

    // Unfortunately, you get back a list of lists. However, the first list is
    //   always length one (but not always index 0)
    go_through:
    for (var l in annotation_lists) {
        for (var a in annotation_lists[l]) {
            var annotation = annotation_lists[l][a];
            console.log(annotation.type);
            if (annotation.type === "error") {
                has_error = true;
                break go_through;
            }
        }
    }

    if (!has_error) {
        try {
            eval(yourCodeFromTextBox);
            prevCode = yourCodeFromTextBox;
        }
        catch (error) {
            eval(prevCode);
        }
    }
}

As far as I know, there are two other types for annotations: "warning" and "info", just in case you'd like to check for those as well.

I kept track of the pervious code that worked in a global (well, outside the scope of the callback function) because often there would be errors in the code but not in the list of annotations. In that case, when eval'ing the errored code, it would be code and eval the older code instead.

Although it seems like two evals would be slower, it seems to me like the performance is no that bad, thus far.

AndrewKS
  • 3,603
  • 2
  • 24
  • 33
2

I found you can subscribe worker events in Ace 1.1.7:

For javascript code, subscribe 'jslint' event:

session.setMode('ace/mode/javascript}');
session.on('changeMode', function() {
  if (session.$worker) {
    session.$worker.on('jslint', function(lint) {
      var messages = lint.data, types;
      if (!messages.length) return ok();
      types = messages.map(function(item) {
        return item.type;
      });
      types.indexOf('error') !== -1 ? ko() : ok();
    });
  }
});

For JSON code, subscribe 'error' and 'ok' event:

session.setMode('ace/mode/json');
session.on('changeMode', function() {

  // session.$worker is available when 'changeMode' event triggered
  // You could subscribe worker events here, whatever changes to the
  // content will trigger 'error' or 'ok' events.

  session.$worker.on('error', ko);
  session.$worker.on('ok', ok);
});
mios
  • 86
  • 4
2

Ace uses JsHint internally (in a worker) and as you can see in the file there is an event emitted:

this.sender.emit("jslint", lint.errors);

You can subscribe to this event, or call the JSHint code yourself (it's pretty short) when needed.

Jan Jongboom
  • 26,598
  • 9
  • 83
  • 120
  • 1
    Subscribing to the event sounds like a good idea, so that I don't parse it twice for each edit, and also because `require("../narcissus/jsparse")` returns `null` for me. Any idea on what object that session is emitted? The editor? The session? Editing your answer with code showing how to subscribe would definitely win you the accept. :) – Phrogz Mar 02 '12 at 17:45
  • Any suggestions on what to register on? – Phrogz Mar 19 '12 at 16:08