Subject: bug#49120: [PATCH] Add commands 'kill-lines' and 'copy-lines'

From: Okam <[email-masked]>
Date: Thu, 01 Jul 2021 23:50:26 +0000

On 6/19/21 1:33 PM, Eli Zaretskii wrote:
 >> Date: Sat, 19 Jun 2021 17:12:25 +0000
 >> From:  Okam via "Bug reports for GNU Emacs,
 >>   the Swiss army knife of text editors" <[email-masked]>
 >>
 >> These commands work similarly to the 'flush-lines' command, but add the
 >> lines to the kill ring as a single item.
 >
 > Thanks.  However, the "as a single item" part is not clear enough and
 > should be clarified.  Do you mean "as a single string"?  If so, I
 > would suggest to say that, and also explicitly say that the string
 > includes the newlines between the lines.
 >
 >> +@findex kill-lines
 >> +@item M-x kill-lines
 >> +Like @code{flush-lines}, but also add the matching lines to the kill
 >> +ring as a single item.
 >
 > I'd suggest to reword:
 >
 >    Like @code{flush-lines}, but also add the matching lines to the kill
 >    ring.  The command adds the matching lines to the kill ring as a
 >    single string, including the newlines that separated the lines.
 >
 > The reason I think it's better to separate this into two sentences is
 > that "also" is only relevant to the first part, not the second.

Done.

 >> +(defalias 'kill-matching-lines 'kill-lines)
 >> +(defalias 'copy-matching-lines 'copy-lines)
 >
 > I wonder why we need these aliases, and in fact why not have only
 > kill-matching-lines without the shorter kill-lines?  The latter omits
 > the crucial reference to the "matching" part, and is too similar to
 > kill-word, kill-paragraph, etc.

I was following the examples of `flush-lines` and `keep-lines`, with
aliases `delete-matching-lines` and `delete-non-matching-lines`,
respectively. The proposed `kill-matching-lines` and
`copy-matching-lines` are just other versions of `flush-lines`.

I have removed the aliases from the patch.

 >> +(defun kill-lines (regexp &optional rstart rend interactive)
 >> +  "Kill lines containing matches for REGEXP.
 >> +
 >> +When called from Lisp (and usually when called interactively as
 >> +well, see below), applies to the part of the buffer after point.
 >> +The line point is in is killed if and only if it contains a match
 >> +for regexp starting after point.
 >         ^^^^^^
 > REGEXP should in all caps.
 >
 >> +Second and third arg RSTART and REND specify the region to
 >                      ^^^
 > "args", in plural.
 >
 >> +operate on.  Lines partially contained in this region are killed
 >> +if and only if they contain a match entirely contained in it.
 >                                                            ^^^^^
 > "in the region" will make this more clear.
 >
 >> +                                                             When
 >> +calling this function from Lisp, you can pretend that it was
 >> +called interactively by passing a non-nil INTERACTIVE argument.
 >
 > This is not specific to this command, so why tell it here?
 >
 > Same comments apply to copy-lines.

Fixed.  Since the documentation string was originally copied from
`flush-lines`, do you want `keep-lines` and `flush-lines` to also have
these changes?

 >> +Return the number of copied matching lines.  When called
 >> +interactively, also print the number."
 >> +  (interactive
 >> +   (progn
 >> +     (barf-if-buffer-read-only)
 >
 > Why barf? this command doesn't modify the buffer, does it?
 >

Fixed. Thank you.

Also, the `flush-lines` command, and so these proposed commands, reports
the number of, for example, deleted regions as the number of lines
removed, ignoring cases when the match spans multiple lines.  Should
that be changed too?
From 891f360919244e966ad1278a2c364b038c8156c4 Mon Sep 17 00:00:00 2001
From: Earl Hyatt <[email-masked]>
Date: Sat, 19 Jun 2021 08:30:31 -0400
Subject: [PATCH] Add commands 'kill-matching-lines' and 'copy-matching-lines'

* doc/emacs/search.texi: Document these additions.
* lisp/replace.el:
Add the commands 'kill-matching-lines' and 'copy-matching-lines'.

'kill-matching-lines' is like 'flush-lines', but adds the lines to the
kill ring as a single string, keeping line endings.
'copy-matching-lines' is like 'kill-matching-lines', but only copies
those lines instead of killing them.
---
 doc/emacs/search.texi |  11 ++++
 etc/NEWS              |   5 ++
 lisp/replace.el       | 124 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 140 insertions(+)

diff --git a/doc/emacs/search.texi b/doc/emacs/search.texi
index e6b066e973..a1760ad66f 100644
--- a/doc/emacs/search.texi
+++ b/doc/emacs/search.texi
@@ -1971,6 +1971,17 @@ Other Repeating Search
 (a newline that ends a line counts as part of that line).
 
 If a match is split across lines, this command keeps all those lines.
+
+@findex kill-matching-lines
+@item M-x kill-matching-lines
+Like @code{flush-lines}, but also add the matching lines to the kill
+ring.  The command adds the matching lines to the kill ring as a
+single string, including the newlines that separated the lines.
+
+@findex copy-matching-lines
+@item M-x copy-matching-lines
+Like @code{kill-matching-lines}, but the matching lines are not
+removed from the buffer.
 @end table
 
 @node Search Customizations
diff --git a/etc/NEWS b/etc/NEWS
index 1693342f0a..eb862538da 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -461,6 +461,11 @@ highlighting on heading lines using standard outline faces.  This
 works well only when there are no conflicts with faces used by the
 major mode.
 
+** New commands 'copy-matching-lines' and 'kill-matching-lines'.
+These commands are similar to the command 'flush-lines',
+but add the matching lines to the kill ring as a single string,
+including the newlines that separate the lines.
+
 
 * Changes in Specialized Modes and Packages in Emacs 28.1
 
diff --git a/lisp/replace.el b/lisp/replace.el
index fe2cbc447a..051112770e 100644
--- a/lisp/replace.el
+++ b/lisp/replace.el
@@ -1054,6 +1054,130 @@ flush-lines
 			       count))
     count))
 
+(defun kill-matching-lines (regexp &optional rstart rend interactive)
+  "Kill lines containing matches for REGEXP.
+
+When called from Lisp (and usually when called interactively as
+well, see below), applies to the part of the buffer after point.
+The line point is in is killed if and only if it contains a match
+for REGEXP starting after point.
+
+If REGEXP contains upper case characters (excluding those
+preceded by `\\') and `search-upper-case' is non-nil, the
+matching is case-sensitive.
+
+Second and third args RSTART and REND specify the region to
+operate on.  Lines partially contained in this region are killed
+if and only if they contain a match entirely contained in the
+region.
+
+Interactively, in Transient Mark mode when the mark is active,
+operate on the contents of the region.  Otherwise, operate from
+point to the end of (the accessible portion of) the buffer.
+
+If a match is split across lines, all the lines it lies in are
+killed.  They are killed _before_ looking for the next match.
+Hence, a match starting on the same line at which another match
+ended is ignored.
+
+Return the number of killed matching lines.  When called
+interactively, also print the number."
+  (interactive
+   (progn
+     (barf-if-buffer-read-only)
+     (keep-lines-read-args "Kill lines containing match for regexp")))
+  (if rstart
+      (progn
+	(goto-char (min rstart rend))
+	(setq rend (copy-marker (max rstart rend))))
+    (if (and interactive (use-region-p))
+	(setq rstart (region-beginning)
+	      rend (copy-marker (region-end)))
+      (setq rstart (point)
+	    rend (point-max-marker)))
+    (goto-char rstart))
+  (let ((count 0)
+        (case-fold-search
+	 (if (and case-fold-search search-upper-case)
+	     (isearch-no-upper-case-p regexp t)
+	   case-fold-search)))
+    (save-excursion
+      (while (and (< (point) rend)
+		  (re-search-forward regexp rend t))
+        (unless (zerop count)
+          (setq last-command 'kill-region))
+	(kill-region (save-excursion (goto-char (match-beginning 0))
+                                     (forward-line 0)
+                                     (point))
+                     (progn (forward-line 1) (point)))
+        (setq count (1+ count))))
+    (set-marker rend nil)
+    (when interactive (message (ngettext "Killed %d matching line"
+					 "Killed %d matching lines"
+					 count)
+			       count))
+    count))
+
+(defun copy-matching-lines (regexp &optional rstart rend interactive)
+  "Copy lines containing matches for REGEXP to the kill ring.
+
+When called from Lisp (and usually when called interactively as
+well, see below), applies to the part of the buffer after point.
+The line point is in is copied if and only if it contains a match
+for REGEXP starting after point.
+
+If REGEXP contains upper case characters (excluding those
+preceded by `\\') and `search-upper-case' is non-nil, the
+matching is case-sensitive.
+
+Second and third args RSTART and REND specify the region to
+operate on.  Lines partially contained in this region are copied
+if and only if they contain a match entirely contained in the
+region.
+
+Interactively, in Transient Mark mode when the mark is active,
+operate on the contents of the region.  Otherwise, operate from
+point to the end of (the accessible portion of) the buffer.
+
+If a match is split across lines, all the lines it lies in are
+copied.
+
+Return the number of copied matching lines.  When called
+interactively, also print the number."
+  (interactive
+   (keep-lines-read-args "Copy lines containing match for regexp"))
+  (if rstart
+      (progn
+	(goto-char (min rstart rend))
+	(setq rend (copy-marker (max rstart rend))))
+    (if (and interactive (use-region-p))
+	(setq rstart (region-beginning)
+	      rend (copy-marker (region-end)))
+      (setq rstart (point)
+	    rend (point-max-marker)))
+    (goto-char rstart))
+  (let ((count 0)
+        (case-fold-search
+	 (if (and case-fold-search search-upper-case)
+	     (isearch-no-upper-case-p regexp t)
+	   case-fold-search)))
+    (save-excursion
+      (while (and (< (point) rend)
+		  (re-search-forward regexp rend t))
+	(unless (zerop count)
+          (setq last-command 'kill-region))
+	(copy-region-as-kill (save-excursion (goto-char (match-beginning 0))
+                                             (forward-line 0)
+                                             (point))
+                             (progn (forward-line 1) (point)))
+        (setq count (1+ count))))
+    (set-marker rend nil)
+    (when interactive (message (ngettext "Copied %d matching line"
+					 "Copied %d matching lines"
+					 count)
+			       count))
+    count))
+
 (defun how-many (regexp &optional rstart rend interactive)
   "Print and return number of matches for REGEXP following point.
 When called from Lisp and INTERACTIVE is omitted or nil, just return
-- 
2.25.1