diff options
| author | Sean Whitton | 2025-04-06 11:18:57 +0800 |
|---|---|---|
| committer | Sean Whitton | 2025-04-06 11:18:57 +0800 |
| commit | 936b2efdb389488d291086d5c2189fd1a7170aa6 (patch) | |
| tree | 00d8a721834dbaf89844d84e7cb400c7ad4efc75 | |
| parent | c0b1b54d734a45698da9df0841700c4c15785b11 (diff) | |
| download | emacs-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/NEWS | 11 | ||||
| -rw-r--r-- | lisp/vc/vc-dir.el | 146 |
2 files changed, 125 insertions, 32 deletions
| @@ -1489,6 +1489,17 @@ its default value. Effectively, the default value hasn't changed, | |||
| 1489 | since 'vc-resolve-conflicts' defaults to t, the previous default value | 1489 | since 'vc-resolve-conflicts' defaults to t, the previous default value |
| 1490 | for 'vc-git-resolve-conflicts'. | 1490 | for 'vc-git-resolve-conflicts'. |
| 1491 | 1491 | ||
| 1492 | --- | ||
| 1493 | *** VC-Dir can now automatically add and remove marks on other lines. | ||
| 1494 | When you try to use a mark or unmark command where doing so would only | ||
| 1495 | be permitted if other lines were marked or unmarked first, Emacs | ||
| 1496 | will now ask you if you'd like to change the marks on those other lines. | ||
| 1497 | For example, if you try to mark a file contained within a directory that | ||
| 1498 | is already marked, Emacs will offer to unmark the directory, first. | ||
| 1499 | Previously, Emacs would simply refuse to make any changes. | ||
| 1500 | You can customize 'vc-dir-allow-mass-mark-changes' to restore the old | ||
| 1501 | behavior 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 | |||
| 132 | When a directory in VC-Dir is marked, then for most VCS, this means that | ||
| 133 | all files within it are implicitly marked as well. | ||
| 134 | For consistency, the mark and unmark commands (principally \\<vc-dir-mode-map>\\[vc-dir-mark] and \\[vc-dir-unmark]) will | ||
| 135 | not explicitly mark or unmark entries if doing so would result in a | ||
| 136 | situation where both a directory and a file or directory within it are | ||
| 137 | both marked. | ||
| 138 | |||
| 139 | With the default value of this variable, `ask', if you attempt to mark | ||
| 140 | or unmark a particular item and doing so consistent with these | ||
| 141 | restrictions would require other items to be marked or unmarked too, | ||
| 142 | Emacs will prompt you to confirm that you do mean for the other items to | ||
| 143 | be marked or unmarked. | ||
| 144 | |||
| 145 | If this variable is nil, the commands will refuse to do anything if they | ||
| 146 | would need to mark or unmark other entries too. | ||
| 147 | If this variable is any other non-nil value, the commands will always | ||
| 148 | proceed to mark and unmark other entries, without asking. | ||
| 149 | |||
| 150 | There is one operation where marking or unmarking other entries in order | ||
| 151 | to mark or unmark the entry at point is unlikely to be surprising: | ||
| 152 | when you use \\[vc-dir-mark] on a directory which already has marked items within it. | ||
| 153 | In this case, the subitems are unmarked regardless of the value of this | ||
| 154 | option." | ||
| 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. | 677 | If 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 "\ | ||
| 877 | Replace 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 | ||