Why Emacs is better editor - a case study for javascript developer

  |   Source

UPDATED: <2014-01-15 Wed>

Let's see an example in real life development.

Note

For people who does not get the key points of this article, here are the points:

  • Emacs has an embedded javascript interpreter which I extend a little bit.
  • Other editors just use external tools or regular expression to do the semantic analysis.
  • The difference of above two is bigger than the difference between machine gun and spear.

Problem

I'm maintaining a big javascript file with thousands of lines of legacy code.

My most urgent issue is how to list all the functions in that javascript file and jump to the definition of specific function easily.

The modern enterprise application usually define the javascript function in some complex data structure. So most editors are not good at listing the functions.

To understand what "complex" means, let's check some code from real world application:

$(el.completeRegistrationForm).validate({
    ignore: " :hidden",
    rules : {
        password : {
            required : function () { return $(el.password).is(":visible"); }
        },
        accountNumber : {
            required : function () {
                return $(el.accountNumber).is(":visible");
            },
            digits : true
        }
        // ... I skipped next 200 lines which are similar to above lines

    },
    messages : {
        password: {
            required : "Please input a valid password"
        },
        accountNumber: {
            required : "Please provide a valid account number",
            digits : "Please enter only digits",
        }
        // ... I skipped next 200 lines which are similar to above lines
    }
});

Most editors like Sublime Text 3 cannot display the javascript functions with meaningful context in this case. All you can see is only a bunch of functions with name "required".: sublime-functions.png

Solution

Emacs has a js2-mode which is basically a javascript interpreter written in lisp. It's created by Steve Yegge and now maintained by mooz.

Since js2-mode is a interpreter, basically it can do anything you want. The key point is to understand the Abstract Syntax Tree (AST) defined in js2-mode.

Here is my patch to make js2-mode display the list of functions with correct context:

commit 56ed89bf18a6b58fd4620056288ea2ab52bd4d77
Author: Chen Bin <chenbin.sh@gmail.com>
Date:   Sun Dec 15 18:18:06 2013 +1100

    more hint for orphan function

diff --git a/js2-imenu-extras.el b/js2-imenu-extras.el
index e8e15a5..17bf158 100644
--- a/js2-imenu-extras.el
+++ b/js2-imenu-extras.el
@@ -174,6 +174,39 @@ prefix any functions defined inside the IIFE with the module name."
          (js2-imenu-record-module-pattern node)))
        t))))

+(defun js2-imenu-get-parent-keyname-list (node)
+  "get the list of keys of parent of node
+for example, for javascript code, {rules:{ password {required: function(){}}}}
+the return will be '(rules password)."
+  (let ((rlt '())
+        (n node))
+    (while (setq n (js2-imenu-parent-prop-node n))
+      (add-to-list 'rlt (js2-prop-node-name (js2-object-prop-node-left n)))
+      )
+    rlt
+    )
+  )
+
+(defun js2-imenu-parent-prop-node (node)
+  "for javascript code: parent-key-name:{ required:function(){} }
+we need know the  parent-key-name.
+ step1, 'required:function(){}' is the js2-object-prop-node
+ step2, '{ required:function(){} }' is the js2-object-node
+ step3, 'parent-key-name:{ required:function(){} }' is js2-object-prop-node
+"
+  (let (p2 p3)
+    ;; step 2
+    (setq p2 (js2-node-parent node))
+    ;; step 3
+    (when (and p2 (js2-object-node-p p2))
+      (setq p3 (js2-node-parent p2))
+      (if (and p3 (js2-object-prop-node-p p3))
+        p3
+        )
+      )
+    )
+  )
+
 (defun js2-imenu-record-orphan-function (node)
   "Record orphan function when it's the value of NODE.
 NODE must be `js2-object-prop-node'."
@@ -181,10 +214,15 @@ NODE must be `js2-object-prop-node'."
     (let ((fn-node (js2-object-prop-node-right node)))
       (unless (and js2-imenu-function-map
                    (gethash fn-node js2-imenu-function-map))
-        (let ((key-node (js2-object-prop-node-left node)))
-          (js2-record-imenu-entry fn-node
-                                  (list js2-imenu-other-functions-ns
-                                        (js2-prop-node-name key-node))
+        (let ((key-node (js2-object-prop-node-left node))
+              (parent-prop-node (js2-imenu-parent-prop-node node))
+              mylist
+              )
+          (setq mylist (append (js2-imenu-get-parent-keyname-list node)
+                               (list (js2-prop-node-name key-node))
+                               ))
+          (add-to-list 'mylist js2-imenu-other-functions-ns)
+          (js2-record-imenu-entry fn-node mylist
                                   (js2-node-abs-pos key-node)))))))

 (defun js2-imenu-record-module-pattern (node)

I already submitted the patch to mooz so everyone will enjoy this feature in the future.

Here is the screen shot of emacs in old js2-mode, the UI is based on Imenu Mode and Helm: emacs-functions.png

The screen shot after we applying the above patch: emacs-functions-improved.png

Summary

That's an example of beauty of Emacs.

It gives you freedom and power. You can base your work on the top geeks like Steve Yegge and mooz. Just a few lines of lisp code to kick ass.

Update

My patch is incorporated into js2-mode since version 20140114.

After installing js2-mode, you need paste one line setup into your ~/.emacs,

(js2-imenu-extras-mode)

The UI to display the candidate is from package Helm.

The Helm version should be 20140125.1101 or higher, you can install Helm from MELPA.

After installing Helm, you can use command M-x helm-imenu to show the list of functions to jump to. Here is the screen shot how I use imenu in my hello2.js:

helm-imenu-and-js2-mode-nq8.png

UPDATE: I suggest using counsel-imenu from Counsel instead of Helm.

Comments powered by Disqus