emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/js2-mode d2636f9 3/3: Merge pull request #576 from Dami


From: ELPA Syncer
Subject: [elpa] externals/js2-mode d2636f9 3/3: Merge pull request #576 from DamienCassou/imenu-mocha
Date: Fri, 5 Nov 2021 08:57:32 -0400 (EDT)

branch: externals/js2-mode
commit d2636f95ebe4d423dc9b4311aff248c7688271c5
Merge: a059c41 6cb8efb
Author: Dmitry Gutov <dgutov@yandex.ru>
Commit: GitHub <noreply@github.com>

    Merge pull request #576 from DamienCassou/imenu-mocha
    
    Support mocha-like test files in Imenu
---
 .github/workflows/test.yml |   1 +
 NEWS.md                    |   4 ++
 js2-imenu-extras.el        | 157 +++++++++++++++++++++++++++++++++++++++++++++
 tests/imenu-mocha.el       | 154 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 316 insertions(+)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9d8b5dc..6ee3f06 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,6 +20,7 @@ jobs:
       matrix:
         emacs_version:
           - 25.1
+          - 26.3
           - 27.2
           - snapshot
     steps:
diff --git a/NEWS.md b/NEWS.md
index 8f2f6e7..4fdb25b 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,6 +2,10 @@
 
 ## Next
 
+* Imenu support for mocha-like (includes Jasmine and Cypress) test
+  files: i.e., `M-x imenu` will now list test blocks defined with
+  `describe()` and `it()`
+  ([#576](https://github.com/mooz/js2-mode/pull/576)).
 * Minor improvements in `js2-jump-to-definition`
   ([#423](https://github.com/mooz/js2-mode/issues/423)).
 * Support for private class members
diff --git a/js2-imenu-extras.el b/js2-imenu-extras.el
index 4859bef..458c57f 100644
--- a/js2-imenu-extras.el
+++ b/js2-imenu-extras.el
@@ -35,6 +35,10 @@
 (require 'cl-lib)
 (require 'js2-mode)
 
+(eval-when-compile
+  (when (<= emacs-major-version 26)
+    (require 'subr-x)))
+
 (defvar js2-imenu-extension-styles
   `((:framework jquery
      :call-re   "\\_<\\(?:jQuery\\|\\$\\|_\\)\\.extend\\s-*("
@@ -60,6 +64,14 @@
      :call-re "\\_<React\\.createClass\\s-*("
      :recorder js2-imenu-record-react-class)
 
+    (:framework mocha
+     :call-re ,(rx line-start
+                   (* (syntax whitespace))
+                   (or "describe" "fdescribe" "describe.only")
+                   (* (syntax whitespace))
+                   "(")
+     :recorder js2-imenu-record-mocha-describe)
+
     (:framework sencha
      :call-re "^\\s-*Ext\\.define\\s-*("
      :recorder js2-imenu-record-sencha-class))
@@ -221,6 +233,151 @@ Currently used for jQuery widgets, Dojo and Enyo 
declarations."
                                      (list name-value))
                                    (js2-node-abs-pos methods))))))
 
+(defun js2-imenu-record-mocha-describe ()
+  "Populate `js2-imenu-recorder' with mocha-like describe/it/beforeEach/… 
nodes."
+  (let ((node (js2-node-at-point (1- (point)))))
+    (when (js2-imenu-extras--mocha-top-level-describe-p node)
+      (js2-imenu-extras--mocha-visit-node node (list)))))
+
+(defun js2-imenu-extras--mocha-visit-node (node qname)
+  "Search NODE and its children for mocha test blocks.
+
+If mocha test blocks are found (e.g., a describe() or it() block)
+they are added to `js2-imenu-recorder' with QNAME as prefix.
+
+QNAME is a list of nodes representing the qualified name of
+NODE's parent.  If NODE has no parent, QNAME is the empty list.
+The last item of QNAME is NODE's parent name while the item
+before that is NODE's grandparent name etc."
+  (js2-visit-ast
+   node
+   (lambda (child end-p)
+     (when (not end-p)
+       (js2-imenu-extras--mocha-check-unknown-node child qname)))))
+
+(defun js2-imenu-extras--mocha-check-unknown-node (node qname)
+  "If NODE is a mocha test block, populate `js2-imenu-recorder'.
+
+QNAME is the same as described in
+`js2-imenu-extras--mocha-visit-node'."
+  (cond
+   ((js2-imenu-extras--mocha-describe-node-p node)
+    (progn
+      (js2-imenu-extras--mocha-visit-describe-node node qname)
+      nil))
+   ((js2-imenu-extras--mocha-it-node-p node)
+    (progn
+      (js2-imenu-extras--mocha-visit-it-node node qname)
+      nil))
+   ((js2-imenu-extras--mocha-before-after-node-p node)
+    (progn
+      (js2-imenu-extras--mocha-visit-before-after-node node qname)
+      nil))
+   ((js2-imenu-extras--mocha-named-function-node-p node)
+    (progn
+      (js2-imenu-extras--mocha-visit-named-function-node node qname)
+      nil))
+   (t t)))
+
+(defun js2-imenu-extras--mocha-top-level-describe-p (node)
+  "Return non-nil if NODE is a top-level mocha describe() block.
+
+A top-level block is one which isn't included in another mocha
+describe() block."
+  (and (js2-imenu-extras--mocha-describe-node-p node)
+       (not (js2-imenu-extras--mocha-is-or-within-describe-block-p 
(js2-node-parent node)))))
+
+(defun js2-imenu-extras--mocha-within-describe-block-p (node)
+  "Return non-nil if NODE is within a mocha describe() block."
+  (js2-imenu-extras--mocha-is-or-within-describe-block-p (js2-node-parent 
node)))
+
+(defun js2-imenu-extras--mocha-is-or-within-describe-block-p (node)
+  "Return non-nil if NODE is a or within a mocha describe() block."
+  (when node
+    (or (js2-imenu-extras--mocha-describe-node-p node)
+        (js2-imenu-extras--mocha-within-describe-block-p node))))
+
+(defun js2-imenu-extras--mocha-describe-node-p (node)
+  "Return non-nil if NODE is a mocha describe() block."
+  (when-let ((name (js2-imenu-extras--call-target-name node)))
+    (member name '("describe" "describe.only" "fdescribe"))))
+
+(defun js2-imenu-extras--mocha-it-node-p (node)
+  "Return non-nil if NODE is a mocha it() block."
+  (when-let ((name (js2-imenu-extras--call-target-name node)))
+    (member name '("it" "it.only" "fit"))))
+
+(defun js2-imenu-extras--mocha-before-after-node-p (node)
+  "Return non-nil if NODE is a `{before,after}{Each,All}' block."
+  (when-let ((name (js2-imenu-extras--call-target-name node)))
+    (member name '("beforeEach" "afterEach" "beforeAll" "afterAll"))))
+
+(defun js2-imenu-extras--mocha-named-function-node-p (node)
+  "Return non-nil if NODE is a function definition."
+  (and (js2-function-node-p node)
+       (js2-function-name node)))
+
+(defun js2-imenu-extras--mocha-visit-describe-node (node qname)
+  "Record NODE, a mocha describe() block, in imenu.
+Also search and record other mocha blocks within NODE's body.
+
+QNAME is the same as described in
+`js2-imenu-extras--mocha-visit-node'."
+  (let* ((args (js2-call-node-args node))
+         (name (cl-first args))
+         (qname (append qname (list name)))
+         (body (car (last args)))
+         (position (js2-node-abs-pos node)))
+    (js2-record-imenu-entry body qname position)
+    (js2-imenu-extras--mocha-visit-node body qname)))
+
+(defun js2-imenu-extras--mocha-visit-it-node (node qname)
+  "Record NODE, a mocha it() block, in imenu.
+
+QNAME is the same as described in
+`js2-imenu-extras--mocha-visit-node'."
+  (let* ((args (js2-call-node-args node))
+         (name (cl-first args))
+         (qname (append qname (list name)))
+         (body (car (last args)))
+         (position (js2-node-abs-pos node)))
+    (js2-record-imenu-entry body qname position)))
+
+(defun js2-imenu-extras--mocha-visit-before-after-node (node qname)
+  "Record NODE, a mocha {before,after}{Each,All}() block, in imenu.
+
+QNAME is the same as described in
+`js2-imenu-extras--mocha-visit-node'."
+  (let* ((args (js2-call-node-args node))
+         (qname (append qname (list (js2-imenu-extras--call-target-name 
node))))
+         (body (car (last args)))
+         (position (js2-node-abs-pos node)))
+    (js2-record-imenu-entry body qname position)))
+
+(defun js2-imenu-extras--mocha-visit-named-function-node (node qname)
+  "Record NODE, a function declaration, in imenu.
+
+QNAME is the same as described in
+`js2-imenu-extras--mocha-visit-node'."
+  (let* ((qname (append qname (list (js2-function-name node))))
+         (position (js2-node-abs-pos node)))
+    (js2-record-imenu-entry node qname position)))
+
+(defun js2-imenu-extras--call-target-name (node)
+  "Return the function name, as string, called by NODE.
+If node is not a function call, return nil."
+  (when (js2-call-node-p node)
+    (js2-imenu-extras--string-content (js2-call-node-target node))))
+
+(defun js2-imenu-extras--string-content (node)
+  "Return a string representing the value of NODE."
+  (if (js2-string-node-p node)
+      (js2-string-node-value node)
+    (let ((start (js2-node-abs-pos node)))
+      (buffer-substring-no-properties
+       start
+       (+ start (js2-node-len node))))))
+
 (defun js2-imenu-walk-ast ()
   (js2-visit-ast
    js2-mode-ast
diff --git a/tests/imenu-mocha.el b/tests/imenu-mocha.el
new file mode 100644
index 0000000..aeae912
--- /dev/null
+++ b/tests/imenu-mocha.el
@@ -0,0 +1,154 @@
+;;; tests/imenu-mocha.el --- Tests for imenu support in mocha buffers.  -*- 
lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Damien Cassou
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'js2-mode)
+(require 'js2-imenu-extras)
+
+(defmacro js2-imenu-mocha-create-buffer (lines &rest body)
+  "Execute BODY in a `js2-mode' buffer containing LINES."
+  `(with-temp-buffer
+     ,@(mapcar (lambda (line) `(insert ,line "\n")) lines)
+     (js2-mode)
+     (js2-parse)
+     (js2-imenu-extras-setup)
+     (setq-local js2-imenu-enabled-frameworks '(mocha))
+     ,@body))
+
+(ert-deftest js2-imenu-mocha-top-level-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result '(("top-level" . 1)))))))
+
+(ert-deftest js2-imenu-mocha-top-level-indented-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("  describe(\"top-level\", () => {});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result '(("top-level" . 3)))))))
+
+(ert-deftest js2-imenu-mocha-top-level-fdescribe ()
+  (js2-imenu-mocha-create-buffer
+   ("fdescribe(\"top-level\", () => {});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result '(("top-level" . 1)))))))
+
+(ert-deftest js2-imenu-mocha-top-level-describe-only ()
+  (js2-imenu-mocha-create-buffer
+   ("describe.only(\"top-level\", () => {});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result '(("top-level" . 1)))))))
+
+(ert-deftest js2-imenu-mocha-top-level-describes ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top1\", () => {});"
+    "describe(\"top2\", () => {});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top1" . 1)
+                      ("top2" . 29)))))))
+
+(ert-deftest js2-imenu-mocha-describe-in-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {"
+    "  describe(\"sub\", () => {})"
+    "});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top-level"
+                       ("<definition-1>" . 1)
+                       ("sub" . 33))))))))
+
+(ert-deftest js2-imenu-mocha-fdescribe-in-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {"
+    "  fdescribe(\"sub\", () => {})"
+    "});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top-level"
+                       ("<definition-1>" . 1)
+                       ("sub" . 33))))))))
+
+(ert-deftest js2-imenu-mocha-two-describes-in-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {"
+    "  describe(\"sub1\", () => {})"
+    "  describe(\"sub2\", () => {})"
+    "});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top-level"
+                       ("<definition-1>" . 1)
+                       ("sub1" . 33)
+                       ("sub2" . 62))))))))
+
+(ert-deftest js2-imenu-mocha-describe-in-describe-in-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {"
+    "  describe(\"sub\", () => {"
+    "    describe(\"subsub\", () => {});"
+    "  });"
+    "});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top-level"
+                       ("<definition-1>" . 1)
+                       ("sub"
+                        ("<definition-1>" . 33)
+                        ("subsub" . 61)))))))))
+
+(ert-deftest js2-imenu-mocha-beforeEach-in-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {"
+    "  beforeEach(() => {})"
+    "});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top-level"
+                       ("<definition-1>" . 1)
+                       ("beforeEach" . 33))))))))
+
+(ert-deftest js2-imenu-mocha-it-in-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {"
+    "  it(\"sub\", () => {})"
+    "});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top-level"
+                       ("<definition-1>" . 1)
+                       ("sub" . 33))))))))
+
+(ert-deftest js2-imenu-mocha-top-level-function ()
+  (js2-imenu-mocha-create-buffer
+   ("function foo () {}")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result '(("foo" . 1)))))))
+
+(ert-deftest js2-imenu-mocha-function-in-describe ()
+  (js2-imenu-mocha-create-buffer
+   ("describe(\"top-level\", () => {"
+    "  function foo () {}"
+    "});")
+   (let ((result (js2-mode-create-imenu-index)))
+     (should (equal result
+                    '(("top-level"
+                       ("<definition-1>" . 1)
+                       ("foo" . 33))))))))



reply via email to

[Prev in Thread] Current Thread [Next in Thread]