Subject: bug#77566: [PATCH] Add option for automatically delete non-existent projects.

From: Elijah Gabe Pérez <[email-masked]>
Date: Tue, 08 Apr 2025 22:23:29 -0600

Elijah Gabe Pérez <[email-masked]> writes:

> Dmitry Gutov <[email-masked]> writes:
>
>  On 06/04/2025 03:29, Sean Whitton wrote:
>  > Thanks, though why do this only when switching projects?
>  > Why not do it proactively whenever fetching the list of projects?
>  > Performance concerns?
>
>  I'm concerned about performance, yes. With my SSD, it takes about 4ms to 
>  "clean up" ~70 projects. The number could be higher, and the hard drive 
>  could be spinning media.
>
> Yeah, that is why i originally added it only when switching projects, projectile does the same.
>
>  So could someone point out what will we get by proactively cleaning up 
>  the list, rather than doing that only when a project is being prompted for?
>
> Maybe for ensure that the zombies were removed?
>
> For example, packages that use project--list for read the projects.
>
> Instead what about purge it once after emacs startup?

Something like this patch(?), it now clean up when
project-prompt-project-name or project-prompt-project-dir are being
called and after project--list was initialized:

>From 5fc38a5205d0c44f695453751352353212b3bd80 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?El=C3=ADas=20Gabriel=20P=C3=A9rez?= <[email-masked]>
Date: Sat, 5 Apr 2025 16:18:27 -0600
Subject: [PATCH] Add option for automatically delete non-existent projects. 
 (bug#77566)

* etc/NEWS: Announce changes.
* lisp/progmodes/project.el (project-prune-zombie-projects):
New user option.
(project--remove-from-project-list, project-forget-project):
Allow not to show forgotten projects.
(project-forget-zombie-projects): Add new function arguments.
(project--ensure-read-project-list, project-prompt-project-dir)
(project-prompt-project-name): Call `project-forget-zombie-projects'.
---
 etc/NEWS                  |  9 ++++++++
 lisp/progmodes/project.el | 43 ++++++++++++++++++++++++++-------------
 2 files changed, 38 insertions(+), 14 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index cc63d03eafe..057f08776f1 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -362,6 +362,15 @@ invoked standalone or from the 'project-switch-commands' dispatch menu.
 This user option describes projects that should always be skipped by
 'project-remember-project'.
 
+---
+*** New user option 'project-prune-zombie-projects'.
+This user option automatically removes projects from 'project-list-file'
+that cannot be accessed when switching projects.
+
+*** New argument NO-REMOTE for 'project-forget-zombie-projects'.
+This function argument tells 'project-forget-zombie-projects' not to
+forget projects that are remote.
+
 ---
 *** New command 'project-save-some-buffers' bound to 'C-x p C-x s'.
 This is like 'C-x s', but only for this project's buffers.
diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el
index e2cd5bfa231..5c2e0b358a2 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -1551,6 +1551,13 @@ project-ignore-buffer-conditions
   :group 'project
   :package-version '(project . "0.8.2"))
 
+(defcustom project-prune-zombie-projects nil
+  "Automatically remove from project list projects that were deleted.
+If non-nil, remove deleted projects only when switching projects."
+  :type 'boolean
+  :version "31.1"
+  :group 'project)
+
 (defun project--read-project-buffer ()
   (let* ((pr (project-current t))
          (current-buffer (current-buffer))
@@ -1888,7 +1895,9 @@ project--read-project-list
 (defun project--ensure-read-project-list ()
   "Initialize `project--list' if it isn't already initialized."
   (when (eq project--list 'unset)
-    (project--read-project-list)))
+    (project--read-project-list)
+    (if (and project-prune-zombie-projects (consp project--list))
+        (project-forget-zombie-projects nil t))))
 
 (defun project--write-project-list ()
   "Save `project--list' in `project-list-file'."
@@ -1933,26 +1942,27 @@ project-remember-project
                       project-list-exclude)
       (project--remember-dir root no-write))))
 
-(defun project--remove-from-project-list (project-root report-message)
+(defun project--remove-from-project-list (project-root report-message &optional no-message)
   "Remove directory PROJECT-ROOT of a missing project from the project list.
 If the directory was in the list before the removal, save the
-result in `project-list-file'.  Announce the project's removal
-from the list using REPORT-MESSAGE, which is a format string
-passed to `message' as its first argument."
+result in `project-list-file'.  If NO-MESSAGE is non-nil,
+Announce the project's removal from the list using REPORT-MESSAGE,
+which is a format string passed to `message' as its first argument."
   (project--ensure-read-project-list)
   (when-let* ((ent (assoc (abbreviate-file-name project-root) project--list)))
     (setq project--list (delq ent project--list))
-    (message report-message project-root)
+    (unless no-message (message report-message project-root))
     (project--write-project-list)))
 
 ;;;###autoload
-(defun project-forget-project (project-root)
+(defun project-forget-project (project-root &optional no-message)
   "Remove directory PROJECT-ROOT from the project list.
 PROJECT-ROOT is the root directory of a known project listed in
-the project list."
+the project list.
+If NO-MESSAGE is non-nil, don't display a message about project removed."
   (interactive (list (funcall project-prompter)))
   (project--remove-from-project-list
-   project-root "Project `%s' removed from known projects"))
+   project-root "Project `%s' removed from known projects" no-message))
 
 (defvar project--dir-history)
 
@@ -1963,6 +1973,7 @@ project-prompt-project-dir
 It's also possible to enter an arbitrary directory not in the list.
 When PROMPT is non-nil, use it as the prompt string."
   (project--ensure-read-project-list)
+  (if project-prune-zombie-projects (project-forget-zombie-projects nil t))
   (let* ((dir-choice "... (choose a dir)")
          (choices
           ;; XXX: Just using this for the category (for the substring
@@ -1992,6 +2003,7 @@ project-prompt-project-name
 see `project-list-file'.
 It's also possible to enter an arbitrary directory not in the list.
 When PROMPT is non-nil, use it as the prompt string."
+  (if project-prune-zombie-projects (project-forget-zombie-projects nil t))
   (let* ((dir-choice "... (choose a dir)")
          project--name-history
          (choices
@@ -2117,12 +2129,15 @@ project-remember-projects-under
                          count) count))
     count))
 
-(defun project-forget-zombie-projects ()
-  "Forget all known projects that don't exist any more."
-  (interactive)
+(defun project-forget-zombie-projects (&optional no-remote no-message)
+  "Forget all known projects that don't exist any more.
+If NO-REMOTE is non-nil, don't forget remote projects.
+If NO-MESSAGE is non-nil, don't display a message about projects removed."
+  (interactive "P")
   (dolist (proj (project-known-project-roots))
-    (unless (file-exists-p proj)
-      (project-forget-project proj))))
+    (when (or (and no-remote (not (file-remote-p proj)) (not (file-exists-p proj)))
+              (not (file-exists-p proj)))
+      (project-forget-project proj no-message))))
 
 (defun project-forget-projects-under (dir &optional recursive)
   "Forget all known projects below a directory DIR.
-- 
2.49.0

-- 
                                          - E.G via GNU Emacs and Org.