Make diff-hunk-kill respect an active region

* lisp/vc/diff-mode.el (diff--revert-kill-hunks): New workhorse
routine.
(diff-hunk-kill, diff-revert-and-kill-hunk): Call it.
(diff-hunk-kill): New BEG and END parameters and interactive
form.
* doc/emacs/files.texi (Diff Mode):
* etc/NEWS: Document the change.
This commit is contained in:
Sean Whitton 2026-01-30 15:06:52 +00:00
parent ae7761598d
commit fcdd8678f9
4 changed files with 98 additions and 51 deletions

View file

@ -1835,7 +1835,8 @@ the start of the @var{n}th previous file.
@findex diff-hunk-kill
@item M-k
Kill the hunk at point (@code{diff-hunk-kill}).
Kill the hunk at point (@code{diff-hunk-kill}). If the region is
active, kills all hunks the region overlaps.
@findex diff-file-kill
@item M-K

View file

@ -2284,9 +2284,10 @@ one as before. This makes them different from 'vc-diff' and
*** 'diff-apply-hunk' now supports creating and deleting files.
+++
*** 'diff-apply-hunk' and 'diff-apply-buffer' now consider the region.
If the region is active, these commands now apply all hunks that the
region overlaps. Otherwise, they have their existing behavior.
*** Diff mode's application and killing commands now consider the region.
If the region is active, 'diff-apply-hunk', 'diff-apply-buffer' and
'diff-hunk-kill' now apply or kill all hunks that the region overlaps.
Otherwise, they have their existing behavior.
+++
*** 'diff-apply-buffer' can reverse-apply.

View file

@ -883,31 +883,19 @@ If the prefix ARG is given, restrict the view to the current file instead."
(goto-char (point-min))
(re-search-forward diff-hunk-header-re nil t)))
(defun diff-hunk-kill ()
"Kill the hunk at point."
(interactive)
(if (not (diff--some-hunks-p))
(error "No hunks")
(diff-beginning-of-hunk t)
(let* ((hunk-bounds (diff-bounds-of-hunk))
(file-bounds (ignore-errors (diff-bounds-of-file)))
;; If the current hunk is the only one for its file, kill the
;; file header too.
(bounds (if (and file-bounds
(progn (goto-char (car file-bounds))
(= (progn (diff-hunk-next) (point))
(car hunk-bounds)))
(progn (goto-char (cadr hunk-bounds))
;; bzr puts a newline after the last hunk.
(while (looking-at "^\n")
(forward-char 1))
(= (point) (cadr file-bounds))))
file-bounds
hunk-bounds))
(inhibit-read-only t))
(apply #'kill-region bounds)
(goto-char (car bounds))
(ignore-errors (diff-beginning-of-hunk t)))))
(defun diff-hunk-kill (&optional beg end)
"Kill the hunk at point.
When killing the last hunk left for a file, kill the file header too.
Interactively, if the region is active, kill all hunks that the region
overlaps.
When called from Lisp with optional arguments BEG and END non-nil, kill
all hunks overlapped by the region from BEG to END as though called
interactively with an active region delimited by BEG and END."
(interactive "R")
(when (xor beg end)
(error "Invalid call to `diff-hunk-kill'"))
(diff--revert-kill-hunks beg end nil))
;; This is not `diff-kill-other-hunks' because we might need to make
;; copies of file headers in order to ensure the new kill ring entry
@ -2283,6 +2271,83 @@ With a prefix argument, try to REVERSE the hunk."
:type 'boolean
:version "31.1")
(defun diff--revert-kill-hunks (beg end revertp)
"Workhorse routine for killing hunks, after possibly reverting them.
If BEG and END are nil, kill the hunk at point.
Otherwise kill all hunks overlapped by region delimited by BEG and END.
When killing a hunk that's the only one remaining for its file, kill the
file header too.
If REVERTP is non-nil, reverse-apply hunks before killing them."
;; With BEG and END non-nil, we push each hunk to the kill ring
;; separately. If we want to push to the kill ring just once, we have
;; to decide how to handle file headers such that the meanings of the
;; hunks in the kill ring entry, considered as a whole patch, do not
;; deviate too far from the meanings the hunks had in this buffer.
;;
;; For example, if we have a single hunk for one file followed by
;; multiple hunks for another file, and we naïvely kill the single
;; hunk and the first of the multiple hunks, our kill ring entry will
;; be a patch applying those two hunks to the first file. This is
;; because killing the single hunk will have brought its file header
;; with it, but not so killing the second hunk. So we will have put
;; together hunks that were previously for two different files.
;;
;; One option is to *copy* every file header that the region overlaps
;; (and that we will not kill, because we are leaving other hunks for
;; that file behind). But then the text this command pushes to the
;; kill ring would be different from the text it removes from the
;; buffer, which would be unintuitive for an Emacs kill command.
;;
;; An alternative might be to have restrictions as follows:
;;
;; Interactively, if the region is active, try to kill all hunks that the
;; region overlaps. This works when either
;; - all the hunks the region overlaps are for the same file; or
;; - the last hunk the region overlaps is the last hunk for its file.
;; These restrictions are so that the text added to the kill ring does not
;; merge together hunks for different files under a single file header.
;;
;; We would error out if neither property is met. When either holds,
;; any file headers the region overlaps are ones we should kill.
(unless (diff--some-hunks-p)
(error "No hunks"))
(if beg
(save-excursion
(goto-char beg)
(setq beg (car (diff-bounds-of-hunk)))
(goto-char end)
(unless (looking-at diff-hunk-header-re)
(setq end (cadr (diff-bounds-of-hunk)))))
(pcase-setq `(,beg ,end) (diff-bounds-of-hunk)))
(when (or (not revertp) (null (diff-apply-buffer beg end t)))
(goto-char end)
(when-let* ((pos (diff--at-diff-header-p)))
(goto-char pos))
(setq beg (copy-marker beg) end (point-marker))
(unwind-protect
(cl-loop initially (goto-char beg)
for (hunk-beg hunk-end) = (diff-bounds-of-hunk)
for file-bounds = (ignore-errors (diff-bounds-of-file))
for (file-beg file-end) = file-bounds
for inhibit-read-only = t
if (and file-bounds
(progn
(goto-char file-beg)
(diff-hunk-next)
(eq (point) hunk-beg))
(progn
(goto-char hunk-end)
;; bzr puts a newline after the last hunk.
(while (looking-at "^\n") (forward-char 1))
(eq (point) file-end)))
do (kill-region file-beg file-end) (goto-char file-beg)
else do (kill-region hunk-beg hunk-end) (goto-char hunk-beg)
do (ignore-errors (diff-beginning-of-hunk t))
until (or (< (point) (marker-position beg))
(eql (point) (marker-position end))))
(set-marker beg nil)
(set-marker end nil))))
(defun diff-revert-and-kill-hunk (&optional beg end)
"Reverse-apply and then kill the hunk at point. Save changed buffer.
Interactively, if the region is active, reverse-apply and kill all
@ -2308,27 +2373,7 @@ BEG and END."
(error "Invalid call to `diff-revert-and-kill-hunk'"))
(when (or (not diff-ask-before-revert-and-kill-hunk)
(y-or-n-p "Really reverse-apply and kill hunk(s)?"))
(if beg
(save-excursion
(goto-char beg)
(setq beg (car (diff-bounds-of-hunk)))
(goto-char end)
(unless (looking-at diff-hunk-header-re)
(setq end (cadr (diff-bounds-of-hunk)))))
(pcase-setq `(,beg ,end) (diff-bounds-of-hunk)))
(when (null (diff-apply-buffer beg end t))
;; Use `diff-hunk-kill' because it properly handles file headers.
(goto-char end)
(when-let* ((pos (diff--at-diff-header-p)))
(goto-char pos))
(setq beg (copy-marker beg) end (point-marker))
(unwind-protect
(cl-loop initially (goto-char beg)
do (diff-hunk-kill)
until (or (< (point) (marker-position beg))
(eql (point) (marker-position end))))
(set-marker beg nil)
(set-marker end nil)))))
(diff--revert-kill-hunks beg end t)))
(defun diff-apply-buffer (&optional beg end reverse test-or-no-save)
"Apply the diff in the entire diff buffer.

View file

@ -811,7 +811,7 @@ This works by considering the current branch as a topic branch
(whether or not it actually is).
If there is a distinct push remote for this branch, assume the target
for outstanding changes is the tracking branch, so return that.
for outstanding changes is the tracking branch, and return that.
Otherwise, fall back to the following algorithm, which requires that the
corresponding trunk exists as a local branch. Find all merge bases