aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBozhidar Batsov <bozhidar@batsov.dev>2026-02-14 15:29:12 +0200
committerBozhidar Batsov <bozhidar@batsov.dev>2026-02-14 15:29:12 +0200
commit073d72120acba3de05184f747630bf7a3641a049 (patch)
tree0b1b2a446eab583382dac481d3b5251fa5bd5304
parent7a3708f146fdc4c483b276fa1bc749278cfdb82b (diff)
Include intermediate directories in projectile-find-dir (#1596)
projectile-project-dirs previously only returned directories that directly contain files, missing intermediate directories that contain only subdirectories (e.g. src/ when it only has src/ComponentA/). Fix by walking each file path's ancestor directories up to the project root, so all intermediate directories are included.
-rw-r--r--CHANGELOG.md1
-rw-r--r--projectile.el15
-rw-r--r--test/projectile-test.el14
3 files changed, 28 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e196f2..7a3a850 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
* [#1748](https://github.com/bbatsov/projectile/issues/1748): Fix `projectile-replace` falling back to the legacy Emacs 25/26 code path on Emacs 27+ because `fileloop` was not loaded.
* [#1741](https://github.com/bbatsov/projectile/issues/1741): Fix `projectile-replace` treating the search string as a regexp instead of a literal string on Emacs 27+.
+* [#1596](https://github.com/bbatsov/projectile/issues/1596): `projectile-find-dir` now includes intermediate directories that contain only subdirectories (e.g. `src/` when it only has `src/ComponentA/`, `src/ComponentB/`).
* [#1551](https://github.com/bbatsov/projectile/issues/1551): Don't add nonexistent files to the project cache (e.g. when visiting a new file with `find-file` and then abandoning the buffer).
* [#1554](https://github.com/bbatsov/projectile/issues/1554): Fix `projectile-files-with-string` failing on special characters when using `grep` or `git-grep` by adding the `-F` (fixed-string) flag.
* [#1897](https://github.com/bbatsov/projectile/issues/1897): Filter deleted-but-unstaged files from `git ls-files` output in alien/hybrid indexing (when `fd` is not used).
diff --git a/projectile.el b/projectile.el
index 6a342da..407fade 100644
--- a/projectile.el
+++ b/projectile.el
@@ -2281,8 +2281,19 @@ project-root for every file."
"Return a list of dirs for PROJECT."
(delete-dups
(delq nil
- (mapcar #'file-name-directory
- (projectile-project-files project)))))
+ (cl-mapcan #'projectile--directory-ancestors
+ (projectile-project-files project)))))
+
+(defun projectile--directory-ancestors (path)
+ "Return a list of the directory of PATH and all its ancestor directories.
+For example, \"src/foo/bar.el\" returns (\"src/foo/\" \"src/\")."
+ (let ((dir (file-name-directory path))
+ result)
+ (while (and dir (not (equal dir "")))
+ (push dir result)
+ (let ((parent (file-name-directory (directory-file-name dir))))
+ (setq dir (unless (equal parent dir) parent))))
+ result))
(defun projectile-current-project-dirs ()
"Return a list of dirs for the current project."
diff --git a/test/projectile-test.el b/test/projectile-test.el
index 7864c09..2d828bd 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -554,6 +554,20 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
(expect files :to-contain "existing.txt")
(expect files :not :to-contain "deleted.txt")))))))
+(describe "projectile-project-dirs"
+ (it "includes intermediate directories that contain only subdirectories"
+ (spy-on 'projectile-project-files
+ :and-return-value '("src/ComponentA/a.cc"
+ "src/ComponentB/b.cc"
+ "config/config_file"))
+ (let ((dirs (projectile-project-dirs "/project/")))
+ ;; Leaf directories
+ (expect dirs :to-contain "src/ComponentA/")
+ (expect dirs :to-contain "src/ComponentB/")
+ (expect dirs :to-contain "config/")
+ ;; Intermediate directory (only has subdirectories, no direct files)
+ (expect dirs :to-contain "src/"))))
+
(describe "projectile-index-directory"
(it "skips unreadable directories"
(unless (eq system-type 'windows-nt)