(ispell-point): Do nothing if there's no word at START.
[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
225(define-key ispell-menu-map [ispell-region]
226 '("Check Region" . ispell-region))
227
228(define-key ispell-menu-map [ispell-buffer]
229 '("Check Buffer" . ispell))
230
231(define-key ispell-menu-map [ispell-word]
232 '("Check Word" . ispell-word))
233
7229064d 234;;;###autoload
d299ee07 235(defun ispell (&optional buf start end)
6370a710
RS
236 "Run Ispell over current buffer's visited file.
237First the file is scanned for misspelled words, then Ispell
d299ee07
RS
238enters a loop with the following commands for every misspelled word:
239
240DIGIT Near miss selector. If the misspelled word is close to
241 some words in the dictionary, they are offered as near misses.
242r Replace. Replace the word with a string you type. Each word
243 of your new string is also checked.
eb8c3be9 244i Insert. Insert this word in your private dictionary (kept in
d299ee07
RS
245 `$HOME/ispell.words').
246a Accept. Accept this word for the rest of this editing session,
eb8c3be9 247 but don't put it in your private dictionary.
d299ee07
RS
248l Lookup. Look for a word in the dictionary by fast binary
249 search, or search for a regular expression in the dictionary
250 using grep.
251SPACE Accept the word this time, but complain if it is seen again.
f30ff39f 252q, \\[keyboard-quit] Leave the command loop. You can come back later with \\[ispell-next]."
d299ee07
RS
253 (interactive)
254 (if (null start)
255 (setq start 0))
256 (if (null end)
257 (setq end 0))
258
259 (if (null buf)
260 (setq buf (current-buffer)))
261 (setq buf (get-buffer buf))
262 (if (null buf)
263 (error "Can't find buffer"))
a48fc390
RS
264 ;; Deactivate the mark, because we'll do it anyway if we change something,
265 ;; and a region highlight while in the Ispell loop is distracting.
0d28f072 266 (deactivate-mark)
d299ee07
RS
267 (save-excursion
268 (set-buffer buf)
269 (let ((filename buffer-file-name)
a48fc390 270 (delete-temp nil))
d299ee07
RS
271 (unwind-protect
272 (progn
273 (cond ((null filename)
274 (setq filename (make-temp-name "/usr/tmp/ispell"))
275 (setq delete-temp t)
276 (write-region (point-min) (point-max) filename))
277 ((and (buffer-modified-p buf)
278 (y-or-n-p (format "Save file %s? " filename)))
279 (save-buffer)))
280 (message "Ispell scanning file...")
281 (if (and ispell-enable-tex-parser
282 (ispell-tex-buffer-p))
283 (ispell-cmd ":tex")
284 (ispell-cmd ":generic"))
285 (ispell-cmd (format ":file %s %d %d" filename start end)))
a48fc390
RS
286 (if delete-temp
287 (condition-case ()
288 (delete-file filename)
289 (file-error nil)))))
d299ee07
RS
290 (message "Parsing ispell output ...")
291 (ispell-flush-bad-words)
292 (let (pos bad-words)
293 (while (numberp (setq pos (ispell-next-message)))
294 ;;ispell may check the words on the line following the end
295 ;;of the region - therefore, don't record anything out of range
296 (if (or (= end 0)
297 (< pos end))
298 (setq bad-words (cons (set-marker (make-marker) (+ pos 1))
299 bad-words))))
300 (setq bad-words (cons pos bad-words))
301 (setq ispell-bad-words (nreverse bad-words))))
302 (cond ((not (markerp (car ispell-bad-words)))
303 (setq ispell-bad-words nil)
304 (message "No misspellings."))
305 (t
306 (message "Ispell parsing done.")
307 (ispell-next))))
308
f305bd74 309;;;###autoload
31e1d920 310(defalias 'ispell-buffer 'ispell)
f305bd74 311
d299ee07 312(defun ispell-next ()
a6c5a8dd 313 "Resume command loop for most recent Ispell command."
d299ee07 314 (interactive)
a48fc390 315 (setq ispell-window-configuration nil)
d299ee07
RS
316 (unwind-protect
317 (catch 'quit
a48fc390
RS
318 ;; There used to be a save-excursion here,
319 ;; but that was annoying: it's better if point doesn't move
320 ;; when you type q.
321 (let (next)
322 (while (markerp (setq next (car ispell-bad-words)))
323 (switch-to-buffer (marker-buffer next))
324 (push-mark)
325 (ispell-point next "at saved position.")
326 (setq ispell-bad-words (cdr ispell-bad-words))
327 (set-marker next nil))))
328 (if ispell-window-configuration
329 (set-window-configuration ispell-window-configuration))
d299ee07
RS
330 (cond ((null ispell-bad-words)
331 (error "Ispell has not yet been run."))
332 ((markerp (car ispell-bad-words))
333 (message (substitute-command-keys
334 "Type \\[ispell-next] to continue.")))
335 ((eq (car ispell-bad-words) nil)
336 (setq ispell-bad-words nil)
337 (message "No more misspellings (but checker was interrupted.)"))
338 ((eq (car ispell-bad-words) t)
339 (setq ispell-bad-words nil)
340 (message "Ispell done."))
341 (t
342 (setq ispell-bad-words nil)
343 (message "Bad ispell internal list"))))
344 (ispell-dump))
345
7229064d 346;;;###autoload
6370a710 347(defun ispell-word (&optional resume)
d299ee07 348 "Check the spelling of the word under the cursor.
a6c5a8dd 349See the command `ispell' for more information.
6370a710
RS
350With a prefix argument, resume handling of the previous Ispell command."
351 (interactive "P")
352 (if resume
353 (ispell-next)
354 (condition-case err
355 (catch 'quit
356 (save-window-excursion
357 (ispell-point (point) "at point."))
358 (ispell-dump))
359 (ispell-startup-error
360 (cond ((y-or-n-p "Problem starting ispell, use old-style spell instead? ")
361 (load-library "spell")
362 (define-key esc-map "$" 'spell-word)
363 (spell-word)))))))
f305bd74
BP
364;;;###autoload
365(define-key esc-map "$" 'ispell-word)
d299ee07 366
7229064d 367;;;###autoload
d299ee07
RS
368(defun ispell-region (start &optional end)
369 "Check the spelling for all of the words in the region."
370 (interactive "r")
371 (ispell (current-buffer) start end))
372
373(defun ispell-letterp (c)
374 (and c
375 (or (and (>= c ?A) (<= c ?Z))
376 (and (>= c ?a) (<= c ?z))
377 (>= c 128))))
378
379(defun ispell-letter-or-quotep (c)
380 (and c
381 (or (and (>= c ?A) (<= c ?Z))
382 (and (>= c ?a) (<= c ?z))
383 (= c ?')
384 (>= c 128))))
385
386(defun ispell-find-word-start ()
387 ;;backward to a letter
388 (if (not (ispell-letterp (char-after (point))))
389 (while (and (not (bobp))
390 (not (ispell-letterp (char-after (- (point) 1)))))
391 (backward-char)))
392 ;;backward to beginning of word
393 (while (ispell-letter-or-quotep (char-after (- (point) 1)))
394 (backward-char))
395 (skip-chars-forward "'"))
396
397(defun ispell-find-word-end ()
398 (while (ispell-letter-or-quotep (char-after (point)))
399 (forward-char))
400 (skip-chars-backward "'"))
401
402(defun ispell-next-word ()
403 (while (and (not (eobp))
404 (not (ispell-letterp (char-after (point)))))
405 (forward-char)))
406
407;if end is nil, then do one word at start
408;otherwise, do all words from the beginning of the word where
409;start points, to the end of the word where end points
410(defun ispell-point (start message)
411 (let ((wend (make-marker))
412 rescan
413 end)
a48fc390
RS
414 ;; There used to be a save-excursion here,
415 ;; but that was annoying: it's better if point doesn't move
416 ;; when you type q.
d299ee07
RS
417 (goto-char start)
418 (ispell-find-word-start) ;find correct word start
419 (setq start (point-marker))
420 (ispell-find-word-end) ;now find correct end
421 (setq end (point-marker))
e8608d6a
RS
422 ;; Do nothing if we don't find a word.
423 (if (< start end)
424 (while (< start end)
425 (goto-char start)
426 (ispell-find-word-end) ;find end of current word
d299ee07
RS
427 ;could be before 'end' if
428 ;user typed replacement
429 ;that is more than one word
e8608d6a
RS
430 (set-marker wend (point))
431 (setq rescan nil)
432 (setq word (buffer-substring start wend))
433 (cond ((ispell-still-bad word)
a48fc390
RS
434;;; This just causes confusion. -- rms.
435;;; (goto-char start)
436;;; (sit-for 0)
e8608d6a
RS
437 (message (format "Ispell checking %s" word))
438 (ispell-cmd word)
439 (let ((message (ispell-next-message)))
440 (cond ((eq message t)
441 (message "%s: ok" word))
442 ((or (null message)
443 (consp message))
444 (setq rescan
445 (ispell-command-loop word start wend message)))
446 (t
447 (error "unknown ispell response %s" message))))))
448 (cond ((null rescan)
449 (goto-char wend)
450 (ispell-next-word)
451 (set-marker start (point))))))
d299ee07
RS
452 ;;clear the choices buffer; otherwise it's hard for the user to tell
453 ;;when we get back to the command loop
454 (let ((buf (get-buffer "*ispell choices*")))
455 (cond (buf
456 (set-buffer buf)
457 (erase-buffer))))
458 (set-marker start nil)
459 (set-marker end nil)
a48fc390 460 (set-marker wend nil)))
d299ee07
RS
461
462(defun ispell-still-bad (word)
463 (let ((words ispell-recently-accepted)
464 (ret t)
465 (case-fold-search t))
466 (while words
467 (cond ((eq (string-match (car words) word) 0)
468 (setq ret nil)
469 (setq words nil)))
470 (setq words (cdr words)))
471 ret))
472
473(defun ispell-show-choices (word message first-line)
f98955ea 474 ;;if there is only one window on the frame, make the ispell
d299ee07
RS
475 ;;messages winow be small. otherwise just use the other window
476 (let* ((selwin (selected-window))
477 (resize (eq selwin (next-window)))
478 (buf (get-buffer-create "*ispell choices*"))
479 w)
a48fc390
RS
480 (or ispell-window-configuration
481 (setq ispell-window-configuration (current-window-configuration)))
d299ee07
RS
482 (setq w (display-buffer buf))
483 (buffer-disable-undo buf)
484 (if resize
485 (unwind-protect
486 (progn
487 (select-window w)
488 (enlarge-window (- 6 (window-height w))))
489 (select-window selwin)))
490 (save-excursion
491 (set-buffer buf)
492 (bury-buffer buf)
493 (set-window-point w (point-min))
494 (set-window-start w (point-min))
495 (erase-buffer)
496 (insert first-line "\n")
497 (insert
498 "SPC skip; A accept; I insert; DIGIT select; R replace; \
499L lookup; Q quit\n")
500 (cond ((not (null message))
501 (let ((i 0))
502 (while (< i 3)
503 (let ((j 0))
504 (while (< j 3)
505 (let* ((n (+ (* j 3) i))
506 (choice (nth n message)))
507 (cond (choice
508 (let ((str (format "%d %s" n choice)))
509 (insert str)
510 (insert-char ? (- 20 (length str)))))))
511 (setq j (+ j 1))))
512 (insert "\n")
513 (setq i (+ i 1)))))))))
514
515(defun ispell-command-loop (word start end message)
516 (let ((flag t)
517 (rescan nil)
518 first-line)
519 (if (null message)
520 (setq first-line (concat "No near misses for '" word "'"))
521 (setq first-line (concat "Near misses for '" word "'")))
522 (while flag
523 (ispell-show-choices word message first-line)
524 (message "Ispell command: ")
8fa51910 525 (undo-boundary)
d299ee07
RS
526 (let ((c (downcase (read-char)))
527 replacement)
528 (cond ((and (>= c ?0)
529 (<= c ?9)
530 (setq replacement (nth (- c ?0) message)))
531 (ispell-replace start end replacement)
532 (setq flag nil))
533 ((= c ?q)
534 (throw 'quit nil))
535 ((= c ? )
536 (setq flag nil))
537 ((= c ?r)
538 (ispell-replace start end (read-string "Replacement: "))
539 (setq rescan t)
540 (setq flag nil))
541 ((= c ?i)
542 (ispell-insert word)
543 (setq flag nil))
544 ((= c ?a)
545 (ispell-accept word)
546 (setq flag nil))
547 ((= c ?l)
548 (let ((val (ispell-do-look word)))
549 (setq first-line (car val))
550 (setq message (cdr val))))
551 ((= c ??)
552 (message
553 "Type 'C-h d ispell' to the emacs main loop for more help")
554 (sit-for 2))
555 (t
556 (message "Bad ispell command")
557 (sit-for 2)))))
558 rescan))
559
560(defun ispell-do-look (bad-word)
561 (let (regex buf words)
562 (cond ((null ispell-have-new-look)
563 (setq regex (read-string "Lookup: ")))
564 (t
565 (setq regex (read-string "Lookup (regex): " "^"))))
566 (setq buf (get-buffer-create "*ispell look*"))
567 (save-excursion
568 (set-buffer buf)
569 (delete-region (point-min) (point-max))
570 (if ispell-have-new-look
571 (call-process "look" nil buf nil "-r" regex)
572 (call-process "look" nil buf nil regex))
573 (goto-char (point-min))
574 (forward-line 10)
575 (delete-region (point) (point-max))
576 (goto-char (point-min))
577 (while (not (= (point-min) (point-max)))
578 (end-of-line)
579 (setq words (cons (buffer-substring (point-min) (point)) words))
580 (forward-line)
581 (delete-region (point-min) (point)))
582 (kill-buffer buf)
583 (cons (format "Lookup '%s'" regex)
584 (reverse words)))))
585
586(defun ispell-replace (start end new)
587 (goto-char start)
588 (insert new)
589 (delete-region (point) end))
590
591(defun reload-ispell ()
a6c5a8dd 592 "Tell Ispell to re-read your private dictionary."
d299ee07
RS
593 (interactive)
594 (ispell-cmd ":reload"))
595
d299ee07
RS
596(defun batch-make-ispell ()
597 (byte-compile-file "ispell.el")
598 (find-file "ispell.texinfo")
599 (let ((old-dir default-directory)
600 (default-directory "/tmp"))
601 (texinfo-format-buffer))
602 (Info-validate)
603 (if (get-buffer " *problems in info file*")
604 (kill-emacs 1))
605 (write-region (point-min) (point-max) "ispell.info"))
606
aa5f8836
RS
607(provide 'ispell)
608
1a06eabd 609;;; ispell.el ends here