Hi,
following the discussion at #58, I share here a hydra I cooked for flyspell spellchecking making use of flyspell-correct-at-point
as basic correction interface. As I mentioned there, it is not strictly within the logic of flyspell-correct
but I believe it may complement it in the particular task of whole buffer checking, so it might be useful. So, this is not a "request" nor a "bug report". It is meant only as sharing some ideas I found worked well for me, for your use in flyspell-correct
as you please, if you please. Of course, if some of the ideas or code are deemed interesting enough to be somehow incorporated to flyspell-correct
, I'll be more than happy with that.
If I understand the design choice correctly, flyspell-correct
is mainly targeted at making fast spell corrections on a local scope, as you type, and excels at that. This is pretty much the same logic of flyspell
itself. But sometimes we also need to do a thorough "whole buffer" check, particularly when we reach a revision stage of a given document. The standard Emacs tool for this task is ispell
. Here, in the sense of being designed for it: it keeps track of a ispell session, it defaults to ispell-buffer
if there is no region, etc. Of course, we can use ispell-word
to correct a word at point or go about a whole buffer with flyspell-goto-next-error
and then using flyspell-correct-at-point
(or vanilla flyspell
facilities for that), but in either case things are less than optimal. (I probably wouldn't start flyspell-correct-wrapper
for a whole buffer spell-checking task myself, but it is of course possible, and seems to be so used, e.g. #60 (comment)).
Sure, we could use either tool where it is most appropriate, and probably this is what many people actually do. But I wanted to keep a single UI for spellchecking to reduce the cognitive strain of switching between quite different interfaces. Flyspell would be then the natural candidate, for it is hard to beat for the "spell-checking as you type" task. I've also been reaching best results for LaTeX with it. And I use flyspell-correct
(-ivy
) as my Flyspell interface, and like it very much.
So I wanted to improve upon flyspell
for this particular task of "whole buffer checking" while keeping the goodness of flyspell-correct
. This is what this hydra is about.
That given, a couple of things stood out as differences with respect to the behavior of flyspell-correct
for this particular task I wanted to address:
-
point handling: flyspell-correct
restores point after its task is finished. I find this brilliant for "correct while typing" kind of task (It was actually the killer behavior that brought me to use flyspell-correct
in the first place). But it is not what I want when doing a "whole buffer" kind of task. Sometimes the spell-checking requires some sort of special intervention/editing for which the want to actually leave the checker, do something else, and then take things back from where we were with spell-checking. Ispell handles this: it offers a way to leave point at the word which was being checked, and we can restart it again with a prefix etc.
-
skipping: when doing a local "correct while typing" kind of task we see what the problem is and want to correct fast, but sometimes we have to skip some words to get there; when doing a "whole buffer" kind of task we are typically at a revision/polishing phase when the ratio of "skipping/correcting" is much larger. So for the first case we want "mostly correcting, and sometimes skipping", whereas for the second "mostly skipping, and sometimes correcting". Ispell is also sensitive to this: it is not by chance that the space-bar skips in ispell
, whereas it replaces in query-replace
.
One thing I wanted to keep from flyspell-correct
is it's bidirectional functioning, which is not really granted by either flyspell
or ispell
. True, this is probably less important for a "whole buffer" kind of task, but I like this flexibility and, in my experience, sometimes things that show up further down the road of a checking session makes us regret a previous decision of skipping or accepting and I want to go back. So it's a nice flexibility I wanted to have: navigating back and forth the spelling mistakes.
Those things given, I came up with the hydra plus auxiliary functions below. It admittedly is not within the logic of flyspell-correct-move
, and just makes use of flyspell-correct-at-point
when needed, which grants the UI experience in line with flyspell-correct
in general. The core are the skip functions (just adapted versions of flyspell-goto-next-error
). The result is more akin to the ispell
logic (given the task envisaged), but with even more flexibility and nicer interface granted by flyspell-correct
.
See how you like it and, as said before, if there's anything you can take from it (including all of it) for flyspell-correct
, I'd be very glad. If not, it is fine too, the fun in sharing and discussing is more than good enough.
(defhydra hydra-flyspell (:color amaranth
:body-pre
(progn
(when mark-active
(deactivate-mark))
(when (or (not (mark t))
(/= (mark t) (point)))
(push-mark (point) t)))
:hint nil)
"
^Flyspell^ ^Errors^ ^Word^
---------------------------------------------------------
_b_ check buffer _c_ correct _s_ save (buffer)
_d_ change dict _n_ goto next _l_ lowercase (buffer)
_u_ undo _p_ goto previous _a_ accept (session)
_q_ quit
"
("b" flyspell-buffer)
("d" ispell-change-dictionary)
("u" undo-tree-undo)
("q" nil :color blue)
("C-/" undo-tree-undo)
("c" my/flyspell-correct-at-point-maybe-next)
("n" my/flyspell-goto-next-error)
("p" my/flyspell-goto-previous-error)
("." my/flyspell-correct-at-point-maybe-next)
("SPC" my/flyspell-goto-next-error)
("DEL" my/flyspell-goto-previous-error)
("s" my/flyspell-accept-word-buffer)
("l" my/flyspell-accept-lowercased-buffer)
("a" my/flyspell-accept-word)
("M->" end-of-buffer)
("M-<" beginning-of-buffer)
("C-v" scroll-up-command)
("M-v" scroll-down-command))
(defun my/flyspell-error-p (&optional position)
"Return non-nil if at a flyspell misspelling, and nil otherwise."
;; The check technique comes from 'flyspell-goto-next-error'.
(let* ((pos (or position (point)))
(ovs (overlays-at pos))
r)
(while (and (not r) (consp ovs))
(if (flyspell-overlay-p (car ovs))
(setq r t)
(setq ovs (cdr ovs))))
r))
(defvar my/hydra-flyspell-direction 'forward)
(defun my/flyspell-correct-at-point-maybe-next ()
(interactive)
(cond ((my/flyspell-error-p)
(save-excursion
(flyspell-correct-at-point)))
((equal my/hydra-flyspell-direction 'forward)
(my/flyspell-goto-next-error)
;; recheck, for 'my/flyspell-goto-next-error' can legitimately stop
;; at the end of buffer
(when (my/flyspell-error-p)
(save-excursion
(flyspell-correct-at-point))))
((equal my/hydra-flyspell-direction 'backward)
(my/flyspell-goto-previous-error)
;; recheck, for 'my/flyspell-goto-previous-error' can legitimately
;; stop at the beginning of buffer
(when (my/flyspell-error-p)
(save-excursion
(flyspell-correct-at-point))))))
;; Just an adapted version of 'flyspell-goto-next-error'.
(defun my/flyspell-goto-previous-error ()
"Go to the previous previously detected error.
In general FLYSPELL-GOTO-PREVIOUS-ERROR must be used after
FLYSPELL-BUFFER."
(interactive)
(setq my/hydra-flyspell-direction 'backward)
(let ((pos (point))
(min (point-min)))
(if (and (eq (current-buffer) flyspell-old-buffer-error)
(eq pos flyspell-old-pos-error))
(progn
(if (= flyspell-old-pos-error min)
;; goto end of buffer
(progn
(message "Restarting from end of buffer")
(goto-char (point-max)))
(backward-word 1))
(setq pos (point))))
;; seek the previous error
(while (and (> pos min)
(not (my/flyspell-error-p pos)))
(setq pos (1- pos)))
(goto-char pos)
(when (eq (char-syntax (preceding-char)) ?w)
(backward-word 1))
;; save the current location for next invocation
(setq flyspell-old-pos-error (point))
(setq flyspell-old-buffer-error (current-buffer))
(if (= pos min)
(message "No more miss-spelled word!")))
;; After moving, check again if we are at a misspelling (accepting a word
;; might have changed this, since the last check). If not, go to the next
;; error again, unless we are at point-min (otherwise we might enter into
;; infinite loop, if there are no remaining errors).
(flyspell-word)
(unless (or (= (point) (point-min))
(my/flyspell-error-p))
(my/flyspell-goto-previous-error))
(when (my/flyspell-error-p)
(swiper--ensure-visible)))
(defun my/flyspell-goto-next-error ()
"Go to the next previously detected error.
In general FLYSPELL-GOTO-NEXT-ERROR must be used after
FLYSPELL-BUFFER."
;; Just a recursive wrapper on the original flyspell function, which takes
;; into account possible changes of accepted words since the last check.
(interactive)
(setq my/hydra-flyspell-direction 'forward)
(flyspell-goto-next-error)
;; After moving, check again if we are at a misspelling. If not, go to
;; the next error again.
(flyspell-word)
(unless (or (= (point) (point-max))
(my/flyspell-error-p))
(my/flyspell-goto-next-error))
(when (my/flyspell-error-p)
(swiper--ensure-visible)))
(defun my/flyspell-accept-word (&optional local-dict lowercase)
"Accept word at point for this session.
If LOCAL-DICT is non-nil, also add it to the buffer-local
dictionary. And if LOWERCASE is non-nil, do so with the word
lower-cased."
(interactive)
(let ((word (flyspell-get-word)))
(if (not (and (consp word)
;; Check if we are actually at a flyspell error.
(my/flyspell-error-p (car (cdr word)))))
(message "Point is not at a misspelling.")
(let ((start (car (cdr word)))
(word (car word)))
(when (and local-dict lowercase)
(setq word (downcase word)))
;; This is just taken (slightly adjusted) from
;; 'flyspell-do-correct', which is essentially what
;; 'ispell-command-loop' also does.
(ispell-send-string (concat "@" word "\n"))
(add-to-list 'ispell-buffer-session-localwords word)
(or ispell-buffer-local-name ; session localwords might conflict
(setq ispell-buffer-local-name (buffer-name)))
(flyspell-unhighlight-at start)
(if (null ispell-pdict-modified-p)
(setq ispell-pdict-modified-p
(list ispell-pdict-modified-p)))
(if local-dict
(ispell-add-per-file-word-list word))))))
(defun my/flyspell-accept-word-buffer ()
"See `my/flyspell-accept-word'."
(interactive)
(my/flyspell-accept-word 'local-dict))
(defun my/flyspell-accept-lowercased-buffer ()
"See `my/flyspell-accept-word'."
(interactive)
(my/flyspell-accept-word 'local-dict 'lowercase))
Notes: 1) I use undo-tree
, place there whatever you use; 2) this lacks the facility of saving the word to the "global" dictionary, which I don't use, but it could easily be added; 3) this is an amaranth hydra, so the session will be mostly "sticky", which is meant, unless you explicitly want to leave it; 4) the hydra includes some navigation commands and alternative bindings for base actions which are not in explicit in the "menu".