From 2f1bd3cf5dd99912b3990f717e58458405894a88 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 24 Aug 2026 11:02:41 +0300 Subject: [PATCH] Load the known projects before listing them `projectile-known-projects' is both a variable and the accessor that lazily loads it from disk, and nothing loads it at startup. Five commands read the variable, so whichever of them a session reached for first was handed nil: `projectile-find-file-in-known-projects' completed over an empty list, and so did `projectile-vc', `projectile-remove-known-project' and the "Switch to project" prompt `projectile-require-project-root' can raise. Measured on a cold Emacs with 124 saved projects: `projectile-all-project-files' returned 0 files before the accessor had been called and 118604 after. Anything that ran `projectile-switch-project' first hid it, which is presumably how it survived this long. --- CHANGELOG.md | 1 + projectile.el | 10 +++++----- test/projectile-known-projects-test.el | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dddf9267c..81f02fd42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ ### Bugs fixed +- [#2171](https://github.com/bbatsov/projectile/pull/2171): The commands that list the known projects now load the persisted list first. They read the `projectile-known-projects` variable, which stays nil until the same-named accessor loads it from disk and which nothing loads at startup - so whichever of them you reached for first in a session offered nothing. `projectile-find-file-in-known-projects` completed over an empty list; `projectile-vc`, `projectile-remove-known-project` and the "Switch to project" prompt of `projectile-require-project-root` were affected too. - [#2170](https://github.com/bbatsov/projectile/pull/2170): Navigating a search results buffer while the scan is still running is no longer undone by the next chunk. Each redraw ended by going to the top of the buffer, so point jumped back on every batch of matches; it now stays where you left it. - [#2167](https://github.com/bbatsov/projectile/pull/2167): The Emacs Lisp search scanner no longer decompresses or decrypts the files it reads. It went through `file-name-handler-alist`, so every archive in the candidate set was run through gzip and every `.gpg` handed to EPA - work discarded a moment later as binary, and on an encrypted file a passphrase prompt in the middle of a project search. - Matches inside compressed files are no longer reported by that scanner, which is what the ripgrep path already did. diff --git a/projectile.el b/projectile.el index e6de3686f..1bb58e8dc 100644 --- a/projectile.el +++ b/projectile.el @@ -3138,7 +3138,7 @@ See also `projectile-acquire-root'." dir (cond ((eq projectile-require-project-root 'prompt) (projectile-completing-read - "Switch to project: " projectile-known-projects + "Switch to project: " (projectile-known-projects) :category 'projectile-project :caller 'projectile-read-project)) (projectile-require-project-root (user-error "Projectile cannot find a project definition in %s" default-directory)) @@ -11561,7 +11561,7 @@ directory to open." (list (projectile-completing-read "Open project VC in: " - projectile-known-projects + (projectile-known-projects) :category 'projectile-project :caller 'projectile-read-project)))) (unless project-root @@ -13885,7 +13885,7 @@ This command will first prompt for the directory the file is in." (defun projectile-all-project-files () "Get a list of all files in all projects." - (projectile-project-group-files projectile-known-projects)) + (projectile-project-group-files (projectile-known-projects))) ;;;###autoload (defun projectile-find-file-in-known-projects () @@ -13893,7 +13893,7 @@ This command will first prompt for the directory the file is in." This is `projectile-find-file-in-projects' over every project you have ever visited." (interactive) - (projectile-find-file-in-projects projectile-known-projects + (projectile-find-file-in-projects (projectile-known-projects) "Find file in projects: ")) (defun projectile-keep-project-p (project) @@ -13996,7 +13996,7 @@ projects removed." (defun projectile-remove-known-project (&optional project) "Remove PROJECT from the list of known projects." (interactive (list (projectile-completing-read - "Remove from known projects: " projectile-known-projects + "Remove from known projects: " (projectile-known-projects) :action 'projectile-remove-known-project :category 'projectile-project :caller 'projectile-read-project))) diff --git a/test/projectile-known-projects-test.el b/test/projectile-known-projects-test.el index 335118c68..6df6c3c65 100644 --- a/test/projectile-known-projects-test.el +++ b/test/projectile-known-projects-test.el @@ -472,4 +472,21 @@ (projectile-ignored-project-function nil)) (expect (projectile-ignored-project-p "/anything/") :to-be nil)))) +(describe "commands that list the known projects" + ;; `projectile-known-projects' the variable is nil until the same-named + ;; accessor loads it from disk, and nothing loads it at startup. Reading + ;; the variable therefore handed an empty list to whichever command asked + ;; first in a session - `s-p F' completed over nothing at all. + (it "loads the persisted list rather than reading the empty variable" + (let ((projectile-known-projects nil) + (loaded nil)) + (spy-on 'projectile-load-known-projects :and-call-fake + (lambda () (setq loaded t + projectile-known-projects '("/a/" "/b/")))) + (spy-on 'projectile-project-group-files :and-return-value '("/a/x")) + (projectile-all-project-files) + (expect loaded :to-be-truthy) + (expect 'projectile-project-group-files + :to-have-been-called-with '("/a/" "/b/"))))) + ;;; projectile-known-projects-test.el ends here