]> code.delx.au - gnu-emacs-elpa/blobdiff - js2-mode.el
Handle unlimited function nesting in imenu index
[gnu-emacs-elpa] / js2-mode.el
index 4c191e0b06bf0956e2656d5c8d38ff0433037ba1..ef97b8b116dd6ecb1b323230f2a3d313f157b459 100644 (file)
@@ -172,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
@@ -201,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
@@ -223,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)
 
@@ -232,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-use-ast-for-indentation-p nil
-  "Non-nil to use AST for indentation and make it more robust."
+(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-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.
@@ -1181,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")))
@@ -2005,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)
@@ -2024,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)
@@ -2149,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
@@ -2584,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")
@@ -2620,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))
@@ -2664,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))
@@ -2687,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'
@@ -2705,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))
@@ -2715,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))
@@ -2794,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)
@@ -2941,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)
@@ -3024,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))
@@ -3129,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))
@@ -3197,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))
@@ -3338,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 (")
@@ -3402,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 ")
@@ -3520,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) "[")
@@ -3613,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))
@@ -3644,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))
@@ -3761,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))
@@ -3948,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))
@@ -4000,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))
@@ -4670,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'."
@@ -4827,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)
@@ -6015,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
@@ -6118,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)
@@ -6259,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
@@ -6475,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.")
 
@@ -6653,10 +6658,11 @@ of a simple name.  Called before EXPR has a parent node."
 (defun js2-record-name-node (node)
   "Saves NODE to `js2-recorded-identifiers' to check for undeclared variables
 later. NODE must be a name node."
-  (push (list node js2-current-scope
-              (setq leftpos (js2-node-abs-pos node))
-              (setq end (+ leftpos (js2-node-len node))))
-        js2-recorded-identifiers))
+  (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 references.
@@ -6684,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.
 
@@ -6733,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)
@@ -6769,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)
@@ -6777,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))
@@ -6799,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)
@@ -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,38 +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))
-      ;; if top-level/external, keep as-is
-      (if (js2-node-top-level-decl-p head)
-          (push chain result)
-        (cond
-         ;; starts with this-reference
-         ((js2-this-node-p head)
-          (setq fn (js2-node-parent-script-or-fn head)
-                chain (cdr chain))) ; discard this-node
-         ;; nested named function
-         ((js2-function-node-p (setq parent (js2-node-parent head)))
-          (setq fn (js2-node-parent-script-or-fn parent)))
-         ;; variable assigned a function expression
-         (t (setq fn (js2-node-parent-script-or-fn head))))
-        (unless (or (null fn) (js2-nested-function-p fn))
-          ;; if the parent function is found, and it's not nested,
-          ;; look it up in function-map.
-          (if (setq parent-chain (and js2-imenu-function-map
-                                      (gethash fn js2-imenu-function-map)))
-              ;; prefix parent fn qname, which is the
-              ;; parent-chain sans tail, to this chain.
-              (push (append (butlast parent-chain) chain) result)
-            ;; parent function is not nested, and not in function-map
-            ;; => it's anonymous top-level wrapper, discard.
-            (push 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)
@@ -7082,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.
@@ -7359,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 ()
@@ -7414,9 +7431,10 @@ NODE is either `js2-array-node', `js2-object-node', or `js2-name-node'."
     (let (leftpos)
       (js2-define-symbol decl-type (js2-name-node-name node)
                          node ignore-not-in-block)
-      (js2-set-face (setq leftpos (js2-node-abs-pos node))
-                    (+ leftpos (js2-node-len node))
-                    face 'record)))
+      (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
@@ -7906,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 ()
@@ -7977,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
@@ -8005,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))
@@ -8017,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"
@@ -8517,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)))
 
@@ -8558,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
@@ -9283,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)
@@ -9377,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
@@ -9704,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.
@@ -9738,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))
@@ -9746,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)))
@@ -9786,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)))
@@ -9842,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
@@ -9871,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
@@ -9902,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.
@@ -9944,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)
@@ -9966,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
@@ -9983,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
@@ -9995,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
@@ -10012,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)
@@ -10105,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'
@@ -10119,6 +10180,7 @@ bracket, brace and statement nesting."
                         (1+ (count-lines (point-min) (point)))))
         positions
         pos
+        main-pos
         anchor
         arglist-cont
         same-indent
@@ -10168,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
@@ -10193,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))
@@ -10208,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)
 
-          ;; put nesting-heuristic position first in list, sort rest
-          (setq positions (nreverse (sort positions '<)))
-          (setq positions (cons normal-col (delete normal-col positions)))
+          ;; nesting-heuristic position, main by default
+          (push (setq main-pos 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).
@@ -10244,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))
@@ -10268,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."
@@ -10284,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
@@ -10298,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))
 
@@ -10340,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
@@ -10417,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."
@@ -10454,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.
@@ -10503,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
@@ -10515,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)
@@ -10612,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))
@@ -10668,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)
@@ -10677,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)
@@ -10688,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.
@@ -10721,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)
@@ -10763,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.
@@ -10820,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 ()
@@ -10834,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)))))
 
@@ -10984,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 "'")
@@ -11004,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