ispell-multi.el -- multiple ispell processes and languages
;; ispell-multi.el -- multiple ispell processes and multiple flyspell languages
;; Copyright (C) 2005 P J Heslin
;; Author: Peter Heslin <[hidden email]>
;; URL: http://www.dur.ac.uk/p.j.heslin/emacs/download/ispell-multi.el ;; Version: 1.1
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; If you do not have a copy of the GNU General Public License, you
;; can obtain one by writing to the Free Software Foundation, Inc., 59
;; Temple Place - Suite 330, Boston, MA 02111-1307, USA.
;; ispell-multi.el enables Emacs to keep a number of ispell processes
;; alive in order to spell-check text efficiently in multiple
;; languages, and it provides a hook that tells flyspell to switch
;; languages depending on the value of a particular text property.
;; Normally, ispell.el only ever keeps one ispell/aspell process
;; alive. So if you have one buffer in which an English local
;; dictionary is used and another in which a German dictionary is
;; used, the ispell process will be killed and restarted every time
;; you run ispell in the other buffer. This is not really a problem,
;; since doing a spellcheck is infrequent and slow anyway. If you are
;; using flyspell-mode, however, the ispell process will be restarted
;; every time you switch buffers. Even this may not matter too much
;; to many people, since switching buffers is also a somewhat slow
;; Where the need for multiple ispell processes becomes really acute
;; is in buffers that have multiple languages in them and a way of
;; telling flyspell to switch local dictionaries depending on where
;; point is. In this case, the starting and stopping of ispell
;; processes very visibly impedes the fluid movement of the cursor.
;; I have written two packages that provide this sort of behavior.
;; One is flyspell-xml-lang.el, which tells flyspell what the local
;; language is in xml files depending on xml:lang attributes, and
;; another is flyspell-babel.el, which does the same with Babel
;; commands in LaTeX files.
;; The present package modifies ispell.el (via defadvice) so that
;; multiple ispell processes are kept alive to check different
;; languages. It requires version 3.6 of ispell, so users of Emacs
;; 21.3 and earlier will have to upgrade. This has only been tested
;; with GNU Emacs.
;; To install this package, just put it somewhere in your load-path
;; and put a (require 'ispell-multi) statement in your .emacs file.
;;; Using ispell-multi
;; If all you want to do is to change the behavior of ispell so that
;; it uses multiple ispell processes, then (require 'ispell-multi) is
;; all you need to do. The rest of this section is for those who want
;; to modify flyspell to switch languages within a buffer, based on
;; some criteria. See flyspell-xml-lang.el and flyspell-babel.el for
;; examples of the usage of all of the facilities described below.
;; Flyspell-mode provides a hook that runs before checking each word;
;; this allows you to change the value of ispell-local-dictionary to a
;; different language, depending on the context. If you have a
;; package that parses a buffer and figures out what languages are in
;; it and where they are, you can tell flyspell about it by setting
;; the text property `ispell-multi-lang' to the correct ispell
;; language (this can be any value that ispell-change-dictionary
;; accepts). Your package should set the value of the buffer-local
;; variable `flyspell-generic-check-word-p' to the symbol
;; `ispell-multi-verify'; do this *after* you have turned on
;; If you the set the buffer-local variable
;; `ispell-multi-flyspell-callback' to a symbol, the associated
;; function will be called every time flyspell is called to check a
;; word, and the `ispell-multi-lang' text property returns nil. This
;; function can be used to parse the buffer incrementally and set the
;; text-property lazily as the user moves through the buffer.
;; Since the parser callback is only invoked when the text-property is
;; nil, there is a possibility that the already-set text-property and
;; the changed contents of the buffer will get out of sync. To fix
;; this you will probably also want to arrange for the parser to be
;; run by an idle-timer. You can arrange for this by calling the
;; function `ispell-multi-idler-setup' with a single argument, giving
;; the delay.
;; If you want to indicate that some text should not be spell-checked,
;; set the `ispell-multi-lang' text property to the string "void". To
;; indicate a reversion to the default ispell dictionary, use the
;; string "default".
;;; Aspell vs. Ispell
;; ispell.el and ispell-multi.el will work happily with aspell instead
;; of ispell, since the former can emulate the latter. Aspell has
;; many advantages over ispell, including a very large selection of
;; language dictionaries, and it is much better able to suggest the
;; correct spelling (which is quite handy when using
;; flyspell-auto-correct-previous-word). If you prefer to use aspell,
;; you can put the following into your .emacs:
;; (setq ispell-program-name "aspell")
;; (setq ispell-really-aspell t)
;; (setq ispell-extra-args '("--sug-mode=fast"))
;; The first two lines tell ispell.el to run aspell instead of ispell,
;; and the third line tells aspell not to use its default algorithm
;; for suggesting spellings, but to use a faster one; the default is
;; very accurate, but can be a bit slow for use with flyspell. If
;; this is not fast enough, try "ultra" instead of "fast" (but even
;; ultra mode is still two times slower than ispell).
;; If you are installing a new aspell dictionary that ispell.el does
;; not know about, you will have to add it to
;; ispell-local-dictionary-alist; see the documentation of
;; flyspell-xml-lang.el for an example.
;;; Implementation Notes
;; We try to share ispell processes between buffers, so that a single
;; process can service all buffers or regions in a given language.
;; But if you put buffer-local variables that modify the behavior of
;; ispell for a given buffer (such as LocalWords), then that buffer's
;; ispell processes will not be shared.
;; Each buffer is responsible for killing the ispell processes that it
;; starts. This means that when the buffer that starts a process for
;; a given language is killed, it will kill all the processes it has
;; started, even if they are in use by another buffer. Thus a new
;; ispell process for that language will have to be started next time
;; you switch to that other buffer.
;;; Bugs and Limitations
;; If you change ispell dictionaries by using the function
;; `ispell-change-dictionary', then an ispell process will be killed,
;; where it would not have been if you had simply set
;; ispell-local-dictionary. That's because this package just modifies
;; the way ispell deals with local variables like
;; ispell-local-dictionary; it doesn't touch the
;; ispell-change-dictionary function. Maybe it should. The necessary
;; ispell process will be re-started next time you need it, so this is
;; not really a bug so much as a slight performance issue.
;; It would be better to keep track of all of the buffers that have
;; used a given process, so that we would refrain from killing it if
;; we know that there is another buffer that might be interested in
;; using it. This would add complexity to the implementation,
;; It might have been nice to put in here the code to inspect a
;; text-property to find out the language of the text, so that ispell
;; in general would obey a certain property and change dictionary
;; accordingly. This won't work, though, since ispell-region works on
;; a line-by-line basis, which would fail in the case of a mid-line
;; In the abstract, it might be nice to share ispell processes between
;; buffers that have identical buffer-local modifications to ispell's
;; behavior, but that would be very complex to implement, and I think
;; unlikely to be useful in practice.
;; flyspell-large-region, which is the fast mode of flyspell that it
;; uses when checking the entirety of a large buffer, does not work at
;; all, since it depends on launching a single ispell process for this
;; purpose and so cannot cope with multiple languages. For this
;; reason, flyspell-large-region should be disabled in buffers using
;; this package.
;; 1.0 Pre-release
;; 1.1 Worked around ispell-current-dictionary / ispell-dictionary
;; inconsistency in Emacs CVS / stand-alone ispell.el
;; For Emacs 21.3, we have to use an updated ispell.el (3.6 or from
;; Emacs CVS), and for some reason we may have to load it again to get
;; ispell-dictionary-alist set properly.
(unless (assoc "english" ispell-dictionary-alist)
;; For updated ispell.el with emacs < 21.3.5
(when (not (fboundp 'set-process-query-on-exit-flag))
(defun set-process-query-on-exit-flag (one two) ()))
; In current Emacs CVS, the variable ispell-current-dictionary is used
; to indicate the dictionary associated with the current ispell
; process, while in the current, separately distributed version of
; ispell.el (3.7beta), this variable does not exist, and
; ispell-dictionary serves this purpose.
(if (boundp 'ispell-current-dictionary)
(defvar ispell-multi-processes nil
"Buffer-local variable to record a list of all of the ispell
processes started by this buffer.")
(defvar ispell-multi-lang-process nil
"Alist mapping languages to ispell processes. Only for
processes without any buffer-local modifications")
(defvar ispell-multi-lang-process-local nil
"As ispell-multi-lang-process, but a buffer-local alist, to use
for processes with buffer-local modifications")
(defvar ispell-multi-flyspell-verify-default nil
"The original value of `flyspell-generic-check-word-p', before
it was overridden in order to invoke this package; taken from
the the `flyspell-mode-predicate' property of the major mode
(defvar ispell-multi-flyspell-callback nil
"Buffer local variable that indicates a function to call when
flyspell is checking a word and the text property
`ispell-multi-lang' is nil. This function will normally set
that property at point and for some of the text in the
(defvar ispell-multi-valid-dictionary-list nil
"Cached value of ispell-valid-dictionary-list.")
(when (fboundp 'ispell-valid-dictionary-list)
;; This is our hook into ispell.el.
(defadvice ispell-accept-buffer-local-defs (around ispell-multi-advice activate)
"Advice that changes ispell to enable multiple ispell processes."
(let* ((local-mods (ispell-multi-buffer-local-modifications-p))
(alist (if local-mods
(stored-process (cdr (assoc ispell-local-dictionary (symbol-value alist)))))
;; Store the currently running process if we haven't already
(when (and ispell-process
(eq (process-status ispell-process) 'run)
(not (rassq ispell-process (symbol-value alist))))
(set alist (cons (cons (symbol-value ispell-multi-current-dictionary-var) ispell-process)
(setq ispell-multi-processes (cons ispell-process ispell-multi-processes)))
;; Do we already have a process for this language?
(if (and stored-process
(eq (process-status stored-process) 'run))
(setq ispell-process stored-process)
;; When ispell-current-dictionary / ispell-dictionary is
;; the same as ispell-local-dictionary, ispell.el will
;; refrain from killing the process
(set ispell-multi-current-dictionary-var ispell-local-dictionary))
;; This is to fool ispell into not killing the old process when
;; it starts the new one. But we don't want a new process if
;; the current one is correct, or if the new dict is void.
(unless (or (equal (symbol-value ispell-multi-current-dictionary-var) ispell-local-dictionary)
(equal ispell-local-dictionary "void"))
(setq ispell-process nil))
;; Possibly start a new process
(unless (equal ispell-local-dictionary "void") ; ensure against error
(defun ispell-multi-kill-processes-hook ()
"Kill all ispell processes started by this buffer"
(setq ispell-process (car ispell-multi-processes))
(when (eq (process-status ispell-process) 'run)
(setq ispell-multi-processes (cdr ispell-multi-processes)))
(defun ispell-multi-hack-flyspell-modeline ()
"Add a modeline entry for flyspell that indicates the current
language in parentheses."
(let ((lang (get-text-property (point) 'ispell-multi-lang)))
(concat " (" (capitalize lang) ")")))))) minor-mode-alist)))