Subject: bug#64531: [PATCH v3] Add new commands for copying VC filenames

From: Ivan Sokolov <[email-masked]>
Date: Mon, 04 Sep 2023 23:08:02 +0300

Hi, Eli and Dmitry!

I tried to write documentation for the previous version of the command
and came to the conclusion that I crammed too much functionality into
it.  So I divided the DWIM command into command for copying name at
point (bound to w) and command for copying marked files (bound to * w).

I also added new command for copying VC related filenames from
everywhere.  An appropriate binding would be C-x v w, but I am not sure
if I should use a top-level binding for such an insignificant command.

Quoting and prefix arguments now copy `dired-copy-filename-as-kill'.

>From 0baa37ff90fb58e43240586a401a6145da78b3f5 Mon Sep 17 00:00:00 2001
From: Ivan Sokolov <[email-masked]>
Date: Mon, 4 Sep 2023 22:33:32 +0300
Subject: [PATCH] Add new commands for copying VC filenames

---
 doc/emacs/maintaining.texi | 13 ++++++++++++
 etc/NEWS                   |  7 +++++++
 lisp/vc/vc-dir.el          | 20 +++++++++++++++++-
 lisp/vc/vc.el              | 43 +++++++++++++++++++++++++++++++-------
 4 files changed, 75 insertions(+), 8 deletions(-)

diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi
index 2dad70d3d13..8e7e2365f0a 100644
--- a/doc/emacs/maintaining.texi
+++ b/doc/emacs/maintaining.texi
@@ -1388,6 +1388,13 @@ You can use this command to mark files that are in one of registered
 states, including edited, added or removed.
 (@code{vc-dir-mark-registered-files}).
 
+@findex vc-dir-copy-marked-files-as-kill
+@item * w
+This command copy names of marked files into the kill ring.  If prefix
+argument is 0 names are absolute, with other prefix arguments names
+are relative to the VC root directory.  Without prefix names are
+relative to the VC buffer directory.
+
 @item G
 Add the file under point to the list of files that the VC should
 ignore (@code{vc-dir-ignore}).  For instance, if the VC is Git, it
@@ -1407,6 +1414,12 @@ point is on a directory entry, unmark all files in that directory tree
 (@code{vc-dir-unmark-all-files}).  With a prefix argument, unmark all
 files and directories.
 
+@item w
+Copy the name of the file at point into the kill ring.  If prefix
+argument is 0 name is absolute, with other prefix arguments name is
+relative to the VC root directory.  Without prefix name is relative to
+the VC buffer directory.
+
 @item x
 Hide files with @samp{up-to-date} or @samp{ignored} status
 (@code{vc-dir-hide-up-to-date}).  With a prefix argument, hide items
diff --git a/etc/NEWS b/etc/NEWS
index c97df11042d..77192b23cb7 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -262,6 +262,13 @@ This is a string or a list of strings that specifies the Git log
 switches for shortlogs, such as the one produced by 'C-x v L'.
 'vc-git-log-switches' is no longer used for shortlogs.
 
+*** New commands for copying names of Version Controlled files
+Commands 'vc-dir-copy-filename-as-kill' and
+'vc-dir-copy-marked-files-as-kill' work like
+'dired-copy-filename-as-kill' but in VC-dir buffer.  Command
+'vc-copy-filename-as-kill' copies name of the interactively chosen
+file.
+
 ** Diff Mode
 
 +++
diff --git a/lisp/vc/vc-dir.el b/lisp/vc/vc-dir.el
index 53d58870b32..3a4b8dbd18d 100644
--- a/lisp/vc/vc-dir.el
+++ b/lisp/vc/vc-dir.el
@@ -353,6 +353,7 @@ See `run-hooks'."
     (define-key map (kbd "M-s a C-s")   #'vc-dir-isearch)
     (define-key map (kbd "M-s a M-C-s") #'vc-dir-isearch-regexp)
     (define-key map "G" #'vc-dir-ignore)
+    (define-key map "w" #'vc-dir-copy-filename-as-kill)
 
     (let ((branch-map (make-sparse-keymap)))
       (define-key map "b" branch-map)
@@ -367,7 +368,8 @@ See `run-hooks'."
     (let ((mark-map (make-sparse-keymap)))
       (define-key map "*" mark-map)
       (define-key mark-map "%" #'vc-dir-mark-by-regexp)
-      (define-key mark-map "r" #'vc-dir-mark-registered-files))
+      (define-key mark-map "r" #'vc-dir-mark-registered-files)
+      (define-key mark-map "w" #'vc-dir-copy-marked-files-as-kill))
 
     ;; Hook up the menu.
     (define-key map [menu-bar vc-dir-mode]
@@ -930,6 +932,22 @@ system."
   (interactive)
   (view-file (vc-dir-current-file)))
 
+(defun vc-dir-copy-filename-as-kill ()
+  "In VC-dir buffer copy name of the file at point into the kill ring.
+With a zero prefix arg, use the absolute file name.
+With \\[universal-argument], use the file name relative to `vc-root-dir'."
+  (interactive)
+  (vc--copy-filenames-as-kill (list (vc-dir-current-file))))
+
+(defun vc-dir-copy-marked-files-as-kill ()
+  "In VC-dir buffer copy names of the marked files into the kill ring.
+With a zero prefix arg, use the absolute file names.
+With \\[universal-argument], use the file names relative to `vc-root-dir'."
+  (interactive)
+  (if-let* ((files (vc-dir-marked-files)))
+      (vc--copy-filenames-as-kill files t)
+    (message "No marked files")))
+
 (defun vc-dir-isearch ()
   "Search for a string through all marked buffers using Isearch."
   (interactive)
diff --git a/lisp/vc/vc.el b/lisp/vc/vc.el
index be7fa46c28e..9e352cfc467 100644
--- a/lisp/vc/vc.el
+++ b/lisp/vc/vc.el
@@ -1205,6 +1205,13 @@ BEWARE: this function may change the current buffer."
      (completing-read prompt (mapcar #'symbol-name backends)
                       nil 'require-match nil nil default))))
 
+(defun vc-read-file (prompt)
+  "Read file name, prompting with PROMPT.
+Default is the current file if it is under version control."
+  (read-file-name prompt nil (when (vc-backend buffer-file-name)
+                               buffer-file-name)
+                  t))
+
 ;; Here's the major entry point.
 
 ;;;###autoload
@@ -3261,10 +3268,7 @@ backend to NEW-BACKEND, and unregister FILE from the current backend.
   "Delete file and mark it as such in the version control system.
 If called interactively, read FILE, defaulting to the current
 buffer's file name if it's under version control."
-  (interactive (list (read-file-name "VC delete file: " nil
-                                     (when (vc-backend buffer-file-name)
-                                       buffer-file-name)
-                                     t)))
+  (interactive (list (vc-read-file "VC delete file: ")))
   (setq file (expand-file-name file))
   (let ((buf (get-file-buffer file))
         (backend (vc-backend file)))
@@ -3305,9 +3309,7 @@ buffer's file name if it's under version control."
   "Rename file OLD to NEW in both work area and repository.
 If called interactively, read OLD and NEW, defaulting OLD to the
 current buffer's file name if it's under version control."
-  (interactive (list (read-file-name "VC rename file: " nil
-                                     (when (vc-backend buffer-file-name)
-                                       buffer-file-name) t)
+  (interactive (list (vc-read-file "VC rename file: ")
                      (read-file-name "Rename to: ")))
   ;; in CL I would have said (setq new (merge-pathnames new old))
   (let ((old-base (file-name-nondirectory old)))
@@ -3340,6 +3342,33 @@ current buffer's file name if it's under version control."
 	(vc-mode-line new (vc-backend new))
 	(set-buffer-modified-p nil)))))
 
+(defun vc--copy-as-kill (str)
+  (if (eq last-command 'kill-region)
+      (kill-append str nil)
+    (kill-new str))
+  (message "%s" str))
+
+(defun vc--copy-filenames-as-kill (files &optional quote)
+  (vc--copy-as-kill
+   (mapconcat
+    (lambda (file)
+      (cond ((eq current-prefix-arg 0)
+             (setq file (expand-file-name file)))
+            ((consp current-prefix-arg)
+             (setq file (file-relative-name file (vc-root-dir)))))
+      (if (and quote (string-match-p "[\s\"']" file))
+          (format "%S" file)
+        file))
+    files " ")))
+
+;;;###autoload
+(defun vc-copy-filename-as-kill (file)
+  "Copy name of the FILE into the kill ring.
+With a zero prefix arg, use the absolute file name.
+With \\[universal-argument], use the file name relative to `vc-root-dir'."
+  (interactive (list (vc-read-file "VC copy filename as kill: ")))
+  (vc--copy-filenames-as-kill (list file)))
+
 ;;;###autoload
 (defun vc-update-change-log (&rest args)
   "Find change log file and add entries from recent version control logs.
-- 
2.41.0