Fixing Emacs's FlySpell Mode for Texts with Multiple Alphabets
Recently I have encountered a problem in Emacs when using flyspell-buffer command to spell-check a text in Emacs. I tracked down the problem and decided to report the bug.
The problem was complicated and it was possible to reproduce it only for texts which use more than one writing system (e. g. Latin and Cyrillic) and only under some circumstances.
Emacs developers were very responsive and after few days Eli Zaretskii tracked down the root of the problem and provided a better fix which is going to be incorporated into Emacs 26.2.
If you happen to use an older version of Emacs and have to spell-check some multilingual texts often, you might want to consider redefining flyspell-external-point-words in your configuration file with the version, provided below.
(defun flyspell-external-point-words () "Mark words from a buffer listing incorrect words in order of appearance. The list of incorrect words should be in `flyspell-external-ispell-buffer'. \(We finish by killing that buffer and setting the variable to nil.) The buffer to mark them in is `flyspell-large-region-buffer'." (let* (words-not-found (flyspell-casechars (flyspell-get-casechars)) (ispell-otherchars (ispell-get-otherchars)) (ispell-many-otherchars-p (ispell-get-many-otherchars-p)) (word-chars (concat flyspell-casechars "+\\(" (if (not (string= "" ispell-otherchars)) (concat ispell-otherchars "?")) flyspell-casechars "+\\)" (if ispell-many-otherchars-p "*" "?"))) (buffer-scan-pos flyspell-large-region-beg) case-fold-search) (with-current-buffer flyspell-external-ispell-buffer (goto-char (point-min)) ;; Loop over incorrect words, in the order they were reported, ;; which is also the order they appear in the buffer being checked. (while (re-search-forward "\\([^\n]+\\)\n" nil t) ;; Bind WORD to the next one. (let ((word (match-string 1)) (wordpos (point))) ;; Here there used to be code to see if WORD is the same ;; as the previous iteration, and count the number of consecutive ;; identical words, and the loop below would search for that many. ;; That code seemed to be incorrect, and on principle, should ;; be unnecessary too. -- rms. (if flyspell-issue-message-flag (message "Spell Checking...%d%% [%s]" (floor (* 100.0 (point)) (point-max)) word)) (with-current-buffer flyspell-large-region-buffer (goto-char buffer-scan-pos) (let ((keep t)) ;; Iterate on string search until string is found as word, ;; not as substring. (while keep (if (search-forward word flyspell-large-region-end t) (let* ((found-list (save-excursion ;; Move back into the match ;; so flyspell-get-word will find it. (forward-char -1) ;; Is this a word that matches the ;; current dictionary? (if (looking-at word-chars) (flyspell-get-word)))) (found (car found-list)) (found-length (length found)) (misspell-length (length word))) (when (or ;; Misspelled word is not from the ;; language supported by the current ;; dictionary. (null found) ;; Size matches, we really found it. (= found-length misspell-length) ;; Matches as part of a boundary-char separated ;; word. (member word (split-string found ispell-otherchars)) ;; Misspelling has higher length than ;; what flyspell considers the word. ;; Caused by boundary-chars mismatch. ;; Validating seems safe. (< found-length misspell-length) ;; ispell treats beginning of some TeX ;; commands as nroff control sequences ;; and strips them in the list of ;; misspelled words thus giving a ;; non-existent word. Skip if ispell ;; is used, string is a TeX command ;; (char before beginning of word is ;; backslash) and none of the previous ;; conditions match. (and (not ispell-really-aspell) (not ispell-really-hunspell) (not ispell-really-enchant) (save-excursion (goto-char (- (nth 1 found-list) 1)) (if (looking-at "[\\]" ) t nil)))) (setq keep nil) ;; Don't try spell-checking words whose ;; characters don't match CASECHARS, because ;; flyspell-word will then consider as ;; misspelling the preceding word that matches ;; CASECHARS. (or (null found) (flyspell-word nil t)) ;; Search for next misspelled word will begin from ;; end of last validated match. (setq buffer-scan-pos (point)))) ;; Record if misspelling is not found and try new one (cl-pushnew (concat " -> " word " - " (int-to-string wordpos)) words-not-found :test #'equal) (setq keep nil))))))) ;; we are done (if flyspell-issue-message-flag (message "Spell Checking completed."))) ;; Warn about not found misspellings (dolist (word words-not-found) (message "%s: word not found" word)) ;; Kill and forget the buffer with the list of incorrect words. (kill-buffer flyspell-external-ispell-buffer) (setq flyspell-external-ispell-buffer nil)))
Alternatively, you can put the function definition into separate file (flyspell-fix.el) inside your Emacs configuration directory (~/.emacs.d) and load it only for Emacs versions which are older than 26.2.
After creating flyspell-fix.el file, as mentioned above, add the following lines into your Emacs's configuration file:
;; fix flyspell for older versions of Emacs. (when (or (< emacs-major-version 26) (and (= emacs-major-version 26) (< emacs-minor-version 2))) (load (concat user-emacs-directory "flyspell-fix.el")))
Categories: Hacks Emacs Programming