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

From: Elijah Gabe Pérez <[email-masked]>
Date: Wed, 09 Apr 2025 22:07:45 -0600

Dmitry Gutov <[email-masked]> writes:

> On 09/04/2025 22:28, Elijah Gabe Pérez wrote:
>> Dmitry Gutov <[email-masked]> writes:
>> 
>>> On 09/04/2025 07:23, Elijah Gabe Pérez wrote:
>>>> Instead what about purge it once after emacs startup?
>>>
>>> I think a concern was voiced that during a long-running session directories can get removed at any time.
>>>
>>> +        (project-forget-zombie-projects nil t))))
>>>
>>> Are we sure that we want to suppress the messages about the removals in this case? Personally, I'd just print it in any case (if some were removed anyway).
>> It would display the message after `startup-echo-area-message'.
>> I think it would be intrusive.
>
> Do we read the project list at startup?

Apparently no, I didn't notice it's just my configuration
that does it.

>> I've changed it to `inhibit-message' instead, which will include
>> the message into *Messages* buffer but will not be displayed.
>
> That's fine too, although we probably do want to show it when called interactively.

Sure.  I've added it as an user option.

>>> +(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.
>>>
>>> Maybe NO-REMOTE should be called INCLUDE-REMOTE instead?
>>>
>>> Right now it sounds like the opposite of its actual meaning.
>> I think EXCLUDE-REMOTES would be a better name,
>> the INCLUDE-REMOTE name would give the impression that it does not
>> delete remote projects by default.  `project-forget-zombie-projects' by
>> default includes remote and no remote projects.
>
> Don't we want to avoid remote projects by default? It might take a long time. Does it here in my testing anyway.
>
> Having the "fast" path to be the default and the "thorough" as the prefix command alternative seems to be a common approach.

You are right, I've change it for exclude remote projects by default.

>From f5e9ac3a37eb74a979c6bf803316c3f3a25fe4b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?El=C3=ADas=20Gabriel=20P=C3=A9rez?= <[email-masked]>
Date: Tue, 8 Apr 2025 22:30:28 -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-inhibit-message): New user options.
(project-forget-zombie-projects): Add new function argument for
include projects that are remotes, by default it exclude them.
(project--ensure-read-project-list, project-prompt-project-dir)
(project-prompt-project-name): Call `project-forget-zombie-projects'
but inhibit its message.
(project-kill-buffers, project-remember-projects-under)
(project-forget-projects-under): Inhibit message if
`project-inhibit-message' is non-nil.
---
 etc/NEWS                  | 17 +++++++++++
 lisp/progmodes/project.el | 59 ++++++++++++++++++++++++++++++++-------
 2 files changed, 66 insertions(+), 10 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index cc63d03eafe..0e021a06ec2 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -362,6 +362,23 @@ 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 prompting for a project.
+By default it is set to 'exclude-remote', which exclude remote projects
+from being removed.  If set to 'all', include remote and no remote
+projects from being removed.
+
++++
+*** New user option 'project-inhibit-message'.
+This option, if non-nil, project commands will not display messages
+in echo area.
+
+*** New optional argument INCLUDE-REMOTES for 'project-forget-zombie-projects'.
+This argument tells 'project-forget-zombie-projects' to include remote
+projects from being forgotten.
+
 ---
 *** 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..78266c6682d 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -1551,6 +1551,25 @@ project-ignore-buffer-conditions
   :group 'project
   :package-version '(project . "0.8.2"))
 
+(defcustom project-prune-zombie-projects 'exclude-remote
+  "Automatically remove from project list projects that were deleted.
+If set to `exclude-remote' (default), remove deleted projects except
+those that are remote.
+If set to `all', include remote and no remote projects from being
+removed."
+  :type '(choice (const :tag "Exclude remote projects" exclude-remote)
+                 (const :tag "All projects" all))
+  :version "31.1"
+  :group 'project)
+
+(defcustom project-inhibit-message nil
+  "Non-nil means any project command will not display messages in echo area.
+The messages are displayed in the messages buffer but it won't
+be shown in the echo area."
+  :type 'boolean
+  :version "31.1"
+  :group 'project)
+
 (defun project--read-project-buffer ()
   (let* ((pr (project-current t))
          (current-buffer (current-buffer))
@@ -1803,7 +1822,8 @@ project-kill-buffers
                        (yes-or-no-p
                         (format "Kill %d buffers in %s? "
                                 (length bufs)
-                                (project-name pr))))))
+                                (project-name pr)))))
+         (inhibit-message project-inhibit-message))
     (cond (no-confirm
            (mapc #'kill-buffer bufs))
           ((null bufs)
@@ -1888,7 +1908,12 @@ 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-let* (project-prune-zombie-projects
+              ((consp project--list))
+              (inhibit-message t))
+        (project-forget-zombie-projects
+         (eq project-prune-zombie-projects 'all)))))
 
 (defun project--write-project-list ()
   "Save `project--list' in `project-list-file'."
@@ -1963,6 +1988,10 @@ 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-let* (project-prune-zombie-projects
+            (inhibit-message t))
+      (project-forget-zombie-projects
+       (eq project-prune-zombie-projects 'all)))
   (let* ((dir-choice "... (choose a dir)")
          (choices
           ;; XXX: Just using this for the category (for the substring
@@ -1992,6 +2021,10 @@ 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-let* (project-prune-zombie-projects
+            (inhibit-message t))
+      (project-forget-zombie-projects
+       (eq project-prune-zombie-projects 'all)))
   (let* ((dir-choice "... (choose a dir)")
          project--name-history
          (choices
@@ -2097,7 +2130,8 @@ project-remember-projects-under
                 (directory-files dir t)))
         (known (make-hash-table :size (* 2 (length project--list))
                                 :test #'equal))
-        (count 0))
+        (count 0)
+        (inhibit-message project-inhibit-message))
     (dolist (project (mapcar #'car project--list))
       (puthash project t known))
     (dolist (subdir dirs)
@@ -2117,12 +2151,16 @@ project-remember-projects-under
                          count) count))
     count))
 
-(defun project-forget-zombie-projects ()
-  "Forget all known projects that don't exist any more."
-  (interactive)
-  (dolist (proj (project-known-project-roots))
-    (unless (file-exists-p proj)
-      (project-forget-project proj))))
+(defun project-forget-zombie-projects (&optional include-remotes)
+  "Forget all known projects that don't exist any more.
+This don't include remote projects, set INCLUDE-REMOTES to non-nil for
+include remote projects from being forgotten."
+  (interactive "P")
+  (let ((inhibit-message project-inhibit-message))
+    (dolist (proj (project-known-project-roots))
+      (when (or (and include-remotes (not (file-exists-p proj)))
+                (not (and (file-remote-p proj) (file-exists-p proj))))
+        (project-forget-project proj)))))
 
 (defun project-forget-projects-under (dir &optional recursive)
   "Forget all known projects below a directory DIR.
@@ -2133,7 +2171,8 @@ project-forget-projects-under
 Display a message at the end summarizing what was forgotten.
 Return the number of forgotten projects."
   (interactive "DDirectory: \nP")
-  (let ((count 0))
+  (let ((count 0)
+        (inhibit-message project-inhibit-message))
     (if recursive
         (dolist (proj (project-known-project-roots))
           (when (file-in-directory-p proj dir)
-- 
2.49.0

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