I want to create a minor mode that translates selected text. So I want to call a function when selection is changed.
How can I detect the change of selection?
Emacs provides a number of hook variables, each of which contains a list of functions that are executed when a specific event happens. But there isn't a hook that indicates that the selection has changed.
The selection, called “region” in Emacs terminology, is the part of the buffer between the point (the cursor) and the mark, and the selection is highlighted when the mark is active. There are hooks for mark (de)activation, but there's no hook triggered when the point (or the mark changes). Furthermore the selection can change because the text changes — in Emacs this can happen even when the selection is active.
So if you want to detect all selection changes, you have to add your code to post-command-hook
.
(add-hook 'post-command-hook 'auto-translate-region-maybe)
If running your code after each command is too expensive, you could run it on a delay instead, by setting an idle timer.
(run-with-idle-timer 1 nil 'auto-translate-region-maybe)
The reason I called the function “maybe” is that you probably want to run it in certain buffers, but post-command-hook
and timers are global. Detecting which buffer may need a translation is not straightforward since the active buffer may have just changed, especially with the timer method.
Doing this correctly and efficiently turns out to be pretty complicated. Unfortunately I didn't find a package that monitors region changes and provides a proper region-changed-hook
. In my answer I'll keep things (relatively) simple and show how to display a translation only for the active window's buffer.
Translation is pretty expensive so you should avoid running it again with the same arguments. Remember the last string you sent and don't update the translation result if that hasn't changed.
(defvar auto-translate-region-mode nil
"Automatically translate the selection.")
(make-variable-buffer-local 'auto-translate-region-mode)
(defvar auto-translate--last-text nil)
(make-variable-buffer-local 'auto-translate--last-text)
(defvar auto-translate--last-translation nil)
(make-variable-buffer-local 'auto-translate--last-translation)
(defun auto-translate-call-engine (text)
"Translate TEXT and return the result."
TODO)
(defun auto-translate-region-redisplay ()
"Redisplay the last cached region translation."
;; Display auto-translate--last-translation somewhere
TODO)
(defun auto-translate-region-now ()
"Display the region's translation."
;; Get the selected text and normalize it a bit
(let* ((raw-text (buffer-substring-no-properties (region-beginning) (region-end)))
(text (replace-regexp-in-string "\\`[^[:alpha:]]+" ""
(replace-regexp-in-string "[^[:alpha:]]+\\'" "" raw-text))))
(unless (string= text auto-translate--last-text)
;; The selected text has changed, so update the translation
(setq auto-translate--last-translation (auto-translate-call-engine text)
auto-translate--last-text text))
;; (Re)display the translated text
(auto-translate-region-redisplay)))
(defun auto-translate-region-maybe ()
"Display the region's translation if automatic region translation is active
and the mark is active."
(if (and auto-translate-region-mode mark-active)
(auto-translate-region-translate)))
(defvar 'auto-translate-region-mode
causes error, is it (defvar auto-translate-region-mode
? And I need put defun
before auto-translate-region-maybe
?
'google-translate-at-point
from google-translate.el
. to replace TODO
, but it doesn't work for me. I'll check the code again later. Thanks for your help!
google-translate-at-point
takes care of looking up the region. With the structure I gave, you'd call google-translate-translate
inside auto-translate-call-engine
.
Commented
Sep 2, 2017 at 13:00