Emacs: Check Interactive Call For Emacspeak
1 Background
Emacspeak uses advice as the means to speech-enable Emacs.
Emacspeak's advice forms need to check if the function being
speech-enabled is being called interactively — otherwise one would
get a lot of chatter as these functions get called from within elisp
programs, e.g. functions like forward-sexp or kill-sexp, that play
the dual role of both an interactive command, as well as a convenient
elisp function.
Until Emacs 24, the solution used was to write code that did the
following check:
(when (interactive-p) ...
In Emacs-24, interactive-p was made obsolete and replaced with
(called-interactively-p 'interactive)
Emacspeak initially used the above form to perform the equivalent
check. However, around the same time, Emacs' advice implementation
went through some changes, and there was an attempt to replace
advice.el with nadvice.el.
At the end of that round of changes, some problems emerged with the
new called-interactively-p implementation; specifically, calling
:called-interactively-p_ within around advice forms resulted in hard
to debug errors, including one case of infinite recursion involving
library smie.el when invoked from within ruby-mode.
After studying the problem in depth in 2014, I decided to create an
Emacspeak-specific implementation of the is-interactive check.
The resulting implementation has worked well for the last 30 months;
this article is here mostly to document how it works, and the reason
for its existence. Note that Emacspeak uses this custom predicate
only within advice forms. Further, this predicate has been coded
to only work within advice forms created by emacspeak. This
constraint can likely be relaxed, but the tighter implementation is
less risky.
2 Implementation — ems-interactive-p
2.1 Overview
Within an advice forms defined by Emacspeak, detect if the enclosing
function call is the result of explicit user interaction, i.e. by
pressing a key, or via an explicit call to
call-interactively. Emacspeak produces auditory feedback only if
this predicate returns t.
We first introduce a flag that will be used to record if the enclosing
(containing) function has an Emacspeak-defined advice on it and is
called interactively — these are the only cases that our predicate
needs to test.
(defvar ems-called-interactively-p nil "Flag that records if containing function was called interactively."
Next, we define a function that checks if interactive calls to a
function should be recorded. We're only interested in functions that
have an advice form defined by Emacspeak — all Emacspeak-defined
advice forms have the name emacspeak.
(defun ems-record-interactive-p (f) "Predicate to test if we need to record interactive calls of this function. Memoizes result for future use by placing a property 'emacspeak on the function symbol." (cond ((not (symbolp f)) nil) ((get f 'emacspeak) t) ; already memoized ((ad-find-some-advice f 'any "emacspeak") ; there is an emacspeak advice (put f 'emacspeak t)) ; memoize for future and return true (t nil)))
This is a memoized function that remembers earlier invocations by
setting property emacspeak on the function symbol.
All advice forms created by Emacspeak are named emacspeak, so we
can test for the presence of such advice forms using the test:
(ad-find-some-advice f 'any "emacspeak")
If this test returns T, we memoize the result and return it.
Next, we advice function call-interactively to check
if the function being called interactively is one of the functions
that has been adviced by Emacspeak. If so, we record the fact in the
previously declared global flag
ems-called-interactively-p.
(defadvice call-interactively (around emacspeak pre act comp) "Set emacspeak interactive flag if there is an Emacspeak advice on the function being called." (let ((ems-called-interactively-p ems-called-interactively-p)) ; preserve enclosing state (when (ems-record-interactive-p (ad-get-arg 0)) (setq ems-called-interactively-p (ad-get-arg 0))) ad-do-it))
We define an equivalent advice form on function
funcall-interactively as well. Now, whenever any function that has
been adviced by Emacspeak is called interactively, that interactive
call gets recorded in the global flag. In the custom Emacspeak
predicate we define, we check the value of this flag, and if
set, consume it, i.e. unset the flag and return T.
(defsubst ems-interactive-p () "Check our interactive flag. Return T if set and we are called from the advice for the current interactive command. Turn off the flag once used." (when ems-called-interactively-p ; interactive call (let ((caller (cl-second (backtrace-frame 1))) ; name of containing function (caller-advice ;advice generated wrapper (ad-get-advice-info-field ems-called-interactively-p 'advicefunname)) (result nil)) (setq result (or (eq caller caller-advice) ; called from our advice (eq ems-called-interactively-p caller))) ; called from advice wrapper (when result (setq ems-called-interactively-p nil) ; turn off now that we used it result))))
The only fragile part of the above predicate is the call to
backtrace-frame which we use to discover the name of the enclosing
function. Notice however that this is no more fragile than the current
implementation of called-interactively-p — which also uses
backtrace-frame; If there are changes in the byte-compiler, this
form may need to be updated. The implementation above has the
advantage of working correctly for Emacspeak's specific use-case.