4

When flyspell is enabled it doesn't show spelling errors on all visible text (like most text editors that support spell checking).

When moving the cursor over text, misspelled words become underlined.

While running flyspell-buffer works, it's a manual operation and can be slow on large files.

Is there a way to run the spell checker on visible text, updating after scrolling? Perhaps using delays so it doesn't make scrolling lag.

4
  • I don't like the window-scroll-functions hook because there are exceptions, which took me quite a bit of time to finally semi-understand [after pestering Eli and Stefan with a few bug reports ...]; and, I combined it with the post-command-hook to try and come up with something for all situations .... Eventually, I took the plunge and started learning C and am now pestering Eli and Stefan on the Emacs devel mailing list instead ... :) To see how this can work in Lisp, have a look at the linum.el library that has been deprecated in favor of built-in line number in Emacs 26+.
    – lawlist
    May 24, 2019 at 0:19
  • It seems the existing hooks aren't well suited to performing this operation, especially since launching a process from a timer is a no-no (gives many Blocking call to accept-process-output with quit inhibited!! messages).
    – ideasman42
    May 24, 2019 at 3:00
  • Why not just turn on the flyspell-mode?
    – scribe
    Jun 21, 2020 at 22:13
  • @scribe because it doesn't show errors of on-screen text, only text you type or move your cursor over. The only alternative is to run it on the entire buffer which can be slow (more than 5+ seconds on larger files).
    – ideasman42
    Jun 22, 2020 at 1:12

3 Answers 3

4

There are minor modes that do just this, although they don't use flyspell.

spell-fu

Example use:

(use-package spell-fu)
(global-spell-fu-mode)

wcheck-mode

Example use:

(use-package wcheck-mode
 :commands (wcheck-mode)
 :init
 (custom-set-faces
  '(wcheck-default-face ((t (:underline (:color: "red" :style wave))))))

 (setq wcheck-language-data
       '(("American English"
          (program . "/usr/bin/enchant-2")
          (args "-l" "-d" "en_US")
          (action-program . "/usr/bin/enchant-2")
          (action-args "-a" "-d" "en_US")
          (action-parser . enchant-suggestions-menu)
          (read-or-skip-faces
           ;; Only check comments & strings.
           ((emacs-lisp-mode c-mode)
            read
            font-lock-comment-face
            read
            font-lock-string-face)
           (nil)))))
 (setq wcheck-language "American English"))

speck-mode

Example use:

(load "speck")
(setq-default speck-syntactic t)  ;; Only check comments and strings.
(setq-default speck-engine 'Aspell)
(setq-default speck-delay 0.1)
(setq-default speck-pause 0.1)

(custom-set-faces
  '(speck-guess ((t (:underline (:color: "red" :style wave)))))
  '(speck-miss ((t (:underline (:color: "orange" :style wave)))))
  '(speck-doublet ((t (:underline (:color: "brown" :style wave)))))
  '(speck-mouse ((t (:underline (:color: "yellow" :style wave)))))
  '(speck-query ((t (:underline (:color: "or" :style wave))))))
(setq speck-personal-dictionary-file ispell-personal-dictionary)
1

For reference: The example below runs the spell checker on a timer, however it gives many messages:

Blocking call to accept-process-output with quit inhibited!!

.. Which I worked around by temporarily disabling logging, however this is far from ideal.

On the other hand, in practice I've found this to be usable.


(defvar-local flyspell-visible-last-window-range nil)

(defun flyspell-visible--overlay-refresh-from-timer ()
  (when (equal flyspell-visible--delay-buffer (current-buffer))
    (let* ((sta (window-start))
           (end (window-end))
           (range (list sta end)))
      (when (not (equal range flyspell-visible-last-window-range))
        (setq inhibit-redisplay t)

        (let ((log-max message-log-max))
          ;; don't log
          (setq message-log-max nil)
          (flyspell-region sta end)
          (setq message-log-max log-max)
          (message nil))
        (setq-local flyspell-visible-last-window-range range)
        (setq inhibit-redisplay nil)))))


;; Timer
(defvar flyspell-visible--delay-timer nil)
(defvar flyspell-visible--delay-buffer nil)

(defun flyspell-visible-turn-on ()
  "Enable spell checking on a timer"
  (when (timerp flyspell-visible--delay-timer)
    (cancel-timer flyspell-visible--delay-timer))
  ;; Delay for spelling - should be ok, only runs when scroll changes.
  (setq flyspell-visible--delay-buffer (current-buffer))
  (setq flyspell-visible--delay-timer
        (run-with-idle-timer
         1.0 t 'flyspell-visible--overlay-refresh-from-timer)))

(defun flyspell-visible-turn-off ()
  (when (timerp flyspell-visible--delay-timer)
    (cancel-timer flyspell-visible--delay-timer))
  (setq flyspell-visible--delay-timer nil)
  (setq flyspell-visible--delay-buffer nil)
  (setq flyspell-visible-last-window-range nil))
1
  • 1
    Instead of a pair of setqs, just do (let ((message-log-max nil)) (flyspell-region sta end) (message nil))
    – npostavs
    May 26, 2019 at 13:59
0

My reputation is not sufficient to "Add a comment", so writing in a full answer...

This is a copy-paste from ideasman42 for reference answer with flyspell-visible--overlay-refresh-from-timer function wrapped into with-local-quit macro. It should solve Blocking call to accept-process-output with quit inhibited!! messages problem.

Also, I replaced (flyspell-region sta end) invocation with (let ((flyspell-issue-message-flag nil)) (flyspell-small-region start end)):

(defvar-local flyspell-visible-last-window-range nil)

(defun flyspell-visible--overlay-refresh-from-timer ()
  (with-local-quit
    (when (equal flyspell-visible--delay-buffer (current-buffer))
      (let* ((start (window-start))
             (end (window-end))
             (range (list start end)))
        (when (not (equal range flyspell-visible-last-window-range))
          (setq inhibit-redisplay t)
          (let ((flyspell-issue-message-flag nil))
            (flyspell-small-region start end))
          (setq-local flyspell-visible-last-window-range range)
          (setq inhibit-redisplay nil))))))

;; Timer
(defvar flyspell-visible--delay-timer nil)
(defvar flyspell-visible--delay-buffer nil)

(defun flyspell-visible-turn-on ()
  (when (timerp flyspell-visible--delay-timer)
    (cancel-timer flyspell-visible--delay-timer))
  ;; Delay for spelling - should be ok, only runs when scroll changes.
  (setq flyspell-visible--delay-buffer (current-buffer))
  (setq flyspell-visible--delay-timer
    (run-with-idle-timer 1.0 t 'flyspell-visible--overlay-refresh-from-timer)))

(defun flyspell-visible-turn-off ()
  (when (timerp flyspell-visible--delay-timer)
    (cancel-timer flyspell-visible--delay-timer))
  (setq flyspell-visible--delay-timer nil)
  (setq flyspell-visible--delay-buffer nil)
  (setq flyspell-visible-last-window-range nil))

Another small improvement could be adding flyspell derived mode that runs flyspell-visible--delay-timer by default:

(define-minor-mode flyspell-visible-mode
  "Flyspell visible mode."
  :lighter " FlyV"
  :parent flyspell-mode
  (if flyspell-visible-mode
      (progn
        (flyspell-mode 1)
        (flyspell-visible-turn-on)
        (message "Flyspell visible mode enabled"))
    (flyspell-mode -1)
    (flyspell-visible-turn-on)
    (message "Flyspell visible mode disabled")))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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