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

  • 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: mishooAug 21 (10:33) 2009RE[2]: Editing JavaScript with Emacs — js2-mode §

      Looks like something even better will happen: js2-mode will be factored into a minor mode.  Thus it shouldn't care about indentation, but about what it does best--parsing the program and pointing out errors.

      Espresso has been included in Emacs now (renamed simply "js" and it will be the default mode for javascript), and js2 is temporarily out until Steve does this refactoring.

      Well, until then I'm still happy with my setup. :-)

  • 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
      )

  • By: Vlad KinzerskiyOct 25 (11:36) 2010RE: Editing JavaScript with Emacs — js2-mode §

    Salut,

    I'm setting up for node.js as well for jquery. (It just for fun thus everything must go smooth)

    Can you recommend REPL setup for js with node-repl and have you used remote js console from emacs to Firefox ?

  • By: Drew WellsOct 25 (16:32) 2010RE: Editing JavaScript with Emacs — js2-mode §

    I copied your code into my .emacs, added espresso to .emacs and restarted.  When I type in your example it does not indent, I tried using tabs still js2-mode's bad indention.  I did M-X my-js2-indent-function and got this error: Symbol's function definition is void: espresso--proper-indentation

    • By: Drew WellsOct 28 (03:01) 2010RE[2]: Editing JavaScript with Emacs — js2-mode §

      It looks like this doesn't work on the windows port of emacs.  I got this working fine on linux at home.  I've modified your code, but I still can't get it to stop putting in 8 spaces for tabs.  I have nothing setup to do 8 spaces for tabs, but it is still doing it.

  • By: EricDec 03 (19:27) 2010RE: Editing JavaScript with Emacs — js2-mode §

    thanks this worked great.

    Previously I was using espresso-mode because the indentation, though I liked js2-mode (syntax warnings / errors).  So now I have the best of both worlds!

  • By: Carsten SengerDec 26 (13:57) 2010RE: Editing JavaScript with Emacs — js2-mode §

    Thanks alot. This made my day.

  • By: Preston LandersJul 22 (01:19) 2011RE: Editing JavaScript with Emacs — js2-mode §

    This page provided the exact solution I needed.  Now I have a great JS editing mode, and indentation behavior I can live with.  Thanks!!

  • By: entrepreneurNov 19 (10:35) 2011RE: Editing JavaScript with Emacs — js2-mode §

    Best message ever!
    Thanks a million!

  • By: naskoJul 11 (16:50) 2012RE[2]: Editing JavaScript with Emacs — js2-mode §

    Works. Thanks

Page info
Created:
2009/08/09 11:51
Modified:
2009/08/09 21:05
Author:
Mihai Bazon
Comments:
21
Tags:
emacs, javascript, js2-mode, programming
See also