;;; 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
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
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-always-indent-assigned-expr-in-decls-p nil
"If both `js2-pretty-multiline-decl-indentation-p' and this are non-nil,
-always additionally indents function expression or array/object literal
+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.
;; 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)
"\\(?: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.")
;; 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.
;; 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)
"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)
(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))
((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)
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.
;; 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
'("call" "apply"))
(js2-call-node-p (js2-node-parent parent))))))))
-(defun js2-browse-postprocess-chains (chains)
+(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 ((js2-imenu-fn-type-map (make-hash-table :test 'eq))
- result head fn fn-type parent-chain p elem parent)
- (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))))
- (when fn
- (setq fn-type (gethash fn js2-imenu-fn-type-map))
- (unless fn-type
- (setq fn-type
- (cond ((js2-nested-function-p fn) 'skip)
- ((setq parent-chain
- (gethash fn js2-imenu-function-map))
- 'named)
- ((js2-wrapper-function-p fn) 'anon)
- (t 'skip)))
- (puthash fn fn-type js2-imenu-fn-type-map))
- (case fn-type
- ('anon (push chain result)) ; anonymous top-level wrapper
- ('named ; top-level named function
- ;; prefix parent fn qname, which is
- ;; parent-chain sans last elem, to this chain.
- (push (append (butlast parent-chain) 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)
: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
(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
(= (current-indentation) saved-indent)))))))))
(defun js-multiline-decl-indentation ()
- "Returns the proper indentation of the current line if it belongs
+ "Returns the declaration indentation column if the current line belongs
to a multiline declaration statement. All declarations are lined up vertically:
var a = 10,
at-opening-bracket)
(save-excursion
(back-to-indentation)
- (when (and (not (looking-at js-declaration-keyword-re))
- (looking-at (concat js2-mode-identifier-re
- "[ \t]*\\(?:=[^=]\\|\\,\\|;\\|$\\)")))
- (while (not (save-excursion
- ;; unary ops
- (skip-chars-backward "-+~! \t")
- (or at-opening-bracket
- ;; explicit semicolon
- (save-excursion (js2-backward-sws)
- (eq (char-before) ?\;))
- ;; implicit semicolon
- (and (bolp)
- (progn (js2-backward-sws)
- (not (eq (char-before) ?,)))
- (progn (skip-chars-backward "[[:punct:]]")
- (not (looking-at js-indent-operator-re)))))))
+ (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))))
(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)
(ctrl-stmt-indent)
+ ((and declaration-indent continued-expr-p)
+ (+ declaration-indent js2-basic-offset))
+
(declaration-indent)
-
+
(bracket
(goto-char bracket)
(cond
;; 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 main-pos same-indent))
;; 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).
;; case 4: on intermediate position: cycle to next position
(t
(setq computed-pos (js2-position (second pos) positions))))
-
+
;; see if any hooks want to indent; otherwise we do it
(loop with result = nil
for hook in js2-indent-hook
finally do
(unless (or result (null computed-pos))
(indent-line-to (nth computed-pos positions)))))
-
+
;; finally
(if js2-mode-indent-inhibit-undo
(setq buffer-undo-list old-buffer-undo-list))
"Indent the current line as JavaScript source text."
(interactive)
(let (parse-status
- current-indent
offset
indent-col
moved
(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 bounce-backwards)
- (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."
(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)
-
- (if (fboundp 'run-mode-hooks)
- (run-mode-hooks 'js2-mode-hook)
- (run-hooks 'js2-mode-hook)))
+ (js2-reparse))
(defun js2-mode-exit ()
"Exit `js2-mode' and clean up."
(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.
(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)
"Handle user pressing the Enter key."
(interactive)
(let ((parse-status (save-excursion
- (syntax-ppss (point)))))
+ (syntax-ppss (point))))
+ (js2-bounce-indent-p nil))
(cond
;; check if we're inside a string
((nth 3 parse-status)
(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.
(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)
(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.