aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHans Jang <hsjang8848@gmail.com>2019-04-03 17:19:45 +1100
committerBozhidar Batsov <bozhidar.batsov@gmail.com>2019-04-03 09:19:45 +0300
commit5bd9db6f4b0a9e1c27136561b134a4d119552cdb (patch)
tree9e3e6db7d76b249dbd187bd5e6d5caea020596a7
parent3beb626dae274333aa66d09d42992b5331d12e00 (diff)
Add new 'related-files-fn' option to use custom function to find test/impl/other files (#1394)
-rw-r--r--CHANGELOG.md5
-rw-r--r--doc/projects.md104
-rw-r--r--projectile.el224
-rw-r--r--test/projectile-test.el346
4 files changed, 491 insertions, 188 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac76a2c..7db0968 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
## master (unreleased)
+### New features
+* Add `related-files-fn` option to use custom function to find test/impl/other files
+* [#1019](https://github.com/bbatsov/projectile/issues/1019): Jump to a test named the same way but in a different directory.
+* [#982](https://github.com/bbatsov/projectile/issues/982) Add heuristic for projectile-find-matching-test
+
### Bugs fixed
* [#97](https://github.com/bbatsov/projectile/issues/97): Respect `.projectile`
diff --git a/doc/projects.md b/doc/projects.md
index 6cddaa0..95eb3f0 100644
--- a/doc/projects.md
+++ b/doc/projects.md
@@ -79,17 +79,18 @@ What this does is:
The available options are:
-Option | Documentation
----------------- | -------------------------------------------------------------------------------------------
-:compilation-dir | A path, relative to the project root, from where to run the tests and compilation commands.
-:compile | A command to compile the project.
-:configure | A command to configure the project. `%s` will be substituted with the project root.
-:run | A command to run the project.
-:src-dir | A path, relative to the project root, where the source code lives.
-:test | A command to test the project.
-:test-dir | A path, relative to the project root, where the test code lives.
-:test-prefix | A prefix to generate test files names.
-:test-suffix | A suffix to generate test files names.
+Option | Documentation
+------------------|--------------------------------------------------------------------------------------------
+:compilation-dir | A path, relative to the project root, from where to run the tests and compilation commands.
+:compile | A command to compile the project.
+:configure | A command to configure the project. `%s` will be substituted with the project root.
+:run | A command to run the project.
+:src-dir | A path, relative to the project root, where the source code lives.
+:test | A command to test the project.
+:test-dir | A path, relative to the project root, where the test code lives.
+:test-prefix | A prefix to generate test files names.
+:test-suffix | A suffix to generate test files names.
+:related-files-fn | A function to specify test/impl/other files in a more flexible way.
#### Returning Projectile Commands from a function
@@ -132,6 +133,87 @@ This works for:
Note that your function has to return a string to work properly.
+### Related file location
+
+For simple projects, `:test-prefix` and `:test-suffix` option with string will
+be enough to specify test prefix/suffix applicable regardless of file extensions
+on any directory path. `projectile-other-file-alist` variable can be also set to
+find other files based on the extension.
+
+For the full control of finding related files, `:related-files-fn` option with a
+custom function can be used. The custom function accepts the relative file name
+from the project root and it should return the related file information as plist
+with the following optional key/value pairs:
+
+| Key | Value | Command applicable |
+|--------|---------------------------------------------------------------|---------------------------------------------------------------------------------|
+| :impl | matching implementation file if the given file is a test file | projectile-toggle-between-implementation-and-test, projectile-find-related-file |
+| :test | matching test file if the given file has test files. | projectile-toggle-between-implementation-and-test, projectile-find-related-file |
+| :other | any other files if the given file has them. | projectile-find-other-file, projectile-find-related-file |
+| :foo | any key other than above | projectile-find-related-file |
+
+
+For each value, following type can be used:
+
+| Type | Meaning |
+|----------------------------|----------------------------------------------------------------------------------------------------------|
+| string / a list of strings | Relative paths from the project root. The paths which actually exist on the file system will be matched. |
+| a function | A predicate which accepts a relative path as the input and return t if it matches. |
+| nil | No match exists. |
+
+Notes:
+ 1. For a big project consisting of many source files, returning strings instead
+ of a function can be fast as it does not iterate over each source file.
+ 2. There is a difference in behaviour between no key and `nil` value for the
+ key. Only when the key does not exist, other project options such as
+ `:test_prefix` or `projectile-other-file-alist` mechanism is tried.
+
+
+#### Example - Same source file name for test and impl
+
+```el
+(defun my/related-files (path)
+ (if (string-match (rx (group (or "src" "test")) (group "/" (1+ anything) ".cpp")) path)
+ (let ((dir (match-string 1 path))
+ (file-name (match-string 2 path)))
+ (if (equal dir "test")
+ (list :impl (concat "src" file-name))
+ (list :test (concat "test" file-name)
+ :other (concat "src" file-name ".def"))))))
+
+(projectile-register-project-type
+ ;; ...
+ :related-files-fn #'my/related-files)
+```
+
+With the above example, src/test directory can contain the same name file for test and its implementation file.
+For example, "src/foo/abc.cpp" will match to "test/foo/abc.cpp" as test file and "src/foo/abc.cpp.def" as other file.
+
+
+#### Example - Different test prefix per extension
+A custom function for the project using multiple programming languages with different test prefixes.
+```
+(defun my/related-files(file)
+ (let ((ext-to-test-prefix '(("cpp" . "Test")
+ ("py" . "test_"))))
+ (if-let ((ext (file-name-extension file))
+ (test-prefix (assoc-default ext ext-to-test-prefix))
+ (file-name (file-name-nondirectory file)))
+ (if (string-prefix-p test-prefix file-name)
+ (let ((suffix (concat "/" (substring file-name (length test-prefix)))))
+ (list :impl (lambda (other-file)
+ (string-suffix-p suffix other-file))))
+ (let ((suffix (concat "/" test-prefix file-name)))
+ (list :test (lambda (other-file)
+ (string-suffix-p suffix other-file))))))))
+```
+
+`projectile-find-related-file` command is also available to find and choose
+related files of any kinds. For example, the custom function can specify the
+related documents with ':doc' key. Note that `projectile-find-related-file` only
+relies on `:related-files-fn` for now.
+
+
## Customizing project root files
You can set the values of `projectile-project-root-files`,
diff --git a/projectile.el b/projectile.el
index 54ffa32..a906862 100644
--- a/projectile.el
+++ b/projectile.el
@@ -464,6 +464,11 @@ Any function that does not take arguments will do."
:group 'projectile
:type 'function)
+(defcustom projectile-related-files-fn-function 'projectile-related-files-fn
+ "Function to find related files based on PROJECT-TYPE."
+ :group 'projectile
+ :type 'function)
+
(defcustom projectile-dynamic-mode-line t
"If true, update the mode-line dynamically.
Only file buffers are affected by this, as the update happens via
@@ -1852,7 +1857,21 @@ https://github.com/abo-abo/swiper")))
"Return a list of dirs for the current project."
(projectile-project-dirs (projectile-ensure-project (projectile-project-root))))
-;;; Interactive commands
+(defun projectile-get-other-files (file-name &optional flex-matching)
+ "Return a list of other files for FILE-NAME.
+The list depends on `:related-files-fn' project option and
+`projectile-other-file-alist'. For the latter, FLEX-MATCHING can be used
+to match any basename."
+ (let* ((candidate-plist (projectile--get-related-file-candidates file-name :other))
+ (predicate (plist-get candidate-plist :predicate)))
+ (cond ((plist-member candidate-plist :paths)
+ (plist-get candidate-plist :paths))
+ (predicate
+ (cl-remove-if-not predicate (projectile-current-project-files)))
+ (t
+ (projectile--get-other-extension-files file-name
+ (projectile-current-project-files)
+ flex-matching)))))
(defun projectile--find-other-file (&optional flex-matching ff-variant)
"Switch between files with the same name but different extensions.
@@ -1862,19 +1881,15 @@ Other file extensions can be customized with the variable
instead of `find-file'. A typical example of such a defun would be
`find-file-other-window' or `find-file-other-frame'"
(let ((ff (or ff-variant #'find-file))
- (other-files (projectile-get-other-files
- (buffer-file-name)
- (projectile-current-project-files)
- flex-matching)))
+ (other-files (projectile-get-other-files (buffer-file-name) flex-matching)))
(if other-files
- (let ((file-name (if (= (length other-files) 1)
- (car other-files)
- (projectile-completing-read "Switch to: "
- other-files))))
+ (let ((file-name (projectile--choose-from-candidates other-files)))
(funcall ff (expand-file-name file-name
(projectile-project-root))))
(error "No other file found"))))
+
+;;; Interactive commands
;;;###autoload
(defun projectile-find-other-file (&optional flex-matching)
"Switch between files with the same name but different extensions.
@@ -1929,7 +1944,7 @@ If no associated other-file-extensions for the complete (nested) extension are f
(throw 'break associated-extensions))
(setq current-extensions (projectile--file-name-extensions current-extensions))))))
-(defun projectile-get-other-files (current-file project-file-list &optional flex-matching)
+(defun projectile--get-other-extension-files (current-file project-file-list &optional flex-matching)
"Narrow to files with the same names but different extensions.
Returns a list of possible files for users to choose.
@@ -2255,9 +2270,77 @@ With a prefix arg INVALIDATE-CACHE invalidates the cache first."
"Return only the test FILES."
(cl-remove-if-not 'projectile-test-file-p files))
+(defun projectile--get-related-file-candidates (file kind)
+ "Return a plist containing related information of KIND for FILE."
+ (if-let ((custom-function (funcall projectile-related-files-fn-function (projectile-project-type)))
+ (retval (funcall custom-function file))
+ (has-kind? (plist-member retval kind)))
+ (let ((kind-value (plist-get retval kind)))
+ (if (functionp kind-value)
+ (list :predicate kind-value)
+ (let ((paths (if (stringp kind-value) (list kind-value) kind-value)))
+ (list :paths (cl-remove-if-not
+ (lambda (f)
+ (projectile-file-exists-p (expand-file-name f (projectile-project-root))))
+ paths)))))))
+
+(defun projectile--get-related-file-kinds(file)
+ "Return a list of keywords meaning related kinds for FILE."
+ (if-let ((custom-function (funcall projectile-related-files-fn-function (projectile-project-type)))
+ (plist (funcall custom-function file)))
+ (cl-loop for key in plist by #'cddr
+ collect key)))
+
+(defun projectile--get-related-files (file kind)
+ "Return a list of related files of KIND for FILE."
+ (let* ((candidate-plist (projectile--get-related-file-candidates file kind))
+ (predicate (plist-get candidate-plist :predicate)))
+ (if (plist-member candidate-plist :paths)
+ (plist-get candidate-plist :paths)
+ (cl-remove-if-not predicate (projectile-current-project-files)))))
+
+(defun projectile--find-related-file (file &optional kind)
+ "Choose a file from files related to FILE as KIND.
+If KIND is not provided, a list of possible kinds can be chosen."
+ (unless kind
+ (if-let ((available-kinds (projectile--get-related-file-kinds file)))
+ (setq kind (if (= (length available-kinds) 1)
+ (car available-kinds)
+ (intern (projectile-completing-read "Kind :" available-kinds))))
+ (error "No related files found")))
+
+ (if-let ((candidates (projectile--get-related-files file kind)))
+ (projectile-expand-root (projectile--choose-from-candidates candidates))
+ (error
+ "No matching related file as `%s' found for project type `%s'"
+ kind (projectile-project-type))))
+
+;;;###autoload
+(defun projectile-find-related-file-other-window ()
+ "Open related file in other window."
+ (interactive)
+ (find-file-other-window
+ (projectile--find-related-file (buffer-file-name))))
+
+;;;###autoload
+(defun projectile-find-related-file-other-frame ()
+ "Open related file in other frame."
+ (interactive)
+ (find-file-other-frame
+ (projectile--find-related-file (buffer-file-name))))
+
+;;;###autoload
+(defun projectile-find-related-file()
+ "Open related file."
+ (interactive)
+ (find-file
+ (projectile--find-related-file (buffer-file-name))))
+
+
(defun projectile-test-file-p (file)
"Check if FILE is a test file."
- (or (cl-some (lambda (pat) (string-prefix-p pat (file-name-nondirectory file)))
+ (or (when (projectile--get-related-file-candidates file :impl) t)
+ (cl-some (lambda (pat) (string-prefix-p pat (file-name-nondirectory file)))
(delq nil (list (funcall projectile-test-prefix-function (projectile-project-type)))))
(cl-some (lambda (pat) (string-suffix-p pat (file-name-sans-extension (file-name-nondirectory file))))
(delq nil (list (funcall projectile-test-suffix-function (projectile-project-type)))))))
@@ -2272,7 +2355,7 @@ The project types are symbols and they are linked to plists holding
the properties of the various project types.")
(cl-defun projectile-register-project-type
- (project-type marker-files &key compilation-dir configure compile test run test-suffix test-prefix src-dir test-dir)
+ (project-type marker-files &key compilation-dir configure compile test run test-suffix test-prefix src-dir test-dir related-files-fn)
"Register a project type with projectile.
A project type is defined by PROJECT-TYPE, a set of MARKER-FILES,
@@ -2287,7 +2370,12 @@ RUN which specifies a command that runs the project,
TEST-SUFFIX which specifies test file suffix, and
TEST-PREFIX which specifies test file prefix.
SRC-DIR which specifies the path to the source relative to the project root.
-TEST-DIR which specifies the path to the tests relative to the project root."
+TEST-DIR which specifies the path to the tests relative to the project root.
+RELATED-FILES-FN which specifies a custom function to find the related files such as
+test/impl/other files as below:
+ CUSTOM-FUNCTION accepts FILE as relative path from the project root and returns
+ a plist containing :test, :impl or :other as key and the relative path/paths or
+ predicate as value. PREDICATE accepts a relative path as the input."
(let ((project-plist (list 'marker-files marker-files
'compilation-dir compilation-dir
'configure-command configure
@@ -2306,6 +2394,9 @@ TEST-DIR which specifies the path to the tests relative to the project root."
(plist-put project-plist 'src-dir src-dir))
(when test-dir
(plist-put project-plist 'test-dir test-dir))
+ (when related-files-fn
+ (plist-put project-plist 'related-files-fn related-files-fn))
+
(setq projectile-project-types
(cons `(,project-type . ,project-plist)
projectile-project-types))))
@@ -2690,6 +2781,10 @@ Fallback to DEFAULT-VALUE for missing attributes."
"Find default test files suffix based on PROJECT-TYPE."
(projectile-project-type-attribute project-type 'test-suffix))
+(defun projectile-related-files-fn (project-type)
+ "Find relative file based on PROJECT-TYPE."
+ (projectile-project-type-attribute project-type 'related-files-fn))
+
(defun projectile-src-directory (project-type)
"Find default src directory based on PROJECT-TYPE."
(projectile-project-type-attribute project-type 'src-dir "src/"))
@@ -2722,55 +2817,70 @@ Fallback to DEFAULT-VALUE for missing attributes."
(nreverse result))))
(lambda (a b) (> (car a) (car b)))))
-(defun projectile-find-matching-test (file)
- "Compute the name of the test matching FILE."
- (let* ((basename (file-name-nondirectory (file-name-sans-extension file)))
+(defun projectile--get-best-or-all-candidates-based-on-parents-dirs (file candidates)
+ "Return a list containing the best one one for FILE from CANDIDATES or all CANDIDATES."
+ (let ((grouped-candidates (projectile-group-file-candidates file candidates)))
+ (if (= (length (car grouped-candidates)) 2)
+ (list (car (last (car grouped-candidates))))
+ (apply 'append (mapcar 'cdr grouped-candidates)))))
+
+(defun projectile--get-impl-to-test-predicate (impl-file)
+ "Return a predicate, which returns t for any test files for IMPL-FILE."
+ (let* ((basename (file-name-sans-extension (file-name-nondirectory impl-file)))
(test-prefix (funcall projectile-test-prefix-function (projectile-project-type)))
(test-suffix (funcall projectile-test-suffix-function (projectile-project-type)))
- (candidates
- (cl-remove-if-not
- (lambda (current-file)
- (let ((name (file-name-nondirectory
- (file-name-sans-extension current-file))))
- (or (when test-prefix
- (string-equal name (concat test-prefix basename)))
- (when test-suffix
- (string-equal name (concat basename test-suffix))))))
- (projectile-current-project-files))))
- (cond
- ((null candidates) nil)
- ((= (length candidates) 1) (car candidates))
- (t (let ((grouped-candidates (projectile-group-file-candidates file candidates)))
- (if (= (length (car grouped-candidates)) 2)
- (car (last (car grouped-candidates)))
- (projectile-completing-read
- "Switch to: "
- (apply 'append (mapcar 'cdr grouped-candidates)))))))))
+ (prefix-name (when test-prefix (concat test-prefix basename)))
+ (suffix-name (when test-suffix (concat basename test-suffix))))
+ (lambda (current-file)
+ (let ((name (file-name-sans-extension (file-name-nondirectory current-file))))
+ (or (string-equal prefix-name name)
+ (string-equal suffix-name name))))))
+
+(defun projectile--find-matching-test (impl-file)
+ "Return a list of test files for IMPL-FILE."
+ (let* ((plist (projectile--get-related-file-candidates impl-file :test))
+ (test-paths (plist-get plist :paths))
+ (test-predicate (plist-get plist :predicate)))
+ (or test-paths
+ (if-let ((predicate (or test-predicate (projectile--get-impl-to-test-predicate impl-file))))
+ (projectile--get-best-or-all-candidates-based-on-parents-dirs
+ impl-file (cl-remove-if-not predicate (projectile-current-project-files)))))))
+
+(defun projectile--get-test-to-impl-predicate (test-file)
+ "Return a predicate, which returns t for any impl files for TEST-FILE."
+ (let* ((basename (file-name-sans-extension (file-name-nondirectory test-file)))
+ (test-prefix (funcall projectile-test-prefix-function (projectile-project-type)))
+ (test-suffix (funcall projectile-test-suffix-function (projectile-project-type))))
+ (lambda (current-file)
+ (let ((name (file-name-nondirectory (file-name-sans-extension current-file))))
+ (or (when test-prefix (string-equal (concat test-prefix name) basename))
+ (when test-suffix (string-equal (concat name test-suffix) basename)))))))
+
+(defun projectile--find-matching-file (test-file)
+ "Return a list of impl files tested by TEST-FILE."
+ (let* ((plist (projectile--get-related-file-candidates test-file :impl))
+ (impl-paths (plist-get plist :paths))
+ (impl-predicate (plist-get plist :predicate)))
+ (or impl-paths
+ (if-let ((predicate (or impl-predicate (projectile--get-test-to-impl-predicate test-file))))
+ (projectile--get-best-or-all-candidates-based-on-parents-dirs
+ test-file (cl-remove-if-not predicate (projectile-current-project-files)))))))
+
+(defun projectile--choose-from-candidates (candidates)
+ "Choose one item from CANDIDATES."
+ (if (= (length candidates) 1)
+ (car candidates)
+ (projectile-completing-read "Switch to: " candidates)))
+
+(defun projectile-find-matching-test (impl-file)
+ "Compute the name of the test matching IMPL-FILE."
+ (if-let ((candidates (projectile--find-matching-test impl-file)))
+ (projectile--choose-from-candidates candidates)))
(defun projectile-find-matching-file (test-file)
"Compute the name of a file matching TEST-FILE."
- (let* ((basename (file-name-nondirectory (file-name-sans-extension test-file)))
- (test-prefix (funcall projectile-test-prefix-function (projectile-project-type)))
- (test-suffix (funcall projectile-test-suffix-function (projectile-project-type)))
- (candidates
- (cl-remove-if-not
- (lambda (current-file)
- (let ((name (file-name-nondirectory
- (file-name-sans-extension current-file))))
- (or (when test-prefix
- (string-equal (concat test-prefix name) basename))
- (when test-suffix
- (string-equal (concat name test-suffix) basename)))))
- (projectile-current-project-files))))
- (cond
- ((null candidates) nil)
- ((= (length candidates) 1) (car candidates))
- (t (let ((grouped-candidates (projectile-group-file-candidates test-file candidates)))
- (if (= (length (car grouped-candidates)) 2)
- (car (last (car grouped-candidates)))
- (projectile-completing-read
- "Switch to: "
- (apply 'append (mapcar 'cdr grouped-candidates)))))))))
+ (if-let ((candidates (projectile--find-matching-file test-file)))
+ (projectile--choose-from-candidates candidates)))
(defun projectile-grep-default-files ()
"Try to find a default pattern for `projectile-grep'.
diff --git a/test/projectile-test.el b/test/projectile-test.el
index 2d92670..e127ea9 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -65,9 +65,29 @@ You'd normally combine this with `projectile-test-with-sandbox'."
files)
,@body))
+(defmacro projectile-test-with-files-using-custom-project (files project-options &rest body)
+ "Evaluate BODY with the custom project having PROJECT-OPTIONS with FILES."
+ (declare (indent 2) (debug (sexp sexp &rest form)))
+ `(let ((projectile-indexing-method 'native)
+ (projectile-projects-cache (make-hash-table :test 'equal))
+ (projectile-projects-cache-time (make-hash-table :test 'equal))
+ (projectile-enable-caching t))
+ ,@(mapcar (lambda (file)
+ (let* ((path (concat "project/" file))
+ (dir (file-name-directory path)))
+ (if (string-suffix-p "/" file)
+ `(make-directory ,path t)
+ `(progn
+ (make-directory ,dir t)
+ (with-temp-file ,path)))))
+ files)
+ (projectile-register-project-type 'sample-project '("somefile") ,@project-options)
+ (spy-on 'projectile-project-type :and-return-value 'sample-project)
+ (spy-on 'projectile-project-root :and-return-value (file-truename (expand-file-name "project/")))
+ ,@body))
+
(defun projectile-test-tmp-file-path ()
- "Return a filename suitable to save data to in the
-test temp directory"
+ "Return a filename suitable to save data to in the test temp directory."
(concat projectile-test-path
"/tmp/temporary-file-" (format "%d" (random))
".eld"))
@@ -723,74 +743,125 @@ test temp directory"
(describe "projectile-get-other-files"
(it "returns files with same names but different extensions"
- (let ((projectile-other-file-alist '(;; handle C/C++ extensions
- ("cpp" . ("h" "hpp" "ipp"))
- ("ipp" . ("h" "hpp" "cpp"))
- ("hpp" . ("h" "ipp" "cpp"))
- ("cxx" . ("hxx" "ixx"))
- ("ixx" . ("cxx" "hxx"))
- ("hxx" . ("ixx" "cxx"))
- ("c" . ("h"))
- ("m" . ("h"))
- ("mm" . ("h"))
- ("h" . ("c" "cpp" "ipp" "hpp" "m" "mm"))
- ("cc" . ("hh"))
- ("hh" . ("cc"))
-
- ;; vertex shader and fragment shader extensions in glsl
- ("vert" . ("frag"))
- ("frag" . ("vert"))
-
- ;; handle files with no extension
- (nil . ("lock" "gpg"))
- ("lock" . (""))
- ("gpg" . (""))
-
- ;; handle files with nested extensions
- ("service.js" . ("service.spec.js"))
- ("js" . ("js"))))
- (source-tree '("src/test1.c"
- "src/test2.c"
- "src/test+copying.m"
- "src/test1.cpp"
- "src/test2.cpp"
- "src/Makefile"
- "src/test.vert"
- "src/test.frag"
- "src/same_name.c"
- "src/some_module/same_name.c"
- "include1/same_name.h"
- "include1/test1.h"
- "include1/test1.h~"
- "include1/test2.h"
- "include1/test+copying.h"
- "include1/test1.hpp"
- "include2/some_module/same_name.h"
- "include2/test1.h"
- "include2/test2.h"
- "include2/test2.hpp"
-
- "src/test1.service.js"
- "src/test2.service.spec.js"
- "include1/test1.service.spec.js"
- "include2/test1.service.spec.js"
- "include1/test2.js"
- "include2/test2.js")))
-
- (expect (projectile-get-other-files "src/test1.c" source-tree) :to-equal '("include1/test1.h" "include2/test1.h"))
- (expect (projectile-get-other-files "src/test1.cpp" source-tree) :to-equal '("include1/test1.h" "include2/test1.h" "include1/test1.hpp"))
- (expect (projectile-get-other-files "test2.c" source-tree) :to-equal '("include1/test2.h" "include2/test2.h"))
- (expect (projectile-get-other-files "test2.cpp" source-tree) :to-equal '("include1/test2.h" "include2/test2.h" "include2/test2.hpp"))
- (expect (projectile-get-other-files "test1.h" source-tree) :to-equal '("src/test1.c" "src/test1.cpp" "include1/test1.hpp"))
- (expect (projectile-get-other-files "test2.h" source-tree) :to-equal '("src/test2.c" "src/test2.cpp" "include2/test2.hpp"))
- (expect (projectile-get-other-files "include1/test1.h" source-tree t) :to-equal '("src/test1.c" "src/test1.cpp" "include1/test1.hpp"))
- (expect (projectile-get-other-files "Makefile.lock" source-tree) :to-equal '("src/Makefile"))
- (expect (projectile-get-other-files "include2/some_module/same_name.h" source-tree) :to-equal '("src/some_module/same_name.c" "src/same_name.c"))
- ;; nested extensions
- (expect (projectile-get-other-files "src/test1.service.js" source-tree) :to-equal '("include1/test1.service.spec.js" "include2/test1.service.spec.js"))
- ;; fallback to outer extensions if no rule for nested extension defined
- (expect (projectile-get-other-files "src/test2.service.spec.js" source-tree) :to-equal '("include1/test2.js" "include2/test2.js"))
- (expect (projectile-get-other-files "src/test+copying.m" source-tree) :to-equal '("include1/test+copying.h")))))
+ (projectile-test-with-sandbox
+ (projectile-test-with-files-using-custom-project
+ ("src/test1.c"
+ "src/test2.c"
+ "src/test+copying.m"
+ "src/test1.cpp"
+ "src/test2.cpp"
+ "src/Makefile"
+ "src/test.vert"
+ "src/test.frag"
+ "src/same_name.c"
+ "src/some_module/same_name.c"
+ "include1/same_name.h"
+ "include1/test1.h"
+ "include1/test1.h~"
+ "include1/test2.h"
+ "include1/test+copying.h"
+ "include1/test1.hpp"
+ "include2/some_module/same_name.h"
+ "include2/test1.h"
+ "include2/test2.h"
+ "include2/test2.hpp"
+ "src/test1.service.js"
+ "src/test2.service.spec.js"
+ "include1/test1.service.spec.js"
+ "include2/test1.service.spec.js"
+ "include1/test2.js"
+ "include2/test2.js")
+ ()
+ (let ((projectile-other-file-alist '(;; handle C/C++ extensions
+ ("cpp" . ("h" "hpp" "ipp"))
+ ("ipp" . ("h" "hpp" "cpp"))
+ ("hpp" . ("h" "ipp" "cpp"))
+ ("cxx" . ("hxx" "ixx"))
+ ("ixx" . ("cxx" "hxx"))
+ ("hxx" . ("ixx" "cxx"))
+ ("c" . ("h"))
+ ("m" . ("h"))
+ ("mm" . ("h"))
+ ("h" . ("c" "cpp" "ipp" "hpp" "m" "mm"))
+ ("cc" . ("hh"))
+ ("hh" . ("cc"))
+
+ ;; vertex shader and fragment shader extensions in glsl
+ ("vert" . ("frag"))
+ ("frag" . ("vert"))
+
+ ;; handle files with no extension
+ (nil . ("lock" "gpg"))
+ ("lock" . (""))
+ ("gpg" . (""))
+
+ ;; handle files with nested extensions
+ ("service.js" . ("service.spec.js"))
+ ("js" . ("js")))))
+ (expect (projectile-get-other-files "src/test1.c") :to-equal '("include1/test1.h" "include2/test1.h"))
+ (expect (projectile-get-other-files "src/test1.cpp") :to-equal '("include1/test1.h" "include2/test1.h" "include1/test1.hpp"))
+ (expect (projectile-get-other-files "test2.c") :to-equal '("include1/test2.h" "include2/test2.h"))
+ (expect (projectile-get-other-files "test2.cpp") :to-equal '("include1/test2.h" "include2/test2.h" "include2/test2.hpp"))
+ (expect (projectile-get-other-files "test1.h") :to-equal '("src/test1.c" "src/test1.cpp" "include1/test1.hpp"))
+ (expect (projectile-get-other-files "test2.h") :to-equal '("src/test2.c" "src/test2.cpp" "include2/test2.hpp"))
+ (expect (projectile-get-other-files "include1/test1.h" t) :to-equal '("src/test1.c" "src/test1.cpp" "include1/test1.hpp"))
+ (expect (projectile-get-other-files "Makefile.lock") :to-equal '("src/Makefile"))
+ (expect (projectile-get-other-files "include2/some_module/same_name.h") :to-equal '("src/some_module/same_name.c" "src/same_name.c"))
+ ;; nested extensions
+ (expect (projectile-get-other-files "src/test1.service.js") :to-equal '("include1/test1.service.spec.js" "include2/test1.service.spec.js"))
+ ;; fallback to outer extensions if no rule for nested extension defined
+ (expect (projectile-get-other-files "src/test2.service.spec.js") :to-equal '("include1/test2.js" "include2/test2.js"))
+ (expect (projectile-get-other-files "src/test+copying.m") :to-equal '("include1/test+copying.h"))))))
+
+ (it "returns files based on the paths returned by :related-files-fn option"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files-using-custom-project
+ ("src/test1.cpp"
+ "src/test1.def"
+ "src/test2.def"
+ "src/test2.cpp"
+ "src/test2.h"
+ "src/test3.cpp"
+ "src/test3.h")
+ (:related-files-fn (lambda (file)
+ (cond ((equal file "src/test1.def") '(:other "src/test1.cpp"))
+ ((equal file "src/test2.def") '(:other ("src/test2.cpp" "src/test2.h" "src/test4.h")))
+ ((equal file "src/test3.cpp") '(:other nil)))))
+ (expect (projectile-get-other-files "src/test1.def") :to-equal '("src/test1.cpp"))
+ (expect (projectile-get-other-files "src/test2.def") :to-equal '("src/test2.cpp" "src/test2.h"))
+ ;; Make sure extension based mechanism is still working
+ (expect (projectile-get-other-files "src/test2.cpp") :to-equal '("src/test2.h"))
+ ;; Make sure that related-files-fn option has priority over existing mechanism
+ (expect (projectile-get-other-files "src/test3.cpp") :to-equal nil))))
+
+ (it "returns files based on the predicate returned by :related-files-fn option"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files-using-custom-project
+ ("src/test1.cpp"
+ "src/test1.def"
+ "src/test2.def"
+ "src/test2.cpp"
+ "src/test2.h"
+ "src/test3.cpp"
+ "src/test3.h")
+ (:related-files-fn
+ (lambda (file)
+ (cond ((equal file "src/test1.def")
+ (list :other (lambda (other-file)
+ (equal other-file "src/test1.cpp"))))
+ ((equal file "src/test2.def")
+ (list :other (lambda (other-file)
+ (or (equal other-file "src/test2.cpp")
+ (equal other-file "src/test2.h")))))
+ ((equal file "src/test3.cpp")
+ (list :other (lambda (other-file) nil))))))
+
+ (expect (projectile-get-other-files "src/test1.def") :to-equal '("src/test1.cpp"))
+ (expect (projectile-get-other-files "src/test2.def") :to-equal '("src/test2.cpp" "src/test2.h"))
+ ;; Make sure extension based mechanism is still working
+ (expect (projectile-get-other-files "src/test2.cpp") :to-equal '("src/test2.h"))
+ ;; Make sure that related-files-fn option has priority over existing mechanism
+ (expect (projectile-get-other-files "src/test3.cpp") :to-equal nil)))))
(describe "projectile-compilation-dir"
(it "returns the compilation directory for a project"
@@ -866,60 +937,95 @@ test temp directory"
(expect (projectile-dirname-matching-count "src/weed/sea.c" "src/food/sea.c") :to-equal 0)
(expect (projectile-dirname-matching-count "test/demo-test.el" "demo.el") :to-equal 0)))
-(describe "projectile-find-matching-test"
+(describe "projectile--find-matching-test"
(it "finds matching test or file"
(projectile-test-with-sandbox
- (projectile-test-with-files
- ("project/app/models/weed/"
- "project/app/models/food/"
- "project/spec/models/weed/"
- "project/spec/models/food/"
- "project/app/models/weed/sea.rb"
- "project/app/models/food/sea.rb"
- "project/spec/models/weed/sea_spec.rb"
- "project/spec/models/food/sea_spec.rb")
- (let ((projectile-indexing-method 'native))
- (spy-on 'projectile-project-type :and-return-value 'rails-rspec)
- (spy-on 'projectile-project-root :and-return-value (file-truename (expand-file-name "project/")))
- (expect (projectile-find-matching-test "app/models/food/sea.rb") :to-equal "spec/models/food/sea_spec.rb")
- (expect (projectile-find-matching-file "spec/models/food/sea_spec.rb") :to-equal "app/models/food/sea.rb")))))
- (it "finds matching test or file in a custom project"
+ (projectile-test-with-files-using-custom-project
+ ("app/models/weed/sea.rb"
+ "app/models/food/sea.rb"
+ "spec/models/weed/sea_spec.rb"
+ "spec/models/food/sea_spec.rb")
+ (:test-suffix "_spec")
+ (expect (projectile--find-matching-test "app/models/food/sea.rb") :to-equal '("spec/models/food/sea_spec.rb"))
+ (expect (projectile--find-matching-file "spec/models/food/sea_spec.rb") :to-equal '("app/models/food/sea.rb")))))
+
+ (it "finds matching test or file with dirs"
(projectile-test-with-sandbox
- (projectile-test-with-files
- ("project/src/foo/"
- "project/src/bar/"
- "project/test/foo/"
- "project/test/bar/"
- "project/src/foo/foo.service.js"
- "project/src/bar/bar.service.js"
- "project/test/foo/foo.service.spec.js"
- "project/test/bar/bar.service.spec.js")
- (let ((projectile-indexing-method 'native))
- (projectile-register-project-type 'npm-project '("somefile") :test-suffix ".spec")
- (spy-on 'projectile-project-type :and-return-value 'npm-project)
- (spy-on 'projectile-project-root :and-return-value (file-truename (expand-file-name "project/")))
- (expect (projectile-find-matching-test "src/foo/foo.service.js") :to-equal "test/foo/foo.service.spec.js")
- (expect (projectile-find-matching-file "test/bar/bar.service.spec.js") :to-equal "src/bar/bar.service.js")))))
- (it "finds matching test or file in a custom project with dirs"
+ (projectile-test-with-files-using-custom-project
+ ("source/foo/foo.service.js"
+ "source/bar/bar.service.js"
+ "spec/foo/foo.service.spec.js"
+ "spec/bar/bar.service.spec.js")
+ (:test-suffix ".spec" :test-dir "spec/" :src-dir "source/")
+ (expect (projectile--find-matching-test "source/foo/foo.service.js") :to-equal '("spec/foo/foo.service.spec.js"))
+ (expect (projectile--find-matching-file "spec/bar/bar.service.spec.js") :to-equal '("source/bar/bar.service.js")))))
+
+ (it "finds matching test or file based on the paths returned by :related-files-fn option"
+ (defun -my/related-files(file)
+ (if (string-match (rx (group (or "src" "test")) (group "/" (1+ anything) ".cpp")) file)
+ (if (equal (match-string 1 file ) "test")
+ (list :impl (concat "src" (match-string 2 file)))
+ (list :test (concat "test" (match-string 2 file))))))
(projectile-test-with-sandbox
- (projectile-test-with-files
- ("project/source/foo/"
- "project/source/bar/"
- "project/spec/foo/"
- "project/spec/bar/"
- "project/source/foo/foo.service.js"
- "project/source/bar/bar.service.js"
- "project/spec/foo/foo.service.spec.js"
- "project/spec/bar/bar.service.spec.js")
- (let ((projectile-indexing-method 'native))
- (projectile-register-project-type 'npm-project '("somefile")
- :test-suffix ".spec"
- :test-dir "spec/"
- :src-dir "source/")
- (spy-on 'projectile-project-type :and-return-value 'npm-project)
- (spy-on 'projectile-project-root :and-return-value (file-truename (expand-file-name "project/")))
- (expect (projectile-find-matching-test "source/foo/foo.service.js") :to-equal "spec/foo/foo.service.spec.js")
- (expect (projectile-find-matching-file "spec/bar/bar.service.spec.js") :to-equal "source/bar/bar.service.js"))))))
+ (projectile-test-with-files-using-custom-project
+ ("src/Foo.cpp"
+ "src/Bar.cpp"
+ "src/Baz.py"
+ "test/Bar.cpp"
+ "test/Foo.cpp"
+ "other/Test_Baz.py")
+ (:related-files-fn #'-my/related-files :test-prefix "Test_")
+ (expect (projectile-test-file-p "test/Foo.cpp") :to-equal t)
+ (expect (projectile-test-file-p "src/Foo.cpp") :to-equal nil)
+ (expect (projectile--find-matching-test "src/Foo.cpp") :to-equal '("test/Foo.cpp"))
+ (expect (projectile--find-matching-test "src/Foo2.cpp") :to-equal nil)
+ (expect (projectile--find-matching-file "test/Foo.cpp") :to-equal '("src/Foo.cpp"))
+ (expect (projectile--find-matching-file "test/Foo2.cpp") :to-equal nil)
+ ;; Make sure that existing mechanism(:test-prefix) still works
+ (expect (projectile-test-file-p "other/Test_Baz.py") :to-equal t)
+ (expect (projectile-test-file-p "other/Baz.py") :to-equal nil)
+ (expect (projectile--find-matching-file "other/Test_Baz.py") :to-equal '("src/Baz.py"))
+ (expect (projectile--find-matching-test "src/Baz.py") :to-equal '("other/Test_Baz.py")))))
+
+ (it "finds matching test or file by the predicate returned by :related-files-fn option"
+ (defun -my/related-files(file)
+ (cond ((equal file "src/Foo.cpp")
+ (list :test (lambda (other-file)
+ (equal other-file "test/Foo.cpp"))))
+ ((equal file "test/Foo.cpp")
+ (list :impl (lambda (other-file)
+ (equal other-file "src/Foo.cpp"))))))
+ (projectile-test-with-sandbox
+ (projectile-test-with-files-using-custom-project
+ ("src/Foo.cpp"
+ "src/Bar.cpp"
+ "test/Bar.cpp"
+ "test/Foo.cpp")
+ (:related-files-fn #'-my/related-files)
+ (expect (projectile-test-file-p "test/Foo.cpp") :to-equal t)
+ (expect (projectile-test-file-p "src/Foo.cpp") :to-equal nil)
+ (expect (projectile--find-matching-test "src/Foo.cpp") :to-equal '("test/Foo.cpp"))
+ (expect (projectile--find-matching-test "src/Foo.cpp") :to-equal '("test/Foo.cpp"))
+ (expect (projectile--find-matching-file "test/Foo.cpp") :to-equal '("src/Foo.cpp"))))))
+
+(describe "projectile--get-related-files"
+ (it "returns related files for the given file"
+ (defun -my/related-files(file)
+ (cond ((equal file "src/Foo.c")
+ (list :test "src/TestFoo.c" :doc "doc/Foo.txt"))
+ ((equal file "src/TestFoo.c")
+ (list :impl (lambda (other-file)
+ (equal other-file "src/Foo.c"))))))
+ (projectile-test-with-sandbox
+ (projectile-test-with-files-using-custom-project
+ ("src/Foo.c"
+ "src/TestFoo.c"
+ "doc/Foo.txt")
+ (:related-files-fn #'-my/related-files)
+ (expect (projectile--get-related-file-kinds "src/Foo.c") :to-equal '(:test :doc))
+ (expect (projectile--get-related-file-kinds "src/TestFoo.c") :to-equal '(:impl))
+ (expect (projectile--get-related-files "src/TestFoo.c" :impl) :to-equal '("src/Foo.c"))
+ (expect (projectile--get-related-files "src/Foo.c" :doc) :to-equal '("doc/Foo.txt"))))))
(describe "projectile-get-all-sub-projects"
(it "excludes out-of-project submodules"