Skip to content

Commit

Permalink
Cache projectile-project-root for performance (#1149)
Browse files Browse the repository at this point in the history
Cache the result of (projectile-project-root) as a buffer local variable to
increase performance - this is implemented the same as was done
for (projectile-project-name) in PR 1906.

To ensure project root cache is used when there is no
project (ie. when (projectile-project-root) is nil) - we set the cached value
to the symbol 'none' and then replace this with nil before we return it - since
the cached value is non-nil we don't reevaluate it - but we don't want to
return 'none' as a result - so we instead substitute 'none' for nil before
returning, and also move the `projectile-require-project-root` error check to
the exit of the function to ensure this is still raised in this case.

Finally, this commit also changes the unit tests to handle the cached project
root by resetting before each evaluation - since the unit tests don't actually
change the (buffer-file-name) we need to do this to ensure they still pass.
  • Loading branch information
alexmurray authored and bbatsov committed Jul 24, 2017
1 parent 5dea114 commit 7951b17
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Changes

* Cache the root of the current project to increase performance
* [#1129](https://github.com/bbatsov/projectile/pull/1129): Fix TRAMP issues.
* Add R DESCRIPTION file to `projectile-project-root-files`.
* Ignore backup files in `projectile-get-other-files`.
Expand Down
85 changes: 61 additions & 24 deletions projectile.el
Original file line number Diff line number Diff line change
Expand Up @@ -857,29 +857,63 @@ topmost sequence of matched directories. Nil otherwise."
(not (projectile-file-exists-p (expand-file-name f (projectile-parent dir)))))))))
(or list projectile-project-root-files-top-down-recurring)))

(defvar-local projectile-cached-project-root nil
"Cached root of the current Projectile project. If non-nil, it
is used as the return value of `projectile-project-root' for
performance (unless the variable `projectile-project-root' is
also set). If nil, it is recalculated the next time
`projectile-project-root' is called.

This variable is reset automatically when Projectile detects that
the `buffer-file-name' has changed. It can also be reset manually
by calling `projectile-reset-cached-project-root'.")

(defvar-local projectile-cached-buffer-file-name nil
"The last known value of `buffer-file-name' for the current
buffer. This is used to detect a change in `buffer-file-name',
which triggers a reset of `projectile-cached-project-root' and
`projectile-cached-project-name'.")

(defun projectile-project-root ()
"Retrieves the root directory of a project if available.
The current directory is assumed to be the project's root otherwise."
;; 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* ((dir default-directory)
(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
(or (when (or is-local is-connected)
(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-files-functions))
(if projectile-require-project-root
(error "You're not in a project")
default-directory))))
;; 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
(or (cl-subst nil 'none
(or (and (equal projectile-cached-buffer-file-name buffer-file-name)
projectile-cached-project-root)
(progn
(setq projectile-cached-buffer-file-name buffer-file-name)
(setq projectile-cached-project-root
;; 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* ((dir default-directory)
(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)
(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-files-functions)))
;; set cached to none so is non-nil so we don't try
;; and look it up again
'none)))))
(if projectile-require-project-root
(error "You're not in a project")
default-directory)))

(defun projectile-file-truename (file-name)
"Return the truename of FILE-NAME.
Expand Down Expand Up @@ -908,10 +942,13 @@ This variable is reset automatically when Projectile detects that
the `buffer-file-name' has changed. It can also be reset manually
by calling `projectile-reset-cached-project-name'.")

(defvar-local projectile-cached-buffer-file-name nil
"The last known value of `buffer-file-name' for the current
buffer. This is used to detect a change in `buffer-file-name',
which triggers a reset of `projectile-cached-project-name'.")
(defun projectile-reset-cached-project-root ()
"Reset the value of `projectile-cached-project-root' to nil.

This means that it is automatically recalculated the next time
function `projectile-project-root' is called."
(interactive)
(setq projectile-cached-project-root nil))

(defun projectile-reset-cached-project-name ()
"Reset the value of `projectile-cached-project-name' to nil.
Expand Down
3 changes: 2 additions & 1 deletion test/projectile-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
,@body))

(defun projectile-test-should-root-in (root directory)
(let ((projectile-project-root-cache (make-hash-table :test 'equal)))
(let ((projectile-project-root-cache (make-hash-table :test 'equal))
(projectile-cached-project-root nil))
(should (equal (file-truename (file-name-as-directory root))
(let ((default-directory
(expand-file-name
Expand Down

0 comments on commit 7951b17

Please sign in to comment.