Subject: bug#23675: 30.0.50: make count-lines optionally ignore invisible lines

From: "J.P." <[email-masked]>
Date: Fri, 04 Aug 2023 17:35:27 -0700

Closed in 2020 (Emacs 28).

Perhaps I'm not understanding how the `ignore-invisible-lines' parameter
for `count-lines' is supposed to work, but it doesn't seem to make the
function consider `invisible' text properties that have lists as values.
I bring this up because ERC will likely be needing a line-counting
function that's list-aware, at least in the manner shown in the attached
tests. The change to `count-lines' accompanying these tests was merely
lifted from `forward-visible-line' to make them pass, but it's quite
possibly flawed and/or incomplete. If a proper solution ever emerges to
address this, hopefully it'll come at the hands of someone better
informed than I in the ways of Emacs invisibility. In the meantime
(2023), ERC will likely be doing its own subpar rendition unless someone
takes up the challenge for 30.1 (and Compat agrees to adopt it). Thanks.

>From 68fb034a60f74ea3cab9acd8e35bdf52ba9d5024 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 3 Aug 2023 22:24:57 -0700
Subject: [PATCH] [POC] Honor invisible text-prop list values in count-lines

* lisp/simple.el (count-lines): Use `invisible-p' when
`ignore-invisible-lines' parameter is non-nil.
* test/lisp/simple-tests.el (simple-tests-count-lines/invisible/interval,
simple-tests-count-lines/invisible/list/single,
simple-tests-count-lines/invisible/list/tail): Add tests.
(Bug#23675)
---
 lisp/simple.el            | 19 ++++--------
 test/lisp/simple-tests.el | 62 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 67 insertions(+), 14 deletions(-)

diff --git a/lisp/simple.el b/lisp/simple.el
index 6dc08ff0eb0..5c414fb364f 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -1782,20 +1782,11 @@ count-lines
 		   done))))
 	    (ignore-invisible-lines
              (goto-char (point-min))
-	     (save-match-data
-	       (- (buffer-size)
-                  (forward-line (buffer-size))
-		  (let ((invisible-count 0)
-		        prop)
-		    (goto-char (point-min))
-		    (while (re-search-forward "\n\\|\r[^\n]" nil t)
-		      (setq prop (get-char-property (1- (point)) 'invisible))
-		      (if (if (eq buffer-invisibility-spec t)
-			      prop
-			    (or (memq prop buffer-invisibility-spec)
-			        (assq prop buffer-invisibility-spec)))
-			  (setq invisible-count (1+ invisible-count))))
-		    invisible-count))))
+             (let ((count (if (eobp) 0 1)))
+               (while (and (zerop (forward-line)) (not (eobp)))
+                 (unless (invisible-p (1- (point)))
+                   (cl-incf count)))
+               count))
 	    (t
              (goto-char (point-max))
              (if (bolp)
diff --git a/test/lisp/simple-tests.el b/test/lisp/simple-tests.el
index 7dabb735522..e8e83c9a5cd 100644
--- a/test/lisp/simple-tests.el
+++ b/test/lisp/simple-tests.el
@@ -67,6 +67,68 @@ simple-test-count-lines/ignore-invisible-lines
     (insert (propertize "\nbar\nbaz\nzut" 'invisible t))
     (should (= (count-lines (point-min) (point-max) t) 2))))
 
+(ert-deftest simple-tests-count-lines/invisible/interval ()
+  (with-current-buffer (get-buffer-create "*count-lines/invisible/interval*")
+    (should (= 0 (count-lines (point-min) (point-max) t))) ; invisible
+    (insert "pre\n"
+            "before\n"
+            (propertize "0\n" 'invisible t)
+            (propertize "1\n" 'invisible t)
+            (propertize "2\n" 'invisible t)
+            (propertize "3\n" 'invisible t)
+            "after\n"
+            "post\n")
+    (should (equal buffer-invisibility-spec 't))
+    (with-restriction 5 (- (point-max) 5)
+      (goto-char (point-min))
+      (should (looking-at "before"))
+      (goto-char (point-max))
+      (should (looking-back "after\n")))
+    (should (= 6 (count-lines 5 (- (point-max) 5))))
+    (should (= 2 (count-lines 5 (- (point-max) 5) t))) ; invisible
+    (when noninteractive (kill-buffer))))
+
+(ert-deftest simple-tests-count-lines/invisible/list/single ()
+  (with-current-buffer (get-buffer-create "*count-lines/invisible/list/head*")
+    (insert "before\n"
+            (propertize "0\n" 'invisible '(0))
+            (propertize "1\n" 'invisible '(1))
+            (propertize "2\n" 'invisible '(2))
+            (propertize "3\n" 'invisible '(3))
+            "after\n")
+    (setq buffer-invisibility-spec '(0 1 (2 . t) 3 t))
+    (should (= 6 (count-lines (point-min) (point-max))))
+    (should (= 2 (count-lines (point-min) (point-max) t))) ; invisible
+    (when noninteractive (kill-buffer)))
+
+  (with-current-buffer (get-buffer-create "*count-lines/invisible/list/alt*")
+    (insert "before\n"
+            (propertize "0\n" 'invisible '(0))
+            "a\n"
+            (propertize "1\n" 'invisible '(1))
+            "b\n"
+            (propertize "2\n" 'invisible '(2))
+            "c\n"
+            (propertize "3\n" 'invisible '(3))
+            "after\n")
+    (setq buffer-invisibility-spec '(0 1 (2 . t) 3 t))
+    (should (= 9 (count-lines (point-min) (point-max))))
+    (should (= 5 (count-lines (point-min) (point-max) t))) ; invisible
+    (when noninteractive (kill-buffer))))
+
+(ert-deftest simple-tests-count-lines/invisible/list/tail ()
+  (with-current-buffer (get-buffer-create "*count-lines/invisible/list/tail*")
+    (insert "before\n"
+            (propertize "0\n" 'invisible '(x 0))
+            (propertize "1\n" 'invisible '(x 1))
+            (propertize "2\n" 'invisible '(x 2))
+            (propertize "3\n" 'invisible '(x 3))
+            "after\n")
+    (setq buffer-invisibility-spec '(0 (1 . t) 2 3 t))
+    (should (= 6 (count-lines (point-min) (point-max))))
+    (should (= 2 (count-lines (point-min) (point-max) t))) ; invisible
+    (when noninteractive (kill-buffer))))
+
 (ert-deftest simple-text-count-lines-non-ascii ()
   (with-temp-buffer
     (insert "あ\nい\nう\nえ\nお\n")
-- 
2.41.0