(ispell-message): New command, with menu bar item.
[bpt/emacs.git] / lisp / textmodes / ispell4.el
CommitLineData
087f61c8 1;;; ispell.el --- this is the GNU EMACS interface to GNU ISPELL version 4.
1a06eabd 2
0d28f072 3;;Copyright (C) 1990, 1991, 1993 Free Software Foundation, Inc.
9750e079 4
e9571d2a
ER
5;; Keywords: wp
6
6370a710 7;;This file is part of GNU Emacs.
d299ee07 8;;
6370a710 9;;GNU Emacs is free software; you can redistribute it and/or modify
d299ee07 10;;it under the terms of the GNU General Public License as published by
c47e1198 11;;the Free Software Foundation; either version 2, or (at your option)
d299ee07
RS
12;;any later version.
13;;
6370a710 14;;GNU Emacs is distributed in the hope that it will be useful,
d299ee07
RS
15;;but WITHOUT ANY WARRANTY; without even the implied warranty of
16;;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17;;GNU General Public License for more details.
18;;
19;;You should have received a copy of the GNU General Public License
6370a710 20;;along with GNU Emacs; see the file COPYING. If not, write to
d299ee07
RS
21;;the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
22
e41b2db1
ER
23;;; Commentary:
24
25;; This package provides a graceful interface to ispell, the GNU
26;; spelling checker.
27
282d89c0
ER
28;;; Code:
29
d299ee07 30(defvar ispell-have-new-look t
a6c5a8dd 31 "Non-nil means use the `-r' option when running `look'.")
d299ee07
RS
32
33(defvar ispell-enable-tex-parser nil
a6c5a8dd 34 "Non-nil enables experimental TeX parser in Ispell for TeX-mode buffers.")
d299ee07 35
a6c5a8dd 36(defvar ispell-process nil "The process running Ispell")
d299ee07 37(defvar ispell-next-message nil
a6c5a8dd
RS
38 "An integer: where in `*ispell*' buffer to find next message from Ispell.")
39
40(defvar ispell-command "ispell"
41 "Command for running Ispell.")
42(defvar ispell-command-options nil
43 "*String (or list of strings) to pass to Ispell as command arguments.
44You can use this to specify the name of your private dictionary.
45The -S option is always passed to Ispell as the last parameter,
46and need not be mentioned here.")
d299ee07
RS
47
48;Each marker in this list points to the start of a word that
49;ispell thought was bad last time it did the :file command.
50;Notice that if the user accepts or inserts a word into his
51;private dictionary, then some "good" words will be on the list.
52;We would like to deal with this by looking up the words again just before
53;presenting them to the user, but that is too slow on machines
54;without the select system call. Therefore, see the variable
55;ispell-recently-accepted.
56(defvar ispell-bad-words nil
a6c5a8dd 57 "A list of markers reflecting the output of the Ispell `:file' command.")
d299ee07
RS
58
59;list of words that the user has accepted, but that might still
60;be on the bad-words list
61(defvar ispell-recently-accepted nil)
62
a48fc390
RS
63;; Non-nil means we have started showing an alternatives window.
64;; This is the window config from before then.
e3a39644 65(defvar ispell-window-configuration nil)
a48fc390 66
d299ee07
RS
67;t when :dump command needed
68(defvar ispell-dump-needed nil)
69
70(defun ispell-flush-bad-words ()
71 (while ispell-bad-words
72 (if (markerp (car ispell-bad-words))
73 (set-marker (car ispell-bad-words) nil))
74 (setq ispell-bad-words (cdr ispell-bad-words)))
75 (setq ispell-recently-accepted nil))
76
77(defun kill-ispell ()
a6c5a8dd 78 "Kill the Ispell process.
eb8c3be9 79Any changes in your private dictionary
d299ee07
RS
80that have not already been dumped will be lost."
81 (interactive)
82 (if ispell-process
83 (delete-process ispell-process))
84 (setq ispell-process nil)
85 (ispell-flush-bad-words))
86
87(put 'ispell-startup-error 'error-conditions
88 '(ispell-startup-error error))
89(put 'ispell-startup-error 'error-message
90 "Problem starting ispell - see buffer *ispell*")
91
a6c5a8dd
RS
92;; Start an ispell subprocess; check the version; and display the greeting.
93
d299ee07 94(defun start-ispell ()
d299ee07
RS
95 (message "Starting ispell ...")
96 (let ((buf (get-buffer "*ispell*")))
97 (if buf
98 (kill-buffer buf)))
99 (condition-case err
a6c5a8dd
RS
100 (setq ispell-process
101 (apply 'start-process "ispell" "*ispell*" ispell-command
102 (append (if (listp ispell-command-options)
103 ispell-command-options
104 (list ispell-command-options))
105 '("-S"))))
d299ee07
RS
106 (file-error (signal 'ispell-startup-error nil)))
107 (process-kill-without-query ispell-process)
108 (buffer-disable-undo (process-buffer ispell-process))
109 (accept-process-output ispell-process)
110 (let (last-char)
111 (save-excursion
112 (set-buffer (process-buffer ispell-process))
113 (bury-buffer (current-buffer))
114 (setq last-char (- (point-max) 1))
115 (while (not (eq (char-after last-char) ?=))
116 (cond ((not (eq (process-status ispell-process) 'run))
117 (kill-ispell)
118 (signal 'ispell-startup-error nil)))
119 (accept-process-output ispell-process)
120 (setq last-char (- (point-max) 1)))
121 (goto-char (point-min))
122 (let ((greeting (read (current-buffer))))
123 (if (not (= (car greeting) 1))
124 (error "Bad ispell version: wanted 1, got %d" (car greeting)))
125 (message (car (cdr greeting))))
126 (delete-region (point-min) last-char))))
127
a6c5a8dd
RS
128;; Make sure ispell is ready for a command.
129;; Leaves buffer set to *ispell*, point at '='.
130
d299ee07 131(defun ispell-sync (intr)
d299ee07
RS
132 (if (or (null ispell-process)
133 (not (eq (process-status ispell-process) 'run)))
134 (start-ispell))
135 (if intr
136 (interrupt-process ispell-process))
137 (let (last-char)
138 (set-buffer (process-buffer ispell-process))
139 (bury-buffer (current-buffer))
140 (setq last-char (- (point-max) 1))
141 (while (not (eq (char-after last-char) ?=))
142 (accept-process-output ispell-process)
143 (setq last-char (- (point-max) 1)))
144 (goto-char last-char)))
145
a6c5a8dd
RS
146;; Send a command to ispell. Choices are:
147;;
148;; WORD Check spelling of WORD. Result is
149;;
150;; nil not found
151;; t spelled ok
152;; list of strings near misses
153;;
154;; :file FILENAME scan the named file, and print the file offsets of
155;; any misspelled words
156;;
157;; :insert WORD put word in private dictionary
158;;
159;; :accept WORD don't complain about word any more this session
160;;
161;; :dump write out the current private dictionary, if necessary.
162;;
163;; :reload reread `~/ispell.words'
164;;
165;; :tex
166;; :troff
167;; :generic set type of parser to use when scanning whole files
d299ee07 168
a6c5a8dd 169(defun ispell-cmd (&rest strings)
d299ee07
RS
170 (save-excursion
171 (ispell-sync t)
172 (set-buffer (process-buffer ispell-process))
173 (bury-buffer (current-buffer))
174 (erase-buffer)
175 (setq ispell-next-message (point-min))
176 (while strings
177 (process-send-string ispell-process (car strings))
178 (setq strings (cdr strings)))
179 (process-send-string ispell-process "\n")
180 (accept-process-output ispell-process)
181 (ispell-sync nil)))
182
183(defun ispell-dump ()
184 (cond (ispell-dump-needed
185 (setq ispell-dump-needed nil)
186 (ispell-cmd ":dump"))))
187
188(defun ispell-insert (word)
189 (ispell-cmd ":insert " word)
190 (if ispell-bad-words
191 (setq ispell-recently-accepted (cons word ispell-recently-accepted)))
192 (setq ispell-dump-needed t))
193
194(defun ispell-accept (word)
195 (ispell-cmd ":accept " word)
196 (if ispell-bad-words
197 (setq ispell-recently-accepted (cons word ispell-recently-accepted))))
198
a6c5a8dd 199;; Return the next message sent by the Ispell subprocess.
d299ee07
RS
200
201(defun ispell-next-message ()
d299ee07
RS
202 (save-excursion
203 (set-buffer (process-buffer ispell-process))
204 (bury-buffer (current-buffer))
205 (save-restriction
206 (goto-char ispell-next-message)
207 (narrow-to-region (point)
208 (progn (forward-sexp 1) (point)))
209 (setq ispell-next-message (point))
210 (goto-char (point-min))
211 (read (current-buffer)))))
212
213(defun ispell-tex-buffer-p ()
214 (memq major-mode '(plain-TeX-mode LaTeX-mode)))
215
44ca409a 216(defvar ispell-menu-map (make-sparse-keymap "Spell"))
e3a39644 217(defalias 'ispell-menu-map ispell-menu-map)
44ca409a
RS
218
219(define-key ispell-menu-map [reload-ispell]
220 '("Reload Dictionary" . reload-ispell))
221
222(define-key ispell-menu-map [ispell-next]
223 '("Continue Check" . ispell-next))
224
36ab15b2
RS
225(define-key ispell-menu-map [ispell-message]
226 '("Check Message" . ispell-message))
227
44ca409a
RS
228(define-key ispell-menu-map [ispell-region]
229 '("Check Region" . ispell-region))
230
231(define-key ispell-menu-map [ispell-buffer]
232 '("Check Buffer" . ispell))
233
234(define-key ispell-menu-map [ispell-word]
235 '("Check Word" . ispell-word))
236
7229064d 237;;;###autoload
d299ee07 238(defun ispell (&optional buf start end)
6370a710
RS
239 "Run Ispell over current buffer's visited file.
240First the file is scanned for misspelled words, then Ispell
d299ee07
RS
241enters a loop with the following commands for every misspelled word:
242
243DIGIT Near miss selector. If the misspelled word is close to
244 some words in the dictionary, they are offered as near misses.
245r Replace. Replace the word with a string you type. Each word
246 of your new string is also checked.
eb8c3be9 247i Insert. Insert this word in your private dictionary (kept in
d299ee07
RS
248 `$HOME/ispell.words').
249a Accept. Accept this word for the rest of this editing session,
eb8c3be9 250 but don't put it in your private dictionary.
d299ee07
RS
251l Lookup. Look for a word in the dictionary by fast binary
252 search, or search for a regular expression in the dictionary
253 using grep.
254SPACE Accept the word this time, but complain if it is seen again.
f30ff39f 255q, \\[keyboard-quit] Leave the command loop. You can come back later with \\[ispell-next]."
d299ee07
RS
256 (interactive)
257 (if (null start)
258 (setq start 0))
259 (if (null end)
260 (setq end 0))
261
262 (if (null buf)
263 (setq buf (current-buffer)))
264 (setq buf (get-buffer buf))
265 (if (null buf)
266 (error "Can't find buffer"))
a48fc390
RS
267 ;; Deactivate the mark, because we'll do it anyway if we change something,
268 ;; and a region highlight while in the Ispell loop is distracting.
0d28f072 269 (deactivate-mark)
d299ee07
RS
270 (save-excursion
271 (set-buffer buf)
272 (let ((filename buffer-file-name)
a48fc390 273 (delete-temp nil))
d299ee07
RS
274 (unwind-protect
275 (progn
276 (cond ((null filename)
277 (setq filename (make-temp-name "/usr/tmp/ispell"))
278 (setq delete-temp t)
279 (write-region (point-min) (point-max) filename))
280 ((and (buffer-modified-p buf)
281 (y-or-n-p (format "Save file %s? " filename)))
282 (save-buffer)))
283 (message "Ispell scanning file...")
284 (if (and ispell-enable-tex-parser
285 (ispell-tex-buffer-p))
286 (ispell-cmd ":tex")
287 (ispell-cmd ":generic"))
288 (ispell-cmd (format ":file %s %d %d" filename start end)))
a48fc390
RS
289 (if delete-temp
290 (condition-case ()
291 (delete-file filename)
292 (file-error nil)))))
d299ee07
RS
293 (message "Parsing ispell output ...")
294 (ispell-flush-bad-words)
295 (let (pos bad-words)
296 (while (numberp (setq pos (ispell-next-message)))
297 ;;ispell may check the words on the line following the end
298 ;;of the region - therefore, don't record anything out of range
299 (if (or (= end 0)
300 (< pos end))
301 (setq bad-words (cons (set-marker (make-marker) (+ pos 1))
302 bad-words))))
303 (setq bad-words (cons pos bad-words))
304 (setq ispell-bad-words (nreverse bad-words))))
305 (cond ((not (markerp (car ispell-bad-words)))
306 (setq ispell-bad-words nil)
307 (message "No misspellings."))
308 (t
309 (message "Ispell parsing done.")
310 (ispell-next))))
311
f305bd74 312;;;###autoload
31e1d920 313(defalias 'ispell-buffer 'ispell)
f305bd74 314
d299ee07 315(defun ispell-next ()
a6c5a8dd 316 "Resume command loop for most recent Ispell command."
d299ee07 317 (interactive)
a48fc390 318 (setq ispell-window-configuration nil)
d299ee07
RS
319 (unwind-protect
320 (catch 'quit
a48fc390
RS
321 ;; There used to be a save-excursion here,
322 ;; but that was annoying: it's better if point doesn't move
323 ;; when you type q.
324 (let (next)
325 (while (markerp (setq next (car ispell-bad-words)))
326 (switch-to-buffer (marker-buffer next))
327 (push-mark)
328 (ispell-point next "at saved position.")
329 (setq ispell-bad-words (cdr ispell-bad-words))
330 (set-marker next nil))))
331 (if ispell-window-configuration
332 (set-window-configuration ispell-window-configuration))
d299ee07
RS
333 (cond ((null ispell-bad-words)
334 (error "Ispell has not yet been run."))
335 ((markerp (car ispell-bad-words))
336 (message (substitute-command-keys
337 "Type \\[ispell-next] to continue.")))
338 ((eq (car ispell-bad-words) nil)
339 (setq ispell-bad-words nil)
340 (message "No more misspellings (but checker was interrupted.)"))
341 ((eq (car ispell-bad-words) t)
342 (setq ispell-bad-words nil)
343 (message "Ispell done."))
344 (t
345 (setq ispell-bad-words nil)
346 (message "Bad ispell internal list"))))
347 (ispell-dump))
348
7229064d 349;;;###autoload
6370a710 350(defun ispell-word (&optional resume)
d299ee07 351 "Check the spelling of the word under the cursor.
a6c5a8dd 352See the command `ispell' for more information.
6370a710
RS
353With a prefix argument, resume handling of the previous Ispell command."
354 (interactive "P")
355 (if resume
356 (ispell-next)
357 (condition-case err
358 (catch 'quit
359 (save-window-excursion
360 (ispell-point (point) "at point."))
361 (ispell-dump))
362 (ispell-startup-error
363 (cond ((y-or-n-p "Problem starting ispell, use old-style spell instead? ")
364 (load-library "spell")
365 (define-key esc-map "$" 'spell-word)
366 (spell-word)))))))
f305bd74
BP
367;;;###autoload
368(define-key esc-map "$" 'ispell-word)
d299ee07 369
7229064d 370;;;###autoload
d299ee07
RS
371(defun ispell-region (start &optional end)
372 "Check the spelling for all of the words in the region."
373 (interactive "r")
374 (ispell (current-buffer) start end))
375
376(defun ispell-letterp (c)
377 (and c
378 (or (and (>= c ?A) (<= c ?Z))
379 (and (>= c ?a) (<= c ?z))
380 (>= c 128))))
381
382(defun ispell-letter-or-quotep (c)
383 (and c
384 (or (and (>= c ?A) (<= c ?Z))
385 (and (>= c ?a) (<= c ?z))
386 (= c ?')
387 (>= c 128))))
388
389(defun ispell-find-word-start ()
390 ;;backward to a letter
391 (if (not (ispell-letterp (char-after (point))))
392 (while (and (not (bobp))
393 (not (ispell-letterp (char-after (- (point) 1)))))
394 (backward-char)))
395 ;;backward to beginning of word
396 (while (ispell-letter-or-quotep (char-after (- (point) 1)))
397 (backward-char))
398 (skip-chars-forward "'"))
399
400(defun ispell-find-word-end ()
401 (while (ispell-letter-or-quotep (char-after (point)))
402 (forward-char))
403 (skip-chars-backward "'"))
404
405(defun ispell-next-word ()
406 (while (and (not (eobp))
407 (not (ispell-letterp (char-after (point)))))
408 (forward-char)))
409
410;if end is nil, then do one word at start
411;otherwise, do all words from the beginning of the word where
412;start points, to the end of the word where end points
413(defun ispell-point (start message)
414 (let ((wend (make-marker))
415 rescan
416 end)
a48fc390
RS
417 ;; There used to be a save-excursion here,
418 ;; but that was annoying: it's better if point doesn't move
419 ;; when you type q.
d299ee07
RS
420 (goto-char start)
421 (ispell-find-word-start) ;find correct word start
422 (setq start (point-marker))
423 (ispell-find-word-end) ;now find correct end
424 (setq end (point-marker))
e8608d6a
RS
425 ;; Do nothing if we don't find a word.
426 (if (< start end)
427 (while (< start end)
428 (goto-char start)
429 (ispell-find-word-end) ;find end of current word
d299ee07
RS
430 ;could be before 'end' if
431 ;user typed replacement
432 ;that is more than one word
e8608d6a
RS
433 (set-marker wend (point))
434 (setq rescan nil)
435 (setq word (buffer-substring start wend))
436 (cond ((ispell-still-bad word)
a48fc390
RS
437;;; This just causes confusion. -- rms.
438;;; (goto-char start)
439;;; (sit-for 0)
e8608d6a
RS
440 (message (format "Ispell checking %s" word))
441 (ispell-cmd word)
442 (let ((message (ispell-next-message)))
443 (cond ((eq message t)
444 (message "%s: ok" word))
445 ((or (null message)
446 (consp message))
447 (setq rescan
448 (ispell-command-loop word start wend message)))
449 (t
450 (error "unknown ispell response %s" message))))))
451 (cond ((null rescan)
452 (goto-char wend)
453 (ispell-next-word)
454 (set-marker start (point))))))
d299ee07
RS
455 ;;clear the choices buffer; otherwise it's hard for the user to tell
456 ;;when we get back to the command loop
457 (let ((buf (get-buffer "*ispell choices*")))
458 (cond (buf
459 (set-buffer buf)
460 (erase-buffer))))
461 (set-marker start nil)
462 (set-marker end nil)
a48fc390 463 (set-marker wend nil)))
d299ee07
RS
464
465(defun ispell-still-bad (word)
466 (let ((words ispell-recently-accepted)
467 (ret t)
468 (case-fold-search t))
469 (while words
470 (cond ((eq (string-match (car words) word) 0)
471 (setq ret nil)
472 (setq words nil)))
473 (setq words (cdr words)))
474 ret))
475
476(defun ispell-show-choices (word message first-line)
f98955ea 477 ;;if there is only one window on the frame, make the ispell
d299ee07
RS
478 ;;messages winow be small. otherwise just use the other window
479 (let* ((selwin (selected-window))
480 (resize (eq selwin (next-window)))
481 (buf (get-buffer-create "*ispell choices*"))
482 w)
a48fc390
RS
483 (or ispell-window-configuration
484 (setq ispell-window-configuration (current-window-configuration)))
d299ee07
RS
485 (setq w (display-buffer buf))
486 (buffer-disable-undo buf)
487 (if resize
488 (unwind-protect
489 (progn
490 (select-window w)
491 (enlarge-window (- 6 (window-height w))))
492 (select-window selwin)))
493 (save-excursion
494 (set-buffer buf)
495 (bury-buffer buf)
496 (set-window-point w (point-min))
497 (set-window-start w (point-min))
498 (erase-buffer)
499 (insert first-line "\n")
500 (insert
501 "SPC skip; A accept; I insert; DIGIT select; R replace; \
502L lookup; Q quit\n")
503 (cond ((not (null message))
504 (let ((i 0))
505 (while (< i 3)
506 (let ((j 0))
507 (while (< j 3)
508 (let* ((n (+ (* j 3) i))
509 (choice (nth n message)))
510 (cond (choice
511 (let ((str (format "%d %s" n choice)))
512 (insert str)
513 (insert-char ? (- 20 (length str)))))))
514 (setq j (+ j 1))))
515 (insert "\n")
516 (setq i (+ i 1)))))))))
517
518(defun ispell-command-loop (word start end message)
519 (let ((flag t)
520 (rescan nil)
521 first-line)
522 (if (null message)
523 (setq first-line (concat "No near misses for '" word "'"))
524 (setq first-line (concat "Near misses for '" word "'")))
525 (while flag
526 (ispell-show-choices word message first-line)
527 (message "Ispell command: ")
8fa51910 528 (undo-boundary)
d299ee07
RS
529 (let ((c (downcase (read-char)))
530 replacement)
531 (cond ((and (>= c ?0)
532 (<= c ?9)
533 (setq replacement (nth (- c ?0) message)))
534 (ispell-replace start end replacement)
535 (setq flag nil))
536 ((= c ?q)
537 (throw 'quit nil))
538 ((= c ? )
539 (setq flag nil))
540 ((= c ?r)
541 (ispell-replace start end (read-string "Replacement: "))
542 (setq rescan t)
543 (setq flag nil))
544 ((= c ?i)
545 (ispell-insert word)
546 (setq flag nil))
547 ((= c ?a)
548 (ispell-accept word)
549 (setq flag nil))
550 ((= c ?l)
551 (let ((val (ispell-do-look word)))
552 (setq first-line (car val))
553 (setq message (cdr val))))
554 ((= c ??)
555 (message
556 "Type 'C-h d ispell' to the emacs main loop for more help")
557 (sit-for 2))
558 (t
559 (message "Bad ispell command")
560 (sit-for 2)))))
561 rescan))
562
563(defun ispell-do-look (bad-word)
564 (let (regex buf words)
565 (cond ((null ispell-have-new-look)
566 (setq regex (read-string "Lookup: ")))
567 (t
568 (setq regex (read-string "Lookup (regex): " "^"))))
569 (setq buf (get-buffer-create "*ispell look*"))
570 (save-excursion
571 (set-buffer buf)
572 (delete-region (point-min) (point-max))
573 (if ispell-have-new-look
574 (call-process "look" nil buf nil "-r" regex)
575 (call-process "look" nil buf nil regex))
576 (goto-char (point-min))
577 (forward-line 10)
578 (delete-region (point) (point-max))
579 (goto-char (point-min))
580 (while (not (= (point-min) (point-max)))
581 (end-of-line)
582 (setq words (cons (buffer-substring (point-min) (point)) words))
583 (forward-line)
584 (delete-region (point-min) (point)))
585 (kill-buffer buf)
586 (cons (format "Lookup '%s'" regex)
587 (reverse words)))))
588
589(defun ispell-replace (start end new)
590 (goto-char start)
591 (insert new)
592 (delete-region (point) end))
593
594(defun reload-ispell ()
a6c5a8dd 595 "Tell Ispell to re-read your private dictionary."
d299ee07
RS
596 (interactive)
597 (ispell-cmd ":reload"))
598
d299ee07
RS
599(defun batch-make-ispell ()
600 (byte-compile-file "ispell.el")
601 (find-file "ispell.texinfo")
602 (let ((old-dir default-directory)
603 (default-directory "/tmp"))
604 (texinfo-format-buffer))
605 (Info-validate)
606 (if (get-buffer " *problems in info file*")
607 (kill-emacs 1))
608 (write-region (point-min) (point-max) "ispell.info"))
609
36ab15b2
RS
610(defun ispell-message ()
611 "Check the spelling for an outgoing mail message."
612 (interactive)
613 (save-excursion
614 (beginning-of-buffer)
615 (search-forward mail-header-separator nil t)
616 (ispell (current-buffer) (point))))
617
aa5f8836
RS
618(provide 'ispell)
619
1a06eabd 620;;; ispell.el ends here