Subject: bug#51242: [PATCH] New option show-paren-context-when-offscreen

From: Daniel Martín <[email-masked]>
Date: Sat, 16 Oct 2021 20:52:23 +0200

Now that show-paren-mode is enabled by default, I've implemented a new
customization option for Emacs 29.

When the attached patch is installed, if
show-paren-context-when-offscreen is t, when you move the point to a
closing delimiter, if the opening delimiter is offscreen, the line where
the opening line is will appear in the echo area.  This gives you
context about what a delimiter is closing without having to move the
point or reinsert it.  There is similar functionality in popular IDEs.

The feature is implemented by first extracting some code from simple.el
to reuse it from simple.el and paren.el.  The rest of the implementation
is simple.

I think it plays well with eldoc, blink-matching-paren, and the rest of
the show-paren-mode options.

I hope you find this feature useful.

>From 6a3cf53c6afeed80a450faba0f66ff5f968142d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Mart=C3=ADn?= <[email-masked]>
Date: Sat, 16 Oct 2021 20:24:19 +0200
Subject: [PATCH] New option show-paren-context-when-offscreen

* lisp/simple.el (blink-paren-open-paren-line-string): Extract
functionality that shows the open paren line in the echo area into its
own function, to reuse it from paren.el.
(blink-matching-open): Use blink-paren-open-paren-line-string.
* lisp/paren.el (show-paren-context-when-offscreen): New option
show-paren-context-when-offscreen.
(show-paren-function): Implement it using
blink-paren-open-paren-line-string.
* lisp/emacs-lisp/eldoc.el (eldoc-display-message-no-interference-p):
Make sure the feature works well with eldoc.
* test/lisp/paren-tests.el (paren-tests-open-paren-line): Test
blink-paren-open-paren-line-string.
* doc/emacs/programs.texi (Matching): Update the documentation.
* etc/NEWS: And announce the new feature.
---
 doc/emacs/programs.texi  |  9 +++++
 etc/NEWS                 |  8 +++++
 lisp/emacs-lisp/eldoc.el |  9 ++++-
 lisp/paren.el            | 21 ++++++++++++
 lisp/simple.el           | 71 +++++++++++++++++++++-------------------
 test/lisp/paren-tests.el | 31 ++++++++++++++++++
 6 files changed, 114 insertions(+), 35 deletions(-)

diff --git a/doc/emacs/programs.texi b/doc/emacs/programs.texi
index 51a48df2e2..0056906e1f 100644
--- a/doc/emacs/programs.texi
+++ b/doc/emacs/programs.texi
@@ -868,6 +868,15 @@ Matching
 line and there is a paren at the first or last non-whitespace position
 on the line, or when point is at the end of a line and there is a
 paren at the last non-whitespace position on the line.
+
+@item
+@vindex show-paren-context-when-offscreen
+@code{show-paren-context-when-offscreen}, when non-@code{nil}, shows
+some context in the echo area when point is in a closing delimiter and
+the opening delimiter is offscreen.  The context is usually the line
+that contains the opening delimiter, except if the opening delimiter
+is on its own line, in which case the context includes the previous
+nonblank line.
 @end itemize
 
 @cindex Electric Pair mode
diff --git a/etc/NEWS b/etc/NEWS
index 2c09d24dde..5313e8e7ca 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -86,6 +86,14 @@ effectively dragged.
 Customize this option to limit the amount of entries in the menu
 "Edit->Paste from Kill Menu".  The default is 60.
 
+** show-paren-mode
+
++++
+*** New user option 'show-paren-context-when-offscreen'.
+When non-nil, if the point is in a closing delimiter and the opening
+delimiter is offscreen, shows some context around the opening
+delimiter in the echo area.
+
 
 * Changes in Specialized Modes and Packages in Emacs 29.1
 
diff --git a/lisp/emacs-lisp/eldoc.el b/lisp/emacs-lisp/eldoc.el
index a1c3c3268f..b30d3fc30f 100644
--- a/lisp/emacs-lisp/eldoc.el
+++ b/lisp/emacs-lisp/eldoc.el
@@ -380,7 +380,14 @@ eldoc-display-message-p
 ;; it undesirable to print eldoc messages right this instant.
 (defun eldoc-display-message-no-interference-p ()
   "Return nil if displaying a message would cause interference."
-  (not (or executing-kbd-macro (bound-and-true-p edebug-active))))
+  (not (or executing-kbd-macro
+           (bound-and-true-p edebug-active)
+           ;; The following configuration shows "Matches..." in the
+           ;; echo area when point is after a closing bracket, which
+           ;; conflicts with eldoc.
+           (and show-paren-context-when-offscreen
+                (not (pos-visible-in-window-p
+                      (overlay-end show-paren--overlay)))))))
 
 
 (defvar eldoc-documentation-functions nil
diff --git a/lisp/paren.el b/lisp/paren.el
index ce6aa9ae13..7e7cf6c262 100644
--- a/lisp/paren.el
+++ b/lisp/paren.el
@@ -88,6 +88,14 @@ show-paren-highlight-openparen
 its position."
   :type 'boolean)
 
+(defcustom show-paren-context-when-offscreen nil
+  "If non-nil, show context in the echo area when the openparen is offscreen.
+The context is usually the line that contains the openparen,
+except if the openparen is on its own line, in which case the
+context includes the previous nonblank line."
+  :type 'boolean
+  :version "29.1")
+
 (defvar show-paren--idle-timer nil)
 (defvar show-paren--overlay
   (let ((ol (make-overlay (point) (point) nil t))) (delete-overlay ol) ol)
@@ -312,6 +320,19 @@ show-paren-function
                             (current-buffer))
             (move-overlay show-paren--overlay
                           there-beg there-end (current-buffer)))
+          ;; If `show-paren-open-line-when-offscreen' is t and point
+          ;; is at a close paren, show the line that contains the
+          ;; openparen in the echo area.
+          (let ((openparen (min here-beg there-beg)))
+            (if (and show-paren-context-when-offscreen
+                     (< there-beg here-beg)
+                     (not (pos-visible-in-window-p openparen)))
+                (let ((open-paren-line-string
+                       (blink-paren-open-paren-line-string openparen))
+                      (message-log-max nil))
+                  (minibuffer-message
+                   "Matches %s"
+                   (substring-no-properties open-paren-line-string)))))
           ;; Always set the overlay face, since it varies.
           (overlay-put show-paren--overlay 'priority show-paren-priority)
           (overlay-put show-paren--overlay 'face face))))))
diff --git a/lisp/simple.el b/lisp/simple.el
index c7bb928cd7..4f711d60ea 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -8577,40 +8577,43 @@ blink-matching-open
                                    (current-buffer))
                      (sit-for blink-matching-delay))
                  (delete-overlay blink-matching--overlay)))))
-       (t
-        (let ((open-paren-line-string
-               (save-excursion
-                 (goto-char blinkpos)
-                 ;; Show what precedes the open in its line, if anything.
-                 (cond
-                  ((save-excursion (skip-chars-backward " \t") (not (bolp)))
-                   (buffer-substring (line-beginning-position)
-                                     (1+ blinkpos)))
-                  ;; Show what follows the open in its line, if anything.
-                  ((save-excursion
-                     (forward-char 1)
-                     (skip-chars-forward " \t")
-                     (not (eolp)))
-                   (buffer-substring blinkpos
-                                     (line-end-position)))
-                  ;; Otherwise show the previous nonblank line,
-                  ;; if there is one.
-                  ((save-excursion (skip-chars-backward "\n \t") (not (bobp)))
-                   (concat
-                    (buffer-substring (progn
-                                        (skip-chars-backward "\n \t")
-                                        (line-beginning-position))
-                                      (progn (end-of-line)
-                                             (skip-chars-backward " \t")
-                                             (point)))
-                    ;; Replace the newline and other whitespace with `...'.
-                    "..."
-                    (buffer-substring blinkpos (1+ blinkpos))))
-                  ;; There is nothing to show except the char itself.
-                  (t (buffer-substring blinkpos (1+ blinkpos)))))))
-          (minibuffer-message
-           "Matches %s"
-           (substring-no-properties open-paren-line-string))))))))
+       ((not show-paren-context-when-offscreen)
+        (minibuffer-message
+         "Matches %s"
+         (substring-no-properties
+          (blink-paren-open-paren-line-string blinkpos))))))))
+
+(defun blink-paren-open-paren-line-string (pos)
+  "Return the line string that contains the openparen at POS."
+  (save-excursion
+    (goto-char pos)
+    ;; Show what precedes the open in its line, if anything.
+    (cond
+     ((save-excursion (skip-chars-backward " \t") (not (bolp)))
+      (buffer-substring (line-beginning-position)
+                        (1+ pos)))
+     ;; Show what follows the open in its line, if anything.
+     ((save-excursion
+        (forward-char 1)
+        (skip-chars-forward " \t")
+        (not (eolp)))
+      (buffer-substring pos
+                        (line-end-position)))
+     ;; Otherwise show the previous nonblank line,
+     ;; if there is one.
+     ((save-excursion (skip-chars-backward "\n \t") (not (bobp)))
+      (concat
+       (buffer-substring (progn
+                           (skip-chars-backward "\n \t")
+                           (line-beginning-position))
+                         (progn (end-of-line)
+                                (skip-chars-backward " \t")
+                                (point)))
+       ;; Replace the newline and other whitespace with `...'.
+       "..."
+       (buffer-substring pos (1+ pos))))
+     ;; There is nothing to show except the char itself.
+     (t (buffer-substring pos (1+ pos))))))
 
 (defvar blink-paren-function 'blink-matching-open
   "Function called, if non-nil, whenever a close parenthesis is inserted.
diff --git a/test/lisp/paren-tests.el b/test/lisp/paren-tests.el
index c4bec5d86d..11249ee9bc 100644
--- a/test/lisp/paren-tests.el
+++ b/test/lisp/paren-tests.el
@@ -117,5 +117,36 @@ paren-tests-default
                          (- (point-max) 1) (point-max)
                          nil)))))
 
+(ert-deftest paren-tests-open-paren-line ()
+  (cl-flet ((open-paren-line ()
+                             (let* ((data (show-paren--default))
+                                    (here-beg (nth 0 data))
+                                    (there-beg (nth 2 data)))
+                               (blink-paren-open-paren-line-string
+                                (min here-beg there-beg)))))
+    ;; Lisp-like
+    (with-temp-buffer
+      (insert "(defun foo ()
+                  (dummy))")
+      (goto-char (point-max))
+      (should (string= "(defun foo ()" (open-paren-line))))
+
+    ;; C-like
+    (with-temp-buffer
+      (insert "int foo() {
+                 int blah;
+             }")
+      (goto-char (point-max))
+      (should (string= "int foo() {" (open-paren-line))))
+
+    ;; C-like with hanging {
+    (with-temp-buffer
+      (insert "int foo()
+               {
+                 int blah;
+               }")
+      (goto-char (point-max))
+      (should (string= "int foo()...{" (open-paren-line))))))
+
 (provide 'paren-tests)
 ;;; paren-tests.el ends here
-- 
2.31.0