reddit / emacs
Implementing abbrev without another insert
avindroth · 2022-11-07 06:25
I am trying to emulate some features in [TeXMacs](https://www.texmacs.org/tmweb/home/welcome.en.html), specifically how when you type "QQ" in TeXMacs, you get "\\mathbb{Q}". It's really nice once you get used to it.
I noticed abbrev-mode does basically what I want, but with a caveat: I need to insert a character before the expansion happens. So (with electric-pair-mode) I may have "$QQ|$" where | is my cursor. Then C-f afterwards does not do the expansion. This is a minor thing, but I feel like you can do a lot with this feature. I have looked through some source code (specifically abbrev.el/expand.el/dabbrev.el), and its seems detection of abbrev-table is built-in somewhere (like in the language modes themselves)? The last thing I'd want to do is add a hook to insert.
Here is the code for abbrev-based expansion.
(define-abbrev-table 'global-abbrev-table
'(
("QQ" "\\mathbb{Q}" nil :case-fixed t :count 0)
("ZZ" "\\mathbb{Z}" nil :case-fixed t :count 0)
))
geza42 · 2022-11-07 09:58
I know that you don't want to modify the insert hook, but here is a `post-self-insert-hook` based solution to a similar problem. In `c++-mode`, this expands `st<` to `static_cast<`, and it expands immediately, no need to press any additional key. Emacs has a problem with this, because `<` is not a word constituent char. So instead of fighting with abbrev, I wrote this little snippet (it was surprising how easy was to implement this):
(defun my-c++-abbrevs-check ()
(let ((beg3 (- (point) 3))
(end (point)))
(when (string= "co<" (buffer-substring beg3 end))
(delete-region beg3 end)
(insert "const_cast<"))
(when (string= "dy<" (buffer-substring beg3 end))
(delete-region beg3 end)
(insert "dynamic_cast<"))
(when (string= "re<" (buffer-substring beg3 end))
(delete-region beg3 end)
(insert "reinterpret_cast<"))
(when (string= "st<" (buffer-substring beg3 end))
(delete-region beg3 end)
(insert "static_cast<"))))
(add-hook 'c++-mode-hook (lambda() (add-hook 'post-self-insert-hook 'my-c++-abbrevs-check nil 'local)))
(Note: this snippet also expands the other 3 C++ casts as well: `const_cast`, `dynamic_cast` and `reinterpret_cast`)
avindroth · 2022-11-07 10:54
Thank you for the code. I will try that as a first-pass. How much of a performance impact will this have, do you imagine?
avindroth · 2022-11-07 12:08
EDIT: It's now here https://github.com/joshcho/iabbrev
I put my spin on it for latex with hash tables. Runtime is `max-length-of-abbrev * O(1)`.
​
;; iabbrev for instant abbreviations
(eval-when-compile
(require 'cl-lib))
(require 'ht)
(require 'dash)
(defvar iabbrev-table (ht<-alist '(("ZZ" . "\\mathbb{Z}")
("<<" . "\\langle")
(">>" . "\\rangle")
("==" . "\\equiv")
("QQ" . "\\mathbb{Q}")
("RR" . "\\mathbb{R}")
("\\sub" . "\\subseteq"))))
(defun iabbrev-if-abbrev-expand ()
(let ((abbrev-max-length (-max (-map #'length (ht-keys iabbrev-table)))))
(cl-loop for len from 1 to abbrev-max-length
for end = (point)
for beg = (- end len)
for text = (buffer-substring beg end)
for expansion = (ht-get iabbrev-table text)
if expansion
do (progn
(delete-region beg end)
(insert expansion))
and return t)))
(defun add-local-iabbrev-hook ()
(add-hook 'post-self-insert-hook #'iabbrev-if-abbrev-expand nil 'local))
(defun add-iabbrev (abbrev expansion)
(ht-set iabbrev-table abbrev expansion))
(defun remove-iabbrev (abbrev)
(ht-remove iabbrev-table abbrev))
;; examples
(add-hook 'TeX-mode-hook #'add-local-iabbrev-hook)
(add-iabbrev "<=" "\\leq")
(add-iabbrev "CC" "\\mathbb{C}")
(remove-iabbrev "CC")
graduale · 2022-11-07 13:52
Wouldn't it be better (genuine question here) to rebind "<" to something like your `my-c++-abbrevs-check` but which, instead of returning `nil` when it is not preceded by either "co", "dy", "re", or "st", insert "<"? Maybe the difference would be negligible, but in principle it seems like it would be better to only call `my-c++-abbrevs-check` when it would make a difference.
I've been playing around with related ideas after reading [this post](https://www.reddit.com/r/emacs/comments/tk8qou/org_speed_keys_boom_great_orgmodes_feature_and_a/) by u/tonyaldon
geza42 · 2022-11-07 14:47
Yeah, for this particular case, it may worth it. In my actual `init.el` I have a lot of other abbreviations (which don't have `<` in them).
geza42 · 2022-11-07 14:51
I didn't notice any performance degradation. I'm not sure for what kind of events `post-self-insert-hook` is called. If it called for non-interactive edits, then maybe it's worth adding a condition "is it called interactively?" at the beginning of the function. If such thing exists.
graduale · 2022-11-07 15:07
Gotcha.
eleven_cupfuls · 2022-11-07 20:26
A simple option would be to make `forward-char` try expanding a abbrev:
(defun ar/forward-char-maybe-expanding (&optional n)
(expand-abbrev)
(forward-char n))
(define-key tex-mode-map [remap forward-char] #'ar/forward-char-maybe-expanding)
You could also tweak `electric-pair-mode` since it has a customization point for the overtyping behavior:
(defun ar/electric-pair-skip-maybe-expanding (char)
(expand-abbrev)
(electric-pair-default-skip-self char))
(setq electric-pair-skip-self #'ar/electric-pair-skip-maybe-expanding)
So if you were to type the closing `$` it would try an abbrev first.
_viz_ · 2022-11-10 15:47
I forget the finer details but abbrev expansion is ultimately done by self-insert-command, specifically it checks if the character before point is a word constiuent and tries to expand abbrev if so (and here the :expand abbrev table also comes into play) and the abbrev gets expanded if the inserted character is not a word constiuent.
What you're looking for is input methods. Check for the Quail library, it is very easy to define a simple input method and the Quail library is also sufficiently expandable for really complex stuff (look at the Japanese IMs for example). If you only care about transliteration and nothing complex, then robin is also worth looking at, it has some free benefits (that Quail should offer as well), take a look at the package's commentary.