diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | projectile.el | 69 | ||||
| -rw-r--r-- | test/projectile-test.el | 38 |
3 files changed, 78 insertions, 30 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f5b55..041055a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * [#1765](https://github.com/bbatsov/projectile/issues/1765): Fix `src-dir`/`test-dir` not defaulting to `"src/"` and `"test/"` with `projectile-toggle-between-implementation-and-test`. * Fix version extraction logic. * [1654](https://github.com/bbatsov/projectile/issues/1654) Fix consecutive duplicates appearing in command history +* [#1755](https://github.com/bbatsov/projectile/issues/1755) Cache failure to find project root ### Changes diff --git a/projectile.el b/projectile.el index d1a71bf..aaa968d 100644 --- a/projectile.el +++ b/projectile.el @@ -1189,42 +1189,53 @@ topmost sequence of matched directories. Nil otherwise." (defun projectile-project-root (&optional dir) "Retrieves the root directory of a project if available. If DIR is not supplied its set to the current directory by default." - ;; the cached value will be 'none in the case of no project root (this is to - ;; ensure it is not reevaluated each time when not inside a project) so use - ;; cl-subst to replace this 'none value with nil so a nil value is used - ;; instead (let ((dir (or dir default-directory))) ;; Back out of any archives, the project will live on the outside and ;; searching them is slow. (when (and (fboundp 'tramp-archive-file-name-archive) (tramp-archive-file-name-p dir)) (setq dir (file-name-directory (tramp-archive-file-name-archive dir)))) + ;; the cached value will be 'none in the case of no project root (this is to + ;; ensure it is not reevaluated each time when not inside a project) so use + ;; cl-subst to replace this 'none value with nil so a nil value is used + ;; instead (cl-subst nil 'none - ;; The `is-local' and `is-connected' variables are - ;; used to fix the behavior where Emacs hangs - ;; because of Projectile when you open a file over - ;; TRAMP. It basically prevents Projectile from - ;; trying to find information about files for which - ;; it's not possible to get that information right - ;; now. - (or (let ((is-local (not (file-remote-p dir))) ;; `true' if the file is local - (is-connected (file-remote-p dir nil t))) ;; `true' if the file is remote AND we are connected to the remote - (when (or is-local is-connected) - ;; Here is where all the magic happens. - ;; We run the functions in `projectile-project-root-functions' until we find a project dir. - (cl-some - (lambda (func) - (let* ((cache-key (format "%s-%s" func dir)) - (cache-value (gethash cache-key projectile-project-root-cache))) - (if (and cache-value (file-exists-p cache-value)) - cache-value - (let ((value (funcall func (file-truename dir)))) - (puthash cache-key value projectile-project-root-cache) - value)))) - projectile-project-root-functions))) - ;; set cached to none so is non-nil so we don't try - ;; and look it up again - 'none)))) + (or + ;; if we've already failed to find a project dir for this + ;; dir, and cached that failure, don't recompute + (let* ((cache-key (format "projectilerootless-%s" dir)) + (cache-value (gethash cache-key projectile-project-root-cache))) + cache-value) + ;; if the file isn't local, and we're not connected, don't try to + ;; find a root now now, but don't cache failure, as we might + ;; re-connect. The `is-local' and `is-connected' variables are + ;; used to fix the behavior where Emacs hangs because of + ;; Projectile when you open a file over TRAMP. It basically + ;; prevents Projectile from trying to find information about + ;; files for which it's not possible to get that information + ;; right now. + (let ((is-local (not (file-remote-p dir))) ;; `true' if the file is local + (is-connected (file-remote-p dir nil t))) ;; `true' if the file is remote AND we are connected to the remote + (unless (or is-local is-connected) + 'none)) + ;; if the file is local or we're connected to it via TRAMP, run + ;; through the project root functions until we find a project dir + (cl-some + (lambda (func) + (let* ((cache-key (format "%s-%s" func dir)) + (cache-value (gethash cache-key projectile-project-root-cache))) + (if (and cache-value (file-exists-p cache-value)) + cache-value + (let ((value (funcall func (file-truename dir)))) + (puthash cache-key value projectile-project-root-cache) + value)))) + projectile-project-root-functions) + ;; if we get here, we have failed to find a root by all + ;; conventional means, and we assume the failure isn't transient + ;; / network related, so cache the failure + (let ((cache-key (format "projectilerootless-%s" dir))) + (puthash cache-key 'none projectile-project-root-cache) + 'none))))) (defun projectile-ensure-project (dir) "Ensure that DIR is non-nil. diff --git a/test/projectile-test.el b/test/projectile-test.el index a83268c..9760e78 100644 --- a/test/projectile-test.el +++ b/test/projectile-test.el @@ -691,7 +691,43 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'. projectile-root-top-down projectile-root-top-down-recurring))) (projectile-test-should-root-in "projectA/src/" "projectA/src/") - (projectile-test-should-root-in "projectA/src/" "projectA/src/html")))))) + (projectile-test-should-root-in "projectA/src/" "projectA/src/html"))))) + + (it "caches permanent failure to find a project root" + (projectile-test-with-sandbox + (projectile-test-with-files + ("projectA/src/") + (let* ((projectile-project-root-functions '()) + (dir "projectA/src") + (cache-key (format "projectilerootless-%s" dir)) + (projectile-project-root-cache (make-hash-table :test 'equal))) + (expect (gethash cache-key projectile-project-root-cache) :to-be nil) + (expect (projectile-project-root dir) :to-be nil) + ;; now that this has run once, the cache should be populated with 'none + (expect (gethash cache-key projectile-project-root-cache) :to-be 'none) + ;; but projectile-project-root should still return nil + (expect (projectile-project-root dir) :to-be nil))))) + + (it "does not cache transitory failure to find a project root" + (projectile-test-with-sandbox + (projectile-test-with-files + ("projectA/src/") + ;; hackish, but override file-remote-p for a moment, which is called in + ;; projectile-project-root with 1 argument to test if the file is remote, + ;; and 3 arguments to see if the file is connected. We want to return t + ;; when checking if remote, and nil when checking if connected. + (cl-letf (((symbol-function 'file-remote-p) + (lambda (&rest args) (eql 1 (length args))))) + (let* ((projectile-project-root-functions '()) + (dir "projectA/src") + (cache-key (format "projectilerootless-%s" dir)) + (projectile-project-root-cache (make-hash-table :test 'equal))) + (expect (gethash cache-key projectile-project-root-cache) :to-be nil) + (expect (projectile-project-root dir) :to-be nil) + ;; since the failure was transitory, there should be nothing cached + (expect (gethash cache-key projectile-project-root-cache) :to-be nil) + ;; and projectile-project-root should still return nil + (expect (projectile-project-root dir) :to-be nil))))))) (describe "projectile-file-exists-p" (it "returns t if file exists" |
