aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSean Whitton2025-04-06 11:18:57 +0800
committerSean Whitton2025-04-06 11:18:57 +0800
commit936b2efdb389488d291086d5c2189fd1a7170aa6 (patch)
tree00d8a721834dbaf89844d84e7cb400c7ad4efc75
parentc0b1b54d734a45698da9df0841700c4c15785b11 (diff)
downloademacs-936b2efdb389488d291086d5c2189fd1a7170aa6.tar.gz
emacs-936b2efdb389488d291086d5c2189fd1a7170aa6.zip
Teach VC-Dir to automatically add and remove marks on other lines
* lisp/vc/vc-dir.el (vc-dir-allow-mass-mark-changes): New option. (vc-dir-parent-marked-p): Replace with ... (vc-dir--parent): ... this. (vc-dir-children-marked-p): Replace with ... (vc-dir--children): ... this. (vc-dir-mark-file): Unmark subitems before marking a directory. Offer to unmark a directory before marking a subitem. (vc-dir-unmark-file): For an implicitly marked item, offer to unmark it by marking everything else that's implicitly marked. For an unmarked directory with marked subitems, offer to unmark them all. * etc/NEWS: Document the changes.
-rw-r--r--etc/NEWS11
-rw-r--r--lisp/vc/vc-dir.el146
2 files changed, 125 insertions, 32 deletions
diff --git a/etc/NEWS b/etc/NEWS
index 61474af7fcb..35e6edcd712 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1489,6 +1489,17 @@ its default value. Effectively, the default value hasn't changed,
1489since 'vc-resolve-conflicts' defaults to t, the previous default value 1489since 'vc-resolve-conflicts' defaults to t, the previous default value
1490for 'vc-git-resolve-conflicts'. 1490for 'vc-git-resolve-conflicts'.
1491 1491
1492---
1493*** VC-Dir can now automatically add and remove marks on other lines.
1494When you try to use a mark or unmark command where doing so would only
1495be permitted if other lines were marked or unmarked first, Emacs
1496will now ask you if you'd like to change the marks on those other lines.
1497For example, if you try to mark a file contained within a directory that
1498is already marked, Emacs will offer to unmark the directory, first.
1499Previously, Emacs would simply refuse to make any changes.
1500You can customize 'vc-dir-allow-mass-mark-changes' to restore the old
1501behavior or dispense with the prompting.
1502
1492** Diff mode 1503** Diff mode
1493 1504
1494+++ 1505+++
diff --git a/lisp/vc/vc-dir.el b/lisp/vc/vc-dir.el
index 93c11fdbc68..1074986090e 100644
--- a/lisp/vc/vc-dir.el
+++ b/lisp/vc/vc-dir.el
@@ -126,6 +126,38 @@ See `run-hooks'."
126(defvar vc-dir-backend nil 126(defvar vc-dir-backend nil
127 "The backend used by the current *vc-dir* buffer.") 127 "The backend used by the current *vc-dir* buffer.")
128 128
129(defcustom vc-dir-allow-mass-mark-changes 'ask
130 "If non-nil, VC-Dir commands may mark or unmark many items at once.
131
132When a directory in VC-Dir is marked, then for most VCS, this means that
133all files within it are implicitly marked as well.
134For consistency, the mark and unmark commands (principally \\<vc-dir-mode-map>\\[vc-dir-mark] and \\[vc-dir-unmark]) will
135not explicitly mark or unmark entries if doing so would result in a
136situation where both a directory and a file or directory within it are
137both marked.
138
139With the default value of this variable, `ask', if you attempt to mark
140or unmark a particular item and doing so consistent with these
141restrictions would require other items to be marked or unmarked too,
142Emacs will prompt you to confirm that you do mean for the other items to
143be marked or unmarked.
144
145If this variable is nil, the commands will refuse to do anything if they
146would need to mark or unmark other entries too.
147If this variable is any other non-nil value, the commands will always
148proceed to mark and unmark other entries, without asking.
149
150There is one operation where marking or unmarking other entries in order
151to mark or unmark the entry at point is unlikely to be surprising:
152when you use \\[vc-dir-mark] on a directory which already has marked items within it.
153In this case, the subitems are unmarked regardless of the value of this
154option."
155 :type '(choice (const :tag "Don't allow" nil)
156 (const :tag "Prompt to allow" ask)
157 (const :tag "Allow without prompting" t))
158 :group 'vc
159 :version "31.1")
160
129(defun vc-dir-move-to-goal-column () 161(defun vc-dir-move-to-goal-column ()
130 ;; Used to keep the cursor on the file name column. 162 ;; Used to keep the cursor on the file name column.
131 (beginning-of-line) 163 (beginning-of-line)
@@ -640,9 +672,9 @@ With prefix argument ARG, move that many lines."
640 (error (vc-dir-next-line 1)))))) 672 (error (vc-dir-next-line 1))))))
641 (funcall mark-unmark-function))) 673 (funcall mark-unmark-function)))
642 674
643(defun vc-dir-parent-marked-p (arg) 675(defun vc-dir--parent (arg &optional if-marked)
644 ;; Non-nil iff a parent directory of arg is marked. 676 "Return the parent node of ARG.
645 ;; Return value, if non-nil is the `ewoc-data' for the marked parent. 677If IF-MARKED, return the nearest marked parent."
646 (let* ((argdir (vc-dir-node-directory arg)) 678 (let* ((argdir (vc-dir-node-directory arg))
647 ;; (arglen (length argdir)) 679 ;; (arglen (length argdir))
648 (crt arg) 680 (crt arg)
@@ -655,46 +687,58 @@ With prefix argument ARG, move that many lines."
655 (dir (vc-dir-node-directory crt))) 687 (dir (vc-dir-node-directory crt)))
656 (and (vc-dir-fileinfo->directory data) 688 (and (vc-dir-fileinfo->directory data)
657 (string-prefix-p dir argdir) 689 (string-prefix-p dir argdir)
658 (vc-dir-fileinfo->marked data) 690 (or (not if-marked) (vc-dir-fileinfo->marked data))
659 (setq found data)))) 691 (setq found crt))))
660 found)) 692 found))
661 693
662(defun vc-dir-children-marked-p (arg) 694(defun vc-dir--children (arg &optional only-marked)
663 ;; Non-nil iff a child of ARG is marked. 695 "Return a list of children of ARG. If ONLY-MARKED, only those marked."
664 ;; Return value, if non-nil, is the `ewoc-data' for the marked child. 696 (let* ((argdir-re (concat "\\`"
665 (let* ((argdir-re (concat "\\`" (regexp-quote (vc-dir-node-directory arg)))) 697 (regexp-quote (vc-dir-node-directory arg))))
666 (is-child t) 698 (is-child t)
667 (crt arg) 699 (crt arg)
668 (found nil)) 700 (found nil))
669 (while (and is-child 701 (while (and is-child
670 (null found)
671 (setq crt (ewoc-next vc-ewoc crt))) 702 (setq crt (ewoc-next vc-ewoc crt)))
672 (let ((data (ewoc-data crt)) 703 (if (string-match argdir-re (vc-dir-node-directory crt))
673 (dir (vc-dir-node-directory crt))) 704 (when (or (not only-marked)
674 (if (string-match argdir-re dir) 705 (vc-dir-fileinfo->marked (ewoc-data crt)))
675 (if (vc-dir-fileinfo->marked data) 706 (push crt found))
676 (setq found data)) 707 ;; We are done, we got to an entry that is not a child of `arg'.
677 ;; We are done, we got to an entry that is not a child of `arg'. 708 (setq is-child nil)))
678 (setq is-child nil))))
679 found)) 709 found))
680 710
681(defun vc-dir-mark-file (&optional arg) 711(defun vc-dir-mark-file (&optional arg)
682 ;; Mark ARG or the current file and move to the next line. 712 ;; Mark ARG or the current file and move to the next line.
683 (let* ((crt (or arg (ewoc-locate vc-ewoc))) 713 (let* ((crt (or arg (ewoc-locate vc-ewoc)))
684 (file (ewoc-data crt)) 714 (file (ewoc-data crt))
685 (isdir (vc-dir-fileinfo->directory file)) 715 (to-inval (list crt)))
686 ;; Forbid marking a directory containing marked files in its 716 ;; We do not allow a state in which a directory is marked and also
687 ;; tree, or a file or directory in a marked directory tree. 717 ;; some of its files are marked. If the user's intent is clear,
688 (child-conflict (and isdir (vc-dir-children-marked-p crt))) 718 ;; adjust things for them so that they can proceed.
689 (parent-conflict (vc-dir-parent-marked-p crt))) 719 (if-let* (((vc-dir-fileinfo->directory file))
690 (when (or child-conflict parent-conflict) 720 (children (vc-dir--children crt t)))
691 (error (if child-conflict 721 ;; The user wants to mark a directory where some of its children
692 "Entry `%s' in this directory is already marked" 722 ;; are already marked. The user's intent is quite clear, so
693 "Parent directory `%s' is already marked") 723 ;; unconditionally unmark the children.
694 (vc-dir-fileinfo->name (or child-conflict 724 (dolist (child children)
695 parent-conflict)))) 725 (setf (vc-dir-fileinfo->marked (ewoc-data child)) nil)
726 (push child to-inval))
727 (when-let* ((parent (vc-dir--parent crt t))
728 (name (vc-dir-fileinfo->name (ewoc-data parent))))
729 ;; The user seems to want to mark an entry whose directory is
730 ;; already marked. As the file is already implicitly marked for
731 ;; most VCS, they may not really intend this.
732 (when (or (not vc-dir-allow-mass-mark-changes)
733 (and (eq vc-dir-allow-mass-mark-changes 'ask)
734 (not (yes-or-no-p
735 (format "`%s' is already marked; unmark it?"
736 name)))))
737 (error "`%s' is already marked" name))
738 (setf (vc-dir-fileinfo->marked (ewoc-data parent)) nil)
739 (push parent to-inval)))
696 (setf (vc-dir-fileinfo->marked file) t) 740 (setf (vc-dir-fileinfo->marked file) t)
697 (ewoc-invalidate vc-ewoc crt) 741 (apply #'ewoc-invalidate vc-ewoc to-inval)
698 (unless (or arg (mouse-event-p last-command-event)) 742 (unless (or arg (mouse-event-p last-command-event))
699 (vc-dir-next-line 1)))) 743 (vc-dir-next-line 1))))
700 744
@@ -816,9 +860,47 @@ Directories must have trailing slashes."
816(defun vc-dir-unmark-file () 860(defun vc-dir-unmark-file ()
817 ;; Unmark the current file and move to the next line. 861 ;; Unmark the current file and move to the next line.
818 (let* ((crt (ewoc-locate vc-ewoc)) 862 (let* ((crt (ewoc-locate vc-ewoc))
819 (file (ewoc-data crt))) 863 (file (ewoc-data crt))
820 (setf (vc-dir-fileinfo->marked file) nil) 864 to-inval)
821 (ewoc-invalidate vc-ewoc crt) 865 (if (vc-dir-fileinfo->marked file)
866 (progn (setf (vc-dir-fileinfo->marked file) nil)
867 (push crt to-inval))
868 ;; The current item is not explicitly marked, but its containing
869 ;; directory is marked. So this item is implicitly marked, for
870 ;; most VCS. Offer to change that.
871 (if-let* ((parent (vc-dir--parent crt t))
872 (all-children (vc-dir--children parent)))
873 (when (and vc-dir-allow-mass-mark-changes
874 (or (not (eq vc-dir-allow-mass-mark-changes 'ask))
875 (yes-or-no-p
876 (format "\
877Replace mark on `%s' with marks on all subitems but this one?"
878 (vc-dir-fileinfo->name file)))))
879 (let ((subtree (if (vc-dir-fileinfo->directory file)
880 (cons crt (vc-dir--children crt))
881 (list crt (vc-dir--parent crt)))))
882 (setf (vc-dir-fileinfo->marked (ewoc-data parent)) nil)
883 (push parent to-inval)
884 (dolist (child all-children)
885 (setf (vc-dir-fileinfo->marked (ewoc-data child))
886 (not (memq child subtree)))
887 (push child to-inval))))
888 ;; The current item is a directory that's not marked, implicitly
889 ;; or explicitly, but it has marked items below it.
890 ;; Offer to unmark those.
891 (when-let*
892 (((vc-dir-fileinfo->directory file))
893 (children (vc-dir--children crt t))
894 ((and vc-dir-allow-mass-mark-changes
895 (or (not (eq vc-dir-allow-mass-mark-changes 'ask))
896 (yes-or-no-p
897 (format "Unmark all items within `%s'?"
898 (vc-dir-fileinfo->name file)))))))
899 (dolist (child children)
900 (setf (vc-dir-fileinfo->marked (ewoc-data child)) nil)
901 (push child to-inval)))))
902 (when to-inval
903 (apply #'ewoc-invalidate vc-ewoc to-inval))
822 (unless (mouse-event-p last-command-event) 904 (unless (mouse-event-p last-command-event)
823 (vc-dir-next-line 1)))) 905 (vc-dir-next-line 1))))
824 906