diff options
| author | Bozhidar Batsov <bozhidar@batsov.dev> | 2026-02-14 15:40:27 +0200 |
|---|---|---|
| committer | Bozhidar Batsov <bozhidar@batsov.dev> | 2026-02-14 15:40:27 +0200 |
| commit | 83ee3f34b630b06f3bcd58f519dea203f8925012 (patch) | |
| tree | b17132099441096592decf5e500e23abf369e9e1 | |
| parent | 073d72120acba3de05184f747630bf7a3641a049 (diff) | |
Fix projectile-root-top-down to search top-down, not bottom-up (#1729)
projectile-root-top-down used projectile-locate-dominating-file which
stops at the first (bottommost) match going up the directory tree.
This made it behave identically to projectile-root-bottom-up.
Add projectile-locate-dominating-file-top-down which walks the entire
directory hierarchy and returns the topmost match. For example, with
nested Makefiles at both project/ and project/subdir/, searching from
project/subdir/ now correctly returns project/ instead of
project/subdir/.
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | projectile.el | 23 | ||||
| -rw-r--r-- | test/projectile-test.el | 11 |
3 files changed, 34 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3a850..930f5a1 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+. +* [#1729](https://github.com/bbatsov/projectile/issues/1729): Fix `projectile-root-top-down` to actually return the topmost matching project root instead of the bottommost. * [#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. diff --git a/projectile.el b/projectile.el index 407fade..0098b2c 100644 --- a/projectile.el +++ b/projectile.el @@ -1312,6 +1312,27 @@ which we're looking." (setq file nil)))) (and root (expand-file-name (file-name-as-directory root))))) +(defun projectile-locate-dominating-file-top-down (file name) + "Look up the directory hierarchy from FILE for a directory containing NAME. +Unlike `projectile-locate-dominating-file' which returns the first (bottommost) +match, this returns the topmost match. Return nil if not found. +Instead of a string, NAME can also be a predicate taking one argument +\(a directory) and returning a non-nil value if that directory is the one for +which we're looking." + (setq file (abbreviate-file-name file)) + (let ((root nil) + try) + (while (not (or (null file) + (string-match locate-dominating-stop-dir-regexp file))) + (setq try (if (stringp name) + (projectile-file-exists-p (projectile-expand-file-name-wildcard name file)) + (funcall name file))) + (when try (setq root file)) + (if (equal file (setq file (file-name-directory + (directory-file-name file)))) + (setq file nil))) + (and root (expand-file-name (file-name-as-directory root))))) + (defvar-local projectile-project-root nil "Defines a custom Projectile project root. This is intended to be used as a file local variable.") @@ -1324,7 +1345,7 @@ This is intended to be used as a file local variable.") "Identify a project root in DIR by top-down search for files in LIST. If LIST is nil, use `projectile-project-root-files' instead. Return the first (topmost) matched directory or nil if not found." - (projectile-locate-dominating-file + (projectile-locate-dominating-file-top-down dir (lambda (dir) (cl-find-if (lambda (f) diff --git a/test/projectile-test.el b/test/projectile-test.el index 2d828bd..2c97762 100644 --- a/test/projectile-test.el +++ b/test/projectile-test.el @@ -682,6 +682,8 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'. "projectA/src/framework/lib/" "projectA/src/framework.conf" "projectA/src/html/index.html") + ;; .git is a directory, so it's not matched by top-down (file markers only); + ;; framework.conf at projectA/src/ is the only match (expect (projectile-root-top-down "projectA/src/framework/lib" '("framework.conf" ".git")) :to-equal (expand-file-name "projectA/src/")) @@ -691,6 +693,15 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'. (expect (projectile-root-top-down "projectA/src/html/" '("index.html")) :to-equal (expand-file-name "projectA/src/html/"))))) + (it "returns the topmost match when file markers exist at multiple levels" + (projectile-test-with-sandbox + (projectile-test-with-files + ("project/Makefile" + "project/subdir/Makefile" + "project/subdir/file.txt") + (expect (projectile-root-top-down "project/subdir" '("Makefile")) + :to-equal + (expand-file-name "project/"))))) (it "does not match directories for file-type markers" (projectile-test-with-sandbox (projectile-test-with-files |
