aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilip Kaludercic2025-08-22 15:05:12 +0200
committerPhilip Kaludercic2025-09-01 22:31:01 +0200
commitd2532a4ef0a1c037075b8a9d44b2dbdce372ef25 (patch)
treed4ba1c6f9ded64c92ac5ed81b3baa3d74b856954
parentb52ccb997d598caa321141c0abb553d3b3803eee (diff)
downloademacs-d2532a4ef0a1c037075b8a9d44b2dbdce372ef25.tar.gz
emacs-d2532a4ef0a1c037075b8a9d44b2dbdce372ef25.zip
Add new library 'timeout'
* lisp/emacs-lisp/timeout.el: Add the file. * etc/NEWS: Mention the library. See https://mail.gnu.org/archive/html/emacs-devel/2025-07/msg00520.html.
-rw-r--r--etc/NEWS6
-rw-r--r--lisp/emacs-lisp/timeout.el243
2 files changed, 249 insertions, 0 deletions
diff --git a/etc/NEWS b/etc/NEWS
index 02a556a557d..630d03a1fa0 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2729,6 +2729,12 @@ enabled for files named "go.work".
2729** New package 'lua-mode'. 2729** New package 'lua-mode'.
2730The 'lua-mode' package from Non-GNU ELPA is now included in Emacs. 2730The 'lua-mode' package from Non-GNU ELPA is now included in Emacs.
2731 2731
2732** New library 'timeout'.
2733This library that provides higher order functions to throttle or
2734debounce Elisp functions. This is useful for corraling over-eager code
2735that is slow and blocks Emacs or does not provide customization options
2736to limit how often it runs.
2737
2732 2738
2733* Incompatible Lisp Changes in Emacs 31.1 2739* Incompatible Lisp Changes in Emacs 31.1
2734 2740
diff --git a/lisp/emacs-lisp/timeout.el b/lisp/emacs-lisp/timeout.el
new file mode 100644
index 00000000000..c949e7a912e
--- /dev/null
+++ b/lisp/emacs-lisp/timeout.el
@@ -0,0 +1,243 @@
1;;; timeout.el --- Throttle or debounce Elisp functions -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2023-2025 Free Software Foundation, Inc.
4
5;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
6;; Maintainer: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
7;; Keywords: convenience, extensions
8;; Version: 2.0
9;; Package-Requires: ((emacs "24.4"))
10;; URL: https://github.com/karthink/timeout
11
12;; This program is free software; you can redistribute it and/or modify
13;; it under the terms of the GNU General Public License as published by
14;; the Free Software Foundation, either version 3 of the License, or
15;; (at your option) any later version.
16
17;; This program is distributed in the hope that it will be useful,
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
21
22;; You should have received a copy of the GNU General Public License
23;; along with this program. If not, see <https://www.gnu.org/licenses/>.
24
25;;; Commentary:
26
27;; timeout is a small Elisp library that provides higher order functions to
28;; throttle or debounce Elisp functions. This is useful for corralling
29;; over-eager code that:
30;; (i) is slow and blocks Emacs, and
31;; (ii) does not provide customization options to limit how often it runs,
32;;
33;; To throttle a function FUNC to run no more than once every 2 seconds, run
34;; (timeout-throttle 'func 2.0)
35;;
36;; To debounce a function FUNC to run after a delay of 0.3 seconds, run
37;; (timeout-debounce 'func 0.3)
38;;
39;; To create a new throttled or debounced version of FUNC instead, run
40;;
41;; (timeout-throttled-func 'func 2.0)
42;; (timeout-debounced-func 'func 0.3)
43;;
44;; You can bind this via `defalias':
45;;
46;; (defalias 'throttled-func (timeout-throttled-func 'func 2.0))
47;;
48;; The interactive spec and documentation of FUNC is carried over to the new
49;; function.
50
51;;; Code:
52
53(require 'nadvice)
54
55(defun timeout--throttle-advice (&optional timeout)
56 "Return a function that throttles its argument function.
57
58TIMEOUT defaults to 1 second.
59
60When FUNC does not run because of the throttle, the result from the
61previous successful call is returned.
62
63This is intended for use as function advice."
64 (let ((throttle-timer)
65 (timeout (or timeout 1.0))
66 (result))
67 (lambda (orig-fn &rest args)
68 "Throttle calls to this function."
69 (prog1 result
70 (unless (and throttle-timer (timerp throttle-timer))
71 (setq result (apply orig-fn args))
72 (setq throttle-timer
73 (run-with-timer
74 timeout nil
75 (lambda ()
76 (cancel-timer throttle-timer)
77 (setq throttle-timer nil)))))))))
78
79(defun timeout--debounce-advice (&optional delay default)
80 "Return a function that debounces its argument function.
81
82DELAY defaults to 0.50 seconds. The function returns immediately with
83value DEFAULT when called the first time. On future invocations, the
84result from the previous call is returned.
85
86This is intended for use as function advice."
87 (let ((debounce-timer nil)
88 (delay (or delay 0.50)))
89 (lambda (orig-fn &rest args)
90 "Debounce calls to this function."
91 (prog1 default
92 (if (timerp debounce-timer)
93 (timer-set-idle-time debounce-timer delay)
94 (setq debounce-timer
95 (run-with-idle-timer
96 delay nil
97 (lambda (buf)
98 (cancel-timer debounce-timer)
99 (setq debounce-timer nil)
100 (setq default
101 (if (buffer-live-p buf)
102 (with-current-buffer buf
103 (apply orig-fn args))
104 (apply orig-fn args))))
105 (current-buffer))))))))
106
107(defun timeout-debounce (func &optional delay default)
108 "Debounce FUNC by making it run DELAY seconds after it is called.
109
110This advises FUNC, when called (interactively or from code), to
111run after DELAY seconds. If FUNC is called again within this time,
112the timer is reset.
113
114DELAY defaults to 0.5 seconds. Using a delay of 0 removes any
115debounce advice.
116
117The function returns immediately with value DEFAULT when called the
118first time. On future invocations, the result from the previous call is
119returned."
120 (if (and delay (= delay 0))
121 (advice-remove func 'debounce)
122 (advice-add func :around (timeout--debounce-advice delay default)
123 '((name . debounce)
124 (depth . -99)))))
125
126(defun timeout-throttle (func &optional throttle)
127 "Make FUNC run no more frequently than once every THROTTLE seconds.
128
129THROTTLE defaults to 1 second. Using a throttle of 0 removes any
130throttle advice.
131
132When FUNC does not run because of the throttle, the result from the
133previous successful call is returned."
134 (if (and throttle (= throttle 0))
135 (advice-remove func 'throttle)
136 (advice-add func :around (timeout--throttle-advice throttle)
137 '((name . throttle)
138 (depth . -98)))))
139
140(defun timeout-throttled-func (func &optional throttle)
141 "Return a throttled version of function FUNC.
142
143The throttled function runs no more frequently than once every THROTTLE
144seconds. THROTTLE defaults to 1 second.
145
146When FUNC does not run because of the throttle, the result from the
147previous successful call is returned."
148 (let ((throttle-timer nil)
149 (throttle (or throttle 1))
150 (result))
151 (if (commandp func)
152 ;; INTERACTIVE version
153 (lambda (&rest args)
154 (:documentation
155 (concat
156 (documentation func)
157 (format "\n\nThrottle calls to this function by %f seconds" throttle)))
158 (interactive (advice-eval-interactive-spec
159 (cadr (interactive-form func))))
160 (prog1 result
161 (unless (and throttle-timer (timerp throttle-timer))
162 (setq result (apply func args))
163 (setq throttle-timer
164 (run-with-timer
165 throttle nil
166 (lambda ()
167 (cancel-timer throttle-timer)
168 (setq throttle-timer nil)))))))
169 ;; NON-INTERACTIVE version
170 (lambda (&rest args)
171 (:documentation
172 (concat
173 (documentation func)
174 (format "\n\nThrottle calls to this function by %f seconds" throttle)))
175 (prog1 result
176 (unless (and throttle-timer (timerp throttle-timer))
177 (setq result (apply func args))
178 (setq throttle-timer
179 (run-with-timer
180 throttle nil
181 (lambda ()
182 (cancel-timer throttle-timer)
183 (setq throttle-timer nil))))))))))
184
185(defun timeout-debounced-func (func &optional delay default)
186 "Return a debounced version of function FUNC.
187
188The debounced function runs DELAY seconds after it is called. DELAY
189defaults to 0.5 seconds.
190
191The function returns immediately with value DEFAULT when called the
192first time. On future invocations, the result from the previous call is
193returned."
194 (let ((debounce-timer nil)
195 (delay (or delay 0.50)))
196 (if (commandp func)
197 ;; INTERACTIVE version
198 (lambda (&rest args)
199 (:documentation
200 (concat
201 (documentation func)
202 (format "\n\nDebounce calls to this function by %f seconds" delay)))
203 (interactive (advice-eval-interactive-spec
204 (cadr (interactive-form func))))
205 (prog1 default
206 (if (timerp debounce-timer)
207 (timer-set-idle-time debounce-timer delay)
208 (setq debounce-timer
209 (run-with-idle-timer
210 delay nil
211 (lambda (buf)
212 (cancel-timer debounce-timer)
213 (setq debounce-timer nil)
214 (setq default
215 (if (buffer-live-p buf)
216 (with-current-buffer buf
217 (apply func args))
218 (apply func args))))
219 (current-buffer))))))
220 ;; NON-INTERACTIVE version
221 (lambda (&rest args)
222 (:documentation
223 (concat
224 (documentation func)
225 (format "\n\nDebounce calls to this function by %f seconds" delay)))
226 (prog1 default
227 (if (timerp debounce-timer)
228 (timer-set-idle-time debounce-timer delay)
229 (setq debounce-timer
230 (run-with-idle-timer
231 delay nil
232 (lambda (buf)
233 (cancel-timer debounce-timer)
234 (setq debounce-timer nil)
235 (setq default
236 (if (buffer-live-p buf)
237 (with-current-buffer buf
238 (apply func args))
239 (apply func args))))
240 (current-buffer)))))))))
241
242(provide 'timeout)
243;;; timeout.el ends here