An Elisp Mystery

Over at the Emacs Reddit, wadcann posts a nice idea. The problem is to have abbreviations that touch their surrounding text. The canonical example is punctuation like the em-dash:

Many national security professionals—those in the CIA, NSA, and similar...

You can't (conveniently) use the normal abbreviation expansion mechanisms here because they expect the symbol to be delimited from the rest of the text by white space.

Wadcann solves that problem with a bit of Elisp. His post is short so you should take a look before continuing. I liked his idea but thought that the Elisp could be cleaned up a bit. Suppose, as Wadcann does, that you use /--- as the abbreviation for the em-dash. The idea is to search backwards to the slash, mark it as the beginning of the abbreviation, return to the original position to mark the end of the abbreviation, and call expand-abbrev to expand it.

That seemed simple enough. Here's my first attempt:

(global-set-key (kbd "M-\"")
                (lambda ()
                  (interactive)
                  (let ((pt (point)))
                    (when (search-backward "/" nil t)
                      (abbrev-prefix-mark t)
                      (goto-char pt)
                      (expand-abbrev)))))

It didn't work. After adding some debugging statements, I discovered that the (goto pt) placed the point at one character short of where it should. When I fixed that up, everything worked just fine:

(global-set-key (kbd "M-\"")
                (lambda ()
                  (interactive)
                  (let ((pt (1+ (point))))
                    (when (search-backward "/" nil t)
                      (abbrev-prefix-mark t)
                      (goto-char pt)
                      (expand-abbrev)))))

The problem was I couldn't figure out why the first solution wasn't correct. Even though the second solution worked it was really just treating the symptoms, not the problem. So I rewrote it using markers instead.

(global-set-key (kbd "M-\"")
                (lambda ()
                  (interactive)
                  (let ((pt (point-marker)))
                    (when (search-backward "/" nil t)
                      (abbrev-prefix-mark t)
                      (goto-char pt)
                      (expand-abbrev))
                    (set-marker pt nil))))

I don't like this solution as much because you have to deactivate the marker at the end. Still, at least this solution made sense. I checked the appropriate Emacs code and as far as I can see, the first solution does pretty much what the third one does.

Does anyone understand what's happening here? I have two working solutions so now it's just a matter of an annoying anomaly that I don't understand. If you've got a clue, please leave a comment.

This entry was posted in Programming and tagged , . Bookmark the permalink.
  • Why not just save-excursion?

    As for the explanation, look at the code of `abbrev-prefix-mark'

    (defun abbrev-prefix-mark (&optional arg)
    (interactive "P")
    (or arg (expand-abbrev))
    (setq abbrev-start-location (point-marker)
    abbrev-start-location-buffer (current-buffer))
    (insert "-")) ;; it adds a character here ;)

    • jcs

      Ah, that explains everything including why saving the point as a marker instead of an integer works. I did read that it inserted the hyphen but didn't make the connection. Once you see the code, everything is clear.

      As for save-excursion that's the obvious solution but the natural implementation changes the behavior slightly. See the comments in the Reddit post for what I mean. Of course, you can code code around that but then you end up with something pretty much like the original.