]> code.delx.au - gnu-emacs-elpa/blobdiff - js2-mode.el
Handle unlimited function nesting in imenu index
[gnu-emacs-elpa] / js2-mode.el
index 8ec3a3f7eb90fa873266ac6160dc10efeb93a218..ef97b8b116dd6ecb1b323230f2a3d313f157b459 100644 (file)
@@ -3,7 +3,8 @@
 ;; Copyright (C) 2009  Free Software Foundation, Inc.
 
 ;; Author:  Steve Yegge <steve.yegge@gmail.com>
-;;          mooz        <stillpedant@gmail.com>
+;; Contributors:  mooz <stillpedant@gmail.com>
+;;                Dmitry Gutov <dgutov@yandex.ru>
 ;; Version:  See `js2-mode-version'
 ;; Keywords:  languages, javascript
 
             UserDataHandler
 
             ;; Window
-            alert confirm document java navigator prompt screen
+            window alert confirm document java navigator prompt screen
             self top
 
             ;; W3C CSS
@@ -171,6 +172,12 @@ Set `js2-include-gears-externs' to t to include them.")
 
 ;;; Variables
 
+(defun js2-mark-safe-local (name pred)
+  "Make the variable NAME buffer-local and mark it as safe file-local
+variable with predicate PRED."
+  (make-variable-buffer-local name)
+  (put name 'safe-local-variable pred))
+
 (defvar js2-emacs22 (>= emacs-major-version 22))
 
 (defcustom js2-highlight-level 2
@@ -200,7 +207,7 @@ Set `js2-include-gears-externs' to t to include them.")
 Similar to `c-basic-offset'."
   :group 'js2-mode
   :type 'integer)
-(make-variable-buffer-local 'js2-basic-offset)
+(js2-mark-safe-local 'js2-basic-offset 'integerp)
 
 ;; TODO(stevey):  move this code into a separate minor mode.
 (defcustom js2-mirror-mode nil
@@ -222,7 +229,8 @@ based on heuristic guessing.  If non-nil, then if the current line is
 already indented to that predetermined column, indenting will choose
 another likely column and indent to that spot.  Repeated invocation of
 the indent-line function will cycle among the computed alternatives.
-See the function `js2-bounce-indent' for details."
+See the function `js2-bounce-indent' for details.  When it is non-nil,
+js2-mode also binds `js2-bounce-indent-backwards' to Shift-Tab."
   :type 'boolean
   :group 'js2-mode)
 
@@ -231,11 +239,22 @@ See the function `js2-bounce-indent' for details."
 regardless of the beginning bracket position."
   :group 'js2-mode
   :type 'boolean)
+(js2-mark-safe-local 'js2-consistent-level-indent-inner-bracket-p 'booleanp)
+
+(defcustom js2-pretty-multiline-decl-indentation-p t
+  "Non-nil to line up multiline declarations vertically. See the
+function `js-multiline-decl-indentation' for details."
+  :group 'js2-mode
+  :type 'boolean)
+(js2-mark-safe-local 'js2-pretty-multiline-decl-indentation-p 'booleanp)
 
-(defcustom js2-use-ast-for-indentation-p nil
-  "Non-nil to use AST for indentation and make it more robust."
+(defcustom js2-always-indent-assigned-expr-in-decls-p nil
+  "If both `js2-pretty-multiline-decl-indentation-p' and this are non-nil,
+always additionally indent function expression or array/object literal
+assigned in a declaration, even when only one var is declared."
   :group 'js2-mode
   :type 'boolean)
+(js2-mark-safe-local 'js2-always-indent-assigned-expr-in-decls-p 'booleanp)
 
 (defcustom js2-indent-on-enter-key nil
   "Non-nil to have Enter/Return key indent the line.
@@ -799,8 +818,8 @@ Will only be used when we finish implementing the interpreter.")
 
 (js2-deflocal js2-nesting-of-function 0)
 
-(js2-deflocal js2-recorded-assignments nil
-  "Tracks assignments found during parsing.")
+(js2-deflocal js2-recorded-identifiers nil
+  "Tracks identifiers found during parsing.")
 
 (defmacro js2-in-lhs (body)
   `(let ((js2-is-in-lhs t))
@@ -1127,21 +1146,21 @@ Not currently used."
 (defcustom js2-post-parse-callbacks nil
   "A list of callback functions invoked after parsing finishes.
 Currently, the main use for this function is to add synthetic
-declarations to `js2-recorded-assignments', which see."
+declarations to `js2-recorded-identifiers', which see."
   :type 'list
   :group 'js2-mode)
 
 (defface js2-external-variable-face
   '((t :foreground "orange"))
-  "Face used to highlight assignments to undeclared variables.
+  "Face used to highlight undeclared variable identifiers.
 An undeclared variable is any variable not declared with var or let
-in the current scope or any lexically enclosing scope.  If you assign
-to such a variable, then you are either expecting it to originate from
+in the current scope or any lexically enclosing scope.  If you use
+such a variable, then you are either expecting it to originate from
 another file, or you've got a potential bug."
   :group 'js2-mode)
 
 (defcustom js2-highlight-external-variables t
-  "Non-nil to higlight assignments to undeclared variables."
+  "Non-nil to highlight undeclared variable identifiers."
   :type 'boolean
   :group 'js2-mode)
 
@@ -1180,6 +1199,8 @@ another file, or you've got a potential bug."
       (mapc (lambda (key)
               (define-key map key #'js2-insert-and-indent))
             js2-electric-keys))
+    (when js2-bounce-indent-p
+      (define-key map (kbd "<backtab>") #'js2-indent-bounce-backwards))
 
     (define-key map [menu-bar javascript]
       (cons "JavaScript" (make-sparse-keymap "JavaScript")))
@@ -2004,8 +2025,8 @@ Returns nil if element is not found in the list."
            ,delta)
        ,form
        (/ (truncate (* (- (float-time (current-time))
-                          (float-time ,beg)))
-                    10000)
+                          (float-time ,beg))
+                       10000))
           10000.0))))
 
 (defsubst js2-same-line (pos)
@@ -2023,6 +2044,10 @@ Returns nil if element is not found in the list."
   "Signal an error when we encounter an unexpected code path."
   (error "failed assertion"))
 
+(defsubst js2-record-text-property (beg end prop value)
+  "Record a text property to set when parsing finishes."
+  (push (list beg end prop value) js2-mode-deferred-properties))
+
 ;; I'd like to associate errors with nodes, but for now the
 ;; easiest thing to do is get the context info from the last token.
 (defsubst js2-record-parse-error (msg &optional arg pos len)
@@ -2148,23 +2173,24 @@ callback is called immediately with a non-nil END-P argument.
 
 The node traversal is approximately lexical-order, although there
 are currently no guarantees around this."
-  (let ((vfunc (get (aref node 0) 'js2-visitor)))
-    ;; visit the node
-    (when  (funcall callback node nil)
-      ;; visit the kids
-      (cond
-       ((eq vfunc 'js2-visit-none)
-        nil)                            ; don't even bother calling it
-       ;; Each AST node type has to define a `js2-visitor' function
-       ;; that takes a node and a callback, and calls `js2-visit-ast'
-       ;; on each child of the node.
-       (vfunc
-        (funcall vfunc node callback))
-       (t
-        (error "%s does not define a visitor-traversal function"
-               (aref node 0)))))
-    ;; call the end-visit
-    (funcall callback node t)))
+  (if node
+    (let ((vfunc (get (aref node 0) 'js2-visitor)))
+      ;; visit the node
+      (when  (funcall callback node nil)
+        ;; visit the kids
+        (cond
+         ((eq vfunc 'js2-visit-none)
+          nil)                            ; don't even bother calling it
+         ;; Each AST node type has to define a `js2-visitor' function
+         ;; that takes a node and a callback, and calls `js2-visit-ast'
+         ;; on each child of the node.
+         (vfunc
+          (funcall vfunc node callback))
+         (t
+          (error "%s does not define a visitor-traversal function"
+                 (aref node 0)))))
+      ;; call the end-visit
+      (funcall callback node t))))
 
 (defstruct (js2-node
             (:constructor nil))  ; abstract
@@ -2583,8 +2609,7 @@ NAME can be a lisp symbol or string.  SYMBOL is a `js2-symbol'."
 (put 'cl-struct-js2-return-node 'js2-printer 'js2-print-return-node)
 
 (defun js2-visit-return-node (n v)
-  (if (js2-return-node-retval n)
-      (js2-visit-ast (js2-return-node-retval n) v)))
+  (js2-visit-ast (js2-return-node-retval n) v))
 
 (defun js2-print-return-node (n i)
   (insert (js2-make-pad i) "return")
@@ -2619,8 +2644,7 @@ NAME can be a lisp symbol or string.  SYMBOL is a `js2-symbol'."
 (defun js2-visit-if-node (n v)
   (js2-visit-ast (js2-if-node-condition n) v)
   (js2-visit-ast (js2-if-node-then-part n) v)
-  (if (js2-if-node-else-part n)
-      (js2-visit-ast (js2-if-node-else-part n) v)))
+  (js2-visit-ast (js2-if-node-else-part n) v))
 
 (defun js2-print-if-node (n i)
   (let ((pad (js2-make-pad i))
@@ -2663,8 +2687,7 @@ NAME can be a lisp symbol or string.  SYMBOL is a `js2-symbol'."
   (js2-visit-ast (js2-try-node-try-block n) v)
   (dolist (clause (js2-try-node-catch-clauses n))
     (js2-visit-ast clause v))
-  (if (js2-try-node-finally-block n)
-      (js2-visit-ast (js2-try-node-finally-block n) v)))
+  (js2-visit-ast (js2-try-node-finally-block n) v))
 
 (defun js2-print-try-node (n i)
   (let ((pad (js2-make-pad i))
@@ -2686,14 +2709,14 @@ NAME can be a lisp symbol or string.  SYMBOL is a `js2-symbol'."
             (:constructor make-js2-catch-node (&key (type js2-CATCH)
                                                     (pos js2-ts-cursor)
                                                     len
-                                                    var-name
+                                                    param
                                                     guard-kwd
                                                     guard-expr
                                                     block
                                                     lp
                                                     rp)))
   "AST node for a catch clause."
-  var-name    ; a `js2-name-node'
+  param       ; destructuring form or simple name node
   guard-kwd   ; relative buffer position of "if" in "catch (x if ...)"
   guard-expr  ; catch condition, a `js2-node'
   block       ; statements, a `js2-block-node'
@@ -2704,7 +2727,7 @@ NAME can be a lisp symbol or string.  SYMBOL is a `js2-symbol'."
 (put 'cl-struct-js2-catch-node 'js2-printer 'js2-print-catch-node)
 
 (defun js2-visit-catch-node (n v)
-  (js2-visit-ast (js2-catch-node-var-name n) v)
+  (js2-visit-ast (js2-catch-node-param n) v)
   (when (js2-catch-node-guard-kwd n)
     (js2-visit-ast (js2-catch-node-guard-expr n) v))
   (js2-visit-ast (js2-catch-node-block n) v))
@@ -2714,7 +2737,7 @@ NAME can be a lisp symbol or string.  SYMBOL is a `js2-symbol'."
         (guard-kwd (js2-catch-node-guard-kwd n))
         (guard-expr (js2-catch-node-guard-expr n)))
     (insert " catch (")
-    (js2-print-ast (js2-catch-node-var-name n) 0)
+    (js2-print-ast (js2-catch-node-param n) 0)
     (when guard-kwd
       (insert " if ")
       (js2-print-ast guard-expr 0))
@@ -2793,8 +2816,7 @@ NAME can be a lisp symbol or string.  SYMBOL is a `js2-symbol'."
 (put 'cl-struct-js2-case-node 'js2-printer 'js2-print-case-node)
 
 (defun js2-visit-case-node (n v)
-  (if (js2-case-node-expr n)  ; nil for default: case
-      (js2-visit-ast (js2-case-node-expr n) v))
+  (js2-visit-ast (js2-case-node-expr n) v)
   (js2-visit-block n v))
 
 (defun js2-print-case-node (n i)
@@ -2940,9 +2962,7 @@ NODE is a `js2-labels-node'.  LABEL is an identifier."
   target) ; target js2-labels-node or loop/switch statement
 
 (defun js2-visit-jump-node (n v)
-  ;; we don't visit the target, since it's a back-link
-  (if (js2-jump-node-label n)
-      (js2-visit-ast (js2-jump-node-label n) v)))
+  (js2-visit-ast (js2-jump-node-label n) v))
 
 (defstruct (js2-break-node
             (:include js2-jump-node)
@@ -3023,12 +3043,10 @@ The `params' field is a lisp list of nodes.  Each node is either a simple
 (put 'cl-struct-js2-function-node 'js2-printer 'js2-print-function-node)
 
 (defun js2-visit-function-node (n v)
-  (if (js2-function-node-name n)
-      (js2-visit-ast (js2-function-node-name n) v))
+  (js2-visit-ast (js2-function-node-name n) v)
   (dolist (p (js2-function-node-params n))
     (js2-visit-ast p v))
-  (when (js2-function-node-body n)
-    (js2-visit-ast (js2-function-node-body n) v)))
+  (js2-visit-ast (js2-function-node-body n) v))
 
 (defun js2-print-function-node (n i)
   (let ((pad (js2-make-pad i))
@@ -3128,8 +3146,7 @@ The type field will be js2-CONST for a const decl."
 
 (defun js2-visit-var-init-node (n v)
   (js2-visit-ast (js2-var-init-node-target n) v)
-  (if (js2-var-init-node-initializer n)
-      (js2-visit-ast (js2-var-init-node-initializer n) v)))
+  (js2-visit-ast (js2-var-init-node-initializer n) v))
 
 (defun js2-print-var-init-node (n i)
   (let ((pad (js2-make-pad i))
@@ -3196,10 +3213,8 @@ The type field inherited from `js2-node' holds the operator."
 (put 'cl-struct-js2-infix-node 'js2-printer 'js2-print-infix-node)
 
 (defun js2-visit-infix-node (n v)
-  (when (js2-infix-node-left n)
-    (js2-visit-ast (js2-infix-node-left n) v))
-  (when (js2-infix-node-right n)
-    (js2-visit-ast (js2-infix-node-right n) v)))
+  (js2-visit-ast (js2-infix-node-left n) v)
+  (js2-visit-ast (js2-infix-node-right n) v))
 
 (defconst js2-operator-tokens
   (let ((table (make-hash-table :test 'eq))
@@ -3337,10 +3352,8 @@ Note that a let declaration such as let x=6, y=7 is a `js2-var-decl-node'."
 (put 'cl-struct-js2-let-node 'js2-printer 'js2-print-let-node)
 
 (defun js2-visit-let-node (n v)
-  (when (js2-let-node-vars n)
-    (js2-visit-ast (js2-let-node-vars n) v))
-  (when (js2-let-node-body n)
-    (js2-visit-ast (js2-let-node-body n) v)))
+  (js2-visit-ast (js2-let-node-vars n) v)
+  (js2-visit-ast (js2-let-node-body n) v))
 
 (defun js2-print-let-node (n i)
   (insert (js2-make-pad i) "let (")
@@ -3401,8 +3414,7 @@ The node type is set to js2-NULL, js2-THIS, etc.")
   (js2-visit-ast (js2-new-node-target n) v)
   (dolist (arg (js2-new-node-args n))
     (js2-visit-ast arg v))
-  (when (js2-new-node-initializer n)
-    (js2-visit-ast (js2-new-node-initializer n) v)))
+  (js2-visit-ast (js2-new-node-initializer n) v))
 
 (defun js2-print-new-node (n i)
   (insert (js2-make-pad i) "new ")
@@ -3519,8 +3531,7 @@ You can tell the quote type by looking at the first character."
 
 (defun js2-visit-array-node (n v)
   (dolist (e (js2-array-node-elems n))
-    (when e  ; can be nil, e.g. [a, ,b]
-      (js2-visit-ast e v))))
+    (js2-visit-ast e v)))
 
 (defun js2-print-array-node (n i)
   (insert (js2-make-pad i) "[")
@@ -3534,8 +3545,10 @@ You can tell the quote type by looking at the first character."
                                                      (pos js2-ts-cursor)
                                                      len
                                                      elems)))
-  "AST node for an object literal expression."
-  elems)  ; a lisp list of `js2-object-prop-node'
+  "AST node for an object literal expression.
+`elems' is a list of either `js2-object-prop-node' or `js2-name-node',
+the latter represents abbreviation in destructuring expressions."
+  elems)
 
 (put 'cl-struct-js2-object-node 'js2-visitor 'js2-visit-object-node)
 (put 'cl-struct-js2-object-node 'js2-printer 'js2-print-object-node)
@@ -3610,10 +3623,8 @@ property `GETTER_SETTER' set to js2-GET or js2-SET. ")
 (put 'cl-struct-js2-prop-get-node 'js2-printer 'js2-print-prop-get-node)
 
 (defun js2-visit-prop-get-node (n v)
-  (when (js2-prop-get-node-left n)
-    (js2-visit-ast (js2-prop-get-node-left n) v))
-  (when (js2-prop-get-node-right n)
-    (js2-visit-ast (js2-prop-get-node-right n) v)))
+  (js2-visit-ast (js2-prop-get-node-left n) v)
+  (js2-visit-ast (js2-prop-get-node-right n) v))
 
 (defun js2-print-prop-get-node (n i)
   (insert (js2-make-pad i))
@@ -3641,10 +3652,8 @@ property `GETTER_SETTER' set to js2-GET or js2-SET. ")
 (put 'cl-struct-js2-elem-get-node 'js2-printer 'js2-print-elem-get-node)
 
 (defun js2-visit-elem-get-node (n v)
-  (when (js2-elem-get-node-target n)
-    (js2-visit-ast (js2-elem-get-node-target n) v))
-  (when (js2-elem-get-node-element n)
-    (js2-visit-ast (js2-elem-get-node-element n) v)))
+  (js2-visit-ast (js2-elem-get-node-target n) v)
+  (js2-visit-ast (js2-elem-get-node-element n) v))
 
 (defun js2-print-elem-get-node (n i)
   (insert (js2-make-pad i))
@@ -3758,8 +3767,7 @@ as opposed to required parens such as those enclosing an if-conditional."
   (js2-visit-ast (js2-array-comp-node-result n) v)
   (dolist (l (js2-array-comp-node-loops n))
     (js2-visit-ast l v))
-  (if (js2-array-comp-node-filter n)
-      (js2-visit-ast (js2-array-comp-node-filter n) v)))
+  (js2-visit-ast (js2-array-comp-node-filter n) v))
 
 (defun js2-print-array-comp-node (n i)
   (let ((pad (js2-make-pad i))
@@ -3945,10 +3953,8 @@ expression."
 (put 'cl-struct-js2-xml-prop-ref-node 'js2-printer 'js2-print-xml-prop-ref-node)
 
 (defun js2-visit-xml-prop-ref-node (n v)
-  (if (js2-xml-prop-ref-node-namespace n)
-      (js2-visit-ast (js2-xml-prop-ref-node-namespace n) v))
-  (if (js2-xml-prop-ref-node-propname n)
-      (js2-visit-ast (js2-xml-prop-ref-node-propname n) v)))
+  (js2-visit-ast (js2-xml-prop-ref-node-namespace n) v)
+  (js2-visit-ast (js2-xml-prop-ref-node-propname n) v))
 
 (defun js2-print-xml-prop-ref-node (n i)
   (insert (js2-make-pad i))
@@ -3997,10 +4003,8 @@ end of the index expression."
 (put 'cl-struct-js2-xml-elem-ref-node 'js2-printer 'js2-print-xml-elem-ref-node)
 
 (defun js2-visit-xml-elem-ref-node (n v)
-  (if (js2-xml-elem-ref-node-namespace n)
-      (js2-visit-ast (js2-xml-elem-ref-node-namespace n) v))
-  (if (js2-xml-elem-ref-node-expr n)
-      (js2-visit-ast (js2-xml-elem-ref-node-expr n) v)))
+  (js2-visit-ast (js2-xml-elem-ref-node-namespace n) v)
+  (js2-visit-ast (js2-xml-elem-ref-node-expr n) v))
 
 (defun js2-print-xml-elem-ref-node (n i)
   (insert (js2-make-pad i))
@@ -4667,10 +4671,11 @@ If NODE is the ast-root, returns nil."
 
 (defsubst js2-nested-function-p (node)
   "Return t if NODE is a nested function, or is inside a nested function."
-  (js2-function-node-p (if (js2-function-node-p node)
-                           (js2-node-parent-script-or-fn node)
-                         (js2-node-parent-script-or-fn
-                          (js2-node-parent-script-or-fn node)))))
+  (unless (js2-ast-root-p node)
+    (js2-function-node-p (if (js2-function-node-p node)
+                             (js2-node-parent-script-or-fn node)
+                           (js2-node-parent-script-or-fn
+                            (js2-node-parent-script-or-fn node))))))
 
 (defsubst js2-function-param-node-p (node)
   "Return non-nil if NODE is a param node of a `js2-function-node'."
@@ -4824,7 +4829,10 @@ You should use `js2-print-tree' instead of this function."
        ;; I'll wait for people to notice incorrect warnings.
        ((and (= tt js2-EXPR_VOID)
              (js2-expr-stmt-node-p node)) ; but not if EXPR_RESULT
-        (js2-node-has-side-effects (js2-expr-stmt-node-expr node)))
+        (let ((expr (js2-expr-stmt-node-expr node)))
+          (or (js2-node-has-side-effects expr)
+              (when (js2-string-node-p expr)
+                (string= "use strict" (js2-string-node-value expr))))))
        ((= tt js2-COMMA)
         (js2-node-has-side-effects (js2-infix-node-right node)))
        ((or (= tt js2-AND)
@@ -6012,7 +6020,7 @@ corresponding number.  Otherwise return -1."
             js2-ts-regexp-flags (js2-collect-string flags)
             js2-token-end js2-ts-cursor)
       ;; tell `parse-partial-sexp' to ignore this range of chars
-      (put-text-property js2-token-beg js2-token-end 'syntax-class '(2)))))
+      (js2-record-text-property js2-token-beg js2-token-end 'syntax-class '(2)))))
 
 (defun js2-get-first-xml-token ()
   (setq js2-ts-xml-open-tags-count 0
@@ -6115,7 +6123,11 @@ corresponding number.  Otherwise return -1."
                            (throw 'return js2-ERROR)))
                         (t
                          (unless (js2-read-entity)
-                           (throw 'return js2-ERROR)))))
+                           (throw 'return js2-ERROR))))
+                      ;; allow bare CDATA section
+                      ;; ex) let xml = <![CDATA[ foo bar baz ]]>;
+                      (when (zerop js2-ts-xml-open-tags-count)
+                        (throw 'return js2-XMLEND)))
                      (??
                       (setq c (js2-get-char)) ;; skip ?
                       (js2-add-to-string c)
@@ -6256,10 +6268,6 @@ FACE is the face to fontify with."
                                     point-entered nil
                                     c-in-sws nil)))
 
-(defsubst js2-record-text-property (beg end prop value)
-  "Record a text property to set when parsing finishes."
-  (push (list beg end prop value) js2-mode-deferred-properties))
-
 (defconst js2-ecma-global-props
   (concat "^"
           (regexp-opt
@@ -6472,7 +6480,7 @@ of a simple name.  Called before EXPR has a parent node."
           "\\(?:param\\|argument\\)"
           "\\)"
           "\\s-*\\({[^}]+}\\)?"         ; optional type
-          "\\s-*\\([a-zA-Z0-9_$]+\\)?"  ; name
+          "\\s-*\\[?\\([a-zA-Z0-9_$]+\\)?\\]?"  ; name
           "\\>")
   "Matches jsdoc tags with optional type and optional param name.")
 
@@ -6645,25 +6653,26 @@ of a simple name.  Called before EXPR has a parent node."
         (js2-set-face (setq leftpos (js2-node-abs-pos name))
                       (+ leftpos (js2-node-len name))
                       'font-lock-function-name-face
-                      'record)))
-    ;; save variable assignments so we can check for undeclared later
-    ;; (can't do it here since var decls can come at end of script)
-    (when (and js2-highlight-external-variables
-               (setq name (js2-member-expr-leftmost-name left)))
-      (push (list name js2-current-scope
-                  (setq leftpos (js2-node-abs-pos name))
-                  (setq end (+ leftpos (js2-node-len name))))
-            js2-recorded-assignments))))
+                      'record)))))
+
+(defun js2-record-name-node (node)
+  "Saves NODE to `js2-recorded-identifiers' to check for undeclared variables
+later. NODE must be a name node."
+  (let (leftpos end)
+    (push (list node js2-current-scope
+                (setq leftpos (js2-node-abs-pos node))
+                (setq end (+ leftpos (js2-node-len node))))
+          js2-recorded-identifiers)))
 
 (defun js2-highlight-undeclared-vars ()
-  "After entire parse is finished, look for undeclared variable assignments.
+  "After entire parse is finished, look for undeclared variable references.
 We have to wait until entire buffer is parsed, since JavaScript permits var
 decls to occur after they're used.
 
 If any undeclared var name is in `js2-externs' or `js2-additional-externs',
 it is considered declared."
   (let (name)
-    (dolist (entry js2-recorded-assignments)
+    (dolist (entry js2-recorded-identifiers)
       (destructuring-bind (name-node scope pos end) entry
         (setq name (js2-name-node-name name-node))
         (unless (or (member name js2-global-externs)
@@ -6673,7 +6682,7 @@ it is considered declared."
           (js2-set-face pos end 'js2-external-variable-face 'record)
           (js2-record-text-property pos end 'help-echo "Undeclared variable")
           (js2-record-text-property pos end 'point-entered #'js2-echo-help))))
-    (setq js2-recorded-assignments nil)))
+    (setq js2-recorded-identifiers nil)))
 
 ;;; IMenu support
 
@@ -6681,7 +6690,7 @@ it is considered declared."
 ;; possibly other browsing mechanisms.
 
 ;; The basic strategy is to identify function assignment targets of the form
-;; `foo.bar.baz', convert them to (list foo bar baz <position>), and push the
+;; `foo.bar.baz', convert them to (list fn foo bar baz <position>), and push the
 ;; list into `js2-imenu-recorder'.  The lists are merged into a trie-like tree
 ;; for imenu after parsing is finished.
 
@@ -6730,13 +6739,14 @@ it is considered declared."
 ;; During parsing we accumulate an entry for each definition in
 ;; the variable `js2-imenu-recorder', like so:
 
-;; '((a 5)
-;;   (b 25)
-;;   (foo 100)
-;;   (foo bar 200)
-;;   (foo bar baz 300)
-;;   (foo bar zab 400))
+;; '((fn a 5)
+;;   (fn b 25)
+;;   (fn foo 100)
+;;   (fn foo bar 200)
+;;   (fn foo bar baz 300)
+;;   (fn foo bar zab 400))
 
+;; Where 'fn' is the respective function node.
 ;; After parsing these entries are merged into this alist-trie:
 
 ;; '((a . 1)
@@ -6766,7 +6776,7 @@ returns nil.  Otherwise returns the string name/value of the node."
     "this")))
 
 (defsubst js2-node-qname-component (node)
-  "Test function:  return the name of this node, if it contributes to a qname.
+  "Return the name of this node, if it contributes to a qname.
 Returns nil if the node doesn't contribute."
   (copy-sequence
    (or (js2-prop-node-name node)
@@ -6774,12 +6784,14 @@ Returns nil if the node doesn't contribute."
                 (js2-function-node-name node))
            (js2-name-node-name (js2-function-node-name node))))))
 
-(defsubst js2-record-function-qname (fn-node qname)
-  "Associate FN-NODE with its QNAME for later lookup.
-This is used in postprocessing the chain list.  When we find a chain
-whose first element is a js2-THIS keyword node, we look up the parent
-function and see (using this map) whether it is the tail of a chain.
-If so, we replace the this-node with a copy of the parent's qname."
+(defsubst js2-record-imenu-entry (fn-node qname pos)
+  "Add an entry to `js2-imenu-recorder'.
+FN-NODE should be the current item's function node.
+
+Associate FN-NODE with its QNAME for later lookup.
+This is used in postprocessing the chain list.  For each chain, we find
+the parent function, look up its qname, then prepend a copy of it to the chain."
+  (push (cons fn-node (append qname (list pos))) js2-imenu-recorder)
   (unless js2-imenu-function-map
     (setq js2-imenu-function-map (make-hash-table :test 'eq)))
   (puthash fn-node qname js2-imenu-function-map))
@@ -6796,17 +6808,13 @@ VAR, if non-nil, is the expression that NODE is being assigned to."
        ((and fun-p
              (not var)
              (setq fname-node (js2-function-node-name node)))
-        (push (setq qname (list fname-node (js2-node-pos node)))
-              js2-imenu-recorder)
-        (js2-record-function-qname node qname))
+        (js2-record-imenu-entry node (list fname-node) (js2-node-pos node)))
        ;; for remaining forms, compute left-side tree branch first
        ((and var (setq qname (js2-compute-nested-prop-get var)))
         (cond
          ;; foo.bar.baz = function
          (fun-p
-          (push (nconc qname (list (js2-node-pos node)))
-                js2-imenu-recorder)
-          (js2-record-function-qname node qname))
+          (js2-record-imenu-entry node qname (js2-node-pos node)))
          ;; foo.bar.baz = object-literal
          ;; look for nested functions:  {a: {b: function() {...} }}
          ((js2-object-node-p node)
@@ -6816,30 +6824,27 @@ VAR, if non-nil, is the expression that NODE is being assigned to."
           (js2-record-object-literal node qname (js2-node-pos node)))))))))
 
 (defun js2-compute-nested-prop-get (node)
-  "If NODE is of form foo.bar.baz, return component nodes as a list.
-Otherwise returns nil.  Element-gets can be treated as property-gets
-if the index expression is a name, a string, or a positive integer."
+  "If NODE is of form foo.bar, foo['bar'], or any nested combination, return
+component nodes as a list.  Otherwise return nil.  Element-gets are treated
+as property-gets if the index expression is a string, or a positive integer."
   (let (left right head)
     (cond
      ((or (js2-name-node-p node)
           (js2-this-node-p node))
       (list node))
      ;; foo.bar.baz is parenthesized as (foo.bar).baz => right operand is a leaf
-     ((or (and (js2-prop-get-node-p node)
-               (setq left (js2-prop-get-node-left node)
-                     right (js2-prop-get-node-right node)))
-          (and (js2-elem-get-node-p node)
-               (setq left (js2-elem-get-node-target node)
-                     right (js2-elem-get-node-element node))))
-      (if (and (or (js2-prop-get-node-p left)     ; left == foo.bar
-                   (js2-elem-get-node-p left)     ; left == foo['bar']
-                   (js2-name-node-p left)         ; left == foo
-                   (js2-this-node-p left))        ; or left == this
-               (or (js2-name-node-p right)        ; .bar
-                   (js2-string-node-p right)      ; ['bar']
-                   (and (js2-number-node-p right) ; [10]
-                        (string-match "^[0-9]+$"
-                                      (js2-number-node-value right)))))
+     ((js2-prop-get-node-p node)        ; foo.bar
+      (setq left (js2-prop-get-node-left node)
+            right (js2-prop-get-node-right node))
+      (if (setq head (js2-compute-nested-prop-get left))
+          (nconc head (list right))))
+     ((js2-elem-get-node-p node)        ; foo['bar'] or foo[101]
+      (setq left (js2-elem-get-node-target node)
+            right (js2-elem-get-node-element node))
+      (if (or (js2-string-node-p right)      ; ['bar']
+              (and (js2-number-node-p right) ; [10]
+                   (string-match "^[0-9]+$"
+                                 (js2-number-node-value right))))
           (if (setq head (js2-compute-nested-prop-get left))
               (nconc head (list right))))))))
 
@@ -6849,10 +6854,9 @@ NODE is an object literal that is the right-hand child of an assignment
 expression.  QNAME is a list of nodes representing the assignment target,
 e.g. for foo.bar.baz = {...}, QNAME is (foo-node bar-node baz-node).
 POS is the absolute position of the node.
-We do a depth-first traversal of NODE.  Any functions we find are prefixed
-with QNAME plus the property name of the function and appended to the
-variable `js2-imenu-recorder'."
-  (let (left right)
+We do a depth-first traversal of NODE.  For any functions we find,
+we append the property name to QNAME, then call `js2-record-imenu-entry'."
+  (let (left right prop-qname)
     (dolist (e (js2-object-node-elems node))  ; e is a `js2-object-prop-node'
       (let ((left (js2-infix-node-left e))
             ;; Element positions are relative to the parent position.
@@ -6864,9 +6868,7 @@ variable `js2-imenu-recorder'."
             ;; As a policy decision, we record the position of the property,
             ;; not the position of the `function' keyword, since the property
             ;; is effectively the name of the function.
-            (push (append qname (list left pos))
-                  js2-imenu-recorder)
-            (js2-record-function-qname right qname)))
+            (js2-record-imenu-entry right (append qname (list left)) pos)))
          ;; foo: {object-literal} -- add foo to qname, offset position, and recurse
          ((js2-object-node-p right)
           (js2-record-object-literal right
@@ -6889,31 +6891,52 @@ that it's an external variable, which must also be in the top-level scope."
       (js2-ast-root-p defining-scope))
      (t t))))
 
-(defun js2-browse-postprocess-chains (chains)
+(defsubst js2-wrapper-function-p (node)
+  "Returns t if NODE is a function expression that's immediately invoked.
+NODE must be `js2-function-node'."
+  (let ((parent (js2-node-parent node)))
+    (or
+     ;; function(){...}();
+     (js2-call-node-p parent)
+     (and (js2-paren-node-p parent)
+          ;; (function(){...})();
+          (or (js2-call-node-p (setq parent (js2-node-parent parent)))
+              ;; (function(){...}).call(this);
+              (and (js2-prop-get-node-p parent)
+                   (member (js2-name-node-name (js2-prop-get-node-right parent))
+                           '("call" "apply"))
+                   (js2-call-node-p (js2-node-parent parent))))))))
+
+(defun js2-browse-postprocess-chains (entries)
   "Modify function-declaration name chains after parsing finishes.
 Some of the information is only available after the parse tree is complete.
-For instance, following a 'this' reference requires a parent function node."
-  (let (result head fn parent-chain p elem)
-    (dolist (chain chains)
-      ;; examine the head of each node to get its defining scope
-      (setq head (car chain))
-      (cond
-       ;; if top-level/external, keep as-is
-       ((js2-node-top-level-decl-p head)
-        (push chain result))
-       ;; check for a this-reference
-       ((eq (js2-node-type head) js2-THIS)
-        (setq fn (js2-node-parent-script-or-fn head))
-        ;; if there is no parent function, or if the parent function
-        ;; is nested, discard the head node and keep the rest of the chain.
-        (if (or (null fn) (js2-nested-function-p fn))
-            (push (cdr chain) result)
-          ;; else look up parent in function-map.  If not found, discard chain.
-          (when (setq parent-chain (and js2-imenu-function-map
-                                        (gethash fn js2-imenu-function-map)))
-            ;; else discard head node and prefix parent fn qname, which is
-            ;; the parent-chain sans tail, to this chain.
-            (push (append (butlast parent-chain) (cdr chain)) result))))))
+For instance, processing a nested scope requires a parent function node."
+  (let (result head fn current-fn parent-qname qname p elem)
+    (dolist (entry entries)
+      ;; function node goes first
+      (destructuring-bind (current-fn &rest chain) entry
+        ;; examine its defining scope;
+        ;; if top-level/external, keep as-is
+        (if (js2-node-top-level-decl-p (car chain))
+            (push chain result)
+          (when (setq fn (js2-node-parent-script-or-fn current-fn))
+            (setq parent-qname (gethash fn js2-imenu-function-map 'not-found))
+            (when (eq parent-qname 'not-found)
+              ;; anonymous function expressions are not recorded
+              ;; during the parse, so we need to handle this case here
+              (setq parent-qname
+                    (if (js2-wrapper-function-p fn)
+                        (let ((grandparent (js2-node-parent-script-or-fn fn)))
+                          (if (js2-ast-root-p grandparent)
+                              nil
+                            (gethash grandparent js2-imenu-function-map 'skip)))
+                      'skip))
+              (puthash fn parent-qname js2-imenu-function-map))
+            (unless (eq parent-qname 'skip)
+              ;; prefix parent fn qname to this chain.
+              (let ((qname (append parent-qname chain)))
+                (puthash current-fn (butlast qname) js2-imenu-function-map)
+                (push qname result)))))))
     ;; finally replace each node in each chain with its name.
     (dolist (chain result)
       (setq p chain)
@@ -7075,7 +7098,7 @@ is only true until the node is added to its parent; i.e., while parsing."
                        'font-lock-comment-face))
     (when (memq js2-ts-comment-type '(html preprocessor))
       ;; Tell cc-engine the bounds of the comment.
-      (put-text-property js2-token-beg (1- js2-token-end) 'c-in-sws t))))
+      (js2-record-text-property js2-token-beg (1- js2-token-end) 'c-in-sws t))))
 
 ;; This function is called depressingly often, so it should be fast.
 ;; Most of the time it's looking at the same token it peeked before.
@@ -7330,15 +7353,14 @@ Scanner should be initialized."
           js2-current-flagged-token js2-EOF
           js2-nesting-of-function 0
           js2-labeled-stmt nil
-          js2-recorded-assignments nil)  ; for js2-highlight
+          js2-recorded-identifiers nil)  ; for js2-highlight
     (while (/= (setq tt (js2-peek-token)) js2-EOF)
       (if (= tt js2-FUNCTION)
           (progn
             (js2-consume-token)
             (setq n (js2-parse-function (if js2-called-by-compile-function
                                             'FUNCTION_EXPRESSION
-                                          'FUNCTION_STATEMENT)))
-            (js2-record-imenu-functions n))
+                                          'FUNCTION_STATEMENT))))
         ;; not a function - parse a statement
         (setq n (js2-parse-statement)))
       ;; add function or statement to script
@@ -7353,9 +7375,10 @@ Scanner should be initialized."
         (js2-node-add-children root comment)))
     (setf (js2-node-len root) (- end pos))
     ;; Give extensions a chance to muck with things before highlighting starts.
-    (dolist (callback js2-post-parse-callbacks)
-      (funcall callback))
-    (js2-highlight-undeclared-vars)
+    (let ((js2-additional-externs js2-additional-externs))
+      (dolist (callback js2-post-parse-callbacks)
+        (funcall callback))
+      (js2-highlight-undeclared-vars))
     root))
 
 (defun js2-function-parser ()
@@ -7400,6 +7423,33 @@ Scanner should be initialized."
     (js2-node-add-children fn-node pn)
     pn))
 
+(defun js2-define-destruct-symbols (node decl-type face &optional ignore-not-in-block)
+  "Declare and fontify destructuring parameters inside NODE.
+NODE is either `js2-array-node', `js2-object-node', or `js2-name-node'."
+  (cond
+   ((js2-name-node-p node)
+    (let (leftpos)
+      (js2-define-symbol decl-type (js2-name-node-name node)
+                         node ignore-not-in-block)
+      (when face
+        (js2-set-face (setq leftpos (js2-node-abs-pos node))
+                      (+ leftpos (js2-node-len node))
+                      face 'record))))
+   ((js2-object-node-p node)
+    (dolist (elem (js2-object-node-elems node))
+      (js2-define-destruct-symbols
+       (if (js2-object-prop-node-p elem)
+           (js2-object-prop-node-right elem)
+         ;; abbreviated destructuring {a, b}
+         elem)
+       decl-type face ignore-not-in-block)))
+   ((js2-array-node-p node)
+    (dolist (elem (js2-array-node-elems node))
+      (when elem
+        (js2-define-destruct-symbols elem decl-type face ignore-not-in-block))))
+   (t (js2-report-error "msg.no.parm" nil (js2-node-abs-pos node)
+                        (js2-node-len node)))))
+
 (defun js2-parse-function-params (fn-node pos)
   (if (js2-match-token js2-RP)
       (setf (js2-function-node-rp fn-node) (- js2-token-beg pos))
@@ -7409,7 +7459,11 @@ Scanner should be initialized."
             (cond
              ;; destructuring param
              ((or (= tt js2-LB) (= tt js2-LC))
-              (push (js2-parse-primary-expr-lhs) params))
+              (setq param (js2-parse-primary-expr-lhs))
+              (js2-define-destruct-symbols param
+                                           js2-LP
+                                           'js2-function-param-face)
+              (push param params))
              ;; simple name
              (t
               (js2-must-match js2-NAME "msg.no.parm")
@@ -7528,7 +7582,7 @@ Last token scanned is the close-curly for the function body."
                              (js2-name-node-name name)
                              fn-node))
       (if (and name
-               (eq function-type 'FUNCTION_EXPRESSION_STATEMENT))
+               (not (eq function-type 'FUNCTION_EXPRESSION)))
           (js2-record-imenu-functions fn-node)))
     (setf (js2-node-len fn-node) (- js2-ts-cursor pos)
           (js2-function-node-member-expr fn-node) member-expr-node)  ; may be nil
@@ -7870,66 +7924,73 @@ Parses for, for-in, and for each-in statements."
     (if (js2-must-match js2-LP "msg.no.paren.for")
         (setq lp (- js2-token-beg for-pos)))
     (setq tt (js2-peek-token))
-    ;; parse init clause
-    (let ((js2-in-for-init t))  ; set as dynamic variable
-      (cond
-       ((= tt js2-SEMI)
-        (setq init (make-js2-empty-expr-node)))
-       ((or (= tt js2-VAR) (= tt js2-LET))
-        (js2-consume-token)
-        (setq init (js2-parse-variables tt js2-token-beg)))
-       (t
-        (setq init (js2-parse-expr)))))
-    (if (js2-match-token js2-IN)
-        (setq is-for-in t
-              in-pos (- js2-token-beg for-pos)
-              cond (js2-parse-expr))  ; object over which we're iterating
-      ;; else ordinary for loop - parse cond and incr
-      (js2-must-match js2-SEMI "msg.no.semi.for")
-      (setq cond (if (= (js2-peek-token) js2-SEMI)
-                     (make-js2-empty-expr-node) ; no loop condition
-                   (js2-parse-expr)))
-      (js2-must-match js2-SEMI "msg.no.semi.for.cond")
-      (setq tmp-pos js2-token-end
-            incr (if (= (js2-peek-token) js2-RP)
-                     (make-js2-empty-expr-node :pos tmp-pos)
-                   (js2-parse-expr))))
-    (if (js2-must-match js2-RP "msg.no.paren.for.ctrl")
-        (setq rp (- js2-token-beg for-pos)))
-    (if (not is-for-in)
-        (setq pn (make-js2-for-node :init init
-                                    :condition cond
-                                    :update incr
-                                    :lp lp
-                                    :rp rp))
-      ;; cond could be null if 'in obj' got eaten by the init node.
-      (if (js2-infix-node-p init)
-          ;; it was (foo in bar) instead of (var foo in bar)
-          (setq cond (js2-infix-node-right init)
-                init (js2-infix-node-left init))
-        (if (and (js2-var-decl-node-p init)
-                 (> (length (js2-var-decl-node-kids init)) 1))
-            (js2-report-error "msg.mult.index")))
-      (setq pn (make-js2-for-in-node :iterator init
-                                     :object cond
-                                     :in-pos in-pos
-                                     :foreach-p is-for-each
-                                     :each-pos each-pos
-                                     :lp lp
-                                     :rp rp)))
+    ;; 'for' makes local scope
+    (js2-push-scope (make-js2-scope))
     (unwind-protect
-        (progn
-          (js2-enter-loop pn)
-          ;; We have to parse the body -after- creating the loop node,
-          ;; so that the loop node appears in the js2-loop-set, allowing
-          ;; break/continue statements to find the enclosing loop.
-          (setf body (js2-parse-statement)
-                (js2-loop-node-body pn) body
-                (js2-node-pos pn) for-pos
-                (js2-node-len pn) (- (js2-node-end body) for-pos))
-          (js2-node-add-children pn init cond incr body))
-      ;; finally
-      (js2-exit-loop))
+        ;; parse init clause
+        (let ((js2-in-for-init t))  ; set as dynamic variable
+          (cond
+           ((= tt js2-SEMI)
+            (setq init (make-js2-empty-expr-node)))
+           ((or (= tt js2-VAR) (= tt js2-LET))
+            (js2-consume-token)
+            (setq init (js2-parse-variables tt js2-token-beg)))
+           (t
+            (setq init (js2-parse-expr)))))
+      (if (js2-match-token js2-IN)
+          (setq is-for-in t
+                in-pos (- js2-token-beg for-pos)
+                ;; scope of iteration target object is not the scope we've created above.
+                ;; stash current scope temporary.
+                cond (let ((js2-current-scope (js2-scope-parent-scope js2-current-scope)))
+                       (js2-parse-expr)))  ; object over which we're iterating
+        ;; else ordinary for loop - parse cond and incr
+        (js2-must-match js2-SEMI "msg.no.semi.for")
+        (setq cond (if (= (js2-peek-token) js2-SEMI)
+                       (make-js2-empty-expr-node) ; no loop condition
+                     (js2-parse-expr)))
+        (js2-must-match js2-SEMI "msg.no.semi.for.cond")
+        (setq tmp-pos js2-token-end
+              incr (if (= (js2-peek-token) js2-RP)
+                       (make-js2-empty-expr-node :pos tmp-pos)
+                     (js2-parse-expr))))
+      (if (js2-must-match js2-RP "msg.no.paren.for.ctrl")
+          (setq rp (- js2-token-beg for-pos)))
+      (if (not is-for-in)
+          (setq pn (make-js2-for-node :init init
+                                      :condition cond
+                                      :update incr
+                                      :lp lp
+                                      :rp rp))
+        ;; cond could be null if 'in obj' got eaten by the init node.
+        (if (js2-infix-node-p init)
+            ;; it was (foo in bar) instead of (var foo in bar)
+            (setq cond (js2-infix-node-right init)
+                  init (js2-infix-node-left init))
+          (if (and (js2-var-decl-node-p init)
+                   (> (length (js2-var-decl-node-kids init)) 1))
+              (js2-report-error "msg.mult.index")))
+        (setq pn (make-js2-for-in-node :iterator init
+                                       :object cond
+                                       :in-pos in-pos
+                                       :foreach-p is-for-each
+                                       :each-pos each-pos
+                                       :lp lp
+                                       :rp rp)))
+      (unwind-protect
+          (progn
+            (js2-enter-loop pn)
+            ;; We have to parse the body -after- creating the loop node,
+            ;; so that the loop node appears in the js2-loop-set, allowing
+            ;; break/continue statements to find the enclosing loop.
+            (setf body (js2-parse-statement)
+                  (js2-loop-node-body pn) body
+                  (js2-node-pos pn) for-pos
+                  (js2-node-len pn) (- (js2-node-end body) for-pos))
+            (js2-node-add-children pn init cond incr body))
+        ;; finally
+        (js2-exit-loop))
+      (js2-pop-scope))
     pn))
 
 (defun js2-parse-try ()
@@ -7941,7 +8002,7 @@ Parses for, for-in, and for each-in statements."
         finally-block
         saw-default-catch
         peek
-        var-name
+        param
         catch-cond
         catch-node
         guard-kwd
@@ -7969,8 +8030,21 @@ Parses for, for-in, and for each-in statements."
             (js2-report-error "msg.catch.unreachable"))
         (if (js2-must-match js2-LP "msg.no.paren.catch")
             (setq lp (- js2-token-beg catch-pos)))
-        (js2-must-match js2-NAME "msg.bad.catchcond")
-        (setq var-name (js2-create-name-node))
+        (js2-push-scope (make-js2-scope))
+        (let ((tt (js2-peek-token)))
+          (cond
+           ;; destructuring pattern
+           ;;     catch ({ message, file }) { ... }
+           ((or (= tt js2-LB) (= tt js2-LC))
+            (setq param
+                  (js2-define-destruct-symbols (js2-parse-primary-expr-lhs)
+                                               js2-LET nil)))
+           ;; simple name
+           (t
+            (js2-must-match js2-NAME "msg.bad.catchcond")
+            (setq param (js2-create-name-node))
+            (js2-define-symbol js2-LET js2-ts-string param))))
+        ;; pattern guard
         (if (js2-match-token js2-IF)
             (setq guard-kwd (- js2-token-beg catch-pos)
                   catch-cond (js2-parse-expr))
@@ -7981,17 +8055,18 @@ Parses for, for-in, and for each-in statements."
         (setq block (js2-parse-statements)
               try-end (js2-node-end block)
               catch-node (make-js2-catch-node :pos catch-pos
-                                              :var-name var-name
+                                              :param param
                                               :guard-expr catch-cond
                                               :guard-kwd guard-kwd
                                               :block block
                                               :lp lp
                                               :rp rp))
+        (js2-pop-scope)
         (if (js2-must-match js2-RC "msg.no.brace.after.body")
             (setq try-end js2-token-beg))
         (setf (js2-node-len block) (- try-end (js2-node-pos block))
               (js2-node-len catch-node) (- try-end catch-pos))
-        (js2-node-add-children catch-node var-name catch-cond block)
+        (js2-node-add-children catch-node param catch-cond block)
         (push catch-node catch-blocks)))
      ((/= peek js2-FINALLY)
       (js2-must-match js2-FINALLY "msg.try.no.catchfinally"
@@ -8433,6 +8508,9 @@ Returns the parsed `js2-var-decl-node' expression node."
           (progn
             (if (and (null init) (not js2-in-for-init))
                 (js2-report-error "msg.destruct.assign.no.init"))
+            (js2-define-destruct-symbols destructuring
+                                         decl-type
+                                         'font-lock-variable-name-face)
             (setf (js2-var-init-node-target vi) destructuring))
         (setf (js2-var-init-node-target vi) name))
       (setf (js2-var-init-node-initializer vi) init)
@@ -8478,8 +8556,8 @@ by `js2-parse-variables'."
       (js2-pop-scope))
     pn))
 
-(defsubst js2-define-new-symbol (decl-type name node)
-  (js2-scope-put-symbol js2-current-scope
+(defsubst js2-define-new-symbol (decl-type name node &optional scope)
+  (js2-scope-put-symbol (or scope js2-current-scope)
                         name
                         (make-js2-symbol decl-type name node)))
 
@@ -8519,7 +8597,8 @@ If NODE is non-nil, it is the AST node associated with the symbol."
               (js2-add-strict-warning "msg.var.redecl" name)
             (if (and js2-strict-var-hides-function-arg-warning (= sdt js2-LP))
                 (js2-add-strict-warning "msg.var.hides.arg" name)))
-        (js2-define-new-symbol decl-type name node)))
+        (js2-define-new-symbol decl-type name node
+                               js2-current-script-or-fn)))
      ((= decl-type js2-LP)
       (if symbol
           ;; must be duplicate parameter. Second parameter hides the
@@ -9244,7 +9323,7 @@ array-literals, array comprehensions and regular expressions."
                                 :value js2-ts-string
                                 :flags flags)
         (js2-set-face px-pos js2-ts-cursor 'font-lock-string-face 'record)
-        (put-text-property px-pos js2-ts-cursor 'syntax-table '(2))))
+        (js2-record-text-property px-pos js2-ts-cursor 'syntax-table '(2))))
      ((or (= tt js2-NULL)
           (= tt js2-THIS)
           (= tt js2-FALSE)
@@ -9267,26 +9346,30 @@ array-literals, array comprehensions and regular expressions."
 
 (defun js2-parse-name (tt-flagged tt)
   (let ((name js2-ts-string)
-        (name-pos js2-token-beg))
-      (if (and (js2-flag-set-p tt-flagged js2-ti-check-label)
-               (= (js2-peek-token) js2-COLON))
-          (prog1
+        (name-pos js2-token-beg)
+        node)
+    (if (and (js2-flag-set-p tt-flagged js2-ti-check-label)
+             (= (js2-peek-token) js2-COLON))
+        (prog1
             ;; Do not consume colon, it is used as unwind indicator
             ;; to return to statementHelper.
             (make-js2-label-node :pos name-pos
                                  :len (- js2-token-end name-pos)
                                  :name name)
-            (js2-set-face name-pos
-                          js2-token-end
-                          'font-lock-variable-name-face 'record))
-        ;; Otherwise not a label, just a name.  Unfortunately peeking
-        ;; the next token to check for a colon has biffed js2-token-beg
-        ;; and js2-token-end.  We store the name's bounds in buffer vars
-        ;; and `js2-create-name-node' uses them.
-        (js2-save-name-token-data name-pos name)
-        (if js2-compiler-xml-available
-            (js2-parse-property-name nil name 0)
-          (js2-create-name-node 'check-activation)))))
+          (js2-set-face name-pos
+                        js2-token-end
+                        'font-lock-variable-name-face 'record))
+      ;; Otherwise not a label, just a name.  Unfortunately peeking
+      ;; the next token to check for a colon has biffed js2-token-beg
+      ;; and js2-token-end.  We store the name's bounds in buffer vars
+      ;; and `js2-create-name-node' uses them.
+      (js2-save-name-token-data name-pos name)
+      (setq node (if js2-compiler-xml-available
+                     (js2-parse-property-name nil name 0)
+                   (js2-create-name-node 'check-activation)))
+      (if js2-highlight-external-variables
+          (js2-record-name-node node))
+      node)))
 
 (defsubst js2-parse-warn-trailing-comma (msg pos elems comma-pos)
   (js2-add-strict-warning
@@ -9310,6 +9393,8 @@ array-literals, array comprehensions and regular expressions."
         elems
         pn
         (continue t))
+    (unless js2-is-in-lhs
+        (js2-push-scope (make-js2-scope))) ; for array comp
     (while continue
       (setq tt (js2-peek-token))
       (cond
@@ -9332,7 +9417,7 @@ array-literals, array comprehensions and regular expressions."
                                       :len (- js2-ts-cursor pos)
                                       :elems (nreverse elems)))
         (apply #'js2-node-add-children pn (js2-array-node-elems pn))
-        (when after-comma
+        (when (and after-comma (not js2-is-in-lhs))
           (js2-parse-warn-trailing-comma "msg.array.trailing.comma"
                                          pos elems after-comma)))
        ;; destructuring binding
@@ -9365,6 +9450,8 @@ array-literals, array comprehensions and regular expressions."
         (push (js2-parse-assign-expr) elems)
         (setq after-lb-or-comma nil
               after-comma nil))))
+    (unless js2-is-in-lhs
+      (js2-pop-scope))
     pn))
 
 (defun js2-parse-array-comprehension (expr pos)
@@ -9373,11 +9460,22 @@ EXPR is the first expression after the opening left-bracket.
 POS is the beginning of the LB token preceding EXPR.
 We should have just parsed the 'for' keyword before calling this function."
   (let (loops
+        loop
+        first
+        prev
         filter
         if-pos
         result)
     (while (= (js2-peek-token) js2-FOR)
-      (push (js2-parse-array-comp-loop) loops))
+      (let ((prev (car loops))) ; rearrange scope chain
+        (push (setq loop (js2-parse-array-comp-loop)) loops)
+        (if prev ; each loop is parent scope to the next one
+            (setf (js2-scope-parent-scope loop) prev)
+          ; first loop takes expr scope's parent
+          (setf (js2-scope-parent-scope (setq first loop))
+                (js2-scope-parent-scope js2-current-scope)))))
+    ;; set expr scope's parent to the last loop
+    (setf (js2-scope-parent-scope js2-current-scope) (car loops))
     (when (= (js2-peek-token) js2-IF)
       (js2-consume-token)
       (setq if-pos (- js2-token-beg pos)  ; relative
@@ -9393,6 +9491,7 @@ We should have just parsed the 'for' keyword before calling this function."
                                            :if-pos if-pos))
     (apply #'js2-node-add-children result expr (car filter)
            (js2-array-comp-node-loops result))
+    (setq js2-current-scope first) ; pop to the first loop
     result))
 
 (defun js2-parse-array-comp-loop ()
@@ -9426,7 +9525,9 @@ Last token peeked should be the initial FOR."
            ((or (= tt js2-LB)
                 (= tt js2-LC))
             ;; handle destructuring assignment
-            (setq iter (js2-parse-primary-expr-lhs)))
+            (setq iter (js2-parse-primary-expr-lhs))
+            (js2-define-destruct-symbols iter js2-LET
+                                         'font-lock-variable-name-face t))
            ((js2-valid-prop-name-token tt)
             (js2-consume-token)
             (setq iter (js2-create-name-node)))
@@ -9515,12 +9616,11 @@ When `js2-is-in-lhs' is t, forms like {a, b, c} will be permitted."
            (= (js2-peek-token) js2-NAME)
            (or (string= prop "get")
                (string= prop "set")))
-      (progn
-        (js2-consume-token)
-        (js2-set-face ppos pend 'font-lock-keyword-face 'record)  ; get/set
-        (js2-record-face 'font-lock-function-name-face)      ; for peeked name
-        (setq name (js2-create-name-node)) ; discard get/set & use peeked name
-        (js2-parse-getter-setter-prop ppos name (string= prop "get"))))
+      (js2-consume-token)
+      (js2-set-face ppos pend 'font-lock-keyword-face 'record)  ; get/set
+      (js2-record-face 'font-lock-function-name-face)      ; for peeked name
+      (setq name (js2-create-name-node)) ; discard get/set & use peeked name
+      (js2-parse-getter-setter-prop ppos name (string= prop "get")))
      ;; abbreviated destructuring bind e.g., {a, b} = c;
      ;; XXX: To be honest, the value of `js2-is-in-lhs' becomes t only when
      ;; patterns are appeared in variable declaration, function parameters, and catch-clause.
@@ -9532,7 +9632,6 @@ When `js2-is-in-lhs' is t, forms like {a, b, c} will be permitted."
              (or (= ctk js2-COMMA)
                  (= ctk js2-RC)
                  (js2-valid-prop-name-token ctk))))
-      (js2-set-face ppos pend 'font-lock-variable-name-face 'record)
       name)
      ;; regular prop
      (t
@@ -9645,24 +9744,23 @@ not `js2-NAME', then we use the token info saved in instance vars."
 ;; Karl for coming up with the initial approach, which packs a lot of
 ;; punch for so little code.
 
-(defconst js-possibly-braceless-keyword-re
-  (regexp-opt
-   '("catch" "do" "else" "finally" "for" "if" "each" "try" "while" "with" "let")
-   'words)
+(defconst js-possibly-braceless-keywords-re
+  (concat "else[ \t]+if\\|for[ \t]+each\\|"
+          (regexp-opt '("catch" "do" "else" "finally" "for" "if"
+                        "try" "while" "with" "let")))
   "Regular expression matching keywords that are optionally
 followed by an opening brace.")
 
-(defconst js-possibly-braceless-keywords-re
-  "\\([ \t}]*else[ \t]+if\\|[ \t}]*for[ \t]+each\\)"
-  "Regular expression which matches the keywords which are consist of more than 2 words
-like 'if else' and 'for each', and optionally followed by an opening brace.")
-
 (defconst js-indent-operator-re
   (concat "[-+*/%<>=&^|?:.]\\([^-+*/]\\|$\\)\\|"
           (regexp-opt '("in" "instanceof") 'words))
   "Regular expression matching operators that affect indentation
 of continued expressions.")
 
+(defconst js-declaration-keyword-re
+  (regexp-opt '("var" "let" "const") 'words)
+  "Regular expression matching variable declaration keywords.")
+
 ;; This function has horrible results if you're typing an array
 ;; such as [[1, 2], [3, 4], [5, 6]].  Bounce indenting -really- sucks
 ;; in conjunction with electric-indent, so just disabling it.
@@ -9679,7 +9777,7 @@ bound to KEY in the global keymap and indents the current line."
         (call-interactively cmd)))
   ;; don't do the electric keys inside comments or strings,
   ;; and don't do bounce-indent with them.
-  (let ((parse-state (parse-partial-sexp (point-min) (point)))
+  (let ((parse-state (syntax-ppss (point)))
         (js2-bounce-indent-p (js2-code-at-bol-p)))
     (unless (or (nth 3 parse-state)
                 (nth 4 parse-state))
@@ -9687,11 +9785,12 @@ bound to KEY in the global keymap and indents the current line."
 
 (defun js-re-search-forward-inner (regexp &optional bound count)
   "Auxiliary function for `js-re-search-forward'."
-  (let ((parse)
-        (saved-point (point-min)))
+  (let (parse saved-point)
     (while (> count 0)
       (re-search-forward regexp bound)
-      (setq parse (parse-partial-sexp saved-point (point)))
+      (setq parse (if saved-point
+                      (parse-partial-sexp saved-point (point))
+                    (syntax-ppss (point))))
       (cond ((nth 3 parse)
              (re-search-forward
               (concat "\\([^\\]\\|^\\)" (string (nth 3 parse)))
@@ -9727,11 +9826,12 @@ comments have been removed."
 
 (defun js-re-search-backward-inner (regexp &optional bound count)
   "Auxiliary function for `js-re-search-backward'."
-  (let ((parse)
-        (saved-point (point-min)))
+  (let (parse saved-point)
     (while (> count 0)
       (re-search-backward regexp bound)
-      (setq parse (parse-partial-sexp saved-point (point)))
+      (setq parse (if saved-point
+                      (parse-partial-sexp saved-point (point))
+                    (syntax-ppss (point))))
       (cond ((nth 3 parse)
              (re-search-backward
               (concat "\\([^\\]\\|^\\)" (string (nth 3 parse)))
@@ -9783,10 +9883,12 @@ a comma)."
         (and (js-re-search-backward "\n" nil t)
             (progn
               (skip-chars-backward " \t")
-              (backward-char)
-              (and (js-looking-at-operator-p)
-                   (and (progn (backward-char)
-                               (not (looking-at "\\*\\|++\\|--\\|/[/*]"))))))))))
+               (unless (bolp)
+                 (backward-char)
+                 (and (js-looking-at-operator-p)
+                      (and (progn
+                             (backward-char)
+                             (not (looking-at "\\*\\|++\\|--\\|/[/*]")))))))))))
 
 (defun js-end-of-do-while-loop-p ()
   "Returns non-nil if word after point is `while' of a do-while
@@ -9812,29 +9914,48 @@ indented to the same column as the current line."
                           "\\<while\\>" (point-at-eol) t))
                     (= (current-indentation) saved-indent)))))))))
 
-(defun js-get-multiline-declaration-offset ()
-  "Returns offset (> 0) if the current line is part of
- multi-line variable declaration like below example, and
- returns 0 otherwise.
+(defun js-multiline-decl-indentation ()
+  "Returns the declaration indentation column if the current line belongs
+to a multiline declaration statement.  All declarations are lined up vertically:
+
+var a = 10,
+    b = 20,
+    c = 30;
 
- var a = 10,
-     b = 20,
-     c = 30;
+Note that if `js2-always-indent-assigned-expr-in-decls-p' is nil, and the first
+assigned expression is a function or array/object literal, it will be indented
+differently:
 
+var o = {                               var bar = 2,
+  foo: 3                                    o = {
+},                                            foo: 3
+    bar = 2;                                };
 "
-  (let* ((node (js2-node-at-point))
-         (pnode (and node (js2-node-parent node)))
-         (pnode-type (and pnode (js2-node-type pnode))))
-    (if (and node
-             (= js2-NAME (js2-node-type node))
-             (or
-              (= js2-VAR pnode-type)
-              (= js2-LET pnode-type)
-              (= js2-CONST pnode-type)))
-        (if (= js2-CONST pnode-type)
-            6
-          4)
-      0)))
+  (let (forward-sexp-function ; use lisp version
+        at-opening-bracket)
+    (save-excursion
+      (back-to-indentation)
+      (when (not (looking-at js-declaration-keyword-re))
+        (when (looking-at js-indent-operator-re)
+          (goto-char (match-end 0))) ; continued expressions are ok
+        (while (and (not at-opening-bracket)
+                    (not (bobp))
+                    (let ((pos (point)))
+                      (save-excursion
+                        (js2-backward-sws)
+                        (or (eq (char-before) ?,)
+                            (and (not (eq (char-before) ?\;))
+                                 (and
+                                  (prog2 (skip-chars-backward "[[:punct:]]")
+                                      (looking-at js-indent-operator-re)
+                                    (js2-backward-sws))
+                                  (not (eq (char-before) ?\;))))
+                            (js2-same-line pos)))))
+          (condition-case err
+              (backward-sexp)
+            (scan-error (setq at-opening-bracket t))))
+        (when (looking-at js-declaration-keyword-re)
+          (- (1+ (match-end 0)) (point-at-bol)))))))
 
 (defun js-ctrl-statement-indentation ()
   "Returns the proper indentation of the current line if it
@@ -9843,28 +9964,22 @@ returns nil."
   (let (forward-sexp-function)  ; temporarily unbind it
     (save-excursion
       (back-to-indentation)
-      (when (save-excursion
-              (and (not (js2-same-line (point-min)))
-                   (not (looking-at "{"))
-                   (js-re-search-backward "[[:graph:]]" nil t)
-                   (not (looking-at "[{([]"))
-                   (progn
-                     (forward-char)
+      (when (and (not (js2-same-line (point-min)))
+                 (not (looking-at "{"))
+                 (js-re-search-backward "[[:graph:]]" nil t)
+                 (not (looking-at "[{([]"))
+                 (progn
+                   (forward-char)
+                   (when (= (char-before) ?\))
                      ;; scan-sexps sometimes throws an error
                      (ignore-errors (backward-sexp))
-                     (when (looking-at "(") (backward-word 1))
-                     (and (save-excursion
-                            (skip-chars-backward " \t}" (point-at-bol))
-                            (or (bolp)
-                                (and (backward-word 1)
-                                     (skip-chars-backward " \t}" (point-at-bol))
-                                     (bolp)
-                                     (looking-at js-possibly-braceless-keywords-re))))
-                          (looking-at js-possibly-braceless-keyword-re)
+                     (skip-chars-backward " \t" (point-at-bol)))
+                   (let ((pt (point)))
+                     (back-to-indentation)
+                     (and (looking-at js-possibly-braceless-keywords-re)
+                          (= (match-end 0) pt)
                           (not (js-end-of-do-while-loop-p))))))
-        (save-excursion
-          (goto-char (match-beginning 0))
-          (+ (current-indentation) js2-basic-offset))))))
+        (+ (current-indentation) js2-basic-offset)))))
 
 (defun js2-indent-in-array-comp (parse-status)
   "Return non-nil if we think we're in an array comprehension.
@@ -9885,7 +10000,8 @@ In particular, return the buffer position of the first `for' kwd."
                     (match-beginning 0)))
             ;; to skip arbitrary expressions we need the parser,
             ;; so we'll just guess at it.
-            (if (re-search-forward "[^,]* \\(for\\) " end t)
+            (if (and (> end (point)) ; not empty literal
+                     (re-search-forward "[^,]]* \\(for\\) " end t))
                 (match-beginning 1))))))))
 
 (defun js2-array-comp-indentation (parse-status for-kwd)
@@ -9907,9 +10023,8 @@ In particular, return the buffer position of the first `for' kwd."
     (let ((ctrl-stmt-indent (js-ctrl-statement-indentation))
           (same-indent-p (looking-at "[]})]\\|\\<case\\>\\|\\<default\\>"))
           (continued-expr-p (js-continued-expression-p))
-          (multiline-declaration-offset (or (and js2-use-ast-for-indentation-p
-                                                 (js-get-multiline-declaration-offset))
-                                            0))
+          (declaration-indent (and js2-pretty-multiline-decl-indentation-p
+                                   (js-multiline-decl-indentation)))
           (bracket (nth 1 parse-status))
           beg)
       (cond
@@ -9924,6 +10039,11 @@ In particular, return the buffer position of the first `for' kwd."
 
        (ctrl-stmt-indent)
 
+       ((and declaration-indent continued-expr-p)
+        (+ declaration-indent js2-basic-offset))
+
+       (declaration-indent)
+
        (bracket
         (goto-char bracket)
         (cond
@@ -9936,13 +10056,15 @@ In particular, return the buffer position of the first `for' kwd."
                      (not js2-consistent-level-indent-inner-bracket-p))
                 (progn (goto-char (1+ (nth 1 p)))
                        (skip-chars-forward " \t"))
-              (back-to-indentation))
+              (back-to-indentation)
+              (when (and js2-pretty-multiline-decl-indentation-p
+                         js2-always-indent-assigned-expr-in-decls-p
+                         (looking-at js-declaration-keyword-re))
+                (goto-char (1+ (match-end 0)))))
             (cond (same-indent-p
                    (current-column))
                   (continued-expr-p
                    (+ (current-column) (* 2 js2-basic-offset)))
-                  ((> multiline-declaration-offset 0)
-                   (+ (current-column) js2-basic-offset multiline-declaration-offset))
                   (t
                    (+ (current-column) js2-basic-offset)))))
          (t
@@ -9953,9 +10075,6 @@ In particular, return the buffer position of the first `for' kwd."
 
        (continued-expr-p js2-basic-offset)
 
-       ((> multiline-declaration-offset 0)
-        (+ multiline-declaration-offset))
-
        (t 0)))))
 
 (defun js2-lineup-comment (parse-status)
@@ -10046,12 +10165,13 @@ Returns `point-at-bol' if the line is empty."
     (skip-chars-forward " \t")
     (point)))
 
-(defun js2-bounce-indent (normal-col parse-status)
+(defun js2-bounce-indent (normal-col parse-status backwards)
   "Cycle among alternate computed indentation positions.
 PARSE-STATUS is the result of `parse-partial-sexp' from the beginning
 of the buffer to the current point.  NORMAL-COL is the indentation
 column computed by the heuristic guesser based on current paren,
-bracket, brace and statement nesting."
+bracket, brace and statement nesting.  If BACKWARDS, cycle positions
+in reverse."
   (let ((cur-indent (js2-current-indent))
         (old-buffer-undo-list buffer-undo-list)
         ;; Emacs 21 only has `count-lines', not `line-number-at-pos'
@@ -10060,6 +10180,7 @@ bracket, brace and statement nesting."
                         (1+ (count-lines (point-min) (point)))))
         positions
         pos
+        main-pos
         anchor
         arglist-cont
         same-indent
@@ -10109,8 +10230,7 @@ bracket, brace and statement nesting."
                           (current-column)))))
           (when pos
             (incf pos js2-basic-offset)
-            (unless (member pos positions)
-              (push pos positions)))
+            (push pos positions))
 
           ;; third likely point:  same indent as previous line of code.
           ;; Make it the first likely point if we're not on an
@@ -10134,12 +10254,12 @@ bracket, brace and statement nesting."
                          (js2-indent-in-objlit-p parse-status))
                      (not (js2-arglist-close)))
                 (setq same-indent pos))
-            (unless (member pos positions)
-              (push pos positions)))
+            (push pos positions))
 
           ;; fourth likely point:  first preceding code with less indentation
           ;; than the immediately preceding code line.
           (setq pos (save-excursion
+                      (back-to-indentation)
                       (js2-backward-sws)
                       (back-to-indentation)
                       (setq anchor (current-column))
@@ -10149,28 +10269,30 @@ bracket, brace and statement nesting."
                                         (current-column))
                                       anchor)))
                       (setq pos (current-column))))
-          (unless (member pos positions)
-            (push pos positions))
+          (push pos positions)
+
+          ;; nesting-heuristic position, main by default
+          (push (setq main-pos normal-col) positions)
 
-          ;; put nesting-heuristic position first in list, sort rest
-          (setq positions (nreverse (sort positions '<)))
-          (setq positions (cons normal-col (delete normal-col positions)))
+          ;; delete duplicates and sort positions list
+          (setq positions (sort (delete-dups positions) '<))
 
           ;; comma-list continuation lines:  prev line indent takes precedence
           (if same-indent
-              (setq positions
-                    (cons same-indent
-                          (sort (delete same-indent positions) '<))))
+              (setq main-pos same-indent))
 
           ;; common special cases where we want to indent in from previous line
           (if (or (js2-indent-case-block-p)
                   (js2-indent-objlit-arg-p parse-status))
-              (setq positions
-                    (cons basic-offset
-                          (delete basic-offset positions))))
+              (setq main-pos basic-offset))
+
+          ;; if bouncing backwards, reverse positions list
+          (if backwards
+              (setq positions (reverse positions)))
 
           ;; record whether we're already sitting on one of the alternatives
           (setq pos (member cur-indent positions))
+
           (cond
            ;; case 0:  we're one one of the alternatives and this is the
            ;; first time they've pressed TAB on this line (best-guess).
@@ -10185,7 +10307,7 @@ bracket, brace and statement nesting."
             (setq computed-pos 0))
            ;; case 2:  not on any of the computed spots => use main spot
            ((not pos)
-            (setq computed-pos 0))
+            (setq computed-pos (js2-position main-pos positions)))
            ;; case 3:  on last position:  cycle to first position
            ((null (cdr pos))
             (setq computed-pos 0))
@@ -10209,6 +10331,12 @@ bracket, brace and statement nesting."
       ;; see commentary for `js2-mode-last-indented-line'
       (setq js2-mode-last-indented-line current-line))))
 
+(defun js2-indent-bounce-backwards ()
+  "Calls `js2-indent-line'.  When `js2-bounce-indent-p',
+cycles between the computed indentation positions in reverse order."
+  (interactive)
+  (js2-indent-line t))
+
 (defsubst js2-1-line-comment-continuation-p ()
   "Return t if we're in a 1-line comment continuation.
 If so, we don't ever want to use bounce-indent."
@@ -10225,13 +10353,10 @@ If so, we don't ever want to use bounce-indent."
                (forward-line 0))
              (looking-at "\\s-*//"))))))
 
-(defun js2-indent-line ()
+(defun js2-indent-line (&optional bounce-backwards)
   "Indent the current line as JavaScript source text."
   (interactive)
-  (when js2-use-ast-for-indentation-p
-    (js2-reparse))
   (let (parse-status
-        current-indent
         offset
         indent-col
         moved
@@ -10239,38 +10364,35 @@ If so, we don't ever want to use bounce-indent."
         ;; 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)))
+                          (syntax-ppss (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)))))))
+       (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 bounce-backwards))
+        ;; just indent to the guesser's likely spot
+        (t (indent-line-to indent-col)))
+       (when (plusp offset)
+         (forward-char offset))))))
 
 (defun js2-indent-region (start end)
   "Indent the region, but don't use bounce indenting."
   (let ((js2-bounce-indent-p nil)
-        (indent-region-function nil))
-    (indent-region start end nil)))  ; nil for byte-compiler
+        (indent-region-function nil)
+        (after-change-functions (remq 'js2-mode-edit
+                                      after-change-functions)))
+    (indent-region start end nil) ; nil for byte-compiler
+    (js2-mode-edit start end (- end start))))
 
 ;;;###autoload (add-to-list 'auto-mode-alist '("\\.js$" . js2-mode))
 
@@ -10281,6 +10403,9 @@ If so, we don't ever want to use bounce-indent."
   (kill-all-local-variables)
   (set-syntax-table js2-mode-syntax-table)
   (use-local-map js2-mode-map)
+  (make-local-variable 'comment-start)
+  (make-local-variable 'comment-end)
+  (make-local-variable 'comment-start-skip)
   (setq major-mode 'js2-mode
         mode-name "JavaScript-IDE"
         comment-start "//"  ; used by comment-region; don't change it
@@ -10358,12 +10483,16 @@ If so, we don't ever want to use bounce-indent."
   (add-to-invisibility-spec '(js2-outline . t))
   (set (make-local-variable 'line-move-ignore-invisible) t)
   (set (make-local-variable 'forward-sexp-function) #'js2-mode-forward-sexp)
+
+  (if (fboundp 'run-mode-hooks)
+      (run-mode-hooks 'js2-mode-hook)
+    (run-hooks 'js2-mode-hook))
+
   (setq js2-mode-functions-hidden nil
         js2-mode-comments-hidden nil
         js2-mode-buffer-dirty-p t
         js2-mode-parsing nil)
-  (js2-reparse)
-  (run-hooks 'js2-mode-hook))
+  (js2-reparse))
 
 (defun js2-mode-exit ()
   "Exit `js2-mode' and clean up."
@@ -10395,7 +10524,25 @@ You can disable this by customizing `js2-cleanup-whitespace'."
       (cancel-timer js2-mode-parse-timer))
   (setq js2-mode-parsing nil)
   (setq js2-mode-parse-timer
-        (run-with-idle-timer js2-idle-timer-delay nil #'js2-reparse)))
+        (run-with-idle-timer js2-idle-timer-delay nil
+                             #'js2-mode-idle-reparse (current-buffer))))
+
+(defun js2-mode-idle-reparse (buffer)
+  "Run `js2-reparse' if BUFFER is the current buffer, or schedule
+it to be reparsed when the buffer is selected."
+  (if (eq buffer (current-buffer))
+      (js2-reparse)
+    ;; reparse when the buffer is selected again
+    (with-current-buffer buffer
+      (add-hook 'window-configuration-change-hook
+                #'js2-mode-idle-reparse-inner
+                t))))
+
+(defun js2-mode-idle-reparse-inner ()
+  (remove-hook 'window-configuration-change-hook
+               #'js2-mode-idle-reparse-inner
+               t)
+  (js2-reparse))
 
 (defun js2-mode-edit (beg end len)
   "Schedule a new parse after buffer is edited.
@@ -10444,11 +10591,9 @@ buffer will only rebuild its `js2-mode-ast' if the buffer is dirty."
           (when (or js2-mode-buffer-dirty-p force)
             (js2-remove-overlays)
             (js2-with-unmodifying-text-property-changes
-              (remove-text-properties (point-min) (point-max) '(syntax-table))
               (setq js2-mode-buffer-dirty-p nil
                     js2-mode-fontifications nil
-                    js2-mode-deferred-properties nil
-                    js2-additional-externs nil)
+                    js2-mode-deferred-properties nil)
               (if js2-mode-verbose-parse-p
                   (message "parsing..."))
               (setq time
@@ -10456,8 +10601,11 @@ buffer will only rebuild its `js2-mode-ast' if the buffer is dirty."
                      (setq interrupted-p
                            (catch 'interrupted
                              (setq js2-mode-ast (js2-parse))
-                             (when (plusp js2-highlight-level)
-                               (js2-mode-fontify-regions))
+                             ;; if parsing is interrupted, comments and regex
+                             ;; literals stay ignored by `parse-partial-sexp'
+                             (remove-text-properties (point-min) (point-max)
+                                                     '(syntax-table))
+                             (js2-mode-apply-deferred-properties)
                              (js2-mode-remove-suppressed-warnings)
                              (js2-mode-show-warnings)
                              (js2-mode-show-errors)
@@ -10553,15 +10701,16 @@ Defaults to point."
         for o in (overlays-at pos)
         thereis (overlay-get o 'js2-error)))
 
-(defun js2-mode-fontify-regions ()
-  "Apply fontifications recorded during parsing."
-  ;; We defer clearing faces as long as possible to eliminate flashing.
-  (js2-clear-face (point-min) (point-max))
-  ;; have to reverse the recorded fontifications so that errors and
-  ;; warnings overwrite the normal fontifications
-  (dolist (f (nreverse js2-mode-fontifications))
-    (put-text-property (first f) (second f) 'face (third f)))
-  (setq js2-mode-fontifications nil)
+(defun js2-mode-apply-deferred-properties ()
+  "Apply fontifications and other text properties recorded during parsing."
+  (when (plusp js2-highlight-level)
+    ;; We defer clearing faces as long as possible to eliminate flashing.
+    (js2-clear-face (point-min) (point-max))
+    ;; Have to reverse the recorded fontifications list so that errors
+    ;; and warnings overwrite the normal fontifications.
+    (dolist (f (nreverse js2-mode-fontifications))
+      (put-text-property (first f) (second f) 'face (third f)))
+    (setq js2-mode-fontifications nil))
   (dolist (p js2-mode-deferred-properties)
     (apply #'put-text-property p))
   (setq js2-mode-deferred-properties nil))
@@ -10609,7 +10758,8 @@ This ensures that the counts and `next-error' are correct."
 (defun js2-echo-error (old-point new-point)
   "Called by point-motion hooks."
   (let ((msg (get-text-property new-point 'help-echo)))
-    (if msg
+    (if (and msg (or (not (current-message))
+                     (string= (current-message) "Quit")))
         (message msg))))
 
 (defalias #'js2-echo-help #'js2-echo-error)
@@ -10618,7 +10768,8 @@ This ensures that the counts and `next-error' are correct."
   "Handle user pressing the Enter key."
   (interactive)
   (let ((parse-status (save-excursion
-                        (parse-partial-sexp (point-min) (point)))))
+                        (syntax-ppss (point))))
+        (js2-bounce-indent-p nil))
     (cond
      ;; check if we're inside a string
      ((nth 3 parse-status)
@@ -10629,12 +10780,10 @@ This ensures that the counts and `next-error' are correct."
      (t
       ;; should probably figure out what the mode-map says we should do
       (if js2-indent-on-enter-key
-          (let ((js2-bounce-indent-p nil))
-            (js2-indent-line)))
+          (js2-indent-line))
       (insert "\n")
       (if js2-enter-indents-newline
-          (let ((js2-bounce-indent-p nil))
-            (js2-indent-line)))))))
+          (js2-indent-line))))))
 
 (defun js2-mode-split-string (parse-status)
   "Turn a newline in mid-string into a string concatenation.
@@ -10662,7 +10811,7 @@ PARSE-STATUS is as documented in `parse-partial-sexp'."
       (backward-char 1))))
 
 (defun js2-mode-extend-comment ()
-  "When inside a comment block, add comment prefix."
+  "Indent the line and, when inside a comment block, add comment prefix."
   (let (star single col first-line needs-close)
     (save-excursion
       (back-to-indentation)
@@ -10704,12 +10853,15 @@ PARSE-STATUS is as documented in `parse-partial-sexp'."
             (insert "\n")
             (indent-to col)
             (insert "*/"))))
-     (single
-      (when (save-excursion
+     ((and single
+           (save-excursion
               (and (zerop (forward-line 1))
-                   (looking-at "\\s-*//")))
-        (indent-to col)
-        (insert "// "))))))
+                   (looking-at "\\s-*//"))))
+      (indent-to col)
+      (insert "// "))
+     ;; don't need to extend the comment after all
+     (js2-enter-indents-newline
+      (js2-indent-line)))))
 
 (defun js2-beginning-of-line ()
   "Toggles point between bol and first non-whitespace char in line.
@@ -10761,7 +10913,7 @@ Also moves past comment delimiters when inside comments."
   "Return non-nil if inside a string.
 Actually returns the quote character that begins the string."
    (let ((parse-state (save-excursion
-                        (parse-partial-sexp (point-min) (point)))))
+                        (syntax-ppss (point)))))
       (nth 3 parse-state)))
 
 (defsubst js2-mode-inside-comment-or-string ()
@@ -10775,7 +10927,7 @@ Actually returns the quote character that begins the string."
      (and comment-start
           (<= comment-start (point))))
    (let ((parse-state (save-excursion
-                        (parse-partial-sexp (point-min) (point)))))
+                        (syntax-ppss (point)))))
      (or (nth 3 parse-state)
          (nth 4 parse-state)))))
 
@@ -10925,7 +11077,7 @@ already have been inserted."
 (defun js2-mode-match-single-quote ()
   "Insert matching single-quote."
   (interactive)
-  (let ((parse-status (parse-partial-sexp (point-min) (point))))
+  (let ((parse-status (syntax-ppss (point))))
     ;; don't match inside comments, since apostrophe is more common
     (if (nth 4 parse-status)
         (insert "'")
@@ -10945,7 +11097,7 @@ already have been inserted."
   "Skip over close-paren rather than inserting, where appropriate."
   (interactive)
   (let* ((here (point))
-         (parse-status (parse-partial-sexp (point-min) here))
+         (parse-status (syntax-ppss here))
          (open-pos (nth 1 parse-status))
          (close last-input-event)
          (open (cond