HomeProjectsEditing JavaScript with Emacs — js2-mode

Editing JavaScript with Emacs — js2-mode

I wrote about this topic before.  My small hackish mode attracted a few people for its good indentation, but it still had some issues that were hard to track down.  In short, it's quite impossible to use c-mode's indentation.

In the last few days I played with Steve Yegge's excellent js2-mode and I'll write about my setup here.  js2-mode is different from most other programming modes that you can find in Emacs (and in any other editor for that matter), because it employs a full, solid parser of the language.  Instead of guessing the syntax with buggy regexps, js2-mode actually parses your code into an abstract syntax tree (AST) and can therefore provide complex information about it, not to mention very good syntax highlighting.  For example, js2-mode can and does warn you about syntax errors such as misplaced parens, or missing semicolon, or trailing comma.  Or about undeclared variables — a ”must have” with JavaScript, since it's usually the source of many subtle bugs.

I found js2-mode about an year ago, but indentation was simply unbearable so this is why I didn't use it.  But now I decided to give it a second try.  Indentation can be fixed.

Installation

js2-mode is now included in the Emacs official source tree, and will be available on the next point release.  However you don't have to wait that long.  I recommend you to install it from the SVN code, rather than using a release, because the SVN contains some fixes required by my setup (thanks Steve for promptly including a few fixes that I suggested!).

Installing from SVN is easy:

svn checkout http://js2-mode.googlecode.com/svn/trunk/ js2-mode-read-only
cd js2-mode-read-only
make
cp build/js2-mode.el* ~/emacs/packages/prog-modes/

The above lines will download, build and install js2-mode.  Note that byte compiling (make) isn't necessary, but without it js2-mode is dog slow.  No, it's unusable.

The last line copies js2-mode.el and js2-mode.elc into your Emacs lisp folder.  I keep most Lisp files in ~/emacs/, and particularly programming modes in ~/emacs/packages/prog-modes/.  Replace this path with whatever is your preferred path for storing emacs packages.

Then in your ~/.emacs add the following:

(autoload 'js2-mode "js2-mode" nil t)
(add-to-list 'auto-mode-alist '("\\.js$" . js2-mode))

Espresso-mode

My setup requires espresso-mode for indentation.  As I mentioned, indentation in js2-mode was quite bad and I figured out I could use indentation from some other mode.  Espresso-mode has good indentation.  (In fact, espresso-mode is pretty good itself for editing JavaScript, but js2-mode is like a hundred times better.)

Download espresso.el and save it into your Emacs lisp folder, then add the following into your ~/.emacs:

(autoload 'espresso-mode "espresso")

OK, now you're done for the first part.  Restart Emacs and type M-x customize-group ENTER js2-mode ENTER if you want to configure various stuff in js2-mode.

Fixing indentation

Try editing a JavaScript file now and test if you like the default indentation of js2-mode.  I just did now and it seems to have improved since last year, however it's still not good enough.  For example, pressing TAB at the beginning of a line won't move the caret to the first non-space character if the line is already properly indented—which is what I'd expect after 11 years of Emacs.

In order to fix indentation I defined the following function:

(defun my-js2-indent-function ()
  (interactive)
  (save-restriction
    (widen)
    (let* ((inhibit-point-motion-hooks t)
           (parse-status (save-excursion (syntax-ppss (point-at-bol))))
           (offset (- (current-column) (current-indentation)))
           (indentation (espresso--proper-indentation parse-status))
           node)

      (save-excursion

        ;; I like to indent case and labels to half of the tab width
        (back-to-indentation)
        (if (looking-at "case\\s-")
            (setq indentation (+ indentation (/ espresso-indent-level 2))))

        ;; consecutive declarations in a var statement are nice if
        ;; properly aligned, i.e:
        ;;
        ;; var foo = "bar",
        ;;     bar = "foo";
        (setq node (js2-node-at-point))
        (when (and node
                   (= js2-NAME (js2-node-type node))
                   (= js2-VAR (js2-node-type (js2-node-parent node))))
          (setq indentation (+ 4 indentation))))

      (indent-line-to indentation)
      (when (> offset 0) (forward-char offset)))))

It indents the current line based on espresso-mode.  I handled two exceptions to my own taste, that is I like case labels to be indented to half of the tab width, and I like consecutive variables declared in a single var block to be indented like this:

var foo = "bar",
    baz = 5;

switch (foo) {
    case "bar":
        code();
        break;
}

[ Note that, even though we got away quite easily with a regexp for the case part, properly aligning variables this way is a lot more complex.  It seems easy for the example above and probably a regexp would do, but look at the following:

var foo = "bar",
    stuff = function() {
        var asdf = 1;
        return asdf;
    },
    obj = new Object("var"),
    test = "it works";

Don't try to do this with a regexp. ;-)  Still, see how easy it was using the information that js2-mode provides about the syntax at point?  It does not fail even in complex cases.  js2-mode rules.  Steve rules.

The small downside when doing it with js2-mode instead of regexps is that, when you're typing very fast, it won't work because js2-mode has yet to parse the code in order to know that you're in a variable declaration.  Parsing takes milliseconds, but because it's done with an idle timer, you still have to wait like 0.3 sec. before indentation will work properly. ]

Indent the innermost block

If you used c-mode (which I did, for years) then you know that there's a neat key binding M-C-q which indents the block starting with the paren under the cursor.  The following function implements this generically and can be used in js2-mode:

(defun my-indent-sexp ()
  (interactive)
  (save-restriction
    (save-excursion
      (widen)
      (let* ((inhibit-point-motion-hooks t)
             (parse-status (syntax-ppss (point)))
             (beg (nth 1 parse-status))
             (end-marker (make-marker))
             (end (progn (goto-char beg) (forward-list) (point)))
             (ovl (make-overlay beg end)))
        (set-marker end-marker end)
        (overlay-put ovl 'face 'highlight)
        (goto-char beg)
        (while (< (point) (marker-position end-marker))
          ;; don't reindent blank lines so we don't set the "buffer
          ;; modified" property for nothing
          (beginning-of-line)
          (unless (looking-at "\\s-*$")
            (indent-according-to-mode))
          (forward-line))
        (run-with-timer 0.5 nil '(lambda(ovl)
                                   (delete-overlay ovl)) ovl)))))

Unlike c-indent-exp, my function above does not require the cursor to be over a paren.  It looks up the innermost block using (syntax-ppss) which is a neat function provided by Emacs, and reindents it.  It also highlights the block for half a second—I just learned to use overlays in Elisp and think they're cool. :-p

Then I defined the following js2-mode hook:

(defun my-js2-mode-hook ()
  (require 'espresso)
  (setq espresso-indent-level 8
        indent-tabs-mode nil
        c-basic-offset 8)
  (c-toggle-auto-state 0)
  (c-toggle-hungry-state 1)
  (set (make-local-variable 'indent-line-function) 'my-js2-indent-function)
  (define-key js2-mode-map [(meta control |)] 'cperl-lineup)
  (define-key js2-mode-map [(meta control \;)] 
    '(lambda()
       (interactive)
       (insert "/* -----[ ")
       (save-excursion
         (insert " ]----- */"))
       ))
  (define-key js2-mode-map [(return)] 'newline-and-indent)
  (define-key js2-mode-map [(backspace)] 'c-electric-backspace)
  (define-key js2-mode-map [(control d)] 'c-electric-delete-forward)
  (define-key js2-mode-map [(control meta q)] 'my-indent-sexp)
  (if (featurep 'js2-highlight-vars)
    (js2-highlight-vars-mode))
  (message "My JS2 hook"))

(add-hook 'js2-mode-hook 'my-js2-mode-hook)

If you paste the above lisp code and those two functions in your ~/.emacs (or wherever you want to keep it) and restart Emacs, you should have good indentation and M-C-q for indenting the innermost block.

But wait, there's more nice stuff.  I wrote a minor mode that highlights all occurrences of the variable under cursor — check it out, it's cool.

Comments — add your comment

  • By: Ithai LeviAug 13 (14:24) 2009RE: Editing JavaScript with Emacs — js2-mode §

    Brilliant, thanks!
    Now I can use js2-mode *and* have nice indentation. I think technique should somehow be merged into js2-mode(?)

  • By: JoeOct 28 (19:40) 2009RE[2]: Editing JavaScript with Emacs — js2-mode §

    Hi thanks a lot for this it really helps!

    I have an issue with indenting comments when I use your method.
    I get
    My JS2 hook
    c-lineup-C-comments: Wrong type argument: stringp, nil [7 times]

    I'm not that good with the lisps, do you have any idea what is going on? Do you get this too? Its not too bad but is a little annoying when trying to format jsdoc blocks.

    • By: ivannMay 05 (09:59) 2010RE[3]: Editing JavaScript with Emacs — js2-mode §

      This happen when region have comments like this:
      /*
       * error on this line
       */
      For fix put code below just before (message "My JS2 hook")) in my-js2-mode-hook function:

        ;; fix bug with my-indent-sexp
        (setq c-current-comment-prefix
       (if (listp c-comment-prefix-regexp)
      (cdr-safe (or (assoc major-mode c-comment-prefix-regexp)
        (assoc 'other c-comment-prefix-regexp)))
               c-comment-prefix-regexp))

  • By: BrendenFeb 19 (22:50) 2010RE: Editing JavaScript with Emacs — js2-mode §

    Thanks for this.  Very useful.

    I added:
    (if (looking-at "default:")
      (setq indentation (+ indentation (/ espresso-indent-level 2))))

    after the (if (looking-at "case\\s-" ...) lines in my-js2-indent-function, so that the "default:" part of a switch statement is tabbed correctly also.

    You also set the espresso-indent level to 8 in my-js2-mode-hook, where i think you meant 4. 8 is a little much.

    Great work though.  The highlighting is very useful too!

    • By: mishooFeb 19 (23:14) 2010RE[2]: Editing JavaScript with Emacs — js2-mode §

      Yeah, I sometimes find 8 spaces too much for indentation myself, but I got used to it.  I also liked the following quote from the Linux Kernel Coding Style (http://lxr.linux.no/#linux+v2.6.32/Documentation/CodingStyle) ;-)

      “Tabs are 8 characters, and thus indentations are also 8 characters.  There are heretic movements that try to make indentations 4 (or even 2!) characters deep, and that is akin to trying to define the value of PI to be 3.”

  • By: balajisMay 30 (22:15) 2010RE: Editing JavaScript with Emacs — js2-mode §

    If you add (back-to-indentation) as the last command in js2.el and then recompile it, it should jump to the first non-space character in an indented line (as expected).

    For example:

    (defun js2-indent-line ()
      "Indent the current line as JavaScript source text."
      (interactive)
      (let (parse-status
            current-indent
            offset
            indent-col
            moved
            ;; don't whine about errors/warnings when we're indenting.
            ;; This has to be set before calling parse-partial-sexp below.
            (inhibit-point-motion-hooks t))
        (setq parse-status (save-excursion
                              (parse-partial-sexp (point-min)
                                                  (point-at-bol)))
              offset (- (point) (save-excursion
                                   (back-to-indentation)
                                   (setq current-indent (current-column))
                                   (point))))
        (js2-with-underscore-as-word-syntax
         (if (nth 4 parse-status)
             (js2-lineup-comment parse-status)
           (setq indent-col (js-proper-indentation parse-status))
           ;; see comments below about js2-mode-last-indented-line
           (when
               (cond
                ;; bounce-indenting is disabled during electric-key indent.
                ;; It doesn't work well on first line of buffer.
                ((and js2-bounce-indent-p
                      (not (js2-same-line (point-min)))
                      (not (js2-1-line-comment-continuation-p)))
                 (js2-bounce-indent indent-col parse-status)
                 (setq moved t))
                ;; just indent to the guesser's likely spot
                ((/= current-indent indent-col)
                 (indent-line-to indent-col)
                 (setq moved t)))
             (when (and moved (plusp offset))
               (forward-char offset))))))
      (back-to-indentation) ;; ensure that last command returns to the proper spot
      )

(not published)
    
Notes
  • We don't publish your email address. It's only useful if you wish to receive a notification when someone replies to your comment.

  • Notifications work by thread. That is, you'll be notified even if someone replies to a reply to one of your comments.

  • Each notification includes a "remove me" link that removes your notification option from that comment forever.

  • If you want to reply a certain comment, be sure to click the "reply to this comment" link into it (will automatically setup threads).