6

I would like to use an idle timer that is local to the current buffer in one of my packages. However, I can't seem to find out how.

How can I create (or fake) the behavior of a buffer local idle timer? Is the only way to call a function that checks (current-buffer)?

PythonNut
  • 10,243
  • 2
  • 29
  • 75
  • 3
    Here is a link to a related thread entitled **Emacs — creating / deleting a `buffer-local` repeating idle-timer**: http://stackoverflow.com/questions/24007822/emacs-creating-deleting-a-buffer-local-repeating-idle-timer – lawlist May 19 '15 at 22:07
  • Ah, thank you. What does common etiquette dictate I do now? – PythonNut May 19 '15 at 22:19
  • 1
    Try out the solution in the related thread -- if it doesn't work entirely to your satisfaction, then clarify your question. If it works for your needs, then you can make the decision how to handle it. There are some people who prefer to treat this forum as a separate entity when classifying a thread as a duplicate, whereas other people would prefer that it be marked as a duplicate (if an answer exists in another forum) or deleted. In either case, you may need to spend some time trying out the solution in various contexts before you are sure it adequately addresses your needs. – lawlist May 19 '15 at 22:22
  • 1
    If it works for you, please post an answer here with appropriate reference to the other one. Closing or deleting this question would deprive our nice experts (who might not be on SO) of the chance to provide a better answer. – Malabarba May 20 '15 at 09:10
  • At minimum it would be helpful to ensure that both Q&As are cross-referenced via comments. Personally I feel that if strong answers already exist on another site in the network, you should endeavour to boost/strengthen that location as the source of answers to this question, rather than risk splitting the solutions across multiple locations (which is detrimental to the people searching for them). (edit: Ah, this comment was written before I actually followed the link. I didn't realise it was referring to one of my answers! FWIW I wasn't attempting to suggest that my answer was definitive.) – phils Jul 19 '15 at 10:32

4 Answers4

5

(defun run-with-local-idle-timer (secs repeat function &rest args)
  "Like `run-with-idle-timer', but always runs in the `current-buffer'.

Cancels itself, if this buffer was killed."
  (let* (;; Chicken and egg problem.
         (fns (make-symbol "local-idle-timer"))
         (timer (apply 'run-with-idle-timer secs repeat fns args))
         (fn `(lambda (&rest args)
                (if (not (buffer-live-p ,(current-buffer)))
                    (cancel-timer ,timer)
                  (with-current-buffer ,(current-buffer)
                    (apply (function ,function) args))))))
    (fset fns fn)
    fn))
politza
  • 3,316
  • 14
  • 16
0
(set (make-local-variable 'my-timer)
     (run-with-idle-timer...
Andreas Röhler
  • 1,894
  • 10
  • 10
0

If you mean a way to run some code with idle timer in such a way that the passed code runs in the original buffer, maybe you can adapt the following snippet that I am currently using. Please keep in mind that this snippet assumes that it is placed in an *.el file with lexical binding.

(defun my-delayed-tex-font-lock ()
  (let ((here (current-buffer)))
    (run-with-idle-timer
     10 nil
     (lambda ()
       (with-current-buffer here
         (my-tex-font-lock))))))
(add-hook 'TeX-mode-hook 'my-delayed-tex-font-lock)

What this snippet does is make my function my-tex-font-lock run after ten seconds of idle time for each TeX file buffer. When I open alice.tex, quickly switch to bob.txt and wait for ten seconds doing nothing, then my-tex-font-lock will kick in and it will affect the alice.tex buffer but not bob.txt.

Jisang Yoo
  • 101
  • 1
0

From looking into this, it's possible for a buffer local minor-mode to manage a global timer.

The global timer is disabled when the buffer-local mode becomes inactive and is re-enabled when it becomes active. There is additional logic to ensure a buffer that becomes inactive isn't left in a stale state too.

This is done using a global idle timer and window-state-change-hook to track changes to the active buffer.

The logic to manage timers is under the section Internal Timer Management.

;;; my-hl-line.el --- Highlight the current line (example package) -*- lexical-binding: t -*-

;; ---------------------------------------------------------------------------
;; Custom Variables

(defgroup my-hl-line nil "Highlight the current line, as an example." :group 'faces)

(defcustom my-hl-line-idle-time 0.1
  "Time after which to highlight the line."
  :group 'my-hl-line
  :type 'float)

;; ---------------------------------------------------------------------------
;; Internal Functions
;;
;; This is sample code, it highlights a line.

(defsubst my-hl-line--do-highlight-clear ()
  "Clear current highlight."
  (remove-overlays (point-min) (point-max) 'my-hl-line t))

(defun my-hl-line--do-highlight-set ()
  "Highlight the current line."
  (my-hl-line--do-highlight-clear)
  (pcase-let* ((`(,beg . ,end) (bounds-of-thing-at-point 'line)))
    (let ((ov (make-overlay beg end)))
      (overlay-put ov 'face '(:inverse-video t :extend t))
      (overlay-put ov 'my-hl-line t))))

;; ---------------------------------------------------------------------------
;; Internal Timer Management
;;
;; This works as follows:
;;
;; - The timer is kept active as long as the local mode is enabled.
;; - Entering a buffer runs the buffer local `window-state-change-hook'
;;   immediately which checks if the mode is enabled,
;;   set up the global timer if it is.
;; - Switching any other buffer wont run this hook,
;;   rely on the idle timer it's self running, which detects the active mode,
;;   canceling it's self if the mode isn't active.
;;
;; This is a reliable way of using a global,
;; repeating idle timer that is effectively buffer local.
;;

;; Global idle timer (repeating), keep active while the buffer-local mode is enabled.
(defvar my-hl-line--global-timer nil)
;; When t, the timer will update buffers in all other visible windows.
(defvar my-hl-line--dirty-flush-all nil)
;; When true, the buffer should be updated when inactive.
(defvar-local my-hl-line--dirty nil)

(defun my-hl-line--time-callback-or-disable ()
  "Callback that run the repeat timer."

  ;; Ensure all other buffers are highlighted on request.
  (let ((is-mode-active (bound-and-true-p my-hl-line-mode)))
    ;; When this buffer is not in the mode, flush all other buffers.
    (cond
      (is-mode-active
        ;; Don't update in the window loop to ensure we always
        ;; update the current buffer in the current context.
        (setq my-hl-line--dirty nil))
      (t
        ;; If the timer ran when in another buffer,
        ;; a previous buffer may need a final refresh, ensure this happens.
        (setq my-hl-line--dirty-flush-all t)))

    (when my-hl-line--dirty-flush-all
      ;; Run the mode callback for all other buffers in the queue.
      (dolist (frame (frame-list))
        (dolist (win (window-list frame -1))
          (let ((buf (window-buffer win)))
            (when
              (and
                (buffer-local-value 'my-hl-line-mode buf)
                (buffer-local-value 'my-hl-line--dirty buf))
              (with-selected-frame frame
                (with-selected-window win
                  (with-current-buffer buf
                    (setq my-hl-line--dirty nil)
                    (my-hl-line--do-highlight-set)))))))))
    ;; Always keep the current buffer dirty
    ;; so navigating away from this buffer will refresh it.
    (if is-mode-active
      (setq my-hl-line--dirty t))

    (cond
      (is-mode-active
        (my-hl-line--do-highlight-set))
      (t ;; Cancel the timer until the current buffer uses this mode again.
        (my-hl-line--time-ensure nil)))))

(defun my-hl-line--time-ensure (state)
  "Ensure the timer is enabled when STATE is non-nil, otherwise disable."
  (cond
    (state
      (unless my-hl-line--global-timer
        (setq my-hl-line--global-timer
          (run-with-idle-timer
            my-hl-line-idle-time
            :repeat 'my-hl-line--time-callback-or-disable))))
    (t
      (when my-hl-line--global-timer
        (cancel-timer my-hl-line--global-timer)
        (setq my-hl-line--global-timer nil)))))

(defun my-hl-line--time-reset ()
  "Run this when the buffer changes."
  ;; Ensure changing windows doesn't leave other buffers with stale highlight.
  (cond
    ((bound-and-true-p my-hl-line-mode)
      (setq my-hl-line--dirty-flush-all t)
      (setq my-hl-line--dirty t)
      (my-hl-line--time-ensure t))
    (t
      (my-hl-line--time-ensure nil))))

(defun my-hl-line--time-buffer-local-enable ()
  "Ensure buffer local state is enabled."
  ;; Needed in case focus changes before the idle timer runs.
  (setq my-hl-line--dirty-flush-all t)
  (setq my-hl-line--dirty t)
  (my-hl-line--time-ensure t)
  (add-hook 'window-state-change-hook #'my-hl-line--time-reset nil t))

(defun my-hl-line--time-buffer-local-disable ()
  "Ensure buffer local state is disabled."
  (kill-local-variable 'my-hl-line--dirty)
  (my-hl-line--time-ensure nil)
  (remove-hook 'window-state-change-hook #'my-hl-line--time-reset t))

;; ---------------------------------------------------------------------------
;; Public Functions

;;;###autoload
(define-minor-mode my-hl-line-mode
  "Idle-Highlight Minor Mode."
  :group 'my-hl-line
  :global nil

  (cond
    (my-hl-line-mode
      (my-hl-line--time-buffer-local-enable))
    (t
      (my-hl-line--time-buffer-local-disable)
      (my-hl-line--do-highlight-clear))))

(provide 'my-hl-line-mode)
;;; my-hl-line-mode.el ends here
ideasman42
  • 8,375
  • 1
  • 28
  • 105