branch: elpa/jabber
commit 77e79c7e4fa12813b07a93547c39e3ae80fa7c48
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>
chat: Jump to the original message with RET on a reply
---
CHANGELOG.org | 1 +
lisp/jabber-chat.el | 36 +++++++++++++++++++++++++++
lisp/jabber-chatbuffer.el | 6 +++--
tests/jabber-test-chat.el | 62 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 103 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 361907c333..ad6b1acd8f 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -3,6 +3,7 @@
* [Unreleased]
** Added
+- RET on a reply jumps to the original message and highlights it; RET
elsewhere still sends
- Replies without an inline quote now show a snippet of the original message,
looked up from local history
- Reply context survives buffer reloads: reply metadata is now stored in the
message database
- Outgoing messages carry an XEP-0359 origin-id, so replies from other clients
reference a stable id
diff --git a/lisp/jabber-chat.el b/lisp/jabber-chat.el
index ee1e454ec1..5b56f69e82 100644
--- a/lisp/jabber-chat.el
+++ b/lisp/jabber-chat.el
@@ -1228,6 +1228,42 @@ snippet of it after the label."
(concat label "\n"))
'face 'shadow)))))
+(defun jabber-chat--reply-target-at-point ()
+ "Return the :reply-to-id of the rendered message at point, or nil.
+Only meaningful in the message area, above the input divider."
+ (and jabber-chat-ewoc
+ (markerp jabber-point-insert)
+ (< (point) jabber-point-insert)
+ (when-let* ((node (ewoc-locate jabber-chat-ewoc (point)))
+ (msg (cadr (ewoc-data node))))
+ (and (listp msg) (plist-get msg :reply-to-id)))))
+
+(defun jabber-chat-goto-reply-target ()
+ "Move point to the message the reply at point references."
+ (interactive)
+ (let ((target (jabber-chat--reply-target-at-point)))
+ (unless target
+ (user-error "No reply at point"))
+ (let ((node (jabber-chat-ewoc-find-by-id target)))
+ (unless node
+ (user-error "The original message is not in this buffer"))
+ (goto-char (ewoc-location node))
+ (require 'pulse)
+ (pulse-momentary-highlight-region
+ (ewoc-location node)
+ (or (and-let* ((next (ewoc-next jabber-chat-ewoc node)))
+ (ewoc-location next))
+ jabber-point-insert)))))
+
+(defun jabber-chat-goto-reply-target-or-send ()
+ "Jump to the replied-to message at point, or send the input.
+On a rendered reply, move point to the original message;
+anywhere else, behave like `jabber-chat-buffer-send'."
+ (interactive)
+ (if (jabber-chat--reply-target-at-point)
+ (jabber-chat-goto-reply-target)
+ (jabber-chat-buffer-send)))
+
(defun jabber-chat-pp--local (data)
"Render a locally sent message from DATA."
(let* ((msg (cadr data))
diff --git a/lisp/jabber-chatbuffer.el b/lisp/jabber-chatbuffer.el
index 47e1e61332..6fb3be98ca 100644
--- a/lisp/jabber-chatbuffer.el
+++ b/lisp/jabber-chatbuffer.el
@@ -131,6 +131,7 @@ previous sequence detect the mismatch and stop.")
(declare-function jabber-correct-last-message "jabber-message-correct" ())
(declare-function jabber-chat-cancel-reply "jabber-message-reply" ())
(declare-function jabber-reactions-react-at-point-or-insert "jabber-reactions"
())
+(declare-function jabber-chat-goto-reply-target-or-send "jabber-chat" ())
(declare-function jabber-chat-image-enlarge-or-self-insert "jabber-chat" (n))
(declare-function jabber-chat-image-shrink-or-self-insert "jabber-chat" (n))
(declare-function jabber-chat-image-reset-size-or-self-insert "jabber-chat"
(n))
@@ -458,7 +459,7 @@ MAM sync in this buffer. Set via the operations menu.")
(insert "\n"))
(defvar-keymap jabber-chat-mode-map
- "RET" #'jabber-chat-buffer-send
+ "RET" #'jabber-chat-goto-reply-target-or-send
"S-<return>" #'jabber-chat-newline
"TAB" #'completion-at-point
"<backtab>" #'backward-button
@@ -727,9 +728,10 @@ or nil if the message was a duplicate."
node))))
(defun jabber-chat-ewoc--msg-matches-id-p (msg stanza-id)
- "Return non-nil when MSG has STANZA-ID as :id or :server-id."
+ "Return non-nil when MSG has STANZA-ID as :id, :origin-id or :server-id."
(and (listp msg)
(or (equal stanza-id (plist-get msg :id))
+ (equal stanza-id (plist-get msg :origin-id))
(equal stanza-id (plist-get msg :server-id)))))
(defun jabber-chat-ewoc--find-by-id-scan (stanza-id)
diff --git a/tests/jabber-test-chat.el b/tests/jabber-test-chat.el
index eb36954285..cbdba749fe 100644
--- a/tests/jabber-test-chat.el
+++ b/tests/jabber-test-chat.el
@@ -261,6 +261,68 @@
(plist (jabber-chat--msg-plist-from-stanza stanza)))
(should-not (plist-get plist :fallback-range))))
+(defmacro jabber-test-chat--with-reply-ewoc (&rest body)
+ "Run BODY in a temp buffer with an original and a reply node.
+The original has id \"orig-1\"; the reply references it. Point
+starts on the reply node; `jabber-point-insert' marks the input
+area after both messages."
+ (declare (indent 0) (debug t))
+ `(with-temp-buffer
+ (let ((jabber-chat-ewoc (ewoc-create
+ (lambda (data)
+ (insert (plist-get (cadr data) :body) "\n"))
+ nil nil 'nosep))
+ (jabber-chat--msg-nodes (make-hash-table :test 'equal)))
+ (jabber-chat-ewoc-enter
+ (list :foreign (list :id "orig-1" :from "[email protected]"
+ :body "the original"
+ :timestamp (current-time))))
+ (let ((reply-node
+ (jabber-chat-ewoc-enter
+ (list :foreign (list :id "r-1" :from "[email protected]"
+ :body "the reply"
+ :reply-to-id "orig-1"
+ :timestamp (current-time))))))
+ (setq-local jabber-point-insert (point-max-marker))
+ (goto-char (ewoc-location reply-node))
+ ,@body))))
+
+(ert-deftest jabber-test-chat-reply-target-at-point ()
+ "The reply target is found on the reply node and nowhere else."
+ (jabber-test-chat--with-reply-ewoc
+ (should (equal "orig-1" (jabber-chat--reply-target-at-point)))
+ (goto-char (point-min))
+ (should-not (jabber-chat--reply-target-at-point))
+ (goto-char (point-max))
+ (should-not (jabber-chat--reply-target-at-point))))
+
+(ert-deftest jabber-test-chat-goto-reply-target-jumps ()
+ "RET on a reply moves point to the original message."
+ (jabber-test-chat--with-reply-ewoc
+ (cl-letf (((symbol-function 'pulse-momentary-highlight-region)
+ #'ignore))
+ (jabber-chat-goto-reply-target))
+ (should (= (point) (point-min)))
+ (should (looking-at "the original"))))
+
+(ert-deftest jabber-test-chat-goto-reply-target-or-send-dispatch ()
+ "RET sends from the input area and jumps from a reply."
+ (jabber-test-chat--with-reply-ewoc
+ (let ((sent nil))
+ (cl-letf (((symbol-function 'jabber-chat-buffer-send)
+ (lambda () (setq sent t)))
+ ((symbol-function 'pulse-momentary-highlight-region)
+ #'ignore))
+ (goto-char (point-max))
+ (jabber-chat-goto-reply-target-or-send)
+ (should sent)
+ (setq sent nil)
+ (goto-char (point-min))
+ (ewoc-goto-next jabber-chat-ewoc 1)
+ (jabber-chat-goto-reply-target-or-send)
+ (should-not sent)
+ (should (= (point) (point-min)))))))
+
(ert-deftest jabber-test-chat-reply-context-synthesizes-quote ()
"A fallback-less reply quotes the original body from the database."
(with-temp-buffer