diff --git a/CHANGELOG.md b/CHANGELOG.md index 2893420db..40a69b437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### New features -* [#1671](https://github.com/bbatsov/projectile/pull/1671) Allow the `:test-dir` option of a project to be set to a function for more flexible test switching. +* [#1671](https://github.com/bbatsov/projectile/pull/1671)/[#1679](https://github.com/bbatsov/projectile/pull/1679) Allow the `:test-dir` and `:src-dir` options of a project to be set to functions for more flexible test switching. ### Bugs fixed diff --git a/doc/modules/ROOT/pages/projects.adoc b/doc/modules/ROOT/pages/projects.adoc index b42689a3f..8787f00e2 100644 --- a/doc/modules/ROOT/pages/projects.adoc +++ b/doc/modules/ROOT/pages/projects.adoc @@ -231,13 +231,13 @@ Bellow is a listing of all the available options for `projectile-register-projec | A command to run the project. | :src-dir -| A path, relative to the project root, where the source code lives. +| A path, relative to the project root, where the source code lives. A function may also be specified which takes one parameter - the directory of a test file, and it should return the directory in which the implementation file should reside. This option is only used for implementation/test toggling. | :test | A command to test the project. | :test-dir -| A path, relative to the project root, where the test code lives. A function may also be specified which takes one parameter - the directory of a file, and it should return the directory in which the test file should reside. +| A path, relative to the project root, where the test code lives. A function may also be specified which takes one parameter - the directory of a file, and it should return the directory in which the test file should reside. This option is only used for implementation/test toggling. | :test-prefix | A prefix to generate test files names. @@ -306,6 +306,14 @@ conjunction with the options `:test-prefix` and `:test-suffix` will then be used to determine the full path of the test file. This option will always be respected if it is set. +Similarly, the `:src-dir` option, the analogue of `:test-dir`, may also take a +function and exhibits exactly the same behaviour as above except that its +parameter corresponds to the directory of a test file and it should return the +directory of the corresponding implementation file. + +It's recommended that either both or neither of these options are set to +functions for consistent behaviour. + Alternatively, for flexible file switching accross a range of projects, the `:related-files-fn` option set to a custom function or a list of custom functions can be used. The custom function accepts the relative @@ -467,16 +475,19 @@ You can also edit specific options of already existing project types: This will keep all existing options for the `sbt` project type, but change the value of the `related-files-fn` option. -=== `:test-dir` vs `:related-files-fn` +=== `:test-dir`/`:src-dir` vs `:related-files-fn` -The `:test-dir` option is useful if the test location for a given implementation -file is almost always going to be in the same place accross all projects -belonging to a given project type, `maven` projects are an example of this: +Setting the `:test-dir` and `:src-dir` options to functions is useful if the +test location for a given implementation file is almost always going to be in +the same place accross all projects belonging to a given project type, `maven` +projects are an example of this: [source,elisp] ---- (projectile-update-project-type 'maven + :src-dir + (lambda (file-path) (projectile-complementary-dir file-path "test" "main")) :test-dir (lambda (file-path) (projectile-complementary-dir file-path "main" "test"))) ---- diff --git a/projectile.el b/projectile.el index 46c54115b..6f3a218aa 100644 --- a/projectile.el +++ b/projectile.el @@ -3300,7 +3300,9 @@ PROJECT-ROOT is the targeted directory. If nil, use (t 'none))) (defun projectile--test-name-for-impl-name (impl-file-path) - "Determine the name of the test file for IMPL-FILE-PATH." + "Determine the name of the test file for IMPL-FILE-PATH. + +IMPL-FILE-PATH may be a absolute path, relative path or a file name." (let* ((project-type (projectile-project-type)) (impl-file-name (file-name-sans-extension (file-name-nondirectory impl-file-path))) (impl-file-ext (file-name-extension impl-file-path)) @@ -3311,6 +3313,22 @@ PROJECT-ROOT is the targeted directory. If nil, use (test-suffix (concat impl-file-name test-suffix "." impl-file-ext)) (t (error "Project type `%s' not supported!" project-type))))) +(defun projectile--impl-name-for-test-name (test-file-path) + "Determine the name of the implementation file for TEST-FILE-PATH. + +TEST-FILE-PATH may be a absolute path, relative path or a file name." + (let* ((project-type (projectile-project-type)) + (test-file-name (file-name-sans-extension (file-name-nondirectory test-file-path))) + (test-file-ext (file-name-extension test-file-path)) + (test-prefix (funcall projectile-test-prefix-function project-type)) + (test-suffix (funcall projectile-test-suffix-function project-type))) + (cond + (test-prefix + (concat (string-remove-prefix test-prefix test-file-name) "." test-file-ext)) + (test-suffix + (concat (string-remove-suffix test-suffix test-file-name) "." test-file-ext)) + (t (error "Project type `%s' not supported!" project-type))))) + (defun projectile--impl-to-test-dir (impl-dir-path) "Return the directory path of a test whose impl file resides in IMPL-DIR-PATH. @@ -3476,6 +3494,17 @@ concatenated with FILENAME-FN applied to the file name of FILE-PATH." (dir (funcall dir-fn (file-name-directory file-path)))) (concat (file-name-as-directory dir) complementary-filename))) +(defun projectile--impl-file-from-src-dir-fn (test-file) + "Return the implementation file path for the absolute path TEST-FILE relative to the project root in the case the current project type's src-dir has been set to a custom function, return nil if this is not the case or the path points to a file that does not exist." + (when-let ((src-dir (projectile-src-directory (projectile-project-type)))) + (when (functionp src-dir) + (let ((impl-file (projectile--complementary-file + test-file + src-dir + #'projectile--impl-name-for-test-name))) + (when (file-exists-p impl-file) + (file-relative-name impl-file (projectile-project-root))))))) + (defun projectile--test-file-from-test-dir-fn (impl-file) "Return the test file path for the absolute path IMPL-FILE relative to the project root, in the case the current project type's test-dir has been set to a custom function, else return nil." (when-let ((test-dir (projectile-test-directory (projectile-project-type)))) @@ -3510,11 +3539,14 @@ concatenated with FILENAME-FN applied to the file name of FILE-PATH." (defun projectile--find-matching-file (test-file) "Return a list of impl files tested by TEST-FILE." - (if-let ((plist (projectile--related-files-plist-by-kind test-file :impl))) - (projectile--related-files-from-plist plist) - (if-let ((predicate (projectile--test-to-impl-predicate test-file))) - (projectile--best-or-all-candidates-based-on-parents-dirs - test-file (cl-remove-if-not predicate (projectile-current-project-files)))))) + (if-let ((impl-file-from-src-dir-fn + (projectile--impl-file-from-src-dir-fn test-file))) + (list impl-file-from-src-dir-fn) + (if-let ((plist (projectile--related-files-plist-by-kind test-file :impl))) + (projectile--related-files-from-plist plist) + (if-let ((predicate (projectile--test-to-impl-predicate test-file))) + (projectile--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." diff --git a/test/projectile-test.el b/test/projectile-test.el index b5b74245a..51dd64469 100644 --- a/test/projectile-test.el +++ b/test/projectile-test.el @@ -1219,7 +1219,7 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'. (projectile-test-with-files-using-custom-project ("src/foo/Foo.cpp" "src/bar/Foo.cpp" - "src/foo/FooTest.cpp") + "test/foo/FooTest.cpp") (:test-dir (lambda (file-path) (projectile-complementary-dir file-path "src" "test")) @@ -1227,7 +1227,22 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'. (expect (projectile--find-matching-test (projectile-expand-root "src/bar/Foo.cpp")) :to-equal - (list "test/bar/FooTest.cpp")))))) + (list "test/bar/FooTest.cpp"))))) + + (it "defers to src-dir property when it's set to a function" + (projectile-test-with-sandbox + (projectile-test-with-files-using-custom-project + ("src/foo/Foo.cpp" + "src/bar/Foo.cpp" + "test/foo/FooTest.cpp") + (:src-dir + (lambda (file-path) + (projectile-complementary-dir file-path "test" "src")) + :test-suffix "Test") + (expect (projectile--find-matching-file + (projectile-expand-root "test/foo/FooTest.cpp")) + :to-equal + (list "src/foo/Foo.cpp")))))) (describe "projectile--related-files" (it "returns related files for the given file" @@ -1672,6 +1687,23 @@ projectile-process-current-project-buffers-current to have similar behaviour" (projectile-process-current-project-buffers-current (lambda () (push (current-buffer) list-b))) (expect list-a :to-equal list-b)))))) +(describe "projectile--impl-name-for-test-name" + :var ((mock-projectile-project-types + '((foo test-suffix "Test") + (bar test-prefix "Test")))) + (it "removes suffix from test file" + (cl-letf (((symbol-function 'projectile-project-type) (lambda () 'foo)) + (projectile-project-types mock-projectile-project-types)) + (expect (projectile--impl-name-for-test-name "FooTest.cpp") + :to-equal + "Foo.cpp"))) + (it "removes prefix from test file" + (cl-letf (((symbol-function 'projectile-project-type) (lambda () 'bar)) + (projectile-project-types mock-projectile-project-types)) + (expect (projectile--impl-name-for-test-name "TestFoo.cpp") + :to-equal + "Foo.cpp")))) + (describe "projectile-find-implementation-or-test" (it "error when test file does not exist and projectile-create-missing-test-files is nil" (cl-letf (((symbol-function 'projectile-test-file-p) #'ignore) @@ -1681,6 +1713,43 @@ projectile-process-current-project-buffers-current to have similar behaviour" (projectile-create-missing-test-files nil)) (expect (projectile-find-implementation-or-test "foo") :to-throw)))) +(describe "projectile--impl-file-from-src-dir-fn" + :var ((mock-projectile-project-types + '((foo src-dir (lambda (impl-file) "/outer/foo/test/dir")) + (bar src-dir "not a function")))) + (it "returns result of projectile--complementary-file when src-dir property is a function" + (cl-letf (((symbol-function 'projectile--complementary-file) + (lambda (impl-file dir-fn file-fn) (funcall dir-fn impl-file))) + ((symbol-function 'projectile-project-type) (lambda () 'foo)) + ((symbol-function 'projectile-project-root) (lambda () "foo")) + ((symbol-function 'file-relative-name) (lambda (f rel) f)) + ((symbol-function 'file-exists-p) (lambda (file) t)) + (projectile-project-types mock-projectile-project-types)) + (expect (projectile--impl-file-from-src-dir-fn "foo") :to-equal "/outer/foo/test/dir"))) + (it "returns file relative to project root" + (cl-letf (((symbol-function 'projectile--complementary-file) + (lambda (impl-file dir-fn file-fn) (funcall dir-fn impl-file))) + ((symbol-function 'projectile-project-type) (lambda () 'foo)) + ((symbol-function 'projectile-project-root) (lambda () "/outer/foo")) + ((symbol-function 'file-exists-p) (lambda (file) t)) + (projectile-project-types mock-projectile-project-types)) + (expect (projectile--impl-file-from-src-dir-fn "/outer/foo/bar") + :to-equal + "test/dir"))) + (it "returns nil when src-dir property is a not function" + (cl-letf (((symbol-function 'projectile-project-type) (lambda () 'bar)) + ((symbol-function 'projectile-project-root) (lambda () "foo")) + (projectile-project-types mock-projectile-project-types)) + (expect (projectile--impl-file-from-src-dir-fn "bar") :to-equal nil))) + (it "returns nil when src-dir function result is not an existing file" + (cl-letf (((symbol-function 'projectile--complementary-file) + (lambda (impl-file dir-fn file-fn) (funcall dir-fn impl-file))) + ((symbol-function 'projectile-project-type) (lambda () 'foo)) + ((symbol-function 'projectile-project-root) (lambda () "/outer/foo")) + ((symbol-function 'file-exists-p) #'ignore) + (projectile-project-types mock-projectile-project-types)) + (expect (projectile--impl-file-from-src-dir-fn "bar") :to-equal nil)))) + (describe "projectile--test-file-from-test-dir-fn" :var ((mock-projectile-project-types '((foo test-dir (lambda (impl-file) "/outer/foo/test/dir"))