]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/js2-mode/js2-imenu-extras.el
js2-mode: Merge from upstream
[gnu-emacs-elpa] / packages / js2-mode / js2-imenu-extras.el
diff --git a/packages/js2-mode/js2-imenu-extras.el b/packages/js2-mode/js2-imenu-extras.el
new file mode 100644 (file)
index 0000000..e8e15a5
--- /dev/null
@@ -0,0 +1,222 @@
+;;; js2-imenu-extras.el --- Imenu support for additional constructs\r
+\r
+;; Copyright (C) 2012-2013  Free Software Foundation, Inc.\r
+\r
+;; Author:    Dmitry Gutov <dgutov@yandex.ru>\r
+;; Keywords:  languages, javascript, imenu\r
+\r
+;; This file is part of GNU Emacs.\r
+\r
+;; GNU Emacs is free software: you can redistribute it and/or modify\r
+;; it under the terms of the GNU General Public License as published by\r
+;; the Free Software Foundation, either version 3 of the License, or\r
+;; (at your option) any later version.\r
+\r
+;; GNU Emacs is distributed in the hope that it will be useful,\r
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+;; GNU General Public License for more details.\r
+\r
+;; You should have received a copy of the GNU General Public License\r
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.\r
+\r
+;;; Commentary:\r
+\r
+;; This package adds Imenu support for additional framework constructs and\r
+;; structural patterns to `js2-mode'.\r
+\r
+;; Usage:\r
+\r
+;; (add-hook 'js2-mode-hook 'js2-imenu-extras-mode)\r
+\r
+;; To customize how it works:\r
+;;   M-x customize-group RET js2-imenu RET\r
+\r
+(eval-when-compile\r
+  (require 'cl))\r
+\r
+(require 'js2-mode)\r
+\r
+(defconst js2-imenu-extension-styles\r
+  `((:framework jquery\r
+     :call-re   "\\_<\\(?:jQuery\\|\\$\\|_\\)\\.extend\\s-*("\r
+     :recorder  js2-imenu-record-jquery-extend)\r
+\r
+    (:framework jquery-ui\r
+     :call-re   "^\\s-*\\(?:jQuery\\|\\$\\)\\.widget\\s-*("\r
+     :recorder  js2-imenu-record-string-declare)\r
+\r
+    (:framework dojo\r
+     :call-re   "^\\s-*dojo.declare\\s-*("\r
+     :recorder  js2-imenu-record-string-declare)\r
+\r
+    (:framework backbone\r
+     :call-re   ,(concat "\\_<" js2-mode-identifier-re "\\.extend\\s-*(")\r
+     :recorder  js2-imenu-record-backbone-extend))\r
+  "List of JavaScript class definition or extension styles.\r
+\r
+:framework is a valid value in `js2-imenu-enabled-frameworks'.\r
+\r
+:call-re is a regular expression that has no capturing groups.\r
+\r
+:recorder is a function name that will be called when the regular\r
+expression matches some text in the buffer.  When it's called, point will be\r
+at the end of the match.  The function must keep the point position.")\r
+\r
+(defconst js2-imenu-available-frameworks\r
+  (mapcar (lambda (style) (plist-get style :framework)) js2-imenu-extension-styles)\r
+  "List of available JavaScript framework symbols.")\r
+\r
+(defcustom js2-imenu-enabled-frameworks js2-imenu-available-frameworks\r
+  "Frameworks to be recognized by `js2-mode'."\r
+  :type (cons 'set (mapcar (lambda (x) (list 'const x))\r
+                           js2-imenu-available-frameworks))\r
+  :group 'js2-imenu)\r
+\r
+(defcustom js2-imenu-show-other-functions t\r
+  "Non-nil to show functions not recognized by other mechanisms,\r
+in a shared namespace."\r
+  :type 'boolean\r
+  :group 'js2-imenu)\r
+\r
+(defcustom js2-imenu-other-functions-ns "?"\r
+  "Namespace name to use for other functions."\r
+  :type 'string\r
+  :group 'js2-imenu)\r
+\r
+(defcustom js2-imenu-show-module-pattern t\r
+  "Non-nil to recognize the module pattern:\r
+\r
+var foobs = (function(a) {\r
+  return {fib: function() {}, fub: function() {}};\r
+})(b);\r
+\r
+We record the returned hash as belonging to the named module, and\r
+prefix any functions defined inside the IIFE with the module name."\r
+  :type 'boolean\r
+  :group 'js2-imenu)\r
+\r
+;;;###autoload\r
+(defun js2-imenu-extras-setup ()\r
+  (when js2-imenu-enabled-frameworks\r
+    (add-hook 'js2-post-parse-callbacks 'js2-imenu-record-declarations t t))\r
+  (when (or js2-imenu-show-other-functions js2-imenu-show-module-pattern)\r
+    (add-hook 'js2-post-parse-callbacks 'js2-imenu-walk-ast t t)))\r
+\r
+(defun js2-imenu-extras-remove ()\r
+  (remove-hook 'js2-post-parse-callbacks 'js2-imenu-record-declarations t)\r
+  (remove-hook 'js2-post-parse-callbacks 'js2-imenu-walk-ast t))\r
+\r
+(defun js2-imenu-record-declarations ()\r
+  (let* ((styles (loop for style in js2-imenu-extension-styles\r
+                       when (memq (plist-get style :framework)\r
+                                  js2-imenu-enabled-frameworks)\r
+                       collect style))\r
+         (re (mapconcat (lambda (style)\r
+                          (concat "\\(" (plist-get style :call-re) "\\)"))\r
+                        styles "\\|")))\r
+    (goto-char (point-min))\r
+    (while (js2-re-search-forward re nil t)\r
+      (loop for i from 0 to (1- (length styles))\r
+            when (match-beginning (1+ i))\r
+            return (funcall (plist-get (nth i styles) :recorder))))))\r
+\r
+(defun js2-imenu-record-jquery-extend ()\r
+  (let ((pred (lambda (subject)\r
+                (and\r
+                 (js2-prop-get-node-p subject)\r
+                 (string= (js2-name-node-name (js2-prop-get-node-right subject))\r
+                          "prototype")))))\r
+    (js2-imenu-record-extend-first-arg (1- (point)) pred\r
+                                       'js2-compute-nested-prop-get)))\r
+\r
+(defun js2-imenu-record-string-declare ()\r
+  (js2-imenu-record-extend-first-arg\r
+   (1- (point)) 'js2-string-node-p\r
+   (lambda (node) (split-string (js2-string-node-value node) "\\." t))))\r
+\r
+(defun js2-imenu-record-extend-first-arg (point pred qname-fn)\r
+  (let* ((node (js2-node-at-point point))\r
+         (args (js2-call-node-args node))\r
+         (subject (first args)))\r
+    (when (funcall pred subject)\r
+      (loop for arg in (cdr args)\r
+            when (js2-object-node-p arg)\r
+            do (js2-record-object-literal\r
+                arg (funcall qname-fn subject) (js2-node-abs-pos arg))))))\r
+\r
+(defun js2-imenu-record-backbone-extend ()\r
+  (let* ((node (js2-node-at-point (1- (point))))\r
+         (args (js2-call-node-args node))\r
+         (methods (first args))\r
+         (parent (js2-node-parent node)))\r
+    (when (js2-object-node-p methods)\r
+      (let ((subject (cond ((js2-var-init-node-p parent)\r
+                            (js2-var-init-node-target parent))\r
+                           ((js2-assign-node-p parent)\r
+                            (js2-assign-node-left parent)))))\r
+        (when subject\r
+          (js2-record-object-literal methods\r
+                                     (js2-compute-nested-prop-get subject)\r
+                                     (js2-node-abs-pos methods)))))))\r
+\r
+(defun js2-imenu-walk-ast ()\r
+  (js2-visit-ast\r
+   js2-mode-ast\r
+   (lambda (node end-p)\r
+     (unless end-p\r
+       (cond\r
+        ((and js2-imenu-show-other-functions\r
+              (js2-object-prop-node-p node))\r
+         (js2-imenu-record-orphan-function node))\r
+        ((and js2-imenu-show-module-pattern\r
+              (js2-assign-node-p node))\r
+         (js2-imenu-record-module-pattern node)))\r
+       t))))\r
+\r
+(defun js2-imenu-record-orphan-function (node)\r
+  "Record orphan function when it's the value of NODE.\r
+NODE must be `js2-object-prop-node'."\r
+  (when (js2-function-node-p (js2-object-prop-node-right node))\r
+    (let ((fn-node (js2-object-prop-node-right node)))\r
+      (unless (and js2-imenu-function-map\r
+                   (gethash fn-node js2-imenu-function-map))\r
+        (let ((key-node (js2-object-prop-node-left node)))\r
+          (js2-record-imenu-entry fn-node\r
+                                  (list js2-imenu-other-functions-ns\r
+                                        (js2-prop-node-name key-node))\r
+                                  (js2-node-abs-pos key-node)))))))\r
+\r
+(defun js2-imenu-record-module-pattern (node)\r
+  "Recognize and record module pattern use instance.\r
+NODE must be `js2-assign-node'."\r
+  (let ((init (js2-assign-node-right node)))\r
+    (when (js2-call-node-p init)\r
+      (let ((target (js2-assign-node-left node))\r
+            (callt (js2-call-node-target init)))\r
+        ;; Just basic call form: (function() {...})();\r
+        ;; TODO: Handle variations without duplicating `js2-wrapper-function-p'?\r
+        (when (and (js2-paren-node-p callt)\r
+                   (js2-function-node-p (js2-paren-node-expr callt)))\r
+          (let* ((fn (js2-paren-node-expr callt))\r
+                 (blk (js2-function-node-body fn))\r
+                 (ret (car (last (js2-block-node-kids blk)))))\r
+            (when (and (js2-return-node-p ret)\r
+                       (js2-object-node-p (js2-return-node-retval ret)))\r
+              ;; TODO: Map function names when revealing module pattern is used.\r
+              (let ((retval (js2-return-node-retval ret))\r
+                    (target-qname (js2-compute-nested-prop-get target)))\r
+                (js2-record-object-literal retval target-qname\r
+                                           (js2-node-abs-pos retval))\r
+                (js2-record-imenu-entry fn target-qname\r
+                                        (js2-node-abs-pos target))))))))))\r
+\r
+;;;###autoload\r
+(define-minor-mode js2-imenu-extras-mode\r
+  "Toggle Imenu support for frameworks and structural patterns."\r
+  :lighter ""\r
+  (if js2-imenu-extras-mode\r
+      (js2-imenu-extras-setup)\r
+    (js2-imenu-extras-remove)))\r
+\r
+(provide 'js2-imenu-extras)\r