Subject: bug#76884: 30.1 problem with flyspell-maybe-correct-transposition

From: Jim Diamond <[email-masked]>
Date: Sun, 9 Mar 2025 13:46:23 -0300

On Sun, Mar  9, 2025 at 13:00 (+0200), Eli Zaretskii wrote:

>> Date: Sat, 8 Mar 2025 20:50:26 -0400
>> From: Jim Diamond <[email-masked]>

>> I recently upgraded to emacs 30.1.  For many years I had been using a
>> function which automatically corrected transposition errors, but it no
>> longer works in 30.1.  The author of that function kindly pointed me to the
>> fact that such functionality has been part of flyspell for a long time, and
>> that I should add
>>     (add-hook 'flyspell-incorrect-hook 'flyspell-maybe-correct-transposition)
>> to my init.el.

>> So I put that line in my init.el and started editing away, only to discover
>> that it doesn't do exactly what I want.  To reproduce the problem,

>> (1) Create a file ABC like this
>> ----------------- cut here --------------------

>> ABC

>> ----------------- cut here --------------------

>> (2) Replace your entire init.el with this one line:
>> (add-hook 'flyspell-incorrect-hook 'flyspell-maybe-correct-transposition)
>> (Or start with "emacs -Q" and make the "obvious" changes to the procedure
>> below.)

>> (3) Fire up a fresh emacs as follows:
>> emacs ABC

>> (4) Turn on flyspell
>> M-x flyspell-mode

>> (5) Navigate to the end of the 'abc' line

>> (6) Type Ctrl-T

>> Result: I get "CAB", whereas I expect "ACB".  This is not merely
>> unexpected, but an actual problem.

> What causes that problem is that add-hook you did.  If you want C-t to
> transpose characters, and Flyspell to not react on C-t, then why did
> you add to that hook?

I added to that hook because, when the above-mentioned "automatically fix
transposition errors" stopped working (with my upgrade to 30.1), I went
looking for a newer version of the software.  The original author told me
that flyspell has had the ability to automatically fix transposition errors
for some time, and thus he has stopped updating his code.  And he suggested
I add that function to that hook.  So I did.

Given that he (presumably!) understands perfectly what his original
software did, I assumed that adding to that hook would do the same thing.
It doesn't, at least the way I am using it.

> IOW, I don't understand what scenario worked for you in older versions
> of Emacs and no longer works in Emacs 30.1.  Could you please show a
> recipe, starting from "emacs -Q", which did NOT convert ABC into CAB
> in Emacs 29 or earlier, but does with Emacs 30.1?   Because if I try
> using that add-hook in Emacs 28.2, I get the same result: ABC => CAB,
> if I type C-t at the end of the ABC line.  So I'm confused regarding
> the problem which you found in Emacs 30 that didn't exist before.

I am thinking you ignored the part where I said "For many years I had been
using a function which automatically corrected transposition errors, but it
no longer works in 30.1.  The author of that function kindly pointed me to
the fact that such functionality has been part of flyspell for a long time,
and that I should add
     (add-hook 'flyspell-incorrect-hook 'flyspell-maybe-correct-transposition)
to my init.el."

(Many years is sincce 1999 or so, in case anyone reading this is wondering.)

So the "recipe" doesn't depend on flyspell.  It depends on abbrev-mode, and
most recently was triggered as follows:
        (advice-add abbrev-expand-function 
            :before (lambda () (fix-transposed-characters)))
This worked in 28.2 and (IIRC) in 29.4.

>> With my old setup in emacs 28 and before, the transposition correction
>> function was triggered when emacs was about to see if there was an
>> abbreviation to expand, so if I was anticipating a problem I could
>> transpose two letters and then move away from the "word" using something
>> other than a space (e.g., arrow keys), and the text would stay how I wanted
>> it.

> Sorry, I don't understand this description.  Could you please show a
> recipe with all the commands and key sequences explicit?

With apologies to the original author of the code (John Wiegley), here is
my somewhat modified version of his code which did what I want in 28.2.  I
hesitated to mention his name because some of my changes may be abhorrent
to him or other people reading this, and I don't want any bad thoughts or
words sent his way.

I don't have a system with 29.3 or 29.4 to test this, but I did test this
on 28.2.

(1) Put this code in a file called auto-correct.el:

%%%%%------------------ auto-correct.el --------------------

(require 'ispell)

(defvar correct-words-applicable-modes
  '(text-mode zsd-mail-mode c-mode c++-mode texinfo-mode plain-tex-mode)
  "*A list of modes in which words should be automatically corrected.")

(defun auto-correct-check-word ()
  "Call ispell-word with ispell-check-only set to t.
Setting this to t will not prompt for corrections, but return nil if the
word is incorrect.  Redefine (message) and (beep) to shut ispell up."
  (let ((ispell-check-only t)
        (beep-def (symbol-function 'beep))
        (message-def (symbol-function 'message))
        (found t)
       )
    (defun beep () (setq found nil))
    (defun message (&rest garbage) t)
    (ispell-word nil t)
    (fset 'beep beep-def)
    (fset 'message message-def)
    found
  )
)

(defun fix-transposed-characters ()
    "Fix any transposed characters in the input."
    (catch 'done
        (if (and (memq major-mode correct-words-applicable-modes)
                 (not (auto-correct-check-word)))
            (let ((end (point)))
                (backward-word 1)
                (if (not (looking-at "[a-zA-Z]+\\([ \t]\\|$\\)"))
                    (prong
                        (goto-char end)
                        (throw 'done t)
                    )
                )
                (if (and
                        (equal major-mode 'plain-tex-mode)
                        (if (not (bobp))
                            (save-excursion
                                (backward-char 1)
                                (looking-at "\\\\")
                            )
                            nil
                        )
                    )
                    (prong
                        (goto-char end)
                        (throw 'done t)
                    )
                )
                (let ((case-fold-search nil))
                    (if (looking-at "[A-Z]+\\([ \t]\\|$\\)")
                        (prong
                            (goto-char end)
                            (throw 'done t)
                        )
                    )
                )
                (forward-char 1)
                (while (< (point) end)
                    (transpose-chars nil)
                    (if (auto-correct-check-word)
                        (progn
                            (goto-char end)
                            (throw 'done t)
                        )
                        (forward-char -1)
                        (transpose-chars nil)
                    )
                )
            )
        )
    )
)

(advice-add abbrev-expand-function 
            :before (lambda () (fix-transposed-characters)))

%%%---------------------------------------------------

(2) In the same directory as this auto-correct.el file, start up emacs 28.2:
       emacs -Q

(3) Load and execute the auto-correction code:
        ESC : (load-file "auto-correct.el") RTE

(4) Visit a file "a.txt"
        C-x C-f a.txt RTE

(5) Turn on abbrev mode and flyspell mode:
        ESC : (abbrev-mode t) RET
        ESC : (flyspell-mode) RTE

(4) Enter the 3 chars "abc" on a single line.
        a b c

(5) At the end of the single line, type Ctrl-T:
        C-t

(6) Note that you now have "acb", not "cab".

Following that, if I now enter a space, "acb" is automagically changed to
"cab", as expected.

But (and this is the salient point) "acb" *is not* changed to "cab"
immediately upon typing Ctrl-T, it is only done when the abbrev
functionality is exercised (I believe this is the correct thing to say).

I realize that the flyspell mechanism doesn't rely on abbrev mode, and so
my original question to the gnu emacs newsgroup was a hope that someone
with more elisp knowledge than me could help me out.  But, as I mentioned,
someone with far more elisp knowledge than me claimed it was a bug, and I
believed him.  So here we are.

Does that fully explain the issue, as well as what I would like to achieve?
If not, let me know and I'll add whatever detail is needed.

Cheers.
                                Jim