From 5bd9db6f4b0a9e1c27136561b134a4d119552cdb Mon Sep 17 00:00:00 2001 From: Hans Jang Date: Wed, 3 Apr 2019 17:19:45 +1100 Subject: Add new 'related-files-fn' option to use custom function to find test/impl/other files (#1394) --- CHANGELOG.md | 5 + doc/projects.md | 104 +++++++++++++-- projectile.el | 224 +++++++++++++++++++++++-------- test/projectile-test.el | 346 +++++++++++++++++++++++++++++++----------------- 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" -- cgit v1.0