(eval-when-compile (require 'cl)) (require 'js2-mode) (defconst js2-imenu-extension-styles `((:framework jquery :call-re "\\_<\\(?:jQuery\\|\\$\\|_\\)\\.extend\\s-*(" :recorder js2-imenu-record-jquery-extend) (:framework jquery-ui :call-re "^\\s-*\\(?:jQuery\\|\\$\\)\\.widget\\s-*(" :recorder js2-imenu-record-string-declare) (:framework dojo :call-re "^\\s-*dojo.declare\\s-*(" :recorder js2-imenu-record-string-declare) (:framework backbone :call-re ,(concat "\\_<" js2-mode-identifier-re "\\.extend\\s-*(") :recorder js2-imenu-record-backbone-extend))) (defconst js2-imenu-available-frameworks (mapcar (lambda (style) (plist-get style :framework)) js2-imenu-extension-styles) "List of available JavaScript framework symbols.") (defcustom js2-imenu-enabled-frameworks js2-imenu-available-frameworks "Frameworks to be recognized by `js2-mode'." :type (cons 'set (mapcar (lambda (x) (list 'const x)) js2-imenu-available-frameworks)) :group 'js2-mode) (defcustom js2-imenu-show-other-functions t "Non-nil to show functions not recognized by other mechanisms, in a shared namespace." :type 'boolean :group 'js2-mode) (defcustom js2-imenu-other-functions-ns "?" "Namespace name to use for other functions." :type 'string :group 'js2-mode) (defcustom js2-imenu-show-module-pattern t "Non-nil to recognize the module pattern: var foobs = (function(a) { return {fib: function() {}, fub: function() {}}; })(b); We record the returned hash as belonging to the named module, and prefix any functions defined inside the IIFE with the module name." :type 'boolean :group 'js2-mode) (defun js2-imenu-extras-setup () (when js2-imenu-enabled-frameworks (add-to-list 'js2-post-parse-callbacks 'js2-imenu-record-declarations t)) (when (or js2-imenu-show-other-functions js2-imenu-show-module-pattern) (add-to-list 'js2-post-parse-callbacks 'js2-imenu-walk-ast t))) (declare (special root)) (defun js2-imenu-record-declarations () (let* ((styles (loop for style in js2-imenu-extension-styles when (memq (plist-get style :framework) js2-imenu-enabled-frameworks) collect style)) (re (mapconcat (lambda (style) (concat "\\(" (plist-get style :call-re) "\\)")) styles "\\|")) ;; Dynamic scoping. Ew. (js2-mode-ast root)) (goto-char (point-min)) (while (js-re-search-forward re nil t) (loop for i from 0 to (1- (length styles)) when (match-beginning (1+ i)) return (funcall (plist-get (nth i styles) :recorder)))))) (defun js2-imenu-record-jquery-extend () (let ((pred (lambda (subject) (and (js2-prop-get-node-p subject) (string= (js2-name-node-name (js2-prop-get-node-right subject)) "prototype"))))) (js2-imenu-record-extend-first-arg (1- (point)) pred 'js2-compute-nested-prop-get))) (defun js2-imenu-record-string-declare () (js2-imenu-record-extend-first-arg (1- (point)) 'js2-string-node-p (lambda (node) (split-string (js2-string-node-value node) "\\." t)))) (defun js2-imenu-record-extend-first-arg (point pred qname-fn) (let* ((node (js2-node-at-point point)) (args (js2-call-node-args node)) (subject (first args))) (when (funcall pred subject) (loop for arg in (cdr args) when (js2-object-node-p arg) do (js2-record-object-literal arg (funcall qname-fn subject) (js2-node-abs-pos arg)))))) (defun js2-imenu-record-backbone-extend () (let* ((node (js2-node-at-point (1- (point)))) (args (js2-call-node-args node)) (methods (first args)) (parent (js2-node-parent node))) (when (js2-object-node-p methods) (let ((subject (cond ((js2-var-init-node-p parent) (js2-var-init-node-target parent)) ((js2-assign-node-p parent) (js2-assign-node-left parent))))) (when subject (js2-record-object-literal methods (js2-compute-nested-prop-get subject) (js2-node-abs-pos methods))))))) (defun js2-imenu-walk-ast () (js2-visit-ast root (lambda (node end-p) (unless end-p (cond ((and js2-imenu-show-other-functions (js2-object-prop-node-p node)) (js2-imenu-record-orphan-function node)) ((and js2-imenu-show-module-pattern (js2-assign-node-p node)) (js2-imenu-record-module-pattern node))) t)))) (defun js2-imenu-record-orphan-function (node) "Record orphan function when it's the value of NODE. NODE must be `js2-object-prop-node'." (when (js2-function-node-p (js2-object-prop-node-right node)) (let ((fn-node (js2-object-prop-node-right node))) (unless (and js2-imenu-function-map (gethash fn-node js2-imenu-function-map)) (let ((key-node (js2-object-prop-node-left node))) (js2-record-imenu-entry fn-node (list js2-imenu-other-functions-ns (js2-prop-node-name key-node)) (js2-node-abs-pos key-node))))))) (defun js2-imenu-record-module-pattern (node) "Recognize and record module pattern use instance. NODE must be `js2-assign-node'." (let ((init (js2-assign-node-right node))) (when (js2-call-node-p init) (let ((target (js2-assign-node-left node)) (callt (js2-call-node-target init))) ;; Just basic call form: (function() {...})(); ;; TODO: Handle variations without duplicating `js2-wrapper-function-p'? (when (and (js2-paren-node-p callt) (js2-function-node-p (js2-paren-node-expr callt))) (let* ((fn (js2-paren-node-expr callt)) (blk (js2-function-node-body fn)) (ret (car (last (js2-block-node-kids blk))))) (when (and (js2-return-node-p ret) (js2-object-node-p (js2-return-node-retval ret))) ;; TODO: Map function names when revealing module pattern is used. (let ((retval (js2-return-node-retval ret))) (js2-record-object-literal retval (js2-compute-nested-prop-get target) (js2-node-abs-pos retval))) (js2-record-imenu-functions fn target)))))))) (provide 'js2-imenu-extras)