Why Emacs is a better editor, part two

  |   Source

If you are impatient, jump to the "Quick Start" and paste my setup into your ~/.emacs. That's all you need to do!

No extra setup needed! Then keep using your js2-mode happily, as if nothing happened. ;)

1 Problem

In my previous article Why Emacs is better editor - a case study for javascript developer, I proved that Emacs is better than Sublime Text.

So for this so-called "Goto Symbol" feature, Emacs wins.

It's because we use a plugin js2-mode. It's actually a javascript parser which creates the symbols from AST.

But in real world, regular expression is better, sometimes.

In modern MVC javascript frameworks (Angular, for example), you will meet below code,

app.controller('MyController', function ($scope, $http) {
  // ...
});

As you can see, using regular expression to extract the string "MyController" is more versatile and simpler.

2 Solution

My latest contribution to js2-mode solves this problem perfectly. It combines the powers of AST and regular expression.

The js2-mode will integrate this feature soon. I will notify you when next version is ready.

Let's cut off the boring technical details and see the demo,

For a simple hello.js with below content,

function helloworld() {
  console.log('hello called');
}

function test() {
  console.log('test called');
}

app.controller('MyController', function ($scope, $http) {
  console.log('MyController registered');

  var that = this;

  $scope.test1 = function() {
    console.log('$scope.test called');
  };

  $scope.hello = function() {
    console.log('$scope.hello called');
  };

  $scope.fn1 = function () {

    function test() {
      console.log('hello world');
    };
    console.log('hello');
  };
});

Emacs: 3ebe6588-61a5-11e4-9e9f-e6eb8218e2ee.png

Sublime3 (build 3047): c33b8fee-61ae-11e4-8072-671935b68d6b.png

Please note Emacs displays two functions with the same name "test" correctly!

BTW, my previous "Why Emacs is better" article got many feedbacks from Sublime users.

One feedback is that my comparison is not fair because I'm comparing Emacs plugin with naked Sublime. Though I did some research before writing the article, I could be wrong. Please enlighten me if you know such Sublime plugins.

Another valuable feedback is that native Sublime provides better experience out of the box for junior developers. I admit that's a good point. But Emacs provides many awesome choices out of the box if junior guys start from setups of the masters (like Steven Purcell).

Sublime users also argue that Sublime3 uses Python. Python is a better programming language. I'm qualified to answer this question because I wrote some large commercial Python application when I worked in Kodak R&D. First version I used was v2.2. So I've got about 10 years experience in Python. And I write lots of Emacs Lisp code these days. My opinion is that both languages are good enough as DSL for text editors. In Python, you can use OO. In Emacs Lisp, you can treat function as object and there are Macros and Advising. Both languages have enough widgets to shoot yourself in the foot. Python is surely newbie-friendly. But number of newbies doesn't matter in high-end rival.

3 Quick Start

I'm still discussing with the js2-mode maintainer Dmitry Gutov about the best way to merge my patch.

Dmitry Gutov updated the algorithm to parse the imenu items by walking the AST instead. It's better than my REGEX hacking because AST could show the context of the function.

But my patch is still useful for extract strings from modern JS framework, as I've shown you in Angular example. I'm just updating my pull request to be compatible with the new AST walk algorithm.

In the meantime, you can paste below code into your ~/.emacs before the patch is officially merged.

;; below regex list could be used in both js-mode and js2-mode
(setq javascript-common-imenu-regex-list
      '(("Controller" "\.controller( *'\\([^']+\\)" 1)
        ("Filter" "\.filter( *'\\([^']+\\)" 1)
        ("Factory" "\.factory( *'\\([^']+\\)" 1)
        ("Service" "\.service( *'\\([^']+\\)" 1)
        ("Directive" "\.directive( *'\\([^']+\\)" 1)
        ("Event" "\.\$on( *'\\([^']+\\)" 1)
        ("Config" "\.config( *function *( *\\([^\)]+\\)" 1)
        ("Config" "\.config( *\\[ *'\\([^']+\\)" 1)
        ("OnChange" " *\$('\\([^']*\\)').*\.change *( *function" 1)
        ("OnClick" " *\$('\\([^']*\\)').*\.click *( *function" 1)
        ("Watch" "\.\$watch( *'\\([^']+\\)" 1)
        ("Function" "function\\s-+\\([^ ]+\\)(" 1)
        ("Function" " \\([^ ]+\\)\\s-*=\\s-*function\\s-*(" 1)))

;; {{ patching imenu in js2-mode
(setq js2-imenu-extra-generic-expression javascript-common-imenu-regex-list)

(defvar js2-imenu-original-item-lines nil
  "List of line infomration of original imenu items.")

(defun js2-imenu--get-line-start-end (pos)
  (let (b e)
    (save-excursion
      (goto-char pos)
      (setq b (line-beginning-position))
      (setq e (line-end-position)))
    (list b e)))

(defun js2-imenu--get-pos (item)
  (let (val)
    (cond
     ((integerp item)
      (setq val item))

     ((markerp item)
      (setq val (marker-position item))))

    val))

(defun js2-imenu--get-extra-item-pos (item)
  (let (val)
    (cond
     ((integerp item)
      (setq val item))

     ((markerp item)
      (setq val (marker-position item)))

     ;; plist
     ((and (listp item) (listp (cdr item)))
      (setq val (js2-imenu--get-extra-item-pos (cadr item))))

     ;; alist
     ((and (listp item) (not (listp (cdr item))))
      (setq val (js2-imenu--get-extra-item-pos (cdr item)))))

    val))

(defun js2-imenu--extract-line-info (item)
  "Recursively parse the original imenu items created by js2-mode.
The line numbers of items will be extracted."
  (let (val)
    (if item
      (cond
       ;; Marker or line number
       ((setq val (js2-imenu--get-pos item))
        (push (js2-imenu--get-line-start-end val)
              js2-imenu-original-item-lines))

       ;; The item is Alist, example: (hello . 163)
       ((and (listp item) (not (listp (cdr item))))
        (setq val (js2-imenu--get-pos (cdr item)))
        (if val (push (js2-imenu--get-line-start-end val)
                      js2-imenu-original-item-lines)))

       ;; The item is a Plist
       ((and (listp item) (listp (cdr item)))
        (js2-imenu--extract-line-info (cadr item))
        (js2-imenu--extract-line-info (cdr item)))

       ;;Error handling
       (t (message "Impossible to here! item=%s" item)
          )))
    ))

(defun js2-imenu--item-exist (pos lines)
  "Try to detect does POS belong to some LINE"
  (let (rlt)
    (dolist (line lines)
      (if (and (< pos (cadr line)) (>= pos (car line)))
          (setq rlt t)))
    rlt))

(defun js2-imenu--is-item-already-created (item)
  (unless (js2-imenu--item-exist
           (js2-imenu--get-extra-item-pos item)
           js2-imenu-original-item-lines)
    item))

(defun js2-imenu--check-single-item (r)
  (cond
   ((and (listp (cdr r)))
    (let (new-types)
      (setq new-types
            (delq nil (mapcar 'js2-imenu--is-item-already-created (cdr r))))
      (if new-types (setcdr r (delq nil new-types))
        (setq r nil))))
   (t (if (js2-imenu--item-exist (js2-imenu--get-extra-item-pos r)
                                 js2-imenu-original-item-lines)
          (setq r nil))))
  r)

(defun js2-imenu--remove-duplicate-items (extra-rlt)
  (delq nil (mapcar 'js2-imenu--check-single-item extra-rlt)))

(defun js2-imenu--merge-imenu-items (rlt extra-rlt)
  "RLT contains imenu items created from AST.
EXTRA-RLT contains items parsed with simple regex.
Merge RLT and EXTRA-RLT, items in RLT has *higher* priority."
  ;; Clear the lines.
  (set (make-variable-buffer-local 'js2-imenu-original-item-lines) nil)
  ;; Analyze the original imenu items created from AST,
  ;; I only care about line number.
  (dolist (item rlt)
    (js2-imenu--extract-line-info item))

  ;; @see https://gist.github.com/redguardtoo/558ea0133daa72010b73#file-hello-js
  ;; EXTRA-RLT sample:
  ;; ((function ("hello" . #<marker 63>) ("bye" . #<marker 128>))
  ;;  (controller ("MyController" . #<marker 128))
  ;;  (hellworld . #<marker 161>))
  (setq extra-rlt (js2-imenu--remove-duplicate-items extra-rlt))
  (append rlt extra-rlt))

(eval-after-load 'js2-mode
  '(progn
     (defadvice js2-mode-create-imenu-index (around my-js2-mode-create-imenu-index activate)
       (let (extra-rlt)
         ad-do-it
         (setq extra-rlt
               (save-excursion
                 (imenu--generic-function js2-imenu-extra-generic-expression)))
         (setq ad-return-value (js2-imenu--merge-imenu-items ad-return-value extra-rlt))
         ad-return-value))
     ))
;; }}
Comments powered by Disqus