2

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?

2

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)))
  • Thanks for detailed answer! It seems much more difficult than I thought. (defvar 'auto-translate-region-mode causes error, is it (defvar auto-translate-region-mode? And I need put defun before auto-translate-region-maybe? – ironsand Sep 2 '17 at 12:16
  • I'm using '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! – ironsand Sep 2 '17 at 12:17
  • @ironsand I fixed the typos, thanks. 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. – Gilles 'SO- stop being evil' Sep 2 '17 at 13:00
-1

From (elisp) Change Hooks:

-- Variable: after-change-functions This variable holds a list of functions to call after Emacs modifies a buffer. Each function receives three arguments: the beginning and end of the region just changed, and the length of the text that existed before the change. All three arguments are integers. The buffer that has been changed is always the current buffer when the function is called.

The length of the old text is the difference between the buffer positions before and after that text as it was before the change. As for the changed text, its length is simply the difference between the first two arguments.

  • That triggers on buffer changes, but buffer changes are not the most common reason for the selection to change: the most common reason is that the selection boundaries (point and mark) have changed, and that's more complicated to detect. – Gilles 'SO- stop being evil' Sep 2 '17 at 10:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.