[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))))))))