diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index c7d157eacfb..e9ec9e2caab 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -131,6 +131,17 @@ been restored with a slightly revised role contingent on a few
assumptions explained in its doc string. For clarity, it has been
renamed 'erc-ensure-target-buffer-on-privmsg'.
+** Improved interplay between buffer truncation and message logging.
+While most of these improvements are subtle, some affect everyday use.
+For example, users of the 'truncate' module may notice that truncation
+now happens between messages rather than arbitrary lines. And those
+with the default 'erc-insert-timestamp-left-and-right' for their
+'erc-insert-timestamp-function' will see date stamps reprinted after
+every "/CLEAR" but omitted from any logs. One notable casualty of
+these changes has been the deprecation of the ancient option
+'erc-truncate-buffer-on-save'. Users of the 'log' module can achieve
+the same effect by issuing a "/CLEAR" at the prompt.
+
** Miscellaneous UX changes.
Some minor quality-of-life niceties have finally made their way to
ERC. For example, the function 'erc-echo-timestamp' is now
diff --git a/lisp/erc/erc-log.el b/lisp/erc/erc-log.el
index 2b58a7c56ed..d3106da4017 100644
--- a/lisp/erc/erc-log.el
+++ b/lisp/erc/erc-log.el
@@ -124,6 +124,7 @@ custom function which returns the directory part and set
(defcustom erc-truncate-buffer-on-save nil
"Erase the contents of any ERC (channel, query, server) buffer when it is saved."
:type 'boolean)
+(make-obsolete 'erc-truncate-buffer-on-save 'erc-cmd-CLEAR "30.1")
(defcustom erc-enable-logging t
"If non-nil, ERC will log IRC conversations.
@@ -230,6 +231,7 @@ also be a predicate function. To only log when you are not set away, use:
(add-hook 'erc-part-hook #'erc-conditional-save-buffer)
;; append, so that 'erc-initialize-log-marker runs first
(add-hook 'erc-connect-pre-hook #'erc-log-setup-logging 'append)
+ (add-hook 'erc--pre-clear-functions #'erc-save-buffer-in-logs)
(dolist (buffer (erc-buffer-list))
(erc-log-setup-logging buffer))
(erc--modify-local-map t "C-c C-l" #'erc-save-buffer-in-logs))
@@ -242,6 +244,7 @@ also be a predicate function. To only log when you are not set away, use:
(remove-hook 'erc-quit-hook #'erc-conditional-save-queries)
(remove-hook 'erc-part-hook #'erc-conditional-save-buffer)
(remove-hook 'erc-connect-pre-hook #'erc-log-setup-logging)
+ (remove-hook 'erc--pre-clear-functions #'erc-save-buffer-in-logs)
(dolist (buffer (erc-buffer-list))
(erc-log-disable-logging buffer))
(erc--modify-local-map nil "C-c C-l" #'erc-save-buffer-in-logs)))
@@ -301,6 +304,8 @@ Returns nil if `erc-server-buffer-p' returns t."
(dolist (buffer (erc-buffer-list))
(erc-save-buffer-in-logs buffer)))
+(defvar erc-log--save-in-progress-p nil)
+
;;;###autoload
(defun erc-logging-enabled (&optional buffer)
"Return non-nil if logging is enabled for BUFFER.
@@ -310,6 +315,7 @@ is writable (it will be created as necessary) and
`erc-enable-logging' returns a non-nil value."
(or buffer (setq buffer (current-buffer)))
(and erc-log-channels-directory
+ (not erc-log--save-in-progress-p)
(or (functionp erc-log-channels-directory)
(erc-directory-writable-p erc-log-channels-directory))
(if (functionp erc-enable-logging)
@@ -399,7 +405,7 @@ automatically.
You can save every individual message by putting this function on
`erc-insert-post-hook'."
(interactive)
- (or buffer (setq buffer (current-buffer)))
+ (unless (bufferp buffer) (setq buffer (current-buffer)))
(when (erc-logging-enabled buffer)
(let ((file (erc-current-logfile buffer))
(coding-system erc-log-file-coding-system))
@@ -423,10 +429,11 @@ You can save every individual message by putting this function on
(write-region start end file t 'nomessage))))
(if (and erc-truncate-buffer-on-save
(called-interactively-p 'interactive))
- (progn
- (let ((inhibit-read-only t)) (erase-buffer))
- (move-marker erc-last-saved-position (point-max))
- (erc-display-prompt))
+ (let ((erc-log--save-in-progress-p t))
+ (erc-cmd-CLEAR)
+ (erc-button--display-error-notice-with-keys
+ (erc-server-buffer) "Option `%s' is deprecated."
+ " Use /CLEAR instead." 'erc-truncate-buffer-on-save))
(move-marker erc-last-saved-position
;; If we place erc-last-saved-position at
;; erc-insert-marker, because text gets
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index 9191bbe5a2a..500f6f3c0c9 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -166,12 +166,14 @@ from entering them and instead jump over them."
(add-hook 'erc-insert-modify-hook #'erc-add-timestamp t)
(add-hook 'erc-send-modify-hook #'erc-add-timestamp t)
(add-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect)
+ (add-hook 'erc--pre-clear-functions #'erc-stamp--reset-on-clear)
(unless erc--updating-modules-p
(erc-buffer-filter #'erc-munge-invisibility-spec)))
((remove-hook 'erc-mode-hook #'erc-munge-invisibility-spec)
(remove-hook 'erc-insert-modify-hook #'erc-add-timestamp)
(remove-hook 'erc-send-modify-hook #'erc-add-timestamp)
(remove-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect)
+ (remove-hook 'erc--pre-clear-functions #'erc-stamp--reset-on-clear)
(erc-with-all-buffers-of-server nil nil
(kill-local-variable 'erc-timestamp-last-inserted)
(kill-local-variable 'erc-timestamp-last-inserted-left)
@@ -584,6 +586,20 @@ enabled when the message was inserted."
(defun erc--echo-ts-csf (_window _before dir)
(erc-echo-timestamp dir (get-text-property (point) 'erc-timestamp)))
+(defun erc-stamp--update-saved-position (&rest _)
+ (remove-function (local 'erc-stamp--insert-date-function)
+ #'erc-stamp--update-saved-position)
+ (move-marker erc-last-saved-position (1- (point))))
+
+(defun erc-stamp--reset-on-clear (pos)
+ "Forget last-inserted stamps when POS is at insert marker."
+ (when (= pos (1- erc-insert-marker))
+ (add-function :after (local 'erc-stamp--insert-date-function)
+ #'erc-stamp--update-saved-position)
+ (setq erc-timestamp-last-inserted nil
+ erc-timestamp-last-inserted-left nil
+ erc-timestamp-last-inserted-right nil)))
+
(provide 'erc-stamp)
;;; erc-stamp.el ends here
diff --git a/lisp/erc/erc-truncate.el b/lisp/erc/erc-truncate.el
index b8fd4ae2e14..8430a68d92b 100644
--- a/lisp/erc/erc-truncate.el
+++ b/lisp/erc/erc-truncate.el
@@ -50,9 +50,9 @@ This prevents the query buffer from getting too large, which can
bring any grown Emacs to its knees after a few days worth of
tracking heavy-traffic channels."
;;enable
- ((add-hook 'erc-insert-post-hook #'erc-truncate-buffer))
+ ((add-hook 'erc-insert-done-hook #'erc-truncate-buffer))
;; disable
- ((remove-hook 'erc-insert-post-hook #'erc-truncate-buffer)))
+ ((remove-hook 'erc-insert-done-hook #'erc-truncate-buffer)))
;;;###autoload
(defun erc-truncate-buffer-to-size (size &optional buffer)
@@ -75,9 +75,11 @@ region is logged if `erc-logging-enabled' returns non-nil."
(save-restriction
(widen)
(let ((end (- erc-insert-marker size)))
- ;; truncate at line boundaries
+ ;; Truncate at message boundary (formerly line boundary
+ ;; before 5.6).
(goto-char end)
- (beginning-of-line)
+ (goto-char (or (previous-single-property-change (point) 'erc-command)
+ (pos-bol)))
(setq end (point))
;; try to save the current buffer using
;; `erc-save-buffer-in-logs'. We use this, in case the
@@ -91,10 +93,7 @@ region is logged if `erc-logging-enabled' returns non-nil."
;; (not (memq 'erc-save-buffer-in-logs
;; erc-insert-post-hook))
;; Comments?
- (when (and (boundp 'erc-enable-logging)
- erc-enable-logging
- (erc-logging-enabled buffer))
- (erc-save-buffer-in-logs))
+ (run-hook-with-args 'erc--pre-clear-functions end)
;; disable undoing for the truncating
(buffer-disable-undo)
(let ((inhibit-read-only t))
@@ -103,10 +102,10 @@ region is logged if `erc-logging-enabled' returns non-nil."
;;;###autoload
(defun erc-truncate-buffer ()
- "Truncates the current buffer to `erc-max-buffer-size'.
-Meant to be used in hooks, like `erc-insert-post-hook'."
+ "Truncate current buffer to `erc-max-buffer-size'."
(interactive)
- (erc-truncate-buffer-to-size erc-max-buffer-size))
+ (save-excursion
+ (erc-truncate-buffer-to-size erc-max-buffer-size)))
(provide 'erc-truncate)
;;; erc-truncate.el ends here
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9c1125a9351..5a91285c1d1 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3436,10 +3436,17 @@ If no USER argument is specified, list the contents of `erc-ignore-list'."
(erc-with-server-buffer
(setq erc-ignore-list (delete user erc-ignore-list))))))
+(defvar erc--pre-clear-functions nil
+ "Abnormal hook run when truncating buffers.
+Called with position indicating boundary of interval to be excised.")
+
(defun erc-cmd-CLEAR ()
"Clear the window content."
(let ((inhibit-read-only t))
- (delete-region (point-min) (line-beginning-position)))
+ (run-hook-with-args 'erc--pre-clear-functions (1- erc-insert-marker))
+ ;; Ostensibly, `line-beginning-position' is for use in lisp code.
+ (delete-region (point-min) (min (line-beginning-position)
+ (1- erc-insert-marker))))
t)
(put 'erc-cmd-CLEAR 'process-not-needed t)
diff --git a/test/lisp/erc/erc-scenarios-log.el b/test/lisp/erc/erc-scenarios-log.el
new file mode 100644
index 00000000000..c37e6b323aa
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-log.el
@@ -0,0 +1,207 @@
+;;; erc-scenarios-log.el --- erc-log scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs. If not, see .
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+ (let ((load-path (cons (ert-resource-directory) load-path)))
+ (require 'erc-scenarios-common)))
+
+(require 'erc-log)
+(require 'erc-truncate)
+
+(defvar erc-timestamp-format-left)
+
+(ert-deftest erc-scenarios-log--kill-hook ()
+ :tags '(:expensive-test)
+ (erc-scenarios-common-with-cleanup
+ ((erc-scenarios-common-dialog "base/assoc/bouncer-history")
+ (dumb-server (erc-d-run "localhost" t 'foonet))
+ (tempdir (make-temp-file "erc-tests-log." t nil nil))
+ (erc-log-channels-directory tempdir)
+ (erc-modules (cons 'log erc-modules))
+ (port (process-contact dumb-server :service))
+ (logfile (expand-file-name (format "#chan!tester@127.0.0.1:%d.txt" port)
+ tempdir))
+ (erc-server-flood-penalty 0.1)
+ (expect (erc-d-t-make-expecter)))
+
+ (ert-info ("Connect to foonet")
+ (with-current-buffer (erc :server "127.0.0.1"
+ :port port
+ :nick "tester"
+ :password "foonet:changeme"
+ :full-name "tester")
+ (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+ (funcall expect 5 "foonet")))
+
+ (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+ (funcall expect 10 "was created on")
+ (funcall expect 10 "please your lordship")
+ (with-current-buffer "foonet"
+ (delete-process erc-server-process)
+ (funcall expect 5 "failed"))
+ (should-not (file-exists-p logfile))
+ (kill-buffer)
+ (should (file-exists-p logfile)))
+
+ (with-temp-buffer
+ (insert-file-contents logfile)
+ (funcall expect 1 "You have joined")
+ (funcall expect 1 "Playback Complete.")
+ (funcall expect 1 "please your lordship"))
+
+ (erc-log-mode -1)
+ (if noninteractive
+ (delete-directory tempdir :recursive)
+ (add-hook 'kill-emacs-hook
+ (lambda () (delete-directory tempdir :recursive))))))
+
+;; This shows that, in addition to truncating the buffer, /clear also
+;; syncs the log.
+
+(ert-deftest erc-scenarios-log--clear-stamp ()
+ :tags '(:expensive-test)
+ (erc-scenarios-common-with-cleanup
+ ((erc-scenarios-common-dialog "base/assoc/bouncer-history")
+ (dumb-server (erc-d-run "localhost" t 'foonet))
+ (tempdir (make-temp-file "erc-tests-log." t nil nil))
+ (erc-log-channels-directory tempdir)
+ (erc-modules (cons 'log erc-modules))
+ (erc-timestamp-format-left "\n[%a %b %e %Y @@STAMP@@]\n")
+ (port (process-contact dumb-server :service))
+ (logfile (expand-file-name (format "#chan!tester@127.0.0.1:%d.txt" port)
+ tempdir))
+ (erc-server-flood-penalty 0.1)
+ (expect (erc-d-t-make-expecter)))
+
+ (unless noninteractive
+ (add-hook 'kill-emacs-hook
+ (lambda () (delete-directory tempdir :recursive))))
+
+ (ert-info ("Connect to foonet")
+ (with-current-buffer (erc :server "127.0.0.1"
+ :port port
+ :nick "tester"
+ :password "foonet:changeme"
+ :full-name "tester")
+ (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+ (funcall expect 5 "foonet")))
+
+ (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+ (funcall expect 10 "@@STAMP@@")
+ (funcall expect 10 "Grows, lives")
+ (should-not (file-exists-p logfile))
+ (goto-char (point-max))
+ (erc-cmd-CLEAR)
+ (should (file-exists-p logfile))
+ (funcall expect 10 "please your lordship")
+ (ert-info ("Buffer truncated")
+ (goto-char (point-min))
+ (funcall expect 10 "@@STAMP@@" (point)) ; reset
+ (funcall expect -0.1 "Grows, lives")
+ (funcall expect 1 "For these two")))
+
+ (ert-info ("Current contents saved")
+ (with-temp-buffer
+ (insert-file-contents logfile)
+ (funcall expect 1 "@@STAMP@@")
+ (funcall expect 1 "You have joined")
+ (funcall expect 1 "Playback Complete.")
+ (funcall expect 1 "Grows, lives")
+ (funcall expect -0.01 "please your lordship")))
+
+ (ert-info ("Remainder saved, timestamp printed when option non-nil")
+ (with-current-buffer "foonet"
+ (delete-process erc-server-process)
+ (funcall expect 5 "failed"))
+ (kill-buffer "#chan")
+ (with-temp-buffer
+ (insert-file-contents logfile)
+ (funcall expect 1 "@@STAMP@@")
+ (funcall expect 1 "Grows, lives")
+ (funcall expect -0.01 "@@STAMP@@")
+ (forward-line 1) ; no blank, no timestamp
+ (should (looking-at (rx " alice: For these two hours,")))
+ (funcall expect 1 "please your lordship")))
+
+ (erc-log-mode -1)
+ (when noninteractive (delete-directory tempdir :recursive))))
+
+(ert-deftest erc-scenarios-log--truncate ()
+ :tags '(:expensive-test)
+ (erc-scenarios-common-with-cleanup
+ ((erc-scenarios-common-dialog "base/assoc/bouncer-history")
+ (dumb-server (erc-d-run "localhost" t 'foonet))
+ (tempdir (make-temp-file "erc-tests-log." t nil nil))
+ (erc-log-channels-directory tempdir)
+ (erc-modules (cons 'truncate (cons 'log erc-modules)))
+ (erc-max-buffer-size 512)
+ (port (process-contact dumb-server :service))
+ (logchan (expand-file-name (format "#chan!tester@127.0.0.1:%d.txt" port)
+ tempdir))
+ (logserv (expand-file-name
+ (format "127.0.0.1:%d!tester@127.0.0.1:%d.txt" port port)
+ tempdir))
+ (erc-server-flood-penalty 0.1)
+ (expect (erc-d-t-make-expecter)))
+
+ (unless noninteractive
+ (add-hook 'kill-emacs-hook
+ (lambda () (delete-directory tempdir :recursive))))
+
+ (ert-info ("Connect to foonet")
+ (with-current-buffer (erc :server "127.0.0.1"
+ :port port
+ :nick "tester"
+ :password "foonet:changeme"
+ :full-name "tester")
+ (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+ (should-not (file-exists-p logserv))
+ (should-not (file-exists-p logchan))
+ (funcall expect 10 "*** MAXLIST=beI:60")
+ (should (= (pos-bol) (point-min)))
+ (should (file-exists-p logserv))))
+
+ (ert-info ("Log file ahead of truncation point")
+ ;; Log contains lines still present in buffer.
+ (with-temp-buffer
+ (insert-file-contents logserv)
+ (funcall expect 10 "*** MAXLIST=beI:60")))
+
+ (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+ (funcall expect 10 "please your lordship")
+ (should (file-exists-p logchan))
+ (funcall expect -0.1 "[07:04:37] alice: Here," (point-min)))
+
+ (ert-info ("Log ahead of truncation point")
+ (with-temp-buffer
+ (insert-file-contents logchan)
+ (funcall expect 1 "You have joined")
+ (funcall expect 1 "[07:04:37] alice: Here,")
+ (funcall expect 1 "loathed enemy")
+ (funcall expect -0.1 "please your lordship")))
+
+ (erc-log-mode -1)
+ (when noninteractive (delete-directory tempdir :recursive))))
+
+;;; erc-scenarios-log.el ends here