[Top][All Lists]

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

flyspell-babel.el, v. 1.2

From: Peter Heslin
Subject: flyspell-babel.el, v. 1.2
Date: Mon, 13 Sep 2004 09:52:54 -0500
User-agent: slrn/ (Linux)

This version removes the dependency on AUCTeX, and fixes a bug.

;; flyspell-babel.el -- Switch flyspell language according to LaTeX
;;                      Babel commands
;; Copyright (C) 2004 P J Heslin
;; Author: Peter Heslin <address@hidden>
;; URL:
;; Version: 1.2
;; 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
;; 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 to be installed (version 1.7f or
;; better) and flyspell-mode to be active.
;; 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.
;; I have only tested this with GNU Emacs.

;;; 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.
;; 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.  If you map a
;; language to 'nil, that means not to spell-check that language,
;; which can be useful for languages without an ispell dictionary.

;;; Customization:
;; The code that follows is an example of my customization of this
;; package.  The first form tells the package to turn on debugging
;; messages to see when we switch dictionaries as we move from place
;; to place.  The second tells it not to spell-check the languages
;; "latin" and "ibycus" (an encoding for ancient Greek), since I don't
;; have ispell dictionaries for them; it also tells it to translate
;; the Babel language "french" to the ispell dictionary "francais".
;; The third form defines some language-switching shortcut commands,
;; so that I can more easily say \fr{merci} and \itl{grazie}.  The
;; fourth defines some short-cut environments, since \begin{german} is
;; a lot easier to write than \begin{otherlanguage}{german}.  The last
;; form defines some shortcut declarations for switching between
;; American and British spelling.
;;    (setq flyspell-babel-verbose t)
;;    (setq flyspell-babel-to-ispell-alist
;;          '(("latin" nil)
;;            ("ibycus" nil)
;;            ("french" "francais")))
;;    (setq flyspell-babel-command-alist
;;          '(("lat" "latin")
;;            ("gk" "ibycus")
;;            ("fr" "french")
;;            ("ger" "german")
;;            ("itl" "italian")))
;;    (setq flyspell-babel-environment-alist
;;          '(("latin" "latin")
;;            ("greek" "ibycus")
;;            ("french" "french")
;;            ("german" "german")
;;            ("italian" "italian")))
;;    (setq flyspell-babel-declaration-alist
;;          '(("yank" "american")
;;            ("brit" "british")))
;; Here is the LaTeX code that defines these short-cuts:
;; \usepackage[ibycus,latin,french,german,italian,british,american]{babel}
;; \newcommand{\lat}[1]{\foreignlanguage{latin}{\emph{#1}}}
;; \newenvironment{latin}{\begin{otherlanguage}{latin}}{\end{otherlanguage}}
;; \newcommand{\fr}[1]{\foreignlanguage{french}{\emph{#1}}}
;; \newenvironment{french}{\begin{otherlanguage}{french}}{\end{otherlanguage}}
;; \newcommand{\ger}[1]{\foreignlanguage{german}{\emph{#1}}}
;; \newenvironment{german}{\begin{otherlanguage}{german}}{\end{otherlanguage}}
;; \newcommand{\itl}[1]{\foreignlanguage{italian}{\emph{#1}}}
;; \newenvironment{italian}{\begin{otherlanguage}{italian}}{\end{otherlanguage}}
;; \newcommand{\yank}{\selectlanguage{american}}
;; \newcommand{\brit}{\selectlanguage{british}}

;;; Implementation
;; 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 added or removed.  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.
;; I also tried a simpler version of the current implementation that
;; went back through the file, trying each Babel command until it
;; found one that was in scope, but for me it was too slow when
;; dealing with large or complex buffers, since the time taken to
;; spell-check each word went up with each language-switch intervening
;; between point and the location of the Babel command currently in
;; force.  In order to place a bound on that time-lag, I have
;; implemented this version to require the strict nesting of
;; declarations, environments and commands.  This means that there is
;; now a maximum of 4 backward searches in the document to find Babel
;; commands and a maximum of 2 forward searches to see if things are
;; in scope.  For me, this runs acceptably fast, even though this
;; happens every time point moves to a new word.  At any rate, the
;; time-lag seems less than the one introduced when I switched from
;; ispell to aspell (which gives better suggested spellings).
;; 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.  If there is
;;                   no such declaration, do nothing.
;;    * 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 there exist any language switching commands in the preamble (for
;; example, using them to define your own switching commands with
;; \newcommand), 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.

;;; Changes
;; 1.2 Removed dependency on AUCTeX and newcomment and fixed bug when
;;     disabling flyspell-large-region
;; 1.1 Removed error report when \usepackage{babel} not present
;; 1.0 Initial public release

(require 'flyspell)

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

(defcustom flyspell-babel-to-ispell-alist ()
  "Maps LaTeX babel language or dialect names to ispell
  :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
  :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)

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

(defvar flyspell-babel-valid-dictionary-list ()
  "Cached value of ispell-valid-dictionary-list")
(setq flyspell-babel-valid-dictionary-list

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

(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"))

(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"))

(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-com-regexp) "\\|"))

(defun flyspell-babel-verify ()
  (let ((here (point))
        (lang 'nil)
        (inhibit-redisplay 't) ;; Seems to be necessary
        (spellcheck 't))

(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 (flyspell-babel-in-comment-p)
            (setq stop t))
        (setq stop t)))))

(defun flyspell-babel-check-com ()
  (if (looking-at flyspell-babel-com-regexp)
        (if (re-search-forward
             "\\=\\\\foreignlanguage[ \t\n]*{\\([^}]+\\)}[ \t\n]*{" nil t)
            (setq lang (match-string 1))
          (if (re-search-forward "\\=\\\\\\([^{ \t\n]+\\)[ \t\n]*{" nil t)
              (setq lang (cadr
                          (assoc (match-string 1)
            (flyspell-babel-message "internal error")))
        (if (< here (point))
            (flyspell-babel-switch-dict lang)

(defun flyspell-babel-search-decl-env ()
  (let ((stop))
    (while (not stop)
      (if (re-search-backward flyspell-babel-decl-env-regexp nil t)
          (unless (flyspell-babel-in-comment-p)
            (setq stop t))
        (setq stop t)))))

(defun flyspell-babel-check-env ()
  (if (looking-at flyspell-babel-env-regexp)
      (let ((env))
        (if (looking-at "\\=\\\\begin{otherlanguage}[ \t\n]*{\\([^}]+\\)}")
            (setq env "otherlanguage" lang (match-string 1))
          (if (looking-at "\\=\\\\begin[ \t\n]*{\\([^}]+\\)}")
              (setq env (match-string 1)
                    lang (cadr
                          (assoc env flyspell-babel-environment-alist-all)))
            (flyspell-babel-message "internal error")))
        (flyspell-babel-find-matching-end env)
        (if (< here (point))
            (flyspell-babel-switch-dict lang)

(defun flyspell-babel-search-decl ()
  (let ((stop))
    (while (not stop)
      (if (re-search-backward flyspell-babel-decl-regexp nil t)
          (unless (flyspell-babel-in-comment-p)
            (setq stop t))
        (setq stop t)))))
(defun flyspell-babel-check-decl ()
  (let ((proceed t))
    (if (looking-at flyspell-babel-decl-regexp)
          (if (looking-at "\\\\selectlanguage[ \t\n]*{\\([^}]+\\)}")
              (setq lang (match-string 1))
            (if (looking-at "\\\\begin[ \t\n]*{document}")
                (if (re-search-backward
                     "\\\\usepackage.*[[,]\\([^]]+\\)\\]{babel}" nil t)
                    (setq lang (match-string 1))
                  (setq proceed nil))
              (when (looking-at "\\\\\\([^{ \t\n]+\\)")
                (setq lang
                      (cadr (assoc (match-string 1)
          (when proceed
            (flyspell-babel-switch-dict lang)))
      (flyspell-babel-message "internal error"))))

(defun flyspell-babel-switch-dict (lang)
  (if (not lang)
      (flyspell-babel-message "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)))
      (if (not lang)
            (setq spellcheck 'nil)
             "current dictionary is set to nil: not checking"))
        (if (string= ispell-local-dictionary lang)
             (concat "current dictionary remains: " lang))
          (if (member lang flyspell-babel-valid-dictionary-list)
                (ispell-kill-ispell t)
                (setq ispell-local-dictionary lang)
                 (concat "dictionary changed to: " lang)))
            (setq spellcheck 'nil)
             (concat "Warning: no dictionary installed for "
                     lang) t)))))))

(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 (env)
  "Find end of current environment, or end of file when there is
  no matching \end."
  (let ((regexp (concat "\\\\\\(begin\\|end\\)[ \t\n]*{" env "}"))
        (level 0)
        (proceed t))
    (while proceed
      (if (re-search-forward regexp nil t)
          (let ((match (match-string 1)))
            (unless (flyspell-babel-in-comment-p)
              (if (string= match "begin")
                  (setq level (1+ level))
                (if (string= match "end")
                    (setq level (1- level))
                  (flyspell-babel-message "internal error")))))
        (goto-char (point-max))
        (setq proceed nil))
      (when (= 0 level)
        (setq proceed nil)))))

(defun flyspell-babel-in-comment-p ()
  "Are we in a Latex comment? (Taken from auctex' tex.el)"
  (if (or (bolp)
          (null comment-start-skip)
          (eq (preceding-char) ?\r))
      (let ((pos (point)))
        (re-search-backward "^\\|\r" nil t)
        (or (looking-at comment-start-skip)
            (re-search-forward comment-start-skip pos t))))))

(defun flyspell-babel-message (mess &optional force)
  (when (or flyspell-babel-verbose force)
    (message "Flyspell-babel -- %s" mess)))

;;;;;;;;;; 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 'nil)))
;; ... 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 'nil))

(provide 'flyspell-babel)

reply via email to

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