Subject: bug#76587: 31.0.50; [FR] project.el should have a way to exclude certain projects from being saved

From: Visuwesh <[email-masked]>
Date: Thu, 27 Feb 2025 09:34:16 +0530

[வியாழன் பிப்ரவரி 27, 2025] Dmitry Gutov wrote:

> Hi!

Hello!

> On 26/02/2025 15:59, Visuwesh wrote:
>> It is quite frustrating to use C-x p p to jump to projects when it can
>> easily get cluttered with projects that you never want to jump to.
>> E.g., as my user-emacs-directory only really tracks init.el, I don't
>> want project.el to save it in the projects file.  When it does, it gets
>> in the way when I am completing ~/lib/ports/emacs which is where I keep
>> my checkout of the Emacs repo.
>> Likewise, I do not want to save remote projects at all.
>
> This sounds reasonable. Should we call it something like
> project-list-exclude[-regexps]?

Sounds good to me.

>> AFAIU, the current way is to use project-forget-project and friends, but
>> they are cumbersome.  They work nice when you want to remove a
>> "completed" project but they don't help when you want to use project
>> commands judiciously for "temporary" projects (like a checkout that
>> you're editing for a patch).
>
> Temporary checkouts might not be worth it in terms of that user option
> (calling M-x ...-forget-project is faster than doing the checkout),
> but that's just my opinion.

You're right.  I don't know/remember what I was thinking when I wrote it
honestly.

>> P.S. I wish there was a way for project backends to bow out from
>> project--write-project-list too.  Context: I have a "sibling-file"
>> project backend which mainly offers meaningful candidates to C-x p f
>> when visiting a file that has associated siblings.
>
> This sounds a bit confusing. When you use this other backend (how? is
> it enabled manually?), do you visit disjoint repositories through it?
> Or only a subset of a files in a repository? What do you do when you
> want to visit other files in it?

There are no repositories here to speak of.  I use this project backend
when there is no associated vc backend with the file I'm visiting (i.e.,
project-vc fails).  To make my example more concrete, consider the
following example of find-sibling-rules:

    '(find-sibling-rules
       (rx-let ((basename (group (1+ (not ?/)))))
          ;; Supplementary info.
         `((,(rx "papers/" basename ".pdf" eos)
            "papers/\\1_si_*.pdf" "papers/\\1_si.pdf")
           (,(rx "papers/" basename "_si" (? "_" (+ num)) ".pdf" eos)
            "papers/\\1.pdf"))))

With this rule in place, when I say M-x find-sibling-file when I'm
visiting ~/doc/.../papers/X.pdf, it visits ~/doc/.../papers/X_si.pdf for
me.  With my sibling-file backend (see at end), I can say M-x
project-list-buffers, or C-x p b, or C-x p f to switch to related files
easily.  I was thinking of writing a project-specific C-x <right>/<left>
commands to switch between project buffers easily.

>> I could use
>> find-sibling-file but using project.el comes with bonuses like
>> project-list-buffer, etc.
>
> From where I'm standing, it might be more ergonomical to add a few
> extra commands for the "siblings" search, and bind them to a
> submap. But I'm probably missing something.

I think we have different notions of a "project."  I see it as a list of
related files, but the current project backend is built upon a singular,
exact definition of a root directory inside which all files can be
found.  For my use case, this root directory is a bit hard to define
since the sibling file need not be restricted to be under the same
directory (see an example of such a rule in the docstring of
find-sibling-rules).  Perhaps, my idea of a project is most closest to
what the satchel package does.

I could definitely add yet another submap to my config but I would like
to merge "regular" project.el projects, and my personal
"transient-but-a-bit-more-defined" projects into a single one.  This
elevates the pain of having to remember two different sets of commands.

All this complexity arises because I don't/can't use Git (or other VC)
for most of my files.  (Emacs' backup and vc-backup does everything I
need.)

Here's the promised implementation:

(defun vz/project-sibling-file (dir)
  "Return project object for current file-visiting buffer's sibling files.
If the current file-visiting buffer has siblings, then return them if
the current file is under DIR."
  (when (and (buffer-file-name)
             (not (string-prefix-p
                   "../"
                   (file-relative-name (buffer-file-name) dir))))
    (let ((siblings (find-sibling-file-search (buffer-file-name))))
      (when siblings
        `(vz/sibling ,(buffer-file-name) ,@siblings)))))

(cl-defmethod project-root ((project (head vz/sibling)))
  (file-name-parent-directory (nth 1 project)))

(cl-defmethod project-external-roots ((_project (head vz/sibling)))
  nil)

(cl-defmethod project-name ((project (head vz/sibling)))
  (file-name-base (nth 1 project)))

(cl-defmethod project-ignores ((_project (head vz/sibling)) _dir)
  nil)

(cl-defmethod project-files ((project (head vz/sibling)) &optional dirs)
  (let ((files (cdr project)))
    (if (or (null dirs)
            (equal (car dirs) (project-root project)))
        files
      (mapcan (lambda (f)
                (when (seq-some (lambda (d)
                                  (string-prefix-p d f))
                                dirs)
                  (list f)))
              files))))

(cl-defmethod project-buffers ((project (head vz/sibling)))
  (mapcan
   (lambda (f)
     (let ((buf (find-buffer-visiting f)))
       (and buf (list buf))))
   (project-files project)))

(with-eval-after-load 'project
 (add-to-list 'project-find-functions #'vz/project-sibling-file t))