;; Dmitry Gutov <dgutov@yandex.ru>
;; URL: https://github.com/mooz/js2-mode/
;; http://code.google.com/p/js2-mode/
-;; Version: 20150713
+;; Version: 20150909
;; Keywords: languages, javascript
;; Package-Requires: ((emacs "24.1") (cl-lib "0.5"))
(require 'cl-lib)
(require 'imenu)
(require 'js)
+(require 'etags)
(eval-and-compile
(if (version< emacs-version "25.0")
"Face used to highlight function name in calls."
:group 'js2-mode)
+(defface js2-object-property
+ '((t :inherit default))
+ "Face used to highlight named property in object literal."
+ :group 'js2-mode)
+
(defface js2-instance-member
'((t :foreground "DarkOrchid"))
"Face used to highlight instance variables in javascript.
(define-key map (kbd "C-c C-o") #'js2-mode-toggle-element)
(define-key map (kbd "C-c C-w") #'js2-mode-toggle-warnings-and-errors)
(define-key map [down-mouse-3] #'js2-down-mouse-3)
+ (define-key map [remap js-find-symbol] #'js2-jump-to-definition)
(define-key map [menu-bar javascript]
(cons "JavaScript" (make-sparse-keymap "JavaScript")))
(defun js2-print-function-node (n i)
(let* ((pad (js2-make-pad i))
- (getter (js2-node-get-prop n 'GETTER_SETTER))
+ (method (js2-node-get-prop n 'METHOD_TYPE))
(name (or (js2-function-node-name n)
(js2-function-node-member-expr n)))
(params (js2-function-node-params n))
(rest-p (js2-function-node-rest-p n))
(body (js2-function-node-body n))
(expr (not (eq (js2-function-node-form n) 'FUNCTION_STATEMENT))))
- (unless (or getter arrow)
+ (unless (or method arrow)
(insert pad "function")
(when (eq (js2-function-node-generator-type n) 'STAR)
(insert "*")))
(js2-print-list (js2-object-node-elems n))
(insert "}"))
+(cl-defstruct (js2-computed-prop-name-node
+ (:include js2-node)
+ (:constructor nil)
+ (:constructor make-js2-computed-prop-name-node
+ (&key
+ (type js2-LB)
+ expr
+ (pos (js2-current-token-beg))
+ (len (- js2-ts-cursor
+ (js2-current-token-beg))))))
+ "AST node for a `ComputedPropertyName'."
+ expr)
+
+(put 'cl-struct-js2-computed-prop-name-node 'js2-visitor 'js2-visit-computed-prop-name-node)
+(put 'cl-struct-js2-computed-prop-name-node 'js2-printer 'js2-print-computed-prop-name-node)
+
+(defun js2-visit-computed-prop-name-node (n v)
+ (js2-visit-ast (js2-computed-prop-name-node-expr n) v))
+
+(defun js2-print-computed-prop-name-node (n i)
+ (insert (js2-make-pad i) "[")
+ (js2-print-ast (js2-computed-prop-name-node-expr n) 0)
+ (insert "]"))
+
(cl-defstruct (js2-object-prop-node
(:include js2-infix-node)
(:constructor nil)
(defun js2-print-object-prop-node (n i)
(let* ((left (js2-object-prop-node-left n))
- (right (js2-object-prop-node-right n))
- (computed (not (or (js2-string-node-p left)
- (js2-number-node-p left)
- (js2-name-node-p left)))))
- (insert (js2-make-pad i))
- (if computed
- (insert "["))
- (js2-print-ast left 0)
- (if computed
- (insert "]"))
+ (right (js2-object-prop-node-right n)))
+ (js2-print-ast left i)
(if (not (js2-node-get-prop n 'SHORTHAND))
(progn
(insert ": ")
(js2-print-ast right 0)))))
-(cl-defstruct (js2-getter-setter-node
+(cl-defstruct (js2-method-node
(:include js2-infix-node)
(:constructor nil)
- (:constructor make-js2-getter-setter-node (&key type ; GET, SET, or FUNCTION
- (pos js2-ts-cursor)
- len left right)))
- "AST node for a getter/setter property in an object literal.
-The `left' field is the `js2-name-node' naming the getter/setter prop.
+ (:constructor make-js2-method-node (&key type ; GET, SET, or FUNCTION
+ (pos js2-ts-cursor)
+ len left right)))
+ "AST node for a method in an object literal or a class body.
+The `left' field is the `js2-name-node' naming the method.
The `right' field is always an anonymous `js2-function-node' with a node
-property `GETTER_SETTER' set to js2-GET, js2-SET, or js2-FUNCTION. ")
+property `METHOD_TYPE' set to js2-GET, js2-SET, or js2-FUNCTION. ")
-(put 'cl-struct-js2-getter-setter-node 'js2-visitor 'js2-visit-infix-node)
-(put 'cl-struct-js2-getter-setter-node 'js2-printer 'js2-print-getter-setter)
+(put 'cl-struct-js2-method-node 'js2-visitor 'js2-visit-infix-node)
+(put 'cl-struct-js2-method-node 'js2-printer 'js2-print-method)
-(defun js2-print-getter-setter (n i)
+(defun js2-print-method (n i)
(let* ((pad (js2-make-pad i))
- (left (js2-getter-setter-node-left n))
- (right (js2-getter-setter-node-right n))
- (computed (not (or (js2-string-node-p left)
- (js2-number-node-p left)
- (js2-name-node-p left)))))
+ (left (js2-method-node-left n))
+ (right (js2-method-node-right n)))
(insert pad)
(if (/= (js2-node-type n) js2-FUNCTION)
(insert (if (= (js2-node-type n) js2-GET) "get " "set ")))
(when (and (js2-function-node-p right)
(eq 'STAR (js2-function-node-generator-type right)))
(insert "*"))
- (when computed
- (insert "["))
(js2-print-ast left 0)
- (when computed
- (insert "]"))
(js2-print-ast right 0)))
(cl-defstruct (js2-prop-get-node
(prop
(if (string-match js2-ecma-object-props prop-name)
'font-lock-constant-face))))))
+ (when (and (not face) target (not call-p) prop-name)
+ (setq face 'js2-object-property))
(when face
(let ((pos (+ (js2-node-pos parent) ; absolute
(js2-node-pos prop)))) ; relative
(js2-node-add-children fn-node pn)
pn))
-(defun js2-define-destruct-symbols-internal
- (node decl-type face &optional ignore-not-in-block name-nodes)
- "Internal version of `js2-define-destruct-symbols'. The only
-difference is that NAME-NODES is passed down recursively."
- (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))
- (setq name-nodes (append name-nodes (list node)))))
- ((js2-object-node-p node)
- (dolist (elem (js2-object-node-elems node))
- (setq name-nodes
- (append name-nodes
- (js2-define-destruct-symbols-internal
- ;; In abbreviated destructuring {a, b}, right == left.
- (js2-object-prop-node-right elem)
- decl-type face ignore-not-in-block name-nodes)))))
- ((js2-array-node-p node)
- (dolist (elem (js2-array-node-elems node))
- (when elem
- (setq name-nodes
- (append name-nodes
- (js2-define-destruct-symbols-internal
- elem decl-type face ignore-not-in-block name-nodes))))))
- (t (js2-report-error "msg.no.parm" nil (js2-node-abs-pos node)
- (js2-node-len node))))
- name-nodes)
-
(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'.
Return a list of `js2-name-node' nodes representing the symbols
declared; probably to check them for errors."
- (js2-define-destruct-symbols-internal node decl-type face ignore-not-in-block))
+ (let (name-nodes)
+ (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))
+ (list node)))
+ ((js2-object-node-p node)
+ (dolist (elem (js2-object-node-elems node))
+ ;; js2-infix-node-p catches both object prop node and initialized
+ ;; binding element (which is directly an infix node).
+ (when (js2-infix-node-p elem)
+ (push (js2-define-destruct-symbols
+ (js2-infix-node-left elem)
+ decl-type face ignore-not-in-block)
+ name-nodes)))
+ (apply #'append (nreverse name-nodes)))
+ ((js2-array-node-p node)
+ (dolist (elem (js2-array-node-elems node))
+ (when elem
+ (if (js2-infix-node-p elem) (setq elem (js2-infix-node-left elem)))
+ (push (js2-define-destruct-symbols
+ elem decl-type face ignore-not-in-block)
+ name-nodes)))
+ (apply #'append (nreverse name-nodes)))
+ (t (js2-report-error "msg.no.parm" nil (js2-node-abs-pos node)
+ (js2-node-len node))
+ nil))))
(defvar js2-illegal-strict-identifiers
'("eval" "arguments")
(eq (js2-current-token-type) js2-NAME)))
params param
param-name-nodes new-param-name-nodes
- default-found rest-param-at)
+ rest-param-at)
(when paren-free-arrow
(js2-unget-token))
(cl-loop for tt = (js2-peek-token)
((and (not paren-free-arrow)
(or (= tt js2-LB) (= tt js2-LC)))
(js2-get-token)
- (when default-found
- (js2-report-error "msg.no.default.after.default.param"))
(setq param (js2-parse-destruct-primary-expr)
new-param-name-nodes (js2-define-destruct-symbols
param js2-LP 'js2-function-param))
(js2-check-strict-function-params param-name-nodes (list param))
(setq param-name-nodes (append param-name-nodes (list param)))
;; default parameter value
- (when (or (and default-found
- (not rest-param-at)
- (js2-must-match js2-ASSIGN
- "msg.no.default.after.default.param"
- (js2-node-pos param)
- (js2-node-len param)))
- (and (>= js2-language-version 200)
- (js2-match-token js2-ASSIGN)))
+ (when (and (>= js2-language-version 200)
+ (js2-match-token js2-ASSIGN))
(cl-assert (not paren-free-arrow))
(let* ((pos (js2-node-pos param))
(tt (js2-current-token-type))
(len (- (js2-node-end right) pos)))
(setq param (make-js2-assign-node
:type tt :pos pos :len len :op-pos op-pos
- :left left :right right)
- default-found t)
+ :left left :right right))
(js2-node-add-children param left right)))
(push param params)))
(when (and rest-param-at (> (length params) (1+ rest-param-at)))
(js2-node-add-children pn test-expr if-true if-false))
pn))
-(defun js2-make-binary (type left parser)
+(defun js2-make-binary (type left parser &optional no-get)
"Helper for constructing a binary-operator AST node.
LEFT is the left-side-expression, already parsed, and the
binary operator should have just been matched.
(op-pos (- (js2-current-token-beg) pos))
(right (if (js2-node-p parser)
parser
- (js2-get-token)
+ (unless no-get (js2-get-token))
(funcall parser)))
(pn (make-js2-infix-node :type type
:pos pos
(setq pn (js2-parse-tagged-template pn (make-js2-string-node :type tt))))
(t
(js2-unget-token)
- (setq continue nil))))
- (if (>= js2-highlight-level 2)
- (js2-parse-highlight-member-expr-node pn))
+ (setq continue nil)))
+ (if (>= js2-highlight-level 2)
+ (js2-parse-highlight-member-expr-node pn)))
pn))
(defun js2-parse-tagged-template (tag-node tpl-node)
(apply #'js2-node-add-children pn (js2-array-node-elems pn)))
;; destructuring binding
(js2-is-in-destructuring
- (push (if (or (= tt js2-LC)
- (= tt js2-LB)
- (= tt js2-NAME))
- ;; [a, b, c] | {a, b, c} | {a:x, b:y, c:z} | a
- (js2-parse-destruct-primary-expr)
- ;; invalid pattern
+ (push (cond
+ ((and (= tt js2-NAME)
+ (= js2-ASSIGN (js2-peek-token)))
+ ;; a=defaultValue
+ (js2-parse-initialized-binding (js2-parse-name js2-NAME)))
+ ((or (= tt js2-LC)
+ (= tt js2-LB)
+ (= tt js2-NAME))
+ ;; [a, b, c] | {a, b, c} | {a:x, b:y, c:z} | a
+ (js2-parse-destruct-primary-expr))
+ ;; invalid pattern
+ (t
(js2-report-error "msg.bad.var")
- (make-js2-error-node))
+ (make-js2-error-node)))
elems)
(setq after-lb-or-comma nil
after-comma nil))
(defun js2-property-key-string (property-node)
"Return the key of PROPERTY-NODE (a `js2-object-prop-node' or
-`js2-getter-setter-node') as a string, or nil if it can't be
+`js2-method-node') as a string, or nil if it can't be
represented as a string (e.g., the key is computed by an
expression)."
(let ((key (js2-infix-node-left property-node)))
+ (when (js2-computed-prop-name-node-p key)
+ (setq key (js2-computed-prop-name-node-expr key)))
(cond
((js2-name-node-p key)
(js2-name-node-name key))
(if after-comma
(js2-parse-warn-trailing-comma "msg.extra.trailing.comma"
pos elems after-comma)))
+ ;; Skip semicolons in a class body
+ ((and class-p
+ (= tt js2-SEMI))
+ nil)
(t
(js2-report-error "msg.bad.prop")
(unless js2-recover-from-parse-errors
(lambda (previous-elem)
(and (setq previous-elem-key-string
(js2-property-key-string previous-elem))
- (string= previous-elem-key-string elem-key-string)))
+ ;; Check if the property is a duplicate.
+ (string= previous-elem-key-string elem-key-string)
+ ;; But make an exception for getter / setter pairs.
+ (not (and (js2-method-node-p elem)
+ (js2-method-node-p previous-elem)
+ (/= (js2-method-node-type elem)
+ (js2-method-node-type previous-elem))))))
elems))
(js2-report-error "msg.dup.obj.lit.prop.strict"
elem-key-string
(defun js2-parse-named-prop (tt pos previous-token)
"Parse a name, string, or getter/setter object property.
When `js2-is-in-destructuring' is t, forms like {a, b, c} will be permitted."
- (let ((key (cond
- ;; Literal string keys: {'foo': 'bar'}
- ((= tt js2-STRING)
- (make-js2-string-node))
- ;; Handle computed keys: {[Symbol.iterator]: ...}, *[1+2]() {...}},
- ;; {[foo + bar]() { ... }}, {[get ['x' + 1]() {...}}
- ((and (= tt js2-LB)
- (>= js2-language-version 200))
- (prog1 (js2-parse-expr)
- (js2-must-match js2-RB "msg.missing.computed.rb")))
- ;; Numeric keys: {12: 'foo'}, {10.7: 'bar'}
- ((= tt js2-NUMBER)
- (make-js2-number-node))
- ;; Unquoted names: {foo: 12}
- ((= tt js2-NAME)
- (js2-create-name-node))
- ;; Anything else is an error
- (t (js2-report-error "msg.bad.prop"))))
+ (let ((key (js2-parse-prop-name tt))
(prop (and previous-token (js2-token-string previous-token)))
(property-type (when previous-token
(if (= (js2-token-type previous-token) js2-MUL)
(>= js2-language-version 200))
(when (js2-name-node-p key) ; highlight function name properties
(js2-record-face 'font-lock-function-name-face))
- (js2-parse-getter-setter-prop pos key property-type))
+ (js2-parse-method-prop pos key property-type))
+ ;; binding element with initializer
+ ((and (= (js2-peek-token) js2-ASSIGN)
+ (>= js2-language-version 200))
+ (js2-parse-initialized-binding key))
;; regular prop
(t
(let ((beg (js2-current-token-beg))
(if (js2-function-node-p
(js2-object-prop-node-right expr))
'font-lock-function-name-face
- 'font-lock-variable-name-face)
+ 'js2-object-property)
'record)
expr)))))
+(defun js2-parse-initialized-binding (name)
+ "Parse a `SingleNameBinding' with initializer.
+
+`name' is the `BindingIdentifier'."
+ (when (js2-match-token js2-ASSIGN)
+ (js2-make-binary js2-ASSIGN name 'js2-parse-assign-expr t)))
+
+(defun js2-parse-prop-name (tt)
+ (cond
+ ;; Literal string keys: {'foo': 'bar'}
+ ((= tt js2-STRING)
+ (make-js2-string-node))
+ ;; Handle computed keys: {[Symbol.iterator]: ...}, *[1+2]() {...}},
+ ;; {[foo + bar]() { ... }}, {[get ['x' + 1]() {...}}
+ ((and (= tt js2-LB)
+ (>= js2-language-version 200))
+ (make-js2-computed-prop-name-node
+ :expr (prog1 (js2-parse-assign-expr)
+ (js2-must-match js2-RB "msg.missing.computed.rb"))))
+ ;; Numeric keys: {12: 'foo'}, {10.7: 'bar'}
+ ((= tt js2-NUMBER)
+ (make-js2-number-node))
+ ;; Unquoted names: {foo: 12}
+ ((= tt js2-NAME)
+ (js2-create-name-node))
+ ;; Anything else is an error
+ (t (js2-report-error "msg.bad.prop"))))
+
(defun js2-parse-plain-property (prop)
"Parse a non-getter/setter property in an object literal.
PROP is the node representing the property: a number, name,
(js2-node-add-children result prop expr)
result))))
-(defun js2-parse-getter-setter-prop (pos prop type-string)
- "Parse getter or setter property in an object literal.
+(defun js2-parse-method-prop (pos prop type-string)
+ "Parse method property in an object literal or a class body.
JavaScript syntax is:
- { get foo() {...}, set foo(x) {...} }
+ { foo(...) {...}, get foo() {...}, set foo(x) {...}, *foo(...) {...} }
and expression closure style is also supported
(js2-report-error "msg.bad.prop")
(if (cl-plusp (length (js2-function-name fn)))
(js2-report-error "msg.bad.prop")))
- (js2-node-set-prop fn 'GETTER_SETTER type) ; for codegen
+ (js2-node-set-prop fn 'METHOD_TYPE type) ; for codegen
(when (string= type-string "*")
(setf (js2-function-node-generator-type fn) 'STAR))
(setq end (js2-node-end fn)
- result (make-js2-getter-setter-node :type type
- :pos pos
- :len (- end pos)
- :left prop
- :right fn))
+ result (make-js2-method-node :type type
+ :pos pos
+ :len (- end pos)
+ :left prop
+ :right fn))
(js2-node-add-children result prop fn)
result))
(unless (js2-ast-root-p fn)
(narrow-to-region beg (+ beg (js2-node-len fn))))))
+(defun js2-jump-to-definition (&optional arg)
+ "Jump to the definition of an object's property, variable or function."
+ (interactive "P")
+ (ring-insert find-tag-marker-ring (point-marker))
+ (let* ((node (js2-node-at-point))
+ (parent (js2-node-parent node))
+ (names (if (js2-prop-get-node-p parent)
+ (reverse (let ((temp (js2-compute-nested-prop-get parent)))
+ (cl-loop for n in temp
+ with result = '()
+ do (push n result)
+ until (equal node n)
+ finally return result)))))
+ node-init)
+ (unless (and (js2-name-node-p node)
+ (not (js2-var-init-node-p parent))
+ (not (js2-function-node-p parent)))
+ (error "Node is not a supported jump node"))
+ (push (or (and names (pop names))
+ (unless (and (js2-object-prop-node-p parent)
+ (eq node (js2-object-prop-node-left parent)))
+ node)) names)
+ (setq node-init (js2-search-scope node names))
+
+ ;; todo: display list of results in buffer
+ ;; todo: group found references by buffer
+ (unless node-init
+ (switch-to-buffer
+ (catch 'found
+ (unless arg
+ (mapc (lambda (b)
+ (with-current-buffer b
+ (when (derived-mode-p 'js2-mode)
+ (setq node-init (js2-search-scope js2-mode-ast names))
+ (if node-init
+ (throw 'found b)))))
+ (buffer-list)))
+ nil)))
+ (setq node-init (if (listp node-init) (car node-init) node-init))
+ (unless node-init
+ (pop-tag-mark)
+ (error "No jump location found"))
+ (goto-char (js2-node-abs-pos node-init))))
+
+(defun js2-search-object (node name-node)
+ "Check if object NODE contains element with NAME-NODE."
+ (cl-assert (js2-object-node-p node))
+ ;; Only support name-node and nodes for the time being
+ (cl-loop for elem in (js2-object-node-elems node)
+ for left = (js2-object-prop-node-left elem)
+ if (or (and (js2-name-node-p left)
+ (equal (js2-name-node-name name-node)
+ (js2-name-node-name left)))
+ (and (js2-string-node-p left)
+ (string= (js2-name-node-name name-node)
+ (js2-string-node-value left))))
+ return elem))
+
+(defun js2-search-object-for-prop (object prop-names)
+ "Return node in OBJECT that matches PROP-NAMES or nil.
+PROP-NAMES is a list of values representing a path to a value in OBJECT.
+i.e. ('name' 'value') = {name : { value: 3}}"
+ (let (node
+ (temp-object object)
+ (temp t) ;temporay node
+ (names prop-names))
+ (while (and temp names (js2-object-node-p temp-object))
+ (setq temp (js2-search-object temp-object (pop names)))
+ (and (setq node temp)
+ (setq temp-object (js2-object-prop-node-right temp))))
+ (unless names node)))
+
+(defun js2-search-scope (node names)
+ "Searches NODE scope for jump location matching NAMES.
+NAMES is a list of property values to search for. For functions
+and variables NAMES will contain one element."
+ (let (node-init
+ (val (js2-name-node-name (car names))))
+ (setq node-init (js2-get-symbol-declaration node val))
+
+ (when (> (length names) 1)
+
+ ;; Check var declarations
+ (when (and node-init (string= val (js2-name-node-name node-init)))
+ (let ((parent (js2-node-parent node-init))
+ (temp-names names))
+ (pop temp-names) ;; First element is var name
+ (setq node-init (when (js2-var-init-node-p parent)
+ (js2-search-object-for-prop
+ (js2-var-init-node-initializer parent)
+ temp-names)))))
+
+ ;; Check all assign nodes
+ (js2-visit-ast
+ js2-mode-ast
+ (lambda (node endp)
+ (unless endp
+ (if (js2-assign-node-p node)
+ (let ((left (js2-assign-node-left node))
+ (right (js2-assign-node-right node))
+ (temp-names names))
+ (when (js2-prop-get-node-p left)
+ (let* ((prop-list (js2-compute-nested-prop-get left))
+ (found (cl-loop for prop in prop-list
+ until (not (string= (js2-name-node-name
+ (pop temp-names))
+ (js2-name-node-name prop)))
+ if (not temp-names) return prop))
+ (found-node (or found
+ (when (js2-object-node-p right)
+ (js2-search-object-for-prop right
+ temp-names)))))
+ (if found-node (push found-node node-init))))))
+ t))))
+ node-init))
+
+(defun js2-get-symbol-declaration (node name)
+ "Find scope for NAME from NODE."
+ (let ((scope (js2-get-defining-scope
+ (or (js2-node-get-enclosing-scope node)
+ node) name)))
+ (if scope (js2-symbol-ast-node (js2-scope-get-symbol scope name)))))
+
(provide 'js2-mode)
;;; js2-mode.el ends here