(require 'map)
(require 'warnings)
(cl-defmacro ement-debug (&rest args)
"Display a debug warning showing the runtime value of ARGS.
The warning automatically includes the name of the containing
function, and it is only displayed if `warning-minimum-log-level'
is `:debug' at expansion time (otherwise the macro expands to a
call to `ignore' with ARGS and is eliminated by the
byte-compiler). When debugging, the form also returns nil so,
e.g. it may be used in a conditional in place of nil.
Each of ARGS may be a string, which is displayed as-is, or a
symbol, the value of which is displayed prefixed by its name, or
a Lisp form, which is displayed prefixed by its first symbol.
Before the actual ARGS arguments, you can write keyword
arguments, i.e. alternating keywords and values. The following
keywords are supported:
:buffer BUFFER Name of buffer to pass to `display-warning'.
:level LEVEL Level passed to `display-warning', which see.
Default is :debug."
(pcase-let* ((fn-name (when byte-compile-current-buffer
(with-current-buffer byte-compile-current-buffer
(save-excursion
(beginning-of-defun)
(cl-second (read (current-buffer)))))))
(plist-args (cl-loop while (keywordp (car args))
collect (pop args)
collect (pop args)))
((map (:buffer buffer) (:level level)) plist-args)
(level (or level :debug))
(string (cl-loop for arg in args
concat (pcase arg
((pred stringp) "%S ")
((pred symbolp)
(concat (upcase (symbol-name arg)) ":%S "))
((pred listp)
(concat "(" (upcase (symbol-name (car arg)))
(pcase (length arg)
(1 ")")
(_ "...)"))
":%S "))))))
(if (eq :debug warning-minimum-log-level)
`(let ((fn-name ,(if fn-name
`',fn-name
`(cl-loop for frame in (backtrace-frames)
for fn = (cl-second frame)
when (not (or (subrp fn)
(special-form-p fn)
(eq 'backtrace-frames fn)))
return (make-symbol (format "%s [interpreted]" fn))))))
(display-warning fn-name (format ,string ,@args) ,level ,buffer)
nil)
`(ignore ,@args))))
(defmacro ement-alist (&rest pairs)
"Expand to an alist of the keys and values in PAIRS."
`(list ,@(cl-loop for (key value) on pairs by #'cddr
collect `(cons ,key ,value))))
(defmacro ement-afirst (form list)
"Return the first element of LIST for which FORM is non-nil.
In FORM, `it' is bound to the element being tested."
(declare (indent 1))
`(cl-loop for it in ,list
do (when ,form
(cl-return it))))
(defmacro ement-aprog1 (first &rest body)
"Like `prog1', but FIRST's value is bound to `it' around BODY."
(declare (indent 1))
`(let ((it ,first))
,@body
it))
(defmacro ement-singly (place-form &rest body)
"If PLACE-FORM is nil, set it non-nil and eval BODY.
BODY should set PLACE-FORM to nil when BODY is eligible to run
again."
(declare (indent defun))
`(unless ,place-form
(setf ,place-form t)
,@body))
(defalias 'ement-progress-update #'ignore
"By default, this function does nothing. But inside
`ement-with-progress-reporter', it's bound to a function that
updates the current progress reporter.")
(defmacro ement-with-progress-reporter (args &rest body)
"Eval BODY with a progress reporter according to ARGS.
ARGS is a plist of these values:
:when If specified, a form evaluated at runtime to determine
whether to make and update a progress reporter. If not
specified, the reporter is always made and updated.
:reporter A list of arguments passed to
`make-progress-reporter', which see.
Around BODY, the function `ement-progress-update' is set to a
function that calls `progress-reporter-update' on the progress
reporter (or if the :when form evaluates to nil, the function is
set to `ignore'). It optionally takes a VALUE argument, and
without one, it automatically updates the value from the
reporter's min-value to its max-value."
(declare (indent defun))
(pcase-let* ((progress-reporter-sym (gensym))
(progress-value-sym (gensym))
(start-time-sym (gensym))
((map (:when when-form) (:reporter reporter-args)) args)
(`(,_message ,min-value ,_max-value) reporter-args)
(update-fn `(cl-function
(lambda (&optional (value (cl-incf ,progress-value-sym)))
(ement-debug "Updating progress reporter to" value)
(progress-reporter-update ,progress-reporter-sym value)))))
`(let* ((,start-time-sym (current-time))
(,progress-value-sym (or ,min-value 0))
(,progress-reporter-sym ,(if when-form
`(when ,when-form
(make-progress-reporter ,@reporter-args))
`(make-progress-reporter ,@reporter-args))))
(cl-letf (((symbol-function 'ement-progress-update)
,(if when-form
`(if ,when-form
,update-fn
#'ignore)
update-fn)))
,@body
(ement-debug (format "Ement: Progress reporter done (took %.2f seconds)"
(float-time (time-subtract (current-time) ,start-time-sym))))))))
(require 'magit-section)
(cl-defmacro ement-with-room-and-session (&rest body)
"Eval BODY with `ement-room' and `ement-session' bound.
If in an `ement-room-list-mode' buffer and `current-prefix-arg'
is nil, use the room and session at point. If in an `ement-room'
buffer and `current-prefix-arg' is nil, use buffer-local value of
`ement-room' and `ement-session'. Otherwise, prompt for them
with `ement-complete-room' or that given with :prompt-form.
BODY may begin with property list arguments, including:
:prompt-form A Lisp form evaluated for the binding of
`ement-room'."
(declare (indent defun))
(pcase-let* ((plist (cl-loop while (keywordp (car body))
append (list (car body) (cadr body))
and do (setf body (cddr body))))
(prompt-form (or (plist-get plist :prompt-form)
'(ement-complete-room :suggest t))))
`(pcase-let* ((`[,list-room ,list-session] (if (eq 'ement-room-list-mode major-mode)
(oref (magit-current-section) value)
[nil nil]))
(ement-room (or list-room ement-room))
(ement-session (or list-session ement-session)))
(when (or current-prefix-arg (not ement-room))
(pcase-let ((`(,room ,session) ,prompt-form))
(setf ement-room room
ement-session session)))
,@body)))
(provide 'ement-macros)