source: tags/1.4/source/cocoa-ide/hemlock/unused/archive/spell/spellcoms.lisp

Last change on this file was 6, checked in by Gary Byers, 21 years ago

Initial revision

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 31.3 KB
Line 
1;;; -*- Log: hemlock.log; Package: Hemlock -*-
2;;;
3;;; **********************************************************************
4;;; This code was written as part of the CMU Common Lisp project at
5;;; Carnegie Mellon University, and has been placed in the public domain.
6;;;
7(ext:file-comment
8 "$Header$")
9;;;
10;;; **********************************************************************
11;;;
12;;; Written by Bill Chiles and Rob Maclachlan.
13;;;
14;;; This file contains the code to implement commands using the spelling
15;;; checking/correcting stuff in Spell-Corr.Lisp and the dictionary
16;;; augmenting stuff in Spell-Augment.Lisp.
17
18(in-package "HEMLOCK")
19
20
21
22(defstruct (spell-info (:print-function print-spell-info)
23 (:constructor make-spell-info (pathname)))
24 pathname ;Dictionary file.
25 insertions) ;Incremental insertions for this dictionary.
26
27(defun print-spell-info (obj str n)
28 (declare (ignore n))
29 (let ((pn (spell-info-pathname obj)))
30 (format str "#<Spell Info~@[ ~S~]>"
31 (and pn (namestring pn)))))
32
33
34(defattribute "Spell Word Character"
35 "One if the character is one that is present in the spell dictionary,
36 zero otherwise.")
37
38(do-alpha-chars (c :both)
39 (setf (character-attribute :spell-word-character c) 1))
40(setf (character-attribute :spell-word-character #\') 1)
41
42
43(defvar *spelling-corrections* (make-hash-table :test #'equal)
44 "Mapping from incorrect words to their corrections.")
45
46(defvar *ignored-misspellings* (make-hash-table :test #'equal)
47 "A hashtable with true values for words that will be quietly ignored when
48 they appear.")
49
50(defhvar "Spell Ignore Uppercase"
51 "If true, then \"Check Word Spelling\" and \"Correct Buffer Spelling\" will
52 ignore unknown words that are all uppercase. This is useful for
53 abbreviations and cryptic formatter directives."
54 :value nil)
55
56
57
58
59;;;; Basic Spelling Correction Command (Esc-$ in EMACS)
60
61(defcommand "Check Word Spelling" (p)
62 "Check the spelling of the previous word and offer possible corrections
63 if the word in unknown. To add words to the dictionary from a text file see
64 the command \"Augment Spelling Dictionary\"."
65 "Check the spelling of the previous word and offer possible correct
66 spellings if the word is known to be misspelled."
67 (declare (ignore p))
68 (spell:maybe-read-spell-dictionary)
69 (let* ((region (spell-previous-word (current-point) nil))
70 (word (if region
71 (region-to-string region)
72 (editor-error "No previous word.")))
73 (folded (string-upcase word)))
74 (message "Checking spelling of ~A." word)
75 (unless (check-out-word-spelling word folded)
76 (get-word-correction (region-start region) word folded))))
77
78
79
80;;;; Auto-Spell mode:
81
82(defhvar "Check Word Spelling Beep"
83 "If true, \"Auto Check Word Spelling\" will beep when an unknown word is
84 found."
85 :value t)
86
87(defhvar "Correct Unique Spelling Immediately"
88 "If true, \"Auto Check Word Spelling\" will immediately attempt to correct any
89 unknown word, automatically making the correction if there is only one
90 possible."
91 :value t)
92
93
94(defhvar "Default User Spelling Dictionary"
95 "This is the pathname of a dictionary to read the first time \"Spell\" mode
96 is entered in a given editing session. When \"Set Buffer Spelling
97 Dictionary\" or the \"dictionary\" file option is used to specify a
98 dictionary, this default one is read also. It defaults to nil."
99 :value nil)
100
101(defvar *default-user-dictionary-read-p* nil)
102
103(defun maybe-read-default-user-spelling-dictionary ()
104 (let ((default-dict (value default-user-spelling-dictionary)))
105 (when (and default-dict (not *default-user-dictionary-read-p*))
106 (spell:maybe-read-spell-dictionary)
107 (spell:spell-read-dictionary (truename default-dict))
108 (setf *default-user-dictionary-read-p* t))))
109
110
111(defmode "Spell"
112 :transparent-p t :precedence 1.0 :setup-function 'spell-mode-setup)
113
114(defun spell-mode-setup (buffer)
115 (defhvar "Buffer Misspelled Words"
116 "This variable holds a ring of marks pointing to misspelled words."
117 :buffer buffer :value (make-ring 10 #'delete-mark))
118 (maybe-read-default-user-spelling-dictionary))
119
120(defcommand "Auto Spell Mode" (p)
121 "Toggle \"Spell\" mode in the current buffer. When in \"Spell\" mode,
122 the spelling of each word is checked after it is typed."
123 "Toggle \"Spell\" mode in the current buffer."
124 (declare (ignore p))
125 (setf (buffer-minor-mode (current-buffer) "Spell")
126 (not (buffer-minor-mode (current-buffer) "Spell"))))
127
128
129(defcommand "Auto Check Word Spelling" (p)
130 "Check the spelling of the previous word and display a message in the echo
131 area if the word is not in the dictionary. To add words to the dictionary
132 from a text file see the command \"Augment Spelling Dictionary\". If a
133 replacement for an unknown word has previously been specified, then the
134 replacement will be made immediately. If \"Correct Unique Spelling
135 Immediately\" is true, then this command will immediately correct words
136 which have a unique correction. If there is no obvious correction, then we
137 place the word in a ring buffer for access by the \"Correct Last Misspelled
138 Word\" command. If \"Check Word Spelling Beep\" is true, then this command
139 beeps when an unknown word is found, in addition to displaying the message."
140 "Check the spelling of the previous word, making obvious corrections, or
141 queuing the word in buffer-misspelled-words if we are at a loss."
142 (declare (ignore p))
143 (unless (eq (last-command-type) :spell-check)
144 (spell:maybe-read-spell-dictionary)
145 (let ((region (spell-previous-word (current-point) t)))
146 (when region
147 (let* ((word (nstring-upcase (region-to-string region)))
148 (len (length word)))
149 (declare (simple-string word))
150 (when (and (<= 2 len spell:max-entry-length)
151 (not (spell:spell-try-word word len)))
152 (let ((found (gethash word *spelling-corrections*))
153 (save (region-to-string region)))
154 (cond (found
155 (undoable-replace-word (region-start region) save found)
156 (message "Corrected ~S to ~S." save found)
157 (when (value check-word-spelling-beep) (beep)))
158 ((and (value spell-ignore-uppercase)
159 (every #'upper-case-p save))
160 (unless (gethash word *ignored-misspellings*)
161 (setf (gethash word *ignored-misspellings*) t)
162 (message "Ignoring ~S." save)))
163 (t
164 (let ((close (spell:spell-collect-close-words word)))
165 (cond ((and close
166 (null (rest close))
167 (value correct-unique-spelling-immediately))
168 (let ((fix (first close)))
169 (undoable-replace-word (region-start region)
170 save fix)
171 (message "Corrected ~S to ~S." save fix)))
172 (t
173 (ring-push (copy-mark (region-end region)
174 :right-inserting)
175 (value buffer-misspelled-words))
176 (let ((nclose
177 (do ((i 0 (1+ i))
178 (words close (cdr words))
179 (nwords () (cons (list i (car words))
180 nwords)))
181 ((null words) (nreverse nwords)))))
182 (message
183 "Word ~S not found.~
184 ~@[ Corrections:~:{ ~D=~A~}~]"
185 save nclose)))))
186 (when (value check-word-spelling-beep) (beep))))))))))
187 (setf (last-command-type) :spell-check))
188
189(defcommand "Correct Last Misspelled Word" (p)
190 "Fix a misspelling found by \"Auto Check Word Spelling\". This prompts for
191 a single character command to determine which action to take to correct the
192 problem."
193 "Prompt for a single character command to determine how to fix up a
194 misspelling detected by Check-Word-Spelling-Command."
195 (declare (ignore p))
196 (spell:maybe-read-spell-dictionary)
197 (do ((info (value spell-information)))
198 ((sub-correct-last-misspelled-word info))))
199
200(defun sub-correct-last-misspelled-word (info)
201 (let* ((missed (value buffer-misspelled-words))
202 (region (cond ((zerop (ring-length missed))
203 (editor-error "No recently misspelled word."))
204 ((spell-previous-word (ring-ref missed 0) t))
205 (t (editor-error "No recently misspelled word."))))
206 (word (region-to-string region))
207 (folded (string-upcase word))
208 (point (current-point))
209 (save (copy-mark point))
210 (res t))
211 (declare (simple-string word))
212 (unwind-protect
213 (progn
214 (when (check-out-word-spelling word folded)
215 (delete-mark (ring-pop missed))
216 (return-from sub-correct-last-misspelled-word t))
217 (move-mark point (region-end region))
218 (command-case (:prompt "Action: "
219 :change-window nil
220 :help "Type a single character command to do something to the misspelled word.")
221 (#\c "Try to find a correction for this word."
222 (unless (get-word-correction (region-start region) word folded)
223 (reprompt)))
224 (#\i "Insert this word in the dictionary."
225 (spell:spell-add-entry folded)
226 (push folded (spell-info-insertions info))
227 (message "~A inserted in the dictionary." word))
228 (#\r "Prompt for a word to replace this word with."
229 (let ((s (prompt-for-string :prompt "Replace with: "
230 :default word
231 :help "Type a string to replace occurrences of this word with.")))
232 (delete-region region)
233 (insert-string point s)
234 (setf (gethash folded *spelling-corrections*) s)))
235 (:cancel "Ignore this word and go to the previous misspelled word."
236 (setq res nil))
237 (:recursive-edit
238 "Go into a recursive edit and leave when it exits."
239 (do-recursive-edit))
240 ((:exit #\q) "Exit and forget about this word.")
241 ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)
242 "Choose this numbered word as the correct spelling."
243 (let ((num (digit-char-p (ext:key-event-char *last-key-event-typed*)))
244 (close-words (spell:spell-collect-close-words folded)))
245 (cond ((> num (length close-words))
246 (editor-error "Choice out of range."))
247 (t (let ((s (nth num close-words)))
248 (setf (gethash folded *spelling-corrections*) s)
249 (undoable-replace-word (region-start region)
250 word s)))))))
251 (delete-mark (ring-pop missed))
252 res)
253 (move-mark point save)
254 (delete-mark save))))
255
256(defhvar "Spelling Un-Correct Prompt for Insert"
257 "When this is set, \"Undo Last Spelling Correction\" will prompt before
258 inserting the old word into the dictionary."
259 :value nil)
260
261(defcommand "Undo Last Spelling Correction" (p)
262 "Undo the last incremental spelling correction.
263 The \"correction\" is replaced with the old word, and the old word is
264 inserted in the dictionary. When \"Spelling Un-Correct Prompt for Insert\"
265 is set, the user is asked about inserting the old word. Any automatic
266 replacement for the old word is eliminated."
267 "Undo the last incremental spelling correction, nuking any undesirable
268 side-effects."
269 (declare (ignore p))
270 (unless (hemlock-bound-p 'last-spelling-correction-mark)
271 (editor-error "No last spelling correction."))
272 (let ((mark (value last-spelling-correction-mark))
273 (words (value last-spelling-correction-words)))
274 (unless words
275 (editor-error "No last spelling correction."))
276 (let* ((new (car words))
277 (old (cdr words))
278 (folded (string-upcase old)))
279 (declare (simple-string old new folded))
280 (remhash folded *spelling-corrections*)
281 (delete-characters mark (length new))
282 (insert-string mark old)
283 (setf (value last-spelling-correction-words) nil)
284 (when (or (not (value spelling-un-correct-prompt-for-insert))
285 (prompt-for-y-or-n
286 :prompt (list "Insert ~A into spelling dictionary? " folded)
287 :default t
288 :default-string "Y"))
289 (push folded (spell-info-insertions (value spell-information)))
290 (spell:maybe-read-spell-dictionary)
291 (spell:spell-add-entry folded)
292 (message "Added ~S to spelling dictionary." old)))))
293
294
295;;; Check-Out-Word-Spelling -- Internal
296;;;
297;;; Return Nil if Word is a candidate for correction, otherwise
298;;; return T and message as to why it isn't.
299;;;
300(defun check-out-word-spelling (word folded)
301 (declare (simple-string word))
302 (let ((len (length word)))
303 (cond ((= len 1)
304 (message "Single character words are not in the dictionary.") t)
305 ((> len spell:max-entry-length)
306 (message "~A is too long for the dictionary." word) t)
307 (t
308 (multiple-value-bind (idx flagp) (spell:spell-try-word folded len)
309 (when idx
310 (message "Found it~:[~; because of ~A~]." flagp
311 (spell:spell-root-word idx))
312 t))))))
313
314;;; Get-Word-Correction -- Internal
315;;;
316;;; Find all known close words to the either unknown or incorrectly
317;;; spelled word we are checking. Word is the unmunged word, and Folded is
318;;; the uppercased word. Mark is a mark which points to the beginning of
319;;; the offending word. Return True if we successfully corrected the word.
320;;;
321(defun get-word-correction (mark word folded)
322 (let ((close-words (spell:spell-collect-close-words folded)))
323 (declare (list close-words))
324 (if close-words
325 (with-pop-up-display (s :height 3)
326 (do ((i 0 (1+ i))
327 (words close-words (cdr words)))
328 ((null words))
329 (format s "~36R=~A " i (car words)))
330 (finish-output s)
331 (let* ((key-event (prompt-for-key-event
332 :prompt "Correction choice: "))
333 (num (digit-char-p (ext:key-event-char key-event) 36)))
334 (cond ((not num) (return-from get-word-correction nil))
335 ((> num (length close-words))
336 (editor-error "Choice out of range."))
337 (t
338 (let ((s (nth num close-words)))
339 (setf (gethash folded *spelling-corrections*) s)
340 (undoable-replace-word mark word s)))))
341 (return-from get-word-correction t))
342 (with-pop-up-display (s :height 1)
343 (write-line "No corrections found." s)
344 nil))))
345
346
347;;; Undoable-Replace-Word -- Internal
348;;;
349;;; Like Spell-Replace-Word, but makes annotations in buffer local variables
350;;; so that "Undo Last Spelling Correction" can undo it.
351;;;
352(defun undoable-replace-word (mark old new)
353 (unless (hemlock-bound-p 'last-spelling-correction-mark)
354 (let ((buffer (current-buffer)))
355 (defhvar "Last Spelling Correction Mark"
356 "This variable holds a park pointing to the last spelling correction."
357 :buffer buffer :value (copy-mark (buffer-start-mark buffer)))
358 (defhvar "Last Spelling Correction Words"
359 "The replacement done for the last correction: (new . old)."
360 :buffer buffer :value nil)))
361 (move-mark (value last-spelling-correction-mark) mark)
362 (setf (value last-spelling-correction-words) (cons new old))
363 (spell-replace-word mark old new))
364
365
366
367;;;; Buffer Correction
368
369(defvar *spell-word-characters*
370 (make-array char-code-limit :element-type 'bit :initial-element 0)
371 "Characters that are legal in a word for spelling checking purposes.")
372
373(do-alpha-chars (c :both)
374 (setf (sbit *spell-word-characters* (char-code c)) 1))
375(setf (sbit *spell-word-characters* (char-code #\')) 1)
376
377
378(defcommand "Correct Buffer Spelling" (p)
379 "Correct spelling over whole buffer. A log of the found misspellings is
380 kept in the buffer \"Spell Corrections\". For each unknown word the
381 user may accept it, insert it in the dictionary, correct its spelling
382 with one of the offered possibilities, replace the word with a user
383 supplied word, or go into a recursive edit. Words may be added to the
384 dictionary in advance from a text file (see the command \"Augment
385 Spelling Dictionary\")."
386 "Correct spelling over whole buffer."
387 (declare (ignore p))
388 (clrhash *ignored-misspellings*)
389 (let* ((buffer (current-buffer))
390 (log (or (make-buffer "Spelling Corrections")
391 (getstring "Spelling Corrections" *buffer-names*)))
392 (point (buffer-end (buffer-point log)))
393 (*standard-output* (make-hemlock-output-stream point))
394 (window (or (car (buffer-windows log)) (make-window point))))
395 (format t "~&Starting spelling checking of buffer ~S.~2%"
396 (buffer-name buffer))
397 (spell:maybe-read-spell-dictionary)
398 (correct-buffer-spelling buffer window)
399 (delete-window window)
400 (close *standard-output*)))
401
402;;; CORRECT-BUFFER-SPELLING scans through buffer a line at a time, grabbing the
403;;; each line's string and breaking it up into words using the
404;;; *spell-word-characters* mask. We try the spelling of each word, and if it
405;;; is unknown, we call FIX-WORD and resynchronize when it returns.
406;;;
407(defun correct-buffer-spelling (buffer window)
408 (do ((line (mark-line (buffer-start-mark buffer)) (line-next line))
409 (info (if (hemlock-bound-p 'spell-information :buffer buffer)
410 (variable-value 'spell-information :buffer buffer)
411 (value spell-information)))
412 (mask *spell-word-characters*)
413 (word (make-string spell:max-entry-length)))
414 ((null line))
415 (declare (simple-bit-vector mask) (simple-string word))
416 (block line
417 (let* ((string (line-string line))
418 (length (length string)))
419 (declare (simple-string string))
420 (do ((start 0 (or skip-apostrophes end))
421 (skip-apostrophes nil nil)
422 end)
423 (nil)
424 ;;
425 ;; Find word start.
426 (loop
427 (when (= start length) (return-from line))
428 (when (/= (bit mask (char-code (schar string start))) 0) (return))
429 (incf start))
430 ;;
431 ;; Find the end.
432 (setq end (1+ start))
433 (loop
434 (when (= end length) (return))
435 (when (zerop (bit mask (char-code (schar string end)))) (return))
436 (incf end))
437 (multiple-value-setq (end skip-apostrophes)
438 (correct-buffer-word-end string start end))
439 ;;
440 ;; Check word.
441 (let ((word-len (- end start)))
442 (cond
443 ((= word-len 1))
444 ((> word-len spell:max-entry-length)
445 (format t "Not checking ~S -- too long for dictionary.~2%"
446 word))
447 (t
448 ;;
449 ;; Copy the word and uppercase it.
450 (do* ((i (1- end) (1- i))
451 (j (1- word-len) (1- j)))
452 ((zerop j)
453 (setf (schar word 0) (char-upcase (schar string i))))
454 (setf (schar word j) (char-upcase (schar string i))))
455 (unless (spell:spell-try-word word word-len)
456 (move-to-position (current-point) start line)
457 (fix-word (subseq word 0 word-len) (subseq string start end)
458 window info)
459 (let ((point (current-point)))
460 (setq end (mark-charpos point)
461 line (mark-line point)
462 string (line-string line)
463 length (length string))))))))))))
464
465;;; CORRECT-BUFFER-WORD-END takes a line string from CORRECT-BUFFER-SPELLING, a
466;;; start, and a end. It places end to exclude from the word apostrophes used
467;;; for quotation marks, possessives, and funny plurals (e.g., A's and AND's).
468;;; Every word potentially can be followed by "'s", and any clown can use the
469;;; `` '' Scribe ligature. This returns the value to use for end of the word
470;;; and the value to use as the end when continuing to find the next word in
471;;; string.
472;;;
473(defun correct-buffer-word-end (string start end)
474 (cond ((and (> (- end start) 2)
475 (char= (char-upcase (schar string (1- end))) #\S)
476 (char= (schar string (- end 2)) #\'))
477 ;; Use roots of possessives and funny plurals (e.g., A's and AND's).
478 (values (- end 2) end))
479 (t
480 ;; Maybe backup over apostrophes used for quotation marks.
481 (do ((i (1- end) (1- i)))
482 ((= i start) (values end end))
483 (when (char/= (schar string i) #\')
484 (return (values (1+ i) end)))))))
485
486;;; Fix-Word -- Internal
487;;;
488;;; Handles the case where the word has a known correction. If is does
489;;; not then call Correct-Buffer-Word-Not-Found. In either case, the
490;;; point is left at the place to resume checking.
491;;;
492(defun fix-word (word unfolded-word window info)
493 (declare (simple-string word unfolded-word))
494 (let ((correction (gethash word *spelling-corrections*))
495 (mark (current-point)))
496 (cond (correction
497 (format t "Replacing ~S with ~S.~%" unfolded-word correction)
498 (spell-replace-word mark unfolded-word correction))
499 ((and (value spell-ignore-uppercase)
500 (every #'upper-case-p unfolded-word))
501 (character-offset mark (length word))
502 (unless (gethash word *ignored-misspellings*)
503 (setf (gethash word *ignored-misspellings*) t)
504 (format t "Ignoring ~S.~%" unfolded-word)))
505 (t
506 (correct-buffer-word-not-found word unfolded-word window info)))))
507
508(defun correct-buffer-word-not-found (word unfolded-word window info)
509 (declare (simple-string word unfolded-word))
510 (let* ((close-words (spell:spell-collect-close-words word))
511 (close-words-len (length (the list close-words)))
512 (mark (current-point))
513 (wordlen (length word)))
514 (format t "Unknown word: ~A~%" word)
515 (cond (close-words
516 (format t "~[~;A~:;Some~]~:* possible correction~[~; is~:;s are~]: "
517 close-words-len)
518 (if (= close-words-len 1)
519 (write-line (car close-words))
520 (let ((n 0))
521 (dolist (w close-words (terpri))
522 (format t "~36R=~A " n w)
523 (incf n)))))
524 (t
525 (write-line "No correction possibilities found.")))
526 (let ((point (buffer-point (window-buffer window))))
527 (unless (displayed-p point window)
528 (center-window window point)))
529 (command-case
530 (:prompt "Action: "
531 :help "Type a single letter command, or help character for help."
532 :change-window nil)
533 (#\i "Insert unknown word into dictionary for future lookup."
534 (spell:spell-add-entry word)
535 (push word (spell-info-insertions info))
536 (format t "~S added to dictionary.~2%" word))
537 (#\c "Correct the unknown word with possible correct spellings."
538 (unless close-words
539 (write-line "There are no possible corrections.")
540 (reprompt))
541 (let ((num (if (= close-words-len 1) 0
542 (digit-char-p (ext:key-event-char
543 (prompt-for-key-event
544 :prompt "Correction choice: "))
545 36))))
546 (unless num (reprompt))
547 (when (> num close-words-len)
548 (beep)
549 (write-line "Response out of range.")
550 (reprompt))
551 (let ((choice (nth num close-words)))
552 (setf (gethash word *spelling-corrections*) choice)
553 (spell-replace-word mark unfolded-word choice)))
554 (terpri))
555 (#\a "Accept the word as correct (that is, ignore it)."
556 (character-offset mark wordlen))
557 (#\r "Replace the unknown word with a supplied replacement."
558 (let ((s (prompt-for-string
559 :prompt "Replacement Word: "
560 :default unfolded-word
561 :help "String to replace the unknown word with.")))
562 (setf (gethash word *spelling-corrections*) s)
563 (spell-replace-word mark unfolded-word s))
564 (terpri))
565 (:recursive-edit
566 "Go into a recursive edit and resume correction where the point is left."
567 (do-recursive-edit)))))
568
569;;; Spell-Replace-Word -- Internal
570;;;
571;;; Replaces Old with New, starting at Mark. The case of Old is used
572;;; to derive the new case.
573;;;
574(defun spell-replace-word (mark old new)
575 (declare (simple-string old new))
576 (let ((res (cond ((lower-case-p (schar old 0))
577 (string-downcase new))
578 ((lower-case-p (schar old 1))
579 (let ((res (string-downcase new)))
580 (setf (char res 0) (char-upcase (char res 0)))
581 res))
582 (t
583 (string-upcase new)))))
584 (with-mark ((m mark :left-inserting))
585 (delete-characters m (length old))
586 (insert-string m res))))
587
588
589
590;;;; User Spelling Dictionaries.
591
592(defvar *pathname-to-spell-info* (make-hash-table :test #'equal)
593 "This maps dictionary files to spelling information.")
594
595(defhvar "Spell Information"
596 "This is the information about a spelling dictionary and its incremental
597 insertions."
598 :value (make-spell-info nil))
599
600(define-file-option "Dictionary" (buffer file)
601 (let* ((dict (merge-pathnames
602 file
603 (make-pathname :defaults (buffer-default-pathname buffer)
604 :type "dict")))
605 (dictp (probe-file dict)))
606 (if dictp
607 (set-buffer-spelling-dictionary-command nil dictp buffer)
608 (loud-message "Couldn't find dictionary ~A." (namestring dict)))))
609
610;;; SAVE-DICTIONARY-ON-WRITE is on the "Write File Hook" in buffers with
611;;; the "dictionary" file option.
612;;;
613(defun save-dictionary-on-write (buffer)
614 (when (hemlock-bound-p 'spell-information :buffer buffer)
615 (save-spelling-insertions
616 (variable-value 'spell-information :buffer buffer))))
617
618
619(defcommand "Save Incremental Spelling Insertions" (p)
620 "Append incremental spelling dictionary insertions to a file. The file
621 is prompted for unless \"Set Buffer Spelling Dictionary\" has been
622 executed in the buffer."
623 "Append incremental spelling dictionary insertions to a file."
624 (declare (ignore p))
625 (let* ((info (value spell-information))
626 (file (or (spell-info-pathname info)
627 (value default-user-spelling-dictionary)
628 (prompt-for-file
629 :prompt "Dictionary File: "
630 :default (dictionary-name-default)
631 :must-exist nil
632 :help
633 "Name of the dictionary file to append dictionary insertions to."))))
634 (save-spelling-insertions info file)
635 (let* ((ginfo (variable-value 'spell-information :global))
636 (insertions (spell-info-insertions ginfo)))
637 (when (and insertions
638 (prompt-for-y-or-n
639 :prompt
640 `("Global spelling insertions exist.~%~
641 Save these to ~A also? "
642 ,(namestring file)
643 :default t
644 :default-string "Y"))
645 (save-spelling-insertions ginfo file))))))
646
647(defun save-spelling-insertions (info &optional
648 (name (spell-info-pathname info)))
649 (when (spell-info-insertions info)
650 (with-open-file (stream name
651 :direction :output :element-type 'base-char
652 :if-exists :append :if-does-not-exist :create)
653 (dolist (w (spell-info-insertions info))
654 (write-line w stream)))
655 (setf (spell-info-insertions info) ())
656 (message "Incremental spelling insertions for ~A written."
657 (namestring name))))
658
659(defcommand "Set Buffer Spelling Dictionary" (p &optional file buffer)
660 "Prompts for the dictionary file to associate with the current buffer.
661 If this file has not been read for any other buffer, then it is read.
662 Incremental spelling insertions from this buffer can be appended to
663 this file with \"Save Incremental Spelling Insertions\"."
664 "Sets the buffer's spelling dictionary and reads it if necessary."
665 (declare (ignore p))
666 (maybe-read-default-user-spelling-dictionary)
667 (let* ((file (truename (or file
668 (prompt-for-file
669 :prompt "Dictionary File: "
670 :default (dictionary-name-default)
671 :help
672 "Name of the dictionary file to add into the current dictionary."))))
673 (file-name (namestring file))
674 (spell-info-p (gethash file-name *pathname-to-spell-info*))
675 (spell-info (or spell-info-p (make-spell-info file)))
676 (buffer (or buffer (current-buffer))))
677 (defhvar "Spell Information"
678 "This is the information about a spelling dictionary and its incremental
679 insertions."
680 :value spell-info :buffer buffer)
681 (add-hook write-file-hook 'save-dictionary-on-write)
682 (unless spell-info-p
683 (setf (gethash file-name *pathname-to-spell-info*) spell-info)
684 (read-spelling-dictionary-command nil file))))
685
686(defcommand "Read Spelling Dictionary" (p &optional file)
687 "Adds entries to the dictionary from a file in the following format:
688
689 entry1/flag1/flag2/flag3
690 entry2
691 entry3/flag1/flag2/flag3/flag4/flag5.
692
693 The flags are single letter indicators of legal suffixes for the entry;
694 the available flags and their correct use may be found at the beginning
695 of spell-correct.lisp in the Hemlock sources. There must be exactly one
696 entry per line, and each line must be flushleft."
697 "Add entries to the dictionary from a text file in a specified format."
698 (declare (ignore p))
699 (spell:maybe-read-spell-dictionary)
700 (spell:spell-read-dictionary
701 (or file
702 (prompt-for-file
703 :prompt "Dictionary File: "
704 :default (dictionary-name-default)
705 :help
706 "Name of the dictionary file to add into the current dictionary."))))
707
708(defun dictionary-name-default ()
709 (make-pathname :defaults (buffer-default-pathname (current-buffer))
710 :type "dict"))
711
712(defcommand "Add Word to Spelling Dictionary" (p)
713 "Add the previous word to the spelling dictionary."
714 "Add the previous word to the spelling dictionary."
715 (declare (ignore p))
716 (spell:maybe-read-spell-dictionary)
717 (let ((word (region-to-string (spell-previous-word (current-point) nil))))
718 ;;
719 ;; SPELL:SPELL-ADD-ENTRY destructively uppercases word.
720 (when (spell:spell-add-entry word)
721 (message "Word ~(~S~) added to the spelling dictionary." word)
722 (push word (spell-info-insertions (value spell-information))))))
723
724(defcommand "Remove Word from Spelling Dictionary" (p)
725 "Prompts for word to remove from the spelling dictionary."
726 "Prompts for word to remove from the spelling dictionary."
727 (declare (ignore p))
728 (spell:maybe-read-spell-dictionary)
729 (let* ((word (prompt-for-string
730 :prompt "Word to remove from spelling dictionary: "
731 :trim t))
732 (upword (string-upcase word)))
733 (declare (simple-string word))
734 (multiple-value-bind (index flagp)
735 (spell:spell-try-word upword (length word))
736 (unless index
737 (editor-error "~A not in dictionary." upword))
738 (if flagp
739 (remove-spelling-word upword)
740 (let ((flags (spell:spell-root-flags index)))
741 (when (or (not flags)
742 (prompt-for-y-or-n
743 :prompt
744 `("Deleting ~A also removes words formed from this root and these flags: ~% ~
745 ~S.~%~
746 Delete word anyway? "
747 ,word ,flags)
748 :default t
749 :default-string "Y"))
750 (remove-spelling-word upword)))))))
751
752;;; REMOVE-SPELLING-WORD removes the uppercase word word from the spelling
753;;; dictionary and from the spelling informations incremental insertions list.
754;;;
755(defun remove-spelling-word (word)
756 (let ((info (value spell-information)))
757 (spell:spell-remove-entry word)
758 (setf (spell-info-insertions info)
759 (delete word (spell-info-insertions info) :test #'string=))))
760
761(defcommand "List Incremental Spelling Insertions" (p)
762 "Display the incremental spelling insertions for the current buffer's
763 associated spelling dictionary file."
764 "Display the incremental spelling insertions for the current buffer's
765 associated spelling dictionary file."
766 (declare (ignore p))
767 (let* ((info (value spell-information))
768 (file (spell-info-pathname info))
769 (insertions (spell-info-insertions info)))
770 (declare (list insertions))
771 (with-pop-up-display (s :height (1+ (length insertions)))
772 (if file
773 (format s "Incremental spelling insertions for dictionary ~A:~%"
774 (namestring file))
775 (write-line "Global incremental spelling insertions:" s))
776 (dolist (w insertions)
777 (write-line w s)))))
778
779
780
781
782;;;; Utilities for above stuff.
783
784;;; SPELL-PREVIOUS-WORD returns as a region the current or previous word, using
785;;; the spell word definition. If there is no such word, return nil. If end-p
786;;; is non-nil, then mark ends the word even if there is a non-delimiter
787;;; character after it.
788;;;
789;;; Actually, if mark is between the first character of a word and a
790;;; non-spell-word characer, it is considered to be in that word even though
791;;; that word is after the mark. This is because Hemlock's cursor is always
792;;; displayed over the next character, so users tend to think of a cursor
793;;; displayed on the first character of a word as being in that word instead of
794;;; before it.
795;;;
796(defun spell-previous-word (mark end-p)
797 (with-mark ((point mark)
798 (mark mark))
799 (cond ((or end-p
800 (zerop (character-attribute :spell-word-character
801 (next-character point))))
802 (unless (reverse-find-attribute mark :spell-word-character)
803 (return-from spell-previous-word nil))
804 (move-mark point mark)
805 (reverse-find-attribute point :spell-word-character #'zerop))
806 (t
807 (find-attribute mark :spell-word-character #'zerop)
808 (reverse-find-attribute point :spell-word-character #'zerop)))
809 (cond ((and (> (- (mark-charpos mark) (mark-charpos point)) 2)
810 (char= (char-upcase (previous-character mark)) #\S)
811 (char= (prog1 (previous-character (mark-before mark))
812 (mark-after mark))
813 #\'))
814 ;; Use roots of possessives and funny plurals (e.g., A's and AND's).
815 (character-offset mark -2))
816 (t
817 ;; Maybe backup over apostrophes used for quotation marks.
818 (loop
819 (when (mark= point mark) (return-from spell-previous-word nil))
820 (when (char/= (previous-character mark) #\') (return))
821 (mark-before mark))))
822 (region point mark)))
Note: See TracBrowser for help on using the repository browser.