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

From: Elijah Gabe Pérez <[email-masked]>
Date: Sun, 06 Apr 2025 18:39:28 -0600

Ship Mints <[email-masked]> writes:

> I think we need to call zombies something else and also consider that remote files may need to be
> ignored by the zombie checker to reduce the costs of checking project paths, to avoid tramp prompts for
> an internal function, and to accommodate that not all tramp connections are available all the time and
> those may not be reachable and should not be considered zombies.

Something like to this patch?

>From adfd76d9d7f046b10548fad30861afb260c888e4 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)
(project-dont-prune-remote-zombie-projects): New user options.
(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--write-project-list):
Call `project-forget-zombie-projects' only if according
`project-auto-forget-zombie-projects' value.
---
 etc/NEWS                  | 15 +++++++++
 lisp/progmodes/project.el | 66 ++++++++++++++++++++++++++++++---------
 2 files changed, 66 insertions(+), 15 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index cc63d03eafe..f88074d7f0f 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -362,6 +362,21 @@ 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.
+
+---
+*** New user option 'project-dont-prune-remote-zombie-projects'.
+This user option tells to `project-forget-zombie-projects' if
+remote projects must be removed from 'project-list-file' only if
+'project-prune-zombie-projects' is non-nil.
+
+*** 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..f0e54f90722 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -1551,6 +1551,28 @@ 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 set to `all', remove projects when project list file
+is being read or written.
+If set to `list-read', only remove projects when project list file
+is being read.
+If set to `list-write', only remove projects when project list file is
+being written.
+If nil, no pruning is performed."
+  :type '(choice (const :tag "All" all)
+                 (const :tag "Only when project list is being read" list-read)
+                 (const :tag "Only when project list is being written" list-write)
+                 (const :tag "Disabled" nil))
+  :version "31.1"
+  :group 'project)
+
+(defcustom project-dont-prune-remote-zombie-projects nil
+  "If non-nil, remote files will no be pruned by `project-forget-zombie-projects'."
+  :type 'boolean
+  :version "31.1"
+  :group 'project)
+
 (defun project--read-project-buffer ()
   (let* ((pr (project-current t))
          (current-buffer (current-buffer))
@@ -1888,7 +1910,11 @@ 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))
+  (when (and (consp project--list)
+             (or (eq project-prune-zombie-projects 'list-read)
+                 (eq project-prune-zombie-projects 'all)))
+    (project-forget-zombie-projects nil 'no-message)))
 
 (defun project--write-project-list ()
   "Save `project--list' in `project-list-file'."
@@ -1903,7 +1929,11 @@ project--write-project-list
                                 (expand-file-name name)))))
                     project--list)
             (current-buffer)))
-      (write-region nil nil filename nil 'silent))))
+      (write-region nil nil filename nil 'silent)))
+  (when (and (consp project--list)
+             (or (eq project-prune-zombie-projects 'list-write)
+                 (eq project-prune-zombie-projects 'all)))
+    (project-forget-zombie-projects nil 'no-message)))
 
 (defun project--remember-dir (root &optional no-write)
   "Add project root ROOT to the front of the project list.
@@ -1933,26 +1963,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)
 
@@ -2117,12 +2148,17 @@ 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))))
+    (unless (or (and no-remote (file-exists-p proj) (not (file-remote-p proj)))
+                (and project-dont-prune-remote-zombie-projects
+                     (file-exists-p proj) (not (file-remote-p proj)))
+                (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

I don't have a remote project so I can't test it.

> Setting aside the zombie name for now, hHow about calling 'project-auto-forget-zombie-projects'
> 'project-prune-zombie-projects' and rather than boolean, accept symbols that could include something
> like 'list-read 'list-write nil so users can decide when pruning happens.

I've added them and also an `all' option which mix `list-read' and
`list-write'.

> Could we rename report-message to no-message and change when to unless to keep it uniform?  The
> word report suggests things other than merely allowing messages.

I've added it as an additional optional argument.

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