Subject: 30.0.50; [PATCH] Fix paragraph navigation in Eshell

From: Jim Porter <[email-masked]>
Date: Thu, 13 Jun 2024 21:33:57 -0700

A while back, I updated how forward- and backward-paragraph work in 
Eshell so that we didn't have to rely on regexps to detect the prompt 
(paragraph navigation in Eshell has always stopped at prompts as well as 
blank lines separating paragraphs). I got that implementation wrong, and 
since then it's only navigated between prompts. Attached is a patch to 
restore the proper behavior, plus some tests to make sure I got it right 
this time.

From f015b7f1d7ef35edf59c7899fe6b4d27f016dc76 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Thu, 13 Jun 2024 21:26:53 -0700
Subject: [PATCH] Ensure navigating by paragraphs in Eshell stops at prompts
 and paragraphs

The previous implementation in 6ae2b74ed20 only stopped at prompts,
which isn't the right behavior.

* lisp/eshell/em-prompt.el (eshell-forward-paragraph)
(eshell-backward-paragraph): Reimplement to handle prompts and
paragraphs (the latter by calling the original 'forward-paragraph').

* test/lisp/eshell/em-prompt-tests.el
(em-prompt-test/next-previous-prompt/multiline): Rename.
(em-prompt-test/forward-backward-paragraph-1): New function.
(em-prompt-test/forward-backward-paragraph)
(em-prompt-test/forward-backward-paragraph/multiline): New tests.
---
 lisp/eshell/em-prompt.el            | 34 ++++++++++++++++----
 test/lisp/eshell/em-prompt-tests.el | 49 ++++++++++++++++++++++++++++-
 2 files changed, 76 insertions(+), 7 deletions(-)

diff --git a/lisp/eshell/em-prompt.el b/lisp/eshell/em-prompt.el
index b6556d29544..fb4e51cdfe0 100644
--- a/lisp/eshell/em-prompt.el
+++ b/lisp/eshell/em-prompt.el
@@ -117,7 +117,7 @@ eshell-prompt-initialize
   "Initialize the prompting code."
   (unless eshell-non-interactive-p
     (add-hook 'eshell-post-command-hook 'eshell-emit-prompt nil t)
-    (eshell-prompt-mode)))
+    (eshell-prompt-mode))
 
 (defun eshell-emit-prompt ()
   "Emit a prompt if eshell is being used interactively."
@@ -167,17 +167,39 @@ eshell-backward-matching-input
 
 (defun eshell-forward-paragraph (&optional n)
   "Move to the beginning of the Nth next prompt in the buffer.
-Like `forward-paragraph', but navigates using fields."
+Like `forward-paragraph', but also stops at the beginning of each prompt."
   (interactive "p")
-  (eshell-next-prompt n)
-  (goto-char (field-beginning (point) t)))
+  (unless n (setq n 1))
+  (let (;; We'll handle the "paragraph" starts ourselves.
+        (paragraph-start regexp-unmatchable)
+        (inhibit-field-text-motion t))
+    (cond
+     ((> n 0)
+      (while (and (> n 0) (< (point) (point-max)))
+        (let ((next-paragraph (save-excursion (forward-paragraph) (point)))
+              (next-prompt (save-excursion
+                             (if-let ((match (text-property-search-forward
+                                              'field 'prompt t t)))
+                                 (prop-match-beginning match)
+                               (point-max)))))
+          (goto-char (min next-paragraph next-prompt)))
+        (setq n (1- n))))
+     ((< n 0)
+      (while (and (< n 0) (> (point) (point-min)))
+        (let ((prev-paragraph (save-excursion (backward-paragraph) (point)))
+              (prev-prompt (save-excursion
+                             (if (text-property-search-backward
+                                  'field 'prompt t)
+                                 (point)
+                               (point-min)))))
+          (goto-char (max prev-paragraph prev-prompt)))
+        (setq n (1+ n)))))))
 
 (defun eshell-backward-paragraph (&optional n)
   "Move to the beginning of the Nth previous prompt in the buffer.
 Like `backward-paragraph', but navigates using fields."
   (interactive "p")
-  (eshell-previous-prompt n)
-  (goto-char (field-beginning (point) t)))
+  (eshell-forward-paragraph (- (or n 1))))
 
 (defun eshell-next-prompt (&optional n)
   "Move to end of Nth next prompt in the buffer."
diff --git a/test/lisp/eshell/em-prompt-tests.el b/test/lisp/eshell/em-prompt-tests.el
index 964609e6410..fbadade061f 100644
--- a/test/lisp/eshell/em-prompt-tests.el
+++ b/test/lisp/eshell/em-prompt-tests.el
@@ -39,6 +39,9 @@ em-prompt-test--with-multiline
 
 ;;; Tests:
 
+
+;; Prompt output
+
 (ert-deftest em-prompt-test/field-properties ()
   "Check that field properties are properly set on Eshell output/prompts."
   (with-temp-eshell
@@ -104,6 +107,9 @@ em-prompt-test/after-failure
                'front-sticky '(read-only field font-lock-face)
                'rear-nonsticky '(read-only field font-lock-face)))))))
 
+
+;; Prompt navigation
+
 (defun em-prompt-test/next-previous-prompt-1 ()
   "Helper for checking forward/backward navigation of old prompts."
   (with-temp-eshell
@@ -150,11 +156,52 @@ em-prompt-test/next-previous-prompt
   "Check that navigating forward/backward through old prompts works correctly."
   (em-prompt-test/next-previous-prompt-1))
 
-(ert-deftest em-prompt-test/next-previous-prompt-multiline ()
+(ert-deftest em-prompt-test/next-previous-prompt/multiline ()
   "Check old prompt forward/backward navigation for multiline prompts."
   (em-prompt-test--with-multiline
    (em-prompt-test/next-previous-prompt-1)))
 
+(defun em-prompt-test/forward-backward-paragraph-1 ()
+  "Helper for checking forward/backward navigation by paragraphs."
+  (with-temp-eshell
+    (cl-flet ((at-prompt-for-command-p (command)
+                (and (equal (point) (field-beginning))
+                     (equal (get-text-property (point) 'field) 'prompt)
+                     (save-excursion
+                       (goto-char (field-end))
+                       (equal (field-string) command)))))
+      (eshell-insert-command "echo 'high five'")
+      (eshell-insert-command "echo 'up high\n\ndown low'")
+      (eshell-insert-command "echo 'too slow'")
+      (insert "echo goodby")            ; A partially-entered command.
+      (ert-info ("Go back to the last prompt")
+        (eshell-backward-paragraph)
+        (should (at-prompt-for-command-p "echo goodby")))
+      (ert-info ("Go back to the paragraph break")
+        (eshell-backward-paragraph 2)
+        (should (looking-at "\ndown low\n")))
+      (ert-info ("Go forward to the third prompt")
+        (eshell-forward-paragraph)
+        (should (at-prompt-for-command-p "echo 'too slow'\n")))
+      (ert-info ("Go backward to before the first prompt")
+        (eshell-backward-paragraph 5)
+        (should (looking-back "Welcome to the Emacs shell\n")))
+      (ert-info ("Go backward to the beginning of the buffer")
+        (eshell-backward-paragraph)
+        (should (bobp)))
+      (ert-info ("Go forward to the second prompt")
+        (eshell-forward-paragraph 3)
+        (should (at-prompt-for-command-p "echo 'up high\n\ndown low'\n"))))))
+
+(ert-deftest em-prompt-test/forward-backward-paragraph ()
+  "Check that navigating forward/backward through paragraphs works correctly."
+  (em-prompt-test/forward-backward-paragraph-1))
+
+(ert-deftest em-prompt-test/forward-backward-paragraph/multiline ()
+  "Check paragraph forward/backward navigation for multiline prompts."
+  (em-prompt-test--with-multiline
+   (em-prompt-test/forward-backward-paragraph-1)))
+
 (defun em-prompt-test/forward-backward-matching-input-1 ()
   "Helper for checking forward/backward navigation via regexps."
   (with-temp-eshell
-- 
2.25.1