diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | doc/projects.md | 42 | ||||
| -rw-r--r-- | projectile.el | 213 | ||||
| -rw-r--r-- | test/projectile-test.el | 134 |
4 files changed, 321 insertions, 69 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7db0968..b2379a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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 +* Support a list of functions for `related-files-fn` options and helper functions ### Bugs fixed diff --git a/doc/projects.md b/doc/projects.md index 95eb3f0..4ce3693 100644 --- a/doc/projects.md +++ b/doc/projects.md @@ -141,9 +141,9 @@ 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: +custom function or a list of custom functions 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 | |--------|---------------------------------------------------------------|---------------------------------------------------------------------------------| @@ -192,7 +192,7 @@ For example, "src/foo/abc.cpp" will match to "test/foo/abc.cpp" as test file and #### Example - Different test prefix per extension A custom function for the project using multiple programming languages with different test prefixes. -``` +```el (defun my/related-files(file) (let ((ext-to-test-prefix '(("cpp" . "Test") ("py" . "test_")))) @@ -213,6 +213,40 @@ 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. +### Related file custom function helper + +`:related-files-fn` can accept a list of custom functions to combine the result +of each custom function. This allows users to write several custom functions +and apply them differently to projects. + +Projectile includes a couple of helpers to generate commonly used custom functions. + +| Helper name and params | Purpose | +|------------------------------------|-------------------------------------------------------------| +| groups KIND GROUPS | Relates files in each group as the specified kind. | +| extensions KIND EXTENSIONS | Relates files with extensions as the specified kind. | +| tests-with-prefix EXTENSION PREFIX | Relates files with prefix and extension as :test and :impl. | +| tests-with-suffix EXTENSION SUFFIX | Relates files with suffix and extension as :test and :impl. | + +Each helper means `projectile-related-files-fn-helper-name` function. + +#### Example usage of projectile-related-files-fn-helpers +```el +(setq my/related-files + (list + (projectile-related-files-fn-extensions :other '("cpp" "h" "hpp")) + (projectile-related-files-fn-test-with-prefix "cpp" "Test") + (projectile-related-files-fn-test-with-suffix "el" "_test") + (projectile-related-files-fn-groups + :doc + '(("doc/common.txt" + "src/foo.h" + "src/bar.h"))))) + +(projectile-register-project-type + ;; ... + :related-files-fn #'my/related-files) +``` ## Customizing project root files diff --git a/projectile.el b/projectile.el index a906862..b393a74 100644 --- a/projectile.el +++ b/projectile.el @@ -1862,16 +1862,11 @@ https://github.com/abo-abo/swiper"))) 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))))) + (if-let ((plist (projectile--related-files-plist-by-kind file-name :other))) + (projectile--related-files-from-plist plist) + (projectile--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. @@ -1944,7 +1939,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-extension-files (current-file project-file-list &optional flex-matching) +(defun projectile--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. @@ -2270,46 +2265,89 @@ 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))) +(defun projectile--merge-related-files-fns (related-files-fns) + "Merge multiple RELATED-FILES-FNS into one function." + (lambda (path) + (let (merged-plist) + (dolist (fn related-files-fns merged-plist) + (let ((plist (funcall fn path))) + (cl-loop for (key value) on plist by #'cddr + do (let ((values (if (consp value) value (list value)))) + (if (plist-member merged-plist key) + (nconc (plist-get merged-plist key) values) + (setq merged-plist (plist-put merged-plist key values)))))))))) + +(defun projectile--related-files-plist (project-root file) + "Return a plist containing all related files information for FILE in PROJECT-ROOT." + (if-let ((rel-path (if (file-name-absolute-p file) + (file-relative-name file project-root) + file)) + (custom-function (funcall projectile-related-files-fn-function (projectile-project-type)))) + (funcall (cond ((functionp custom-function) + custom-function) + ((consp custom-function) + (projectile--merge-related-files-fns custom-function)) + (t + (error "Unsupported value type of :related-files-fn"))) + rel-path))) + +(defun projectile--related-files-plist-by-kind (file kind) + "Return a plist containing :paths and/or :predicate of KIND for FILE." + (if-let ((project-root (projectile-project-root)) + (plist (projectile--related-files-plist project-root file)) + (has-kind? (plist-member plist kind))) + (let* ((kind-value (plist-get plist kind)) + (values (if (cl-typep kind-value '(or string function)) + (list kind-value) + kind-value)) + (paths (delete-dups (cl-remove-if-not 'stringp values))) + (predicates (delete-dups (cl-remove-if-not 'functionp values)))) + (append + ;; Make sure that :paths exists even with nil if there is no predicates + (when (or paths (null predicates)) + (list :paths (cl-remove-if-not + (lambda (f) + (projectile-file-exists-p (expand-file-name f project-root))) + paths))) + (when predicates + (list :predicate (if (= 1 (length predicates)) + (car predicates) + (lambda (other-file) + (cl-some (lambda (predicate) + (funcall predicate other-file)) + predicates))))))))) + +(defun projectile--related-files-from-plist (plist) + "Return a list of files matching to PLIST from current project files." + (let* ((predicate (plist-get plist :predicate)) + (paths (plist-get plist :paths))) + (delete-dups (append + paths + (when predicate + (cl-remove-if-not predicate (projectile-current-project-files))))))) + +(defun projectile--related-files-kinds(file) + "Return a list o keywords meaning available related kinds for FILE." + (if-let ((project-root (projectile-project-root)) + (plist (projectile--related-files-plist project-root file))) (cl-loop for key in plist by #'cddr collect key))) -(defun projectile--get-related-files (file kind) +(defun projectile--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))))) + (projectile--related-files-from-plist (projectile--related-files-plist-by-kind file kind))) (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))) + (if-let ((available-kinds (projectile--related-files-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))) + (if-let ((candidates (projectile--related-files file kind))) (projectile-expand-root (projectile--choose-from-candidates candidates)) (error "No matching related file as `%s' found for project type `%s'" @@ -2336,14 +2374,73 @@ If KIND is not provided, a list of possible kinds can be chosen." (find-file (projectile--find-related-file (buffer-file-name)))) +;;;###autoload +(defun projectile-related-files-fn-groups(kind groups) + "Generate a related-files-fn which relates as KIND for files in each of GROUPS." + (lambda (path) + (if-let ((group-found (cl-find-if (lambda (group) + (member path group)) + groups))) + (list kind (cl-remove path group-found :test 'equal))))) + +;;;###autoload +(defun projectile-related-files-fn-extensions(kind extensions) + "Generate a related-files-fn which relates as KIND for files having EXTENSIONS." + (lambda (path) + (let* ((ext (file-name-extension path)) + (basename (file-name-base path)) + (basename-regexp (regexp-quote basename))) + (when (member ext extensions) + (list kind (lambda (other-path) + (and (string-match-p basename-regexp other-path) + (equal basename (file-name-base other-path)) + (let ((other-ext (file-name-extension other-path))) + (and (member other-ext extensions) + (not (equal other-ext ext))))))))))) + +;;;###autoload +(defun projectile-related-files-fn-test-with-prefix(extension test-prefix) + "Generate a related-files-fn which relates tests and impl for files with EXTENSION based on TEST-PREFIX." + (lambda (path) + (when (equal (file-name-extension path) extension) + (let* ((file-name (file-name-nondirectory path)) + (find-impl? (string-prefix-p test-prefix file-name)) + (file-name-to-find (if find-impl? + (substring file-name (length test-prefix)) + (concat test-prefix file-name)))) + (list (if find-impl? :impl :test) + (lambda (other-path) + (and (string-suffix-p file-name-to-find other-path) + (equal (file-name-nondirectory other-path) file-name-to-find)))))))) + +;;;###autoload +(defun projectile-related-files-fn-test-with-suffix(extension test-suffix) + "Generate a related-files-fn which relates tests and impl for files with EXTENSION based on TEST-SUFFIX." + (lambda (path) + (when (equal (file-name-extension path) extension) + (let* ((file-name (file-name-nondirectory path)) + (dot-ext (concat "." extension)) + (suffix-ext (concat test-suffix dot-ext)) + (find-impl? (string-suffix-p suffix-ext file-name)) + (file-name-to-find (if find-impl? + (concat (substring file-name 0 (- (length suffix-ext))) + dot-ext) + (concat (substring file-name 0 (- (length dot-ext))) + suffix-ext)))) + (list (if find-impl? :impl :test) + (lambda (other-path) + (and (string-suffix-p file-name-to-find other-path) + (equal (file-name-nondirectory other-path) file-name-to-find)))))))) (defun projectile-test-file-p (file) "Check if FILE is a test 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))))))) + (let ((kinds (projectile--related-files-kinds file))) + (cond ((member :impl kinds) t) + ((member :test kinds) nil) + (t (or (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)))))))))) (defun projectile-current-project-test-files () "Return a list of test files for the current project." @@ -2817,14 +2914,14 @@ Fallback to DEFAULT-VALUE for missing attributes." (nreverse result)))) (lambda (a b) (> (car a) (car b))))) -(defun projectile--get-best-or-all-candidates-based-on-parents-dirs (file candidates) +(defun projectile--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) +(defun projectile--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))) @@ -2838,15 +2935,13 @@ Fallback to DEFAULT-VALUE for missing attributes." (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) + (if-let ((plist (projectile--related-files-plist-by-kind impl-file :test))) + (projectile--related-files-from-plist plist) + (if-let ((predicate (projectile--impl-to-test-predicate impl-file))) + (projectile--best-or-all-candidates-based-on-parents-dirs + impl-file (cl-remove-if-not predicate (projectile-current-project-files)))))) + +(defun projectile--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))) @@ -2858,13 +2953,11 @@ Fallback to DEFAULT-VALUE for missing attributes." (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))))))) + (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 e127ea9..0d56c57 100644 --- a/test/projectile-test.el +++ b/test/projectile-test.el @@ -1008,7 +1008,7 @@ You'd normally combine this with `projectile-test-with-sandbox'." (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" +(describe "projectile--related-files" (it "returns related files for the given file" (defun -my/related-files(file) (cond ((equal file "src/Foo.c") @@ -1022,10 +1022,134 @@ You'd normally combine this with `projectile-test-with-sandbox'." "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")))))) + (expect (projectile--related-files-kinds "src/Foo.c") :to-equal '(:test :doc)) + (expect (projectile--related-files-kinds "src/TestFoo.c") :to-equal '(:impl)) + (expect (projectile--related-files "src/TestFoo.c" :impl) :to-equal '("src/Foo.c")) + (expect (projectile--related-files "src/Foo.c" :doc) :to-equal '("doc/Foo.txt")) + ;; Support abspath + (expect (projectile--related-files-kinds (concat (projectile-project-root) "src/Foo.c")) :to-equal '(:test :doc)) + (expect (projectile--related-files (concat (projectile-project-root) "src/Foo.c") :doc) :to-equal '("doc/Foo.txt")))))) + +(describe "projectile--merge-related-files-fns" + (it "returns a new function which returns the merged plist from each fn" + (defun -first-fn(file) + (list :foo "file1")) + (defun -second-fn(file) + (list :foo (list "file2" "file3"))) + (defun -third-fn(file) + (list :bar "file4")) + (let ((fn (projectile--merge-related-files-fns '(-first-fn -second-fn)))) + (expect (funcall fn "something") :to-equal '(:foo ("file1" "file2" "file3")))) + (let ((fn (projectile--merge-related-files-fns '(-first-fn -third-fn)))) + (expect (funcall fn "something") :to-equal '(:foo ("file1") :bar ("file4")))))) + +(describe "projectile-related-files-fn-groups" + (it "generate related files fn which relates members of each group as a specified kind" + (let ((fn (projectile-related-files-fn-groups :foo '(("a.cpp" "req/a.txt" "doc/a.uml") + ("b.cpp" "req/b.txt"))))) + (expect (funcall fn "a.cpp") :to-equal '(:foo ("req/a.txt" "doc/a.uml"))) + (expect (funcall fn "req/a.txt") :to-equal '(:foo ("a.cpp" "doc/a.uml"))) + (expect (funcall fn "b.cpp") :to-equal '(:foo ("req/b.txt"))) + (expect (funcall fn "c.cpp") :to-equal nil)))) + +(describe "projectile-related-files-fn-extensions" + (it "generate related files fn which relates files with the given extnsions" + (let* ((fn (projectile-related-files-fn-extensions :foo '("cpp" "h" "hpp"))) + (plist (funcall fn "a.cpp")) + (predicate (plist-get plist :foo))) + (expect plist :to-contain :foo) + (expect (funcall predicate "a.h") :to-equal t) + (expect (funcall predicate "a.hpp") :to-equal t) + (expect (funcall predicate "b.cpp") :to-equal nil) + (expect (funcall predicate "a.cpp") :to-equal nil)))) + +(describe "projectile-related-files-fn-tests-with-prefix" + (it "generate related files fn which relates tests and impl based on extension and prefix" + (let ((fn (projectile-related-files-fn-test-with-prefix "py" "test_"))) + (let* ((plist (funcall fn "foo/a.py")) + (predicate (plist-get plist :test))) + (expect plist :to-contain :test) + (expect (funcall predicate "bar/test_a.py") :to-equal t) + (expect (funcall predicate "bar/test_a.cpp") :to-equal nil)) + (let* ((plist (funcall fn "foo/test_a.py")) + (predicate (plist-get plist :impl))) + (expect plist :to-contain :impl) + (expect (funcall predicate "bar/a.py") :to-equal t) + (expect (funcall predicate "bar/a.cpp") :to-equal nil) + (expect (funcall predicate "bar/test_a.cpp") :to-equal nil))))) + +(describe "projectile-related-files-fn-tests-with-suffix" + (it "generate related files fn which relates tests and impl based on extension and suffix" + (let ((fn (projectile-related-files-fn-test-with-suffix "py" "-test"))) + (let* ((plist (funcall fn "foo/a.py")) + (predicate (plist-get plist :test))) + (expect plist :to-contain :test) + (expect (funcall predicate "bar/a-test.py") :to-equal t) + (expect (funcall predicate "bar/a-test.cpp") :to-equal nil)) + (let* ((plist (funcall fn "foo/a-test.py")) + (predicate (plist-get plist :impl))) + (expect plist :to-contain :impl) + (expect (funcall predicate "bar/a.py") :to-equal t) + (expect (funcall predicate "bar/a.cpp") :to-equal nil) + (expect (funcall predicate "bar/a-test.cpp") :to-equal nil))))) + +(describe "projectile--related-files-plist-by-kind" + (defun -sample-predicate (other-file) + (equal other-file "src/foo.c")) + (defun -sample-predicate2 (other-file) + (equal other-file "src/bar.c")) + (describe "when :related-files-fn returns paths" + (it "returns a plist containing :paths only with the existing files on file system without duplication" + (projectile-test-with-sandbox + (projectile-test-with-files-using-custom-project + ("src/foo.c") + (:related-files-fn (lambda (_) + (list :foo '("src/foo.c" "src/bar.c" "src/foo.c")))) + (expect (projectile--related-files-plist-by-kind "something" :foo) + :to-equal '(:paths ("src/foo.c"))))))) + (describe "when :related-files-fn returns one predicate" + (it "returns a plist containing :predicate with the same predicate" + (projectile-test-with-sandbox + (projectile-test-with-files-using-custom-project + ("src/foo.c") ; Contents does not matter + (:related-files-fn (lambda (_) + (list :foo '-sample-predicate))) + (expect (projectile--related-files-plist-by-kind "something" :foo) + :to-equal '(:predicate -sample-predicate)))))) + (describe "when :related-files-fn returns multiple predicates" + (it "returns a plist containing :predicate with a merging predicate" + (projectile-test-with-sandbox + (projectile-test-with-files-using-custom-project + ("src/foo.c") ; Contents does not matter + (:related-files-fn (lambda (_) + (list :foo (list '-sample-predicate '-sample-predicate2)))) + (let* ((plist (projectile--related-files-plist-by-kind "something" :foo)) + (predicate (plist-get plist :predicate))) + (expect plist :to-contain :predicate) + (expect (funcall predicate "src/foo.c") :to-equal t) + (expect (funcall predicate "src/bar.c") :to-equal t)))))) + (describe "when :related-files-fn returns both paths and predicates" + (it "returns a plist containing both :paths and :predicates" + (projectile-test-with-sandbox + (projectile-test-with-files-using-custom-project + ("src/foo.c") + (:related-files-fn (lambda (_) + (list :foo '("src/foo.c" -sample-predicate)))) + (expect (projectile--related-files-plist-by-kind "something" :foo) + :to-equal '(:paths ("src/foo.c") :predicate -sample-predicate)))))) + (describe "when :related-files-fn is a list of functions" + (it "returns a plist containing the merged results" + (defun -sample-fn(file) + (list :foo "src/foo.c")) + (defun -sample-fn2(file) + (list :foo '-sample-predicate)) + (projectile-test-with-sandbox + (projectile-test-with-files-using-custom-project + ("src/foo.c" + "src/bar.c") + (:related-files-fn (list '-sample-fn '-sample-fn2)) + (expect (projectile--related-files-plist-by-kind "something" :foo) + :to-equal '(:paths ("src/foo.c") :predicate -sample-predicate))))))) (describe "projectile-get-all-sub-projects" (it "excludes out-of-project submodules" |
