From e652afb6bcf434f03e240253b4ed998ebbb0d60e Mon Sep 17 00:00:00 2001 From: takeokunn Date: Sat, 14 Feb 2026 17:20:18 +0900 Subject: [PATCH] feat: add projectile-invalidate-cache-all command to clear all project caches - Introduce projectile-invalidate-cache-all to remove file caches for all known projects, including persistent caches and related hash tables - Update troubleshooting documentation with usage instructions for the new command - Add menu entry for "Invalidate all caches" - Add tests covering cache clearing, persistent cache handling, and edge cases - Document new command in CHANGELOG --- CHANGELOG.md | 1 + doc/modules/ROOT/pages/troubleshooting.adoc | 3 + projectile.el | 26 +++++++++ test/projectile-test.el | 61 +++++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 883289758..d2d5697e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New features * [#1837](https://github.com/bbatsov/projectile/issues/1837): Add `eat` project terminal commands with keybindings `x x` and `x 4 x`. +* [#1694](https://github.com/bbatsov/projectile/issues/1694): Add `projectile-invalidate-cache-all` command to clear the file cache for all known projects at once. ### Bugs fixed diff --git a/doc/modules/ROOT/pages/troubleshooting.adoc b/doc/modules/ROOT/pages/troubleshooting.adoc index e8004a99b..29a9fefcf 100644 --- a/doc/modules/ROOT/pages/troubleshooting.adoc +++ b/doc/modules/ROOT/pages/troubleshooting.adoc @@ -69,6 +69,9 @@ Note that Projectile caches the operation of checking which project (if any) a file belongs to. If you have already opened a file, then later added a marker file like `.projectile`, run `M-x projectile-invalidate-cache` to reset the cache. +To invalidate the cache for all known projects at once (useful before +using `projectile-find-file-in-known-projects`), run `M-x +projectile-invalidate-cache-all`. === I upgraded Projectile using `package.el` and nothing changed diff --git a/projectile.el b/projectile.el index 1f520807b..7718a8392 100644 --- a/projectile.el +++ b/projectile.el @@ -1124,6 +1124,31 @@ argument)." (when (fboundp 'recentf-cleanup) (recentf-cleanup))) +;;;###autoload +(defun projectile-invalidate-cache-all () + "Remove all known projects' files from `projectile-projects-cache'. + +Also clears `projectile-projects-cache-time', +`projectile-project-type-cache', and `projectile-project-root-cache'. + +When persistent caching is enabled, on-disk cache files are also +cleared for all known projects (excluding remote TRAMP paths)." + (interactive) + (setq projectile-project-root-cache (make-hash-table :test 'equal)) + (let ((count (hash-table-count projectile-projects-cache))) + (setq projectile-projects-cache (make-hash-table :test 'equal)) + (setq projectile-projects-cache-time (make-hash-table :test 'equal)) + (setq projectile-project-type-cache (make-hash-table :test 'equal)) + (when (projectile-persistent-cache-p) + (dolist (project projectile-known-projects) + (when (and (not (file-remote-p project)) + (file-exists-p project)) + (projectile-serialize nil (projectile-project-cache-file project))))) + (when (fboundp 'recentf-cleanup) + (recentf-cleanup)) + (when projectile-verbose + (message "Invalidated Projectile cache for %d projects." count)))) + (defun projectile-time-seconds () "Return the number of seconds since the unix epoch." (if (fboundp 'time-convert) @@ -6374,6 +6399,7 @@ thing shown in the mode line otherwise." "--" ["Cache current file" projectile-cache-current-file] ["Invalidate cache" projectile-invalidate-cache] + ["Invalidate all caches" projectile-invalidate-cache-all] ["Regenerate [e|g]tags" projectile-regenerate-tags] "--" ["Toggle project wide read-only" projectile-toggle-project-read-only] diff --git a/test/projectile-test.el b/test/projectile-test.el index 7864c091e..bbb96f043 100644 --- a/test/projectile-test.el +++ b/test/projectile-test.el @@ -656,6 +656,67 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'. (spy-on 'file-newer-than-file-p :and-return-value t) (expect (projectile-maybe-invalidate-cache nil) :to-be-truthy))) +(describe "projectile-invalidate-cache-all" + (it "should clear projectile-projects-cache" + (puthash "/project1/" '("file1.el") projectile-projects-cache) + (puthash "/project2/" '("file2.el") projectile-projects-cache) + (projectile-invalidate-cache-all) + (expect (hash-table-count projectile-projects-cache) :to-equal 0)) + (it "should clear projectile-projects-cache-time" + (puthash "/project1/" 1234567890 projectile-projects-cache-time) + (projectile-invalidate-cache-all) + (expect (hash-table-count projectile-projects-cache-time) :to-equal 0)) + (it "should clear projectile-project-root-cache" + (puthash "buffer1" "/project1/" projectile-project-root-cache) + (projectile-invalidate-cache-all) + (expect (hash-table-count projectile-project-root-cache) :to-equal 0)) + (it "should clear projectile-project-type-cache" + (puthash "/project1/" 'generic projectile-project-type-cache) + (projectile-invalidate-cache-all) + (expect (hash-table-count projectile-project-type-cache) :to-equal 0)) + (it "should call projectile-serialize for persistent cache" + (spy-on 'projectile-serialize) + (let* ((dir1 (make-temp-file "projectile-test1" t)) + (dir2 (make-temp-file "projectile-test2" t)) + (projectile-enable-caching 'persistent) + (projectile-known-projects (list (file-name-as-directory dir1) + (file-name-as-directory dir2)))) + (unwind-protect + (progn + (projectile-invalidate-cache-all) + (expect 'projectile-serialize :to-have-been-called-times 2)) + (delete-directory dir1 t) + (delete-directory dir2 t)))) + (it "should work when no projects are cached" + (projectile-invalidate-cache-all) + (expect (hash-table-count projectile-projects-cache) :to-equal 0)) + (it "should call recentf-cleanup when available" + (spy-on 'recentf-cleanup) + (projectile-invalidate-cache-all) + (expect 'recentf-cleanup :to-have-been-called)) + (it "should skip non-existent projects for persistent cache" + (spy-on 'projectile-serialize) + (let* ((dir1 (make-temp-file "projectile-test1" t)) + (projectile-enable-caching 'persistent) + (projectile-known-projects (list (file-name-as-directory dir1) + "/definitely-nonexistent-dir/"))) + (unwind-protect + (progn + (projectile-invalidate-cache-all) + (expect 'projectile-serialize :to-have-been-called-times 1)) + (delete-directory dir1 t)))) + (it "should skip remote projects for persistent cache" + (spy-on 'projectile-serialize) + (let* ((dir1 (make-temp-file "projectile-test1" t)) + (projectile-enable-caching 'persistent) + (projectile-known-projects (list (file-name-as-directory dir1) + "/ssh:host:/remote/project/"))) + (unwind-protect + (progn + (projectile-invalidate-cache-all) + (expect 'projectile-serialize :to-have-been-called-times 1)) + (delete-directory dir1 t))))) + (describe "projectile-root-top-down" (it "identifies the root directory of a project by top-down search" (projectile-test-with-sandbox