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

From: Okam <[email-masked]>
Date: Sat, 19 Jun 2021 17:12:25 +0000

These commands work similarly to the 'flush-lines' command, but add the
lines to the kill ring as a single item.

For example, when working with text lists, such as in

     - Documentation: Do thing 1
     - Source: Add new function
     - Documentation: Do thing 2

if one wanted to copy only the lines containing "Documentation", while
leaving the buffer untouched, the 'copy-lines' command could be used.
>From b35c21cdb2d3c051bd374bcd2f9e3fe16ffb9380 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-lines' and 'copy-lines'

* doc/emacs/search.texi: Document these additions.
* lisp/replace.el: Add the commands 'kill-lines' and 'copy-lines'.
'kill-lines' (alias 'kill-matching-lines') is like 'flush-lines',
 but adds the lines to the kill ring as a single item.
'copy-lines' (alias 'copy-matching-lines') is like 'kill-lines',
 but only copies those lines instead of killing them.
---
 doc/emacs/search.texi |  10 ++++
 etc/NEWS              |   4 ++
 lisp/replace.el       | 130 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 144 insertions(+)

diff --git a/doc/emacs/search.texi b/doc/emacs/search.texi
index e6b066e973..2ddb44c310 100644
--- a/doc/emacs/search.texi
+++ b/doc/emacs/search.texi
@@ -1971,6 +1971,16 @@ 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-lines
+@item M-x kill-lines
+Like @code{flush-lines}, but also add the matching lines to the kill
+ring as a single item.
+
+@findex copy-lines
+@item M-x copy-lines
+Like @code{kill-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..42210315b6 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -461,6 +461,10 @@ 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-lines' and 'kill-lines'.
+These commands are similar to the command 'flush-lines',
+but add the matching lines to the kill ring as a single item.
+
 
 * Changes in Specialized Modes and Packages in Emacs 28.1
 
diff --git a/lisp/replace.el b/lisp/replace.el
index fe2cbc447a..edbd85f2b3 100644
--- a/lisp/replace.el
+++ b/lisp/replace.el
@@ -912,6 +912,8 @@ read-regexp
 
 (defalias 'delete-non-matching-lines 'keep-lines)
 (defalias 'delete-matching-lines 'flush-lines)
+(defalias 'kill-matching-lines 'kill-lines)
+(defalias 'copy-matching-lines 'copy-lines)
 (defalias 'count-matches 'how-many)
 
 (defun keep-lines-read-args (prompt)
@@ -1054,6 +1056,134 @@ flush-lines
 			       count))
     count))
 
+(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.
+
+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 arg 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 it.
+
+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.  When
+calling this function from Lisp, you can pretend that it was
+called interactively by passing a non-nil INTERACTIVE argument.
+
+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-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 arg 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 it.
+
+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.  When
+calling this function from Lisp, you can pretend that it was
+called interactively by passing a non-nil INTERACTIVE argument.
+
+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
+   (progn
+     (barf-if-buffer-read-only)
+     (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