gnu-emacs-sources
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

flyspell-babel.el -- Switch flyspell lang according to LaTeX commands


From: Peter Heslin
Subject: flyspell-babel.el -- Switch flyspell lang according to LaTeX commands
Date: Wed, 14 Jul 2004 20:03:28 -0500
User-agent: slrn/0.9.8.0 (Linux)

;; flyspell-babel.el -- Switch flyspell language according to LaTeX
;;                      Babel commands
;;
;; Copyright (C) 2004 P J Heslin
;;
;; Author: Peter Heslin <address@hidden>
;; URL: http://www.dur.ac.uk/p.j.heslin/emacs/download/flyspell-babel.el
;; Version: 1.0
;;
;; 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.

;;; Installation:
;;
;; Flyspell is an Emacs package that highlights misspelled words as
;; you type; Babel is the standard mechanism for switching languages
;; in LaTeX.  There are a number of Emacs packages available that will
;; try to guess the current language of a buffer or part of a buffer,
;; and make flyspell switch to a different dictionary; but I didn't
;; find one that used the explicit language-switching commands
;; available in a LaTeX file for this purpose.  This file makes
;; flyspell use the correct dictionary for the language used in each
;; part of a LaTeX file.  It can slow up your editing session
;; considerably, but I find it usable.  There are some restrictions on
;; the usage of Babel commands, on which see below.
;;
;; flyspell-babel requires flyspell and AUCTeX to be installed.  I
;; have tested it with version 1.7e of flyspell, which I obtained at:
;; 
http://www-sop.inria.fr/mimosa/personnel/Manuel.Serrano/flyspell/flyspell-1.7e.el,
;; and a CVS checkout of AUCTeX, which I got from
;; http://savannah.gnu.org/cvs/?group=auctex.
;;
;; To use this file, put it somewhere in your load-path, and add this
;; to your .emacs file:
;;
;; (add-hook 'LaTeX-mode-hook '(lambda ()
;;                                  (require 'flyspell-babel)))
;;
;; You will need to reload flyspell-babel.el if you install any new
;; ispell languages or language aliases.

;;; Commentary:
;;
;; Every time flyspell spell-checks a word (when you type a new word
;; or move the cursor to a new word), the buffer is examined to find a
;; relevant Babel command, and if necessary, the ispell/aspell precess
;; is stopped, and a new one for the new language is started.
;;
;; It's possible that a better way to do this might be to parse and
;; tag the whole buffer in the background, which would mean that less
;; work would have to be done whenever flyspell is actively checking
;; words. One could imagine a version of flyspell-babel that would
;; parse the document periodically and use overlays to identify the
;; language in each part of the document.  Flyspell would then only
;; need to check the overlay, rather than parse the LaTeX every time
;; it checked a word.  I didn't know how to implement this reliably,
;; however, since the re-parsing would have to be triggered every time
;; a Babel command is opened or closed.  So I tried it this way as a
;; proof of concept, and it works fast enough for my purposes.  It
;; might not be fast enough on a slow machine or with a big buffer.
;;
;; The parsing done by this package is very limited, and it will not
;; work with arbitrary LaTeX code.  In particular, certain types of
;; nesting will not work.  I hope that these restrictions will not in
;; practice impinge on the typical usage of most people.  Commands can
;; be nested within environments, and environments can be nested
;; within declarations, but none of these elements can be nested
;; within themselves.  Declarations have a scope until the next
;; declaration or the end of the buffer.  The first declaration is
;; determined by the final language option passed to the babel
;; \usepackage command.
;;
;; Thus, you can declare the language of a document in a
;; \usepackage[language1,language2]{babel} declaration, and thereafter
;; switch the declared language with \selectlanguage statements.  You
;; can select a different language from the current declaration by
;; using the otherlanguage environment or a \foreignlanguage command.
;; You can even nest a \foreignlanguage command within an
;; otherlanguage environment and nest that within a \selectlanguage
;; declaration.
;;
;; This package does not understand complex LaTeX constructs, such as
;; \input.  If you want to set the default language for a particular
;; file (for example, one that has no babel declaration, but is going
;; to be \input into a file that does), you can just put a redundant
;; \selectlanguage declaration at the start of the file.
;;
;; By default, \selectlanguage is recognized as a declaration,
;; otherlanguage and its starred variant are recognized as
;; environments, and \foreignlanguage is recognized as a command, all
;; of which are defined by Babel.  You can customize this package by
;; specifying other custom LaTeX declarations, environments and
;; commands that you might use as shortcuts to switch languages.
;;
;; By default, an ispell dictionary is invoked with the same name as
;; the current Babel language or dialect, which works in many cases.
;; If your ispell has a different name for that language, you have two
;; options.  You can make ispell recognize the Babel name by adding
;; symlinks under that name in your Ispell directory.  Alternatively,
;; you can customize flyspell-babel-to-ispell-alist, which maps Babel
;; languages and dialects to Ispell language names.

;; Here is the flow of what happens every time a word is checked:
;;
;; 1. Search backwards for any babel declaration, environment or command.  
;;    Did we find anything?
;;    * No.  Stop; do nothing.
;;    * Yes. Proceed to step 2.
;;
;; 2. Have we found a command?
;;    * Yes.  Is it in scope?
;;            * Yes.  Make sure language is set accordingly.
;;            * No. Search backwards again for any babel declaration or 
;;              environment.  Did we find anything?
;;              * No.  Stop; do nothing.
;;              * Yes. Proceed to step 3.
;;    * No. Proceed to step 3.
;;
;; 3. Have we found an environment?
;;    * Yes.  Is it in scope?
;;            * Yes.  Make sure language is set accordingly.
;;            * No. Search backwards again for any babel declaration.  
;;              Did we find anything?
;;              * No.  Stop; do nothing.
;;              * Yes. Proceed to step 4.
;;    * No. Proceed to step 4.
;;
;; 4. Have we found a declaration or \begin{document}?
;;    * Yes.  Is it a declaration?
;;            * Yes. Make sure language is set accordingly.
;;            Is it \begin{document}?
;;            * Yes. Search back through preamble to the \usepackage{babel}
;;                   declaration, and set language accordingly.
;;    * No. Signal error.

;;; Bugs:
;;
;; flyspell-large-region, which is the fast mode of flyspell, used
;; when checking the entirety of a large buffer, does not work at
;; all, since it depends on launching a single ispell process,
;; whereas flyspell-babel kills and relaunches ispell every time you
;; move from one language to another.  For this reason,
;; flyspell-large-region is disabled in buffers using this package.
;;
;; If you use any language switching commands in the preamble (for
;; example to define your own switching commands), then these may
;; interfere with the selection of the correct language when the
;; cursor is in the preamble.  All should be well, however, after
;; beginning of the document proper.
;;
;; If you nest environments and declarations in a way that is not
;; allowed by the instructions above, then the wrong language
;; dictionary will probably be selected for text after the improperly
;; nested element.

(defgroup flyspell-babel nil
  "Switch flyspell language according to LaTeX babel commands"
  :tag "Switch flyspell language according to Babel commands"
  :group 'emacs
  :prefix "flyspell-babel-")

(defcustom flyspell-babel-to-ispell-alist ()
  "Maps LaTeX babel language or dialect names to ispell
  dictionaries"
  :type 'alist
  :group 'flyspell-babel)

(defcustom flyspell-babel-declaration-alist ()
  "Maps LaTeX language-switching declarations (other than the
  built-in babel \\selectlanguage declaration) to babel
  languages" 
  :type 'alist
  :group 'flyspell-babel)

(defcustom flyspell-babel-environment-alist ()
  "Maps LaTeX language-switching environments (other than the
  built-in babel \"otherlanguage\" environment) to babel languages"
  :type 'alist
  :group 'flyspell-babel)

(defcustom flyspell-babel-command-alist ()
  "Maps LaTeX language-switching commands (other than the
  built-in babel \\foreignlanguage command) to babel languages"
  :type 'alist
  :group 'flyspell-babel)

(defvar flyspell-babel-verbose t
  "Whether routinely to report changing from one language to another"
  :type 'boolean
  :group 'flyspell-babel)

;; We need comment-beginning
(require 'newcomment) 
(comment-normalize-vars)

(setq flyspell-babel-declaration-alist-all
      (append '(("selectlanguage" "selectlanguage")) 
flyspell-babel-declaration-alist))

(setq flyspell-babel-decl-regexp
      (concat "\\\\begin[ \t\n]*{document}" "\\|"
              (mapconcat (lambda (pair) (concat "\\\\" (car pair) "[ \t\n{]"))
                         flyspell-babel-declaration-alist-all "\\|")))

(setq flyspell-babel-environment-alist-all
      (append '(("otherlanguage" "otherlanguage")) 
flyspell-babel-environment-alist))

(setq flyspell-babel-env-regexp
      (mapconcat (lambda (pair) (concat "\\\\begin{" (car pair) "}"))
                 flyspell-babel-environment-alist-all "\\|"))

(setq flyspell-babel-command-alist-all
      (append '(("foreignlanguage" "foreignlanguage")) 
flyspell-babel-command-alist))

(setq flyspell-babel-com-regexp
       (mapconcat (lambda (pair) (concat "\\\\" (car pair) "[ \t\n{]"))
                  flyspell-babel-command-alist-all "\\|"))

(setq flyspell-babel-decl-env-regexp
       (mapconcat 'identity (list flyspell-babel-decl-regexp 
                                  flyspell-babel-env-regexp) "\\|"))

(setq flyspell-babel-decl-env-com-regexp
       (mapconcat 'identity (list flyspell-babel-decl-regexp 
                                  flyspell-babel-env-regexp
                                  flyspell-babel-com-regexp) "\\|"))

(defun flyspell-babel-search-decl-env-com ()
  (let ((stop))
    (while (not stop)
      (if (re-search-backward flyspell-babel-decl-env-com-regexp nil t)
          (unless (save-excursion (comment-beginning))
            (flyspell-babel-check-com)
            (setq stop t))
        (setq stop t)))))
  
(defun flyspell-babel-search-decl-env ()
  (let ((stop))
    (while (not stop)
      (if (re-search-backward flyspell-babel-decl-env-regexp nil t)
          (unless (save-excursion (comment-beginning))
            (flyspell-babel-check-env)
            (setq stop t))
        (setq stop t)))))

(defun flyspell-babel-search-decl ()
  (let ((stop))
    (while (not stop)
      (if (re-search-backward flyspell-babel-decl-regexp nil t)
          (unless (save-excursion (comment-beginning))
            (flyspell-babel-check-decl)
            (setq stop t))
        (setq stop t)))))
  
(defun flyspell-babel-switch-dict (lang)
  (if (not lang)
      (message "%s" "Flyspell-babel error: nil language detected")
    (let ((trans (assoc lang flyspell-babel-to-ispell-alist)))
      (when trans
        ;; We have a translation of a language name from babel to ispell
        ;; nomenclature
        (setq lang (cadr trans)))
      (when flyspell-babel-debug
        (if lang
            (flyspell-babel-message (concat "Current ispell dictionary is: " 
lang))
          (flyspell-babel-message
           "Current ispell dictionary is set to nil: not checking")))
      (if (not lang)
          (setq spellcheck 'nil)
        (unless (string= ispell-local-dictionary lang)
          (if (member lang flyspell-babel-valid-dictionary-list)
              (progn
                (ispell-kill-ispell t)
                (setq ispell-local-dictionary lang)
                (ispell-init-process)
                (flyspell-babel-message
                 (concat "Flyspell-babel -- dictionary changed to: " lang)))
            (message
             "%s" (concat "Flyspell-babel error: ispell dictionary not 
installed for "
                          lang))))))))

(defun flyspell-babel-verify ()
  (let ((here (point))
        (lang 'nil)
        (inhibit-redisplay 't) ;; Seems to be necessary
        (spellcheck 't))
    (save-excursion
      (flyspell-babel-search-decl-env-com))
    spellcheck))

(defun flyspell-babel-check-com ()
  (if (looking-at flyspell-babel-com-regexp)
      (progn
        (if (re-search-forward "\\=\\\\foreignlanguage[ \t\n]*{\\([^}]+\\)}[ 
\t\n]*{"
                               nil t)
            (setq lang (match-string 1))
          (when (re-search-forward "\\=\\\\\\([^{ \t\n]+\\)[ \t\n]*{" nil t)
            (setq lang (cadr (assoc (match-string 1) 
flyspell-babel-command-alist-all)))))
        (backward-char)
        (flyspell-babel-forward-sexp)
        (if (< here (point))
            (flyspell-babel-switch-dict lang)
          (flyspell-babel-search-decl-env)))
    (flyspell-babel-check-env)))
              
(defun flyspell-babel-check-env ()
  (if (looking-at flyspell-babel-env-regexp)
      (progn
        (if (re-search-forward "\\=\\\\begin{otherlanguage}[ 
\t\n]*{\\([^}]+\\)}" nil t)
            (setq lang (match-string 1))
          (when (re-search-forward "\\=\\\\begin[ \t\n]*{\\([^}]+\\)}" nil t)
            (setq lang (cadr (assoc (match-string 1)
                                    flyspell-babel-environment-alist-all)))))
        (flyspell-babel-find-matching-end)
        (backward-char)
        (if (< here (point))
            (flyspell-babel-switch-dict lang)
          (flyspell-babel-search-decl)))
    (flyspell-babel-check-decl)))

(defun flyspell-babel-check-decl ()
  (if (looking-at flyspell-babel-decl-regexp)
      (progn
        (if (looking-at "\\\\selectlanguage[ \t\n]*{\\([^}]+\\)}")
            (setq lang (match-string 1))
          (if (looking-at "\\\\begin[ \t\n]*{document}")
              (progn
                (re-search-backward "\\\\usepackage.*[[,]\\([^]]+\\)\\]{babel}" 
nil t)
                (setq lang (match-string 1)))
            (when (looking-at "\\\\\\([^{ \t\n]+\\)")
              (setq lang (cadr (assoc (match-string 1)
                                      flyspell-babel-declaration-alist-all))))))
        (flyspell-babel-switch-dict lang))
    (message "%s" "flyspell-babel internal error!")))

(defun flyspell-babel-forward-sexp (&optional arg)
  "Makes sure to ignore comments when using forward-sexp, and
  trap errors for unbalanced braces."
  (interactive "p")
  (let ((parse-sexp-ignore-comments t))
    (condition-case nil 
        (forward-sexp arg)
      (scan-error (goto-char (point-max))))))

(defun flyspell-babel-find-matching-end ()
  "Find end of current environment, but trap error when there is
  no matching \end.  Unfortunately, LaTeX-find-matching-end does
  not handle commented-out lines specially at all."
  (interactive)
  (condition-case nil 
      (LaTeX-find-matching-end)
    (error (goto-char (point-max)))))

(defun flyspell-babel-message (mess)
  (when flyspell-babel-verbose
    (message "%s" mess)))
  
(defvar flyspell-babel-valid-dictionary-list ()
  "Cached value of ispell-valid-dictionary-list")
(setq flyspell-babel-valid-dictionary-list
      (ispell-valid-dictionary-list))

;;;;;;;;;; Here is our hook into flyspell:

;; This is right for future invocations of flyspell-mode ...
(put 'latex-mode 'flyspell-mode-predicate 'flyspell-babel-verify)
(add-hook 'LaTeX-mode-hook '(lambda () 
                              (make-local-variable 'flyspell-large-region)
                              (setq flyspell-large-region (point-max))))
;;
;; ... but what if we have been loaded from a mode hook, and flyspell-mode
;; has already been turned on?
(when (and flyspell-mode
         (eq major-mode 'latex-mode))
  ;; already buffer-local
  (setq flyspell-generic-check-word-p 'flyspell-babel-verify)
  (make-local-variable 'flyspell-large-region)
  (setq flyspell-large-region (point-max)))

;; Here's another possible way to do it, which has the advantage that
;; it doesn't add this to all Latex files, but only to those that use
;; babel.  The problem is that \include'd files may not have the
;; necessary \usepackage{babel} declaration.  It seems that the
;; TeX-master variable does not prompt hooks to be run based on a
;; preamble in another document.
;;
;;(TeX-add-style-hook "babel"
;;  (function (lambda ()
;;            ;; flyspell-generic-check-word-p is already buffer-local
;;            (setq flyspell-generic-check-word-p 'flyspell-babel-verify)
;;            (make-local-variable 'flyspell-large-region)
;;            (setq flyspell-large-region (point-max)))))

(provide 'flyspell-babel)


reply via email to

[Prev in Thread] Current Thread [Next in Thread]