;;; -*- lexical-binding: t -*- (require 'cl-lib) (require 'counsel) (defun my/prod-file-p (file) (not (or (string-match-p "test" file) (string-match-p "spec" file)))) (defun my/matching-test-file-p (test-file base-name extension) (cl-loop for pattern in (list (concat "test/.*" base-name "[^/]*\\." extension "$") (concat "spec/.*" base-name "[^/]*\\." extension "$") (concat "test[^/]*" base-name "[^/]*\\." extension "$") (concat base-name "[^/]*test\\." extension "$") (concat base-name "[^/]*spec\\." extension "$")) for matched = (string-match-p pattern test-file) until matched finally return (not (null matched)))) (defun my/matching-prod-file-p (prod-file base-name extension) (dolist (pattern '("test" "it.spec" "spec" "^\\.*" "\\.*$")) (setq base-name (replace-regexp-in-string pattern "" base-name))) (and (my/prod-file-p prod-file) (string-match-p (concat base-name "[^/]*\\." extension "$") prod-file))) (defun my/find-prod-or-test-file (&optional initial-directory) "Find test file in the current project. INITIAL-DIRECTORY, if non-nil, is used as the root directory for search." (interactive (list (when current-prefix-arg (read-directory-name "From directory: ")))) (counsel-require-program "rg") (let* ((default-directory (or initial-directory (locate-dominating-file default-directory ".git") default-directory)) (files (split-string (shell-command-to-string counsel-rg-files-command) "\n" t)) (base-name (file-name-sans-extension (file-name-nondirectory (buffer-file-name)))) (extension (file-name-extension (buffer-file-name))) (predicate (if (my/prod-file-p (buffer-file-name)) #'my/matching-test-file-p #'my/matching-prod-file-p)) (results nil)) (dolist (file files) (if (funcall predicate file base-name extension) (cl-pushnew file results))) (cl-case (length results) (0 (message "No matching file found.")) (1 (find-file (car results))) (t (ivy-read "Find test file" results :action #'counsel-git-action :caller 'my/find-prod-or-test-file)))))