;;; rspec-mode.el --- Enhance ruby-mode for RSpec -*- lexical-binding: t -*-

;; Copyright (C) 2008-2015 Peter Williams <http://barelyenough.org> and others
;; Author: Peter Williams, et al.
;; URL: http://github.com/pezra/rspec-mode
;; Created: 2011
;; Package-Version: 20260618.548
;; Package-Revision: b5d48de9b56a
;; Keywords: rspec ruby
;; Package-Requires: ((ruby-mode "1.0") (cl-lib "0.4"))

;; This file is NOT part of GNU Emacs.
;;
;; 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.
;;
;; See <http://www.gnu.org/licenses/> for a copy of the GNU General
;; Public License.

;;; Commentary:
;;
;; This minor mode provides some enhancements to ruby-mode in the
;; contexts of RSpec specifications.  Refer to the README for a
;; summary of keybindings and their descriptions.
;;
;; You can choose whether to run specs using 'rake spec' or the 'rspec'
;; command. Use the customization interface (customize-group
;; rspec-mode) or override using (setq rspec-use-rake-when-possible TVAL).
;;
;; Options will be loaded from spec.opts or .rspec if it exists and
;; rspec-use-opts-file-when-available is not set to nil, otherwise it
;; will fallback to defaults.
;;
;; You can also launch specs from Dired buffers, to do that, add this:
;;
;;   (add-hook 'dired-mode-hook 'rspec-dired-mode)
;;
;; It has almost the same keybindings, but there's no toggle-spec
;; command, and `rspec-dired-verify-single' runs all marked files, or
;; the file at point.
;;
;; Dependencies:
;;
;; If `rspec-use-rvm` is set to true `rvm.el' is required.
;;
;;; Change Log:
;;
;; 1.21 - New option `rspec-docker-file-name'.
;; 1.20 - Fix a regression of `rspec-run-last-failed'
;; 1.19 - Fix bugs about change of buffer naming
;; 1.18 - Add `rspec-before-verification-hook' and `rspec-after-verification-hook'
;;        hooks
;; 1.17 - Support for running multiple rspec processes at once
;; 1.16 - Add `rspec-yank-last-command' function (Sergiy Kukunin)
;; 1.15 - Add option to run spec commands in a Docker container
;;        through "docker exec".
;; 1.14 - Add option to run spec commands in a Vagrant box through
;;        "vagrant ssh -c".
;; 1.13 - Add a variable to autosave current buffer where it makes sense
;; 1.12 - Run specs for single method (Renan Ranelli)
;; 1.11 - Switching between method, its specs and back (Renan Ranelli)
;; 1.9 - Support for RSpec 3.
;; 1.8 - Support for Capybara's acceptance test DSL (Ales Guzik)
;; 1.7 - Support for Spring (Tomy Kaira)
;;     - New commands: `rspec-verify-matching', `rspec-verify-continue'
;;       (Jean-Louis Giordano)
;;     - Run specs from Dired (Adam Sokolnicki)
;;     - Include Yasnippet snippets collection (Dmitry Gutov)
;; 1.6 - Improved keymaps and compile buffer (Dmitry Gutov)
;; 1.5 - Allow key prefix to be customized (`rspec-key-command-prefix')
;; 1.4 - Allow .rspec/spec.opts files to be ignored (user option
;;       `rspec-use-opts-file-when-available')
;; 1.3 - Bundler support (JD Huntington)
;; 1.2 - Rspec2 compatibility  (Anantha Kumaran)
;; 1.1 - Run verification processes from project root directory (Joe Hirn)
;; 1.0 - Advance to end of compilation buffer even if it not the other window (byplayer)
;; 0.8 - RVM support (Peter Williams)
;; 0.7 - follow RoR conventions for file in lib directory (Tim Harper)
;; 0.6 - support for arbitrary spec and rake commands (David Yeu)
;; 0.5 - minor changes from Tim Harper
;; 0.4 - ansi colorization of compilation buffers (teaforthecat)
;; 0.3 - Dave Nolan implements respect for spec.opts config and
;;       custom option to use 'rake spec' task or 'spec' command
;; 0.2 - Tim Harper implemented support for imenu to generate a basic
;;       tag outline
;; 0.1 - Pezra's version in master

;;; Code:
(require 'ruby-mode)
(require 'ansi-color)
(require 'compile)
(require 'cl-lib)

(defvar rspec-verifiable-mode-keymap)
(define-prefix-command 'rspec-verifiable-mode-keymap)
(defvar rspec-mode-keymap)
(define-prefix-command 'rspec-mode-keymap)

(define-key rspec-verifiable-mode-keymap (kbd "v") 'rspec-verify)
(define-key rspec-verifiable-mode-keymap (kbd "a") 'rspec-verify-all)
(define-key rspec-verifiable-mode-keymap (kbd "t") 'rspec-toggle-spec-and-target)
(define-key rspec-verifiable-mode-keymap (kbd "e") 'rspec-toggle-spec-and-target-find-example)
(define-key rspec-verifiable-mode-keymap (kbd "4 t") 'rspec-find-spec-or-target-other-window)
(define-key rspec-verifiable-mode-keymap (kbd "4 e") 'rspec-find-spec-or-target-find-example-other-window)
(define-key rspec-verifiable-mode-keymap (kbd "r") 'rspec-rerun)
(define-key rspec-verifiable-mode-keymap (kbd "y") 'rspec-yank-last-command)
(define-key rspec-verifiable-mode-keymap (kbd "m") 'rspec-verify-matching)
(define-key rspec-verifiable-mode-keymap (kbd "c") 'rspec-verify-continue)
(define-key rspec-verifiable-mode-keymap (kbd "s") 'rspec-verify-method)
(define-key rspec-verifiable-mode-keymap (kbd "f") 'rspec-run-last-failed)

(set-keymap-parent rspec-mode-keymap rspec-verifiable-mode-keymap)

(define-key rspec-mode-keymap (kbd "s") 'rspec-verify-single)
(define-key rspec-mode-keymap (kbd "d") 'rspec-toggle-example-pendingness)

(defvar rspec-dired-mode-keymap)
(define-prefix-command 'rspec-dired-mode-keymap)
(define-key rspec-dired-mode-keymap (kbd "v") 'rspec-dired-verify)
(define-key rspec-dired-mode-keymap (kbd "s") 'rspec-dired-verify-single)
(define-key rspec-dired-mode-keymap (kbd "a") 'rspec-verify-all)
(define-key rspec-dired-mode-keymap (kbd "r") 'rspec-rerun)

(defgroup rspec-mode nil
  "RSpec minor mode."
  :group 'languages)

(defcustom rspec-use-rake-when-possible nil
  "When non-nil and Rakefile is present, run specs via rake spec task."
  :tag "RSpec runner command"
  :type '(radio (const :tag "Use 'rake spec' task" t)
                (const :tag "Use 'rspec' command" nil))
  :group 'rspec-mode)

(defcustom rspec-rake-command "rake"
  "The command for rake."
  :type 'string
  :group 'rspec-mode)

(defcustom rspec-spec-command "rspec"
  "The command for spec."
  :type 'string
  :group 'rspec-mode)

(defcustom rspec-use-rvm nil
  "When t, use RVM. Requires rvm.el."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-use-chruby nil
  "When t, use chruby. Requires chruby.el."
  :type 'boolean
  :group 'rspec-mode)

(defvar rspec--docker-commands
  '("docker exec"
    "docker run"
    "docker-compose exec"
    "docker-compose run"
    "nerdctl compose exec"
    "nerdctl compose run"
    "nerdctl exec"
    "nerdctl run"
    "podman exec"
    "podman run"
    "podman-compose exec"
    "podman-compose run")
  "List of acceptable docker commands to use.")

(defcustom rspec-docker-command "docker-compose run"
  "Docker command to run."
  :type 'string
  :group 'rspec-mode
  :safe (lambda (value)
          (member value rspec--docker-commands)))

(defcustom rspec-docker-container "rspec-container-name"
  "Name of the docker container to run rspec in."
  :type 'string
  :group 'rspec-mode
  :safe 'stringp)

(defcustom rspec-docker-cwd "/app/"
  "Working directory when running inside Docker.  Use trailing slash."
  :type 'string
  :group 'rspec-mode)

(defcustom rspec-docker-wrapper-fn 'rspec--docker-default-wrapper
  "Function for wrapping a command for execution inside a dockerized environment."
  :type 'function
  :group 'rspec-mode)

(defcustom rspec-docker-file-name "docker-compose.yml"
  "File name to look for to determine whether to use Docker."
  :type 'string
  :group 'rspec-mode
  :safe 'stringp)

(defcustom rspec-vagrant-cwd "/vagrant/"
  "Working directory when running inside Vagrant. Use trailing slash."
  :type 'string
  :group 'rspec-mode)

(defcustom rspec-use-relative-path nil
  "When t the file path will be relative."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-use-bundler-when-possible t
  "When t and Gemfile is present, run specs with `bundle exec'.
Not used when running specs using Zeus or Spring."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-use-docker-when-possible nil
  "When t and a file `rspec-docker-file-name' exists, run specs inside Docker.
The command that will be used is defined by `rspec-docker-command'."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-use-vagrant-when-possible nil
  "When t and Vagrant file is present, run specs inside Vagrant box.
Use shell command `vagrant ssh -c'."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-use-zeus-when-possible t
  "When t and .zeus.sock is present, run specs with `zeus'."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-use-spring-when-possible t
  "When t and spring.pid is present, run specs with `spring'."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-use-opts-file-when-available t
  "When t, RSpec should use .rspec/spec.opts."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-key-command-prefix  (kbd "C-c ,")
  "The prefix for all rspec related key commands."
  :type 'string
  :group 'rspec-mode)

(defcustom rspec-command-options "--format documentation"
  "Default options used with rspec-command."
  :type 'string
  :group 'rspec-mode)

(defcustom rspec-snippets-fg-syntax nil
  "Defines whether to use the full or concise FactoryGirl syntax in snippets.
When the value is neither `full', nor `concise', use the concise syntax if
there's an `include FactoryGirl::Syntax::Methods' statement in spec_helper."
  :type '(choice
          (const full)
          (const concise)
          (const nil))
  :group 'rspec-mode)

(defcustom rspec-factory-gem 'factory-girl
  "Defines whether to use the FactoryGirl or FactoryBot module in snippets."
  :type '(choice
          (const factory-girl)
          (const factory-bot))
  :group 'rspec-mode)

(defcustom rspec-compilation-skip-threshold 2
  "Compilation motion commands skip less important messages.
The value can be either 2 -- skip anything less than error, 1 --
skip anything less than warning or 0 -- don't skip any messages.
Note that all messages not positively identified as warning or
info, are considered errors."
  :type '(choice (const :tag "Skip warnings and info" 2)
                 (const :tag "Skip info" 1)
                 (const :tag "No skip" 0))
  :group 'rspec-mode)

(defcustom rspec-expose-dsl-globally nil
  "Defines whether the RSpec DSL is assumed to be exposed globally.
If t, prepend snippets at the top level with `RSpec.'."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-primary-source-dirs '("app" "lib")
  "Suppression list when searching for spec files.
List of directories whose names should be omitted when looking
for spec files corresponding to files inside them."
  :type '(repeat string)
  :safe 'listp
  :group 'rspec-mode)

(defcustom rspec-autosave-buffer nil
  "If t save the current buffer when running
`rspec-verify', `rspec-verify-single', `rspec-verify-matching',
`rspec-verify-continue' & `rspec-run-last-failed'."
  :type 'boolean
  :group 'rspec-mode)

(defcustom rspec-after-verification-hook nil
  "Hooks run after `rspec-verify' and its variants.
They execute after failures have been stored in `rspec-last-failed-specs'."
  :type 'hook
  :group 'rspec-mode)

(defcustom rspec-before-verification-hook nil
  "Hooks run before `rspec-verify' and its variants."
  :type 'hook
  :group 'rspec-mode)

(defcustom rspec-allow-multiple-compilation-buffers nil
  "If t allows multiple RSpec to run in distinct compilation
buffers concurrently"
  :type 'boolean
  :group 'rspec-mode)

(declare-function yas-activate-extra-mode "yasnippet" (mode))
(declare-function yas-deactivate-extra-mode "yasnippet" (mode))
(declare-function yas-load-directory "yasnippet" (top-level-dir &optional use-jit interactive))

;;;###autoload
(define-minor-mode rspec-mode
  "Minor mode for RSpec files

\\{rspec-mode-map}"
  :lighter " RSpec" :keymap `((,rspec-key-command-prefix . rspec-mode-keymap))
  (if rspec-mode
      (progn
        (rspec-set-imenu-generic-expression)
        (when (fboundp 'yas-activate-extra-mode)
          (yas-activate-extra-mode 'rspec-mode)))
    (setq imenu-create-index-function 'ruby-imenu-create-index)
    (setq imenu-generic-expression nil)
    (when (fboundp 'yas-deactivate-extra-mode)
      (yas-deactivate-extra-mode 'rspec-mode))))

;;;###autoload
(define-minor-mode rspec-verifiable-mode
  "Minor mode for Ruby files that have specs

\\{rspec-verifiable-mode-map}"
  :lighter "" :keymap `((,rspec-key-command-prefix . rspec-verifiable-mode-keymap)))

;;;###autoload
(define-minor-mode rspec-dired-mode
  "Minor mode for Dired buffers with spec files

\\{rspec-dired-mode-map}"
  :lighter "" :keymap `((,rspec-key-command-prefix . rspec-dired-mode-keymap)))

(defconst rspec-imenu-generic-expression
  '(("Methods" "^\\s *def\\s +\\([^\(\n; ]+\\)" 1)
    ("Examples" "^\\( *\\(its?\\|specify\\|example\\|describe\\|context\\|feature\\|scenario\\) +.+\\)" 1))
  "The imenu regex to parse an outline of the rspec file")

(defconst rspec-spec-file-name-re "\\(_\\|-\\)spec\\.rb\\'"
  "The regex to identify spec files")

(defun rspec-set-imenu-generic-expression ()
  (make-local-variable 'imenu-generic-expression)
  (make-local-variable 'imenu-create-index-function)
  (setq imenu-create-index-function 'imenu-default-create-index-function)
  (setq imenu-generic-expression rspec-imenu-generic-expression))

(defvar rspec-snippets-dir
  (let ((current (or load-file-name (buffer-file-name))))
    (expand-file-name "snippets" (file-name-directory current)))
  "The directory containing rspec snippets.")

(defun rspec-install-snippets ()
  "Add `rspec-snippets-dir' to `yas-snippet-dirs' and load snippets from it."
  (require 'yasnippet)
  (defvar yas-snippet-dirs)
  (add-to-list 'yas-snippet-dirs rspec-snippets-dir t)
  (yas-load-directory rspec-snippets-dir))

(defun rspec-class-from-file-name ()
  "Guess the name of the class the spec is for."
  (let* ((name (file-relative-name (buffer-file-name)
                                   (rspec-spec-directory (buffer-file-name))))
         (rules `((,rspec-spec-file-name-re . "") ("/" . "::") ("_" . "")))
         (class (capitalize name)))
    (dolist (rule rules)
      (setq class (replace-regexp-in-string (car rule) (cdr rule) class t t)))
    class))

(defun rspec-type-from-file-name ()
  "Guess the type of the spec is for."
  (let* ((name (file-relative-name (buffer-file-name)
                                   (rspec-spec-directory (buffer-file-name))))
         (rules `(("/.*" . "") ("s$" . ""))))
    (dolist (rule rules)
      (setq name (replace-regexp-in-string (car rule) (cdr rule) name)))
    name))

(defun rspec-top-level-desc-p ()
  "Return t if point is on the first \"describe\" block opener."
  (save-excursion
    (save-restriction
      (widen)
      (beginning-of-line)
      (not (catch 'found
             (while (re-search-backward "\\_<\\(describe\\|feature\\)\\_>" nil t)
               (unless (nth 8 (syntax-ppss))
                 (throw 'found t))))))))

(defun rspec-beginning-of-example ()
  "Moves point to the beginning of the example in which the point current is."
  (interactive)
  (let ((start (point)))
    (goto-char
     (save-excursion
       (end-of-line)
       (unless (and (re-search-backward "^[[:space:]]*\\(it\\|scenario\\)[[:space:]]*(?[\"']" nil t)
                    (save-excursion (ruby-end-of-block) (< start (point))))
         (error "Unable to find an example"))
       (point)))))

(defun rspec-example-pending-p ()
  "True if the example under point is pending. Otherwise false."
  (interactive)
  (save-excursion
    (rspec-beginning-of-example)
    (re-search-forward "^[[:space:]]*pending\\([[:space:](]\\|$\\)" (save-excursion (ruby-end-of-block) (point)) t)))

(defun rspec-toggle-example-pendingness ()
  "Disable active examples and enables pending examples."
  (interactive)
  (if (rspec-example-pending-p)
      (rspec-enable-example)
    (rspec-disable-example)))

(defun rspec-disable-example ()
  "Disable the example at point."
  (interactive)
  (when (not (rspec-example-pending-p))
    (save-excursion
      (rspec-beginning-of-example)
      (end-of-line)
      (insert "\npending")
      (indent-for-tab-command))))

(defun rspec-enable-example ()
  "Enable the example at point."
  (interactive)
  (when (rspec-example-pending-p)
    (save-excursion
      (rspec-beginning-of-example)
      (re-search-forward "^[[:space:]]*pending\\([[:space:](]\\|$\\)"
                         (save-excursion (ruby-end-of-block) (point)))
      (beginning-of-line)
      (delete-region (save-excursion (beginning-of-line) (point))
                     (save-excursion (forward-line 1) (point))))))

(defun rspec-verify ()
  "Run the specified spec, or the spec file for the current buffer."
  (interactive)
  (rspec--autosave-buffer-maybe)
  (rspec-run-single-file (rspec-spec-file-for (buffer-file-name))
                         (rspec-core-options)))

(defun rspec-verify-matching ()
  "Run the specs related to the current buffer.
This is more fuzzy that a simple verify."
  (interactive)
  (rspec--autosave-buffer-maybe)
  (rspec-run-multiple-files (rspec-all-related-spec-files (buffer-file-name))
                            (rspec-core-options)))

(defvar rspec-last-failed-specs nil
  "The file and line number of the specs that failed during the last run.")

(defun rspec-run-last-failed ()
  "Run just the specs that failed during the last invocation."
  (interactive)
  (rspec--autosave-buffer-maybe)
  (rspec-run-multiple-files rspec-last-failed-specs (rspec-core-options)))

(defun rspec-verify-continue ()
  "Run the current spec file and the spec files located after it.
This is most useful in combination with the option `--fail-fast',
in long-running test suites."
  (interactive)
  (rspec--autosave-buffer-maybe)
  (let ((current-spec-file (rspec-compress-spec-file
                            (rspec-spec-file-for (buffer-file-name)))))
    (rspec-run-multiple-files
     (cl-loop for file in (rspec-all-spec-files (buffer-file-name))
              when (not (string-lessp file current-spec-file))
              collect file)
     (rspec-core-options))))

(defun rspec-verify-single ()
  "Run the specified example at point."
  (interactive)
  (rspec--autosave-buffer-maybe)
  (rspec-run-single-file
   (cons
    (rspec-spec-file-for (buffer-file-name))
    (save-restriction
      (widen)
      (number-to-string (line-number-at-pos))))
   (rspec-core-options)))

(declare-function dired-current-directory "dired")
(declare-function dired-get-marked-files "dired")

(defun rspec-dired-verify ()
  "Run all specs in the current directory."
  (interactive)
  (rspec-run-single-file (dired-current-directory) (rspec-core-options)))

(defun rspec-dired-verify-single ()
  "Run marked specs or spec at point (works with directories too)."
  (interactive)
  (rspec-compile (dired-get-marked-files)
                 (rspec-core-options)))

(defun rspec-verify-all ()
  "Run the `spec' rake task for the project of the current file."
  (interactive)
  (rspec-run (rspec-core-options)))

(defun rspec-toggle-spec-and-target ()
  "Switch to the spec or the target file for the current buffer.
If the current buffer is visiting a spec file, switches to the
target, otherwise the spec."
  (interactive)
  (find-file (rspec-spec-or-target)))

(defun rspec-verify-method ()
  "Just like `rspec-verify-single' but tries to find examples for
the method at point."
  (interactive)
  (save-excursion
    (when (rspec--toggle-spec-and-target-find-method
           (lambda () (set-buffer (find-file-noselect (rspec-spec-or-target)))))
      (rspec-verify-single))))

(defun rspec--toggle-spec-and-target-find-method (toggle-function)
  (cl-labels
      ((get-spec-name ()
                      (save-excursion
                        (end-of-line)
                        (or
                         (re-search-backward "\\(?:describe\\|context\\)\s*(?[\s\n]*['\"][#\\.]\\([a-zA-Z_?!]*\\)['\"].*[\n\s)]* ?do" nil t)
                         (error "No method spec before point"))
                        (match-string 1)))
       (get-method-name ()
                        (save-excursion
                          (end-of-line)
                          (or
                           (re-search-backward "def \\(?:self\\)?\\(.?[a-zA-Z_?!]+\\)" nil t)
                           (error "No method definition before point"))
                          (match-string 1))))
    (let* ((spec-p (rspec-buffer-is-spec-p))
           (target-regexp (if spec-p
                              (format "def \\(self\\)?\\.?%s" (regexp-quote (get-spec-name)))
                            (format "\\(describe\\|context\\)[\s(\n]+['\"]#?%s['\"]" (regexp-quote (get-method-name))))))
      (funcall toggle-function)
      (if (string-match-p target-regexp (buffer-string))
          (progn
            (goto-char (point-min))
            (re-search-forward target-regexp))
        (message "No matching %s" (if spec-p "method" "spec"))
        nil))))

(defun rspec-toggle-spec-and-target-find-example ()
  "Just like `rspec-toggle-spec-and-target' but tries to toggle between
the method and its corresponding examples."
  (interactive)
  (rspec--toggle-spec-and-target-find-method 'rspec-toggle-spec-and-target))

(defun rspec-find-spec-or-target-other-window ()
  "Find in the other window the spec or the target file.
If the current buffer is visiting a spec file, finds the target,
otherwise the spec."
  (interactive)
  (find-file-other-window (rspec-spec-or-target)))

(defun rspec-find-spec-or-target-find-example-other-window ()
  "Find in the other window the spec or the target file, and try
to navigate to the example or method corresponding to point."
  (interactive)
  (rspec--toggle-spec-and-target-find-method 'rspec-find-spec-or-target-other-window))

(defun rspec-spec-or-target ()
  (if (rspec-buffer-is-spec-p)
      (rspec-target-file-for (buffer-file-name))
    (rspec-spec-file-for (buffer-file-name))))

(defun rspec-spec-file-for (a-file-name)
  "Find spec for the specified file."
  (if (rspec-spec-file-p a-file-name)
      a-file-name
    (let ((replace-regex (if (rspec-target-in-holder-dir-p a-file-name)
                             "^\\.\\./[^/]+/"
                           "^\\.\\./"))
          (relative-file-name (file-relative-name a-file-name (rspec-spec-directory a-file-name))))
      (rspec-specize-file-name (expand-file-name (replace-regexp-in-string replace-regex "" relative-file-name)
                                                 (rspec-spec-directory a-file-name))))))

(defun rspec-target-in-holder-dir-p (a-file-name)
  (string-match (concat "^" (concat
                             (regexp-quote
                              (rspec-project-root a-file-name))
                             (regexp-opt rspec-primary-source-dirs)
                             "/"))
                a-file-name))

(defun rspec-target-file-for (a-spec-file-name)
  "Find the target for A-SPEC-FILE-NAME."
  (cl-loop for extension in (list "rb" "rake")
           for candidate = (rspec-targetize-file-name a-spec-file-name
                                                      extension)
           for filename = (cl-loop for dir in (cons "."
                                                    rspec-primary-source-dirs)
                                   for target = (replace-regexp-in-string
                                                 "/spec/"
                                                 (concat "/" dir "/")
                                                 candidate)
                                   if (file-exists-p target)
                                   return target)
           if filename
           return filename))

(defun rspec-specize-file-name (a-file-name)
  "Return A-FILE-NAME but converted in to a spec file name."
  (concat
   (file-name-directory a-file-name)
   (replace-regexp-in-string "\\(\\.\\(rb\\|rake\\)\\)?$" "_spec.rb" (file-name-nondirectory a-file-name))))

(defun rspec-targetize-file-name (a-file-name extension)
  "Return A-FILE-NAME but converted into a non-spec file name with EXTENSION."
  (concat (file-name-directory a-file-name)
          (rspec-file-name-with-default-extension
           (replace-regexp-in-string "_spec\\.rb" (concat "." extension)
                                     (file-name-nondirectory a-file-name)))))

(defun rspec-file-name-with-default-extension (a-file-name)
  "Add .rb file extension to A-FILE-NAME if it does not already have an extension."
  (if (file-name-extension a-file-name)
      a-file-name ;; file has a extension already so do nothing
    (concat a-file-name ".rb")))

(defun rspec-parent-directory (a-directory)
  "Returns the directory of which A-DIRECTORY is a child"
  (file-name-directory (directory-file-name a-directory)))

(defun rspec-root-directory-p (a-directory)
  "Return t if A-DIRECTORY is the root."
  (equal a-directory (rspec-parent-directory a-directory)))

(defun rspec-spec-directory (a-file)
  "Return the nearest spec directory that could contain specs for A-FILE."
  (if (file-directory-p a-file)
      (or
       (car (directory-files a-file t "^spec$"))
       (if (rspec-root-directory-p a-file)
           nil
         (rspec-spec-directory (rspec-parent-directory a-file))))
    (rspec-spec-directory (rspec-parent-directory a-file))))

(defun rspec-all-related-spec-files (a-file)
  (let* ((expected-name (file-name-nondirectory (rspec-spec-file-for a-file)))
         (expected-spec-file (concat "/" expected-name)))
    (cl-loop for file in (rspec-all-spec-files a-file)
             when (string-match-p expected-spec-file file)
             collect file)))

(defun rspec-all-files-under-directory (dir)
  (let ((files (file-expand-wildcards (concat dir "/*") nil)))
    (if (null files)
        files
      (delete-dups
       (append files
               (rspec-all-files-under-directory (concat dir "/*")))))))

(defun rspec-compress-spec-file (a-file)
  (file-relative-name a-file (rspec-project-root)))

(defun rspec-all-spec-files (a-file)
  (mapcar 'rspec-compress-spec-file
          (sort (cl-loop for file in (rspec-all-files-under-directory
                                      (rspec-spec-directory a-file))
                         when (rspec-spec-file-p file)
                         collect file)
                'string-lessp)))

(defun rspec-spec-file-p (a-file-name)
  "Return true if the specified A-FILE-NAME is a spec."
  (numberp (string-match rspec-spec-file-name-re a-file-name)))

(defun rspec-core-options ()
  "Return string of options that instructs spec to use options
file if it exists, or sensible defaults otherwise."
  (cond ((and rspec-use-opts-file-when-available
              (file-readable-p (rspec-spec-opts-file)))
         (concat "--options " (rspec--shell-quote-local (rspec-spec-opts-file))))
        (t rspec-command-options)))

(defun rspec-bundle-p ()
  (and rspec-use-bundler-when-possible
       (file-readable-p (concat (rspec-project-root) "Gemfile"))))

(defun rspec-docker-p ()
  (and rspec-use-docker-when-possible
       (file-readable-p (concat (rspec-project-root) rspec-docker-file-name))))

(defun rspec-vagrant-p ()
  (and rspec-use-vagrant-when-possible
       (file-readable-p (concat (rspec-project-root) "Vagrantfile"))))

(defun rspec-zeus-file-path ()
  (or (getenv "ZEUSSOCK")
      (concat (rspec-project-root) ".zeus.sock")))

(defun rspec-zeus-p ()
  (and rspec-use-zeus-when-possible
       (file-exists-p (rspec-zeus-file-path))))

(defun rspec-rake-p ()
  (and rspec-use-rake-when-possible
       ;; Looks inefficient, but the calculation of the root is quite
       ;; fast. Unless this is used over TRAMP, I suppose.
       (not (or (rspec-spring-p) (rspec-zeus-p)))
       (file-exists-p (concat (rspec-project-root) "Rakefile"))))

(defun rspec-spring-p ()
  (and rspec-use-spring-when-possible
       (let ((root (directory-file-name (rspec-project-root))))
         (or
          ;; Older versions
          (file-exists-p (format "%s/tmp/spring/spring.pid" root))
          ;; 0.9.2+
          (file-exists-p (format "%s/spring/%s.pid" temporary-file-directory (md5 root)))
          ;; 1.2.0+
          (let* ((path (or (getenv "XDG_RUNTIME_DIR") temporary-file-directory))
                 (ruby-version (shell-command-to-string "ruby -e 'print RUBY_VERSION'"))
                 (application-id (md5 (concat ruby-version root))))
            (or
             (file-exists-p (format "%s/spring/%s.pid" path application-id))
             ;; 1.5.0+
             (file-exists-p (format "%s/spring-%s/%s.pid" path (user-real-uid) application-id))))))))

(defun rspec2-p ()
  (or (string-match "rspec" rspec-spec-command)
      (file-readable-p (concat (rspec-project-root) ".rspec"))))

(defun rspec-spec-opts-file ()
  "Return filename of spec opts file."
  (if (rspec2-p)
      (expand-file-name ".rspec" (rspec-project-root))
    (expand-file-name "spec.opts" (rspec-spec-directory (rspec-project-root)))))

(defun rspec--shell-quote-local (file)
  (let ((remote (file-remote-p file))
        (docker (rspec-docker-p))
        (vagrant (rspec-vagrant-p)))
    (shell-quote-argument
     (cond
      (rspec-use-relative-path (file-relative-name file (rspec-project-root)))
      (remote (substring file (length remote)))
      (docker (replace-regexp-in-string (regexp-quote (rspec-project-root))
                                         rspec-docker-cwd file))
      (vagrant (replace-regexp-in-string (regexp-quote (rspec-project-root))
                                         rspec-vagrant-cwd file))
      (t  file)))))

(defun rspec--docker-default-wrapper (docker-command docker-container command)
  "Function for wrapping a command for execution inside a dockerized environment. "
  (format "%s %s sh -c \"%s\"" docker-command docker-container command))

(defun rspec--docker-wrapper (command)
  (if (rspec-docker-p)
      (funcall rspec-docker-wrapper-fn
               rspec-docker-command
               rspec-docker-container
               command)
    command))

(defun rspec--vagrant-wrapper (command)
  (if (rspec-vagrant-p)
      (format "vagrant ssh -c 'cd %s; %s'"
              (shell-quote-argument rspec-vagrant-cwd)
              command)
    command))

(defun rspec-runner ()
  "Return command line to run rspec."
  (let ((bundle-command (if (rspec-bundle-p) "bundle exec " ""))
        (zeus-command (if (rspec-zeus-p) "zeus " nil))
        (spring-command (if (rspec-spring-p) "spring " nil)))
    (concat (or zeus-command spring-command bundle-command)
            (if (rspec-rake-p)
                (concat rspec-rake-command " spec")
              rspec-spec-command))))

(defun rspec-runner-options (&optional opts)
  "Return string of options from OPTS for command line."
  (let ((opts (if (listp opts)
                  opts
                (list opts)))
        (use-rake (rspec-rake-p)))
    (concat (when use-rake "SPEC_OPTS=\'")
            (mapconcat 'identity opts " ")
            (when use-rake "\'"))))

(defun rspec-runner-target (target)
  "Processes TARGET to pass it to the runner.
TARGET can be a file, a directory, a list of such,
or a cons (FILE . LINE), to run one example."
  (let ((use-rake (rspec-compile-target-use-rake target))
        (specs (rspec-compile-target-specs target)))
    (concat (when use-rake "SPEC=\'")
            (mapconcat (lambda (s)
                         (concat (rspec--shell-quote-local (car s))
                                 (and (cdr s)
                                      (concat ":" (cdr s)))))
                       specs
                       " ")
            (when use-rake "\'"))))

;;;###autoload
(defun rspec-buffer-is-spec-p ()
  "Return true if the current buffer is a spec."
  (and (buffer-file-name)
       (rspec-spec-file-p (buffer-file-name))))

(defun rspec-run (&optional opts)
  "Run spec with the specified options OPTS."
  (rspec-compile (rspec-spec-directory (rspec-project-root))
                 opts))

(defun rspec-run-single-file (spec-file &rest opts)
  "Run spec on SPEC-FILE with the specified options OPTS."
  (rspec-compile spec-file opts))

(defun rspec-run-multiple-files (spec-files &rest opts)
  "Run spec on a list of SPEC-FILES with the specified options OPTS."
  (if (null spec-files)
      (message "No spec files found!")
    (rspec-compile spec-files opts)))

(defvar rspec-last-directory nil
  "Directory the last spec process ran in.")

(defvar rspec-last-arguments nil
  "Arguments passed to `rspec-compile' at the last invocation.")

(cl-defstruct rspec-compile-target
  use-rake specs directory)

(defun rspec-rerun ()
  "Re-run the last RSpec invocation."
  (interactive)
  (if (not rspec-last-directory)
      (error "No previous verification")
    (let ((default-directory rspec-last-directory))
      (apply #'rspec-compile rspec-last-arguments))))

(defun rspec-yank-last-command ()
  "Yank the last RSpec command to the clipboard."
  (interactive)
  (if (not rspec-last-directory)
      (error "No previous verification")
    (let ((default-directory rspec-last-directory))
      (kill-new (apply #'rspec-compile-command rspec-last-arguments)))))

(declare-function rvm-activate-corresponding-ruby nil)
(declare-function chruby-use-corresponding nil)

(defun rspec-compile (target &optional opts)
  "Run a compile for TARGET with the specified options OPTS."
  (let ((compile-target (rspec-make-rspec-compile-target target)))
    (setq rspec-last-directory default-directory
          rspec-last-arguments (list compile-target opts))

    (if rspec-use-rvm
        (rvm-activate-corresponding-ruby))

    (if rspec-use-chruby
        (chruby-use-corresponding))

    (let ((default-directory (or (rspec-project-root) default-directory))
          (compilation-buffer-name-function (and rspec-allow-multiple-compilation-buffers
                                                 'rspec-compilation-buffer-name))
          (process-environment (cons "RUBY_DEBUG_NO_RELINE=true"
                                     process-environment)))
      (setf (rspec-compile-target-directory compile-target) default-directory)
      (compile
       (rspec-compile-command compile-target opts)
       'rspec-compilation-mode))))

(defun rspec-make-rspec-compile-target (target)
  "Processes TARGET to pass it to the runner.
TARGET can be a file, a directory, a list of such,
or a cons (FILE . LINE), to run one example."
  (if (rspec-compile-target-p target)
      target
    (make-rspec-compile-target
     :use-rake (rspec-rake-p)
     :specs (cond ((and (listp target) (listp (cdr target)))
                   (mapcar (lambda (f) (list f)) target))
                  ((listp target)
                   (list target))
                  (t
                   (list (list target)))))))

(defun rspec-compile-command (target &optional opts)
  "Composes RSpec command line for the compile function"
  (rspec--vagrant-wrapper
    (rspec--docker-wrapper
    (mapconcat 'identity `(,(rspec-runner)
                            ,(rspec-runner-options opts)
                            ,(rspec-runner-target target)) " "))))

(defvar rspec-compilation-mode-font-lock-keywords
  '((compilation--ensure-parse)
    ("^\\(Pending\\|Failures\\):$"
     (0 font-lock-function-name-face))
    ("^[0-9]+ examples?, 0 failures.*$"
     (0 compilation-info-face))
    ("^[0-9]+ examples?, \\([0-9]+ failures?\\)"
     (1 compilation-error-face))))

(defvar rspec-compilation-error-regexp-alist-alist
  '((rspec-capybara-html "^ +HTML screenshot: \\([0-9A-Za-z@_./\:-]+\\.html\\)" 1 nil nil 0 1)
    (rspec-capybara-screenshot "^ +\\(Image \\)?\\[?[sS]creenshot\\( Image\\)?\\]?: \\(.+\\.png\\)" 3 nil nil 0 3)
    (rspec "^ +# \\([0-9A-Za-z@_./:-]+\\.rb\\):\\([0-9]+\\):in" 1 2 nil 2 1)
    (rspec-pendings "^ +# \\([0-9A-Za-z@_./:-]+\\.rb\\):\\([0-9]+\\)" 1 2 nil 1 1)
    (rspec-summary "^rspec \\([0-9A-Za-z@_./:-]+\\.rb\\):\\([0-9]+\\)" 1 2 nil 2 1)))

(defvar rspec-compilation-error-regexp-alist
  (mapcar 'car rspec-compilation-error-regexp-alist-alist))

(define-compilation-mode rspec-compilation-mode "RSpec Compilation"
  "Compilation mode for RSpec output."
  (add-hook 'compilation-start-hook 'rspec-run-before-verification-hooks nil t)
  (add-hook 'compilation-filter-hook 'rspec-colorize-compilation-buffer nil t)
  (add-hook 'compilation-finish-functions 'rspec-store-failures nil t)
  (add-hook 'compilation-finish-functions 'rspec-handle-error nil t)
  (add-hook 'compilation-finish-functions 'rspec-run-after-verification-hooks t t))

(defun rspec-store-failures (&rest _)
  "Store the file and line number of the failed examples from this run."
  (let (failures)
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward "^rspec \\([0-9A-Za-z@_./:-]+\\.rb:[0-9]+\\)" nil t)
        (push (match-string-no-properties 1) failures)))
    (setq rspec-last-failed-specs (reverse failures))))

(defun rspec-colorize-compilation-buffer ()
  (ansi-color-apply-on-region compilation-filter-start (point)))

(defun rspec-handle-error (&rest _)
  (save-excursion
    (goto-char (point-max))
    (when (save-excursion
            (forward-line -10)
            (search-forward "`+' for LL():Rake::Scope::EmptyScope" nil t))
      (let ((inhibit-modification-hooks t)
            (inhibit-read-only t)
            (url "https://github.com/pezra/rspec-mode/issues/84"))
        (insert (format "\n%s\n"
                        (propertize
                         "You seem to be using Rake 0.9. Rake 10 is recommended."
                         'font-lock-face 'error)))
        (insert "See ")
        (insert-text-button url 'type 'help-url 'help-args (list url))
        (insert ".\n")))))

(defun rspec-run-after-verification-hooks (&rest _)
  "Executes any functions in `rspec-after-verification-hook'"
  (run-hooks 'rspec-after-verification-hook))

(defun rspec-run-before-verification-hooks (&rest _)
  "Executes any functions in `rspec-before-verification-hook'"
  (run-hooks 'rspec-before-verification-hook))

(defun rspec-project-root-directory-p (directory)
  (or (file-regular-p (expand-file-name "Rakefile" directory))
      (file-regular-p (expand-file-name "Gemfile" directory))
      (file-regular-p (expand-file-name "Berksfile" directory))))

(defun rspec-project-root (&optional directory)
  "Find the root directory of the project.
Walk the directory tree until it finds a rake file."
  (let ((directory (file-name-as-directory (or directory default-directory))))
    (cond ((rspec-root-directory-p directory)
           (error "Could not determine the project root."))
          ((rspec-project-root-directory-p directory) (expand-file-name directory))
          (t (rspec-project-root (file-name-directory (directory-file-name directory)))))))

(defun rspec--factory-girl-module-name ()
  (if (eq rspec-factory-gem 'factory-bot)
      "FactoryBot"
    "FactoryGirl"))

(defun rspec--include-fg-syntax-methods-p ()
  "Check if FactoryGirl is available.
Check whether FactoryGirl::Syntax::Methods is included
in rails_helper or spec_helper."
  (cl-case rspec-snippets-fg-syntax
    (full nil)
    (concise t)
    (t
     (cl-find-if
      (lambda (path)
        (let ((expanded-path (expand-file-name path (rspec-project-root))))
          (when (file-exists-p expanded-path)
            (with-temp-buffer
              (insert-file-contents expanded-path)
              (ruby-mode)
              (when (re-search-forward (concat "include +" (rspec--factory-girl-module-name) "::Syntax::Methods") nil t)
                (not (nth 4 (syntax-ppss))))))))
      '("spec/rails_helper.rb" "spec/spec_helper.rb")))))

(defun rspec--autosave-buffer-maybe ()
  "Saves the current buffer if `rspec-autosave-buffer' is t and
the buffer is a spec or a target file."
  (when rspec-autosave-buffer
    (save-buffer)))

(defun rspec-snippets-fg-method-call (method)
  "Return FactoryGirl method call for METHOD, for use in snippets.
Looks at FactoryGirl::Syntax::Methods usage in spec_helper."
  (if (rspec--include-fg-syntax-methods-p)
      method
    (concat (rspec--factory-girl-module-name) "." method)))

(defun rspec-parse-runner-target (target)
  "Parses the `rspec-runner-target' string"
  (when (string= "SPEC=\'" (cl-subseq target 0 6))
    ;; Removes SPEC=' prefix and ' suffix
    (setq target (cl-subseq target 6 (1- (length target)))))
  (mapcar (lambda (s) (split-string s ":"))
          (split-string target " ")))

(defun rspec-compile-target-first-spec-file (target)
  (let ((file (caar (rspec-compile-target-specs target))))
    (and (rspec-spec-file-p file) file)))

(defun rspec-compile-target-first-spec-filename (target)
  (let ((spec-filepath (caar (rspec-compile-target-specs target))))
    (and (rspec-spec-file-p spec-filepath)
         (file-name-sans-extension
          (file-name-nondirectory spec-filepath)))))

(defun rspec-compile-target-project-name (target)
  (let ((directory (rspec-compile-target-directory target)))
    (and (rspec-project-root-directory-p directory)
         (file-name-nondirectory (directory-file-name directory)))))

(defconst rspec-compilation-buffer-name-base "*rspec-compilation*")

(defun rspec-compilation-buffer-name-with (suffix)
  (format (concat rspec-compilation-buffer-name-base " <%s>") suffix))

(defun rspec-compilation-buffer-name-with-spec-file-name (target)
  (let ((spec-filename (rspec-compile-target-first-spec-filename target)))
    (and spec-filename
         (rspec-compilation-buffer-name-with (file-name-sans-extension spec-filename)))))

(defun rspec-compilation-buffer-name-with-project-name (target)
  (let ((project-name (rspec-compile-target-project-name target)))
    (and project-name
         (rspec-compilation-buffer-name-with project-name))))

(defun rspec-compilation-buffer-name-with-spec-and-project-name (target)
  (let ((spec-filename (rspec-compile-target-first-spec-filename target))
        (project-name (rspec-compile-target-project-name target)))
    (and spec-filename
         project-name
         (rspec-compilation-buffer-name-with
          (concat project-name "/" (file-name-sans-extension spec-filename))))))

(defun rspec-compilation-buffer-name-with-spec-and-project-path (target)
  (let ((spec-filepath (caar (rspec-compile-target-specs target)))
        (project-name (rspec-compile-target-project-name target)))
    (and project-name
         (rspec-compilation-buffer-name-with
          (concat project-name "/" (file-name-sans-extension
                                    (rspec-compress-spec-file spec-filepath)))))))

(defun rspec-compilation-buffer-name-candidates ()
  (let ((target (car rspec-last-arguments))
        candidates)
    (cl-flet ((choose (name)
                      (when name
                        (push name candidates))))
      (choose rspec-compilation-buffer-name-base)
      (choose (rspec-compilation-buffer-name-with-spec-file-name target))
      (choose (rspec-compilation-buffer-name-with-project-name target))
      (choose (rspec-compilation-buffer-name-with-spec-and-project-name target))
      (choose (rspec-compilation-buffer-name-with-spec-and-project-path target))
      (nreverse candidates))))

(defun rspec-compilation-buffer-name (&rest _)
  "Determines the buffer name of the current rspec compilation"
  (let* ((candidates (rspec-compilation-buffer-name-candidates))
         (first-candidate (car candidates)))
    (or (cl-find-if (lambda (buffer-name-candidate)
                      (let ((process (get-buffer-process buffer-name-candidate)))
                        (not (process-live-p process))))
                    candidates)
        first-candidate)))

;;;###autoload
(defun rspec-enable-appropriate-mode ()
  (if (rspec-buffer-is-spec-p)
      (rspec-mode)
    (rspec-verifiable-mode)))

;; Hook up all Ruby buffers.
;;;###autoload
(dolist (hook '(ruby-mode-hook ruby-ts-mode-hook enh-ruby-mode-hook))
  (add-hook hook 'rspec-enable-appropriate-mode))

;; Add verify related spec keybinding to rails minor mode buffers
;;;###autoload
(add-hook 'rails-minor-mode-hook 'rspec-verifiable-mode)

(provide 'rspec-mode)
;;; rspec-mode.el ends here
