Emacs 8 - A Case for Extensibility
Emacs Killer Feature
I have heard a lot of Emacs related comments and reviews wondering about which is the one feature that makes Emacs so awesome. Many pointing to Org Mode, others choose Magit as the killer feature. A less common opinion is that the extensibility of Emacs is what differentiates Emacs from other text editors. In other words, people love Emacs because it is not only a text editor.
A Lisp Parser
Emacs is essentially a Lisp parser. Allowing the modification of most of its core functions while it runs, Emacs is designed for extensibility.
An Incentive for Hacking
As function definitions are readily available in Emacs, the user is invited open the hood, check out the internals, inspect the behaviors and even debug and change the running functions.
My Case for Extensibility
This post showcases a new functionality I added to Emacs by writing an extension to one of its built-in functionalities in GNU Emacs called thing-at-point
.
The actual implementation is heavily inspired by the work published in Emacs Redux.
I will walk through the process of implementing the extension and I hope you enjoy the tour.
Thing At Point
There is a very useful Emacs built-in package called Thing At Point that allows you to programmatically read a certain type of text object under the cursor.
A thing
defined in the package can be a symbol
, sexp
, defun
, filename
, url
, email
, word
, sentence
, whitespace
, line
, number
or a page
.
This package was written to allow extensibility and also contains information on how to define a symbol as a valid thing
.
The Objective
As casual vim user, I noticed that C-a and C-x respectively incremented and decremented a number at point.
Trying to do the same in Emacs, I realized that this functionality was not available out of the box.
I’ve tried simply to use the thing
defined as number
, but the behavior was not reliable.
At this point I already knew that thing-at-point was extensible and there are some implementations of integer-at-point around the internet. Then I started studying the ideas and implementations to achieve the following goals:
arg
+keypad plus
should add the value ofarg
to the integer at pointarg
+keypad minus
should subtract the value ofarg
from the integer at pointarg
+keypad times
should multiply the value ofarg
by the integer at pointarg
+keypad minus
should divide the integer at point by value ofarg
Given that, if no arg
is provided, the code will use the default value of 1.
Extending Thing At Point
As given by the package documentation, the thing at point
is defined by finding the boundaries of that thing
.
To extend the package we should define functions to go from the current cursor position (known in Emacs as point
) to the beginning-of-thing
and to the end-of-thing
.
A function bounds-of-thing
will be used to read that boundaries.
Implementating beginning-of-integer
This function moves the cursor from the current position to the beginning of the number by skipping all digit characters. In the end it moves left to include the plus
or minus
signs if any.
(defun thing-at-point--beginning-of-integer ()
"Go to the beginning of the integer at point."
(let ((inhibit-changing-match-data t))
(skip-chars-backward "[:digit:]")
(unless (looking-at "[+-]?[[:digit:]]")
(error "No integer here"))
(when (looking-back "[+-]")
(backward-char 1))))
Implementating end-of-integer
This function moves the cursor from the current position to the end of the number by skipping all digit characters. In the beginning of its execution it checks if the current position contains the plus
or minus
signs. If it finds such character it moves one char forward.
(defun thing-at-point--end-of-integer ()
"Go to the end of the integer at point."
(let ((inhibit-changing-match-data t))
(when (looking-at "[+-]")
(forward-char 1))
(skip-chars-forward "[:digit:]")
(unless (looking-back "[[:digit:]]")
(error "No integer here"))))
Implementing bounds-of-integer-at-point
This functions uses the previously defined functions to walk through the end and beginning of the integer and returns a list with the cursor positions of the start and the end of the integer number at point.
(defun thing-at-point--bounds-of-integer-at-point ()
"Get boundaries of integer at point."
(save-excursion
(let (beg end)
(thing-at-point--end-of-integer)
(setq end (point))
(thing-at-point--beginning-of-integer)
(setq beg (point))
(cons beg end))))
Implementing integer-at-point
This functions returns the value of the integer at point as a number.
(defun thing-at-point-integer-at-point ()
"Get integer at point."
(let ((bounds (bounds-of-thing-at-point 'integer)))
(string-to-number (buffer-substring (car bounds) (cdr bounds)))))
Registering the New Thing
Store integer symbol’s property name with the above defined functions. This configures the functions to be used by thing-at-point.
(put 'integer 'beginning-op 'thing-at-point--beginning-of-integer)
(put 'integer 'end-op 'thing-at-point--end-of-integer)
(put 'integer 'bounds-of-thing-at-point 'thing-at-point--bounds-of-integer-at-point)
(put 'integer 'thing-at-point 'thing-at-point-integer-at-point)
Adding the Operations
To allow the four basic math operations I created a generic function that receives the operation and the argument and replaces the integer at point with the result of this operation. I also created the specific operation functions to easily bind them to shortcuts.
(defun operate-integer-at-point (operation &optional arg)
(interactive "p")
(if (not (numberp (thing-at-point 'integer)))
(error "thing at point is not a number"))
(let ((start-pos (point))
(operand (or arg 1))
(value (thing-at-point 'integer)))
(save-excursion
(let ((bounds (bounds-of-thing-at-point 'integer)))
(kill-region (car bounds) (cdr bounds))
(insert (number-to-string (funcall operation value operand)))))
(goto-char start-pos)))
(defun multiply-integer-at-point (&optional arg)
(interactive "p")
(operate-integer-at-point '* arg))
(defun divide-integer-at-point (&optional arg)
(interactive "p")
(operate-integer-at-point '/ arg))
(defun increment-integer-at-point (&optional arg)
(interactive "p")
(operate-integer-at-point '+ arg))
(defun decrement-integer-at-point (&optional arg)
(interactive "p")
(let ((increment (or arg 1)))
(increment-integer-at-point (- 0 increment))))
Creating keybinding
I use modal editing with a very personal set of keybindings and I am not intended to preach to anyone to do something similar, but to showcase a possible way of using this, I created a set of global keybindings to add to this post.
(global-set-key (kbd "<kp-add>") 'increment-integer-at-point)
(global-set-key (kbd "<kp-subtract>") 'decrement-integer-at-point)
(global-set-key (kbd "<kp-multiply>") 'multiply-integer-at-point)
(global-set-key (kbd "<kp-divide>") 'divide-integer-at-point)
Full implementation
(require 'thingatpt)
(defun thing-at-point--beginning-of-integer ()
"Go to the beginning of the integer at point."
(let ((inhibit-changing-match-data t))
(skip-chars-backward "[:digit:]")
(unless (looking-at "[+-]?[[:digit:]]")
(error "No integer here"))
(when (looking-back "[+-]")
(backward-char 1))))
(defun thing-at-point--end-of-integer ()
"Go to the end of the integer at point."
(let ((inhibit-changing-match-data t))
(when (looking-at "[+-]")
(forward-char 1))
(skip-chars-forward "[:digit:]")
(unless (looking-back "[[:digit:]]")
(error "No integer here"))))
(defun thing-at-point--bounds-of-integer-at-point ()
"Get boundaries of integer at point."
(save-excursion
(let (beg end)
(thing-at-point--end-of-integer)
(setq end (point))
(thing-at-point--beginning-of-integer)
(setq beg (point))
(cons beg end))))
(defun thing-at-point-integer-at-point ()
"Get integer at point."
(let ((bounds (bounds-of-thing-at-point 'integer)))
(string-to-number (buffer-substring (car bounds) (cdr bounds)))))
(put 'integer 'beginning-op 'thing-at-point--beginning-of-integer)
(put 'integer 'end-op 'thing-at-point--end-of-integer)
(put 'integer 'bounds-of-thing-at-point 'thing-at-point--bounds-of-integer-at-point)
(put 'integer 'thing-at-point 'thing-at-point-integer-at-point)
(defun operate-integer-at-point (operation &optional arg)
(interactive "p")
(if (not (numberp (thing-at-point 'integer)))
(error "thing at point is not a number"))
(let ((start-pos (point))
(operand (or arg 1))
(value (thing-at-point 'integer)))
(save-excursion
(let ((bounds (bounds-of-thing-at-point 'integer)))
(kill-region (car bounds) (cdr bounds))
(insert (number-to-string (funcall operation value operand)))))
(goto-char start-pos)))
(defun multiply-integer-at-point (&optional arg)
(interactive "p")
(operate-integer-at-point '* arg))
(defun divide-integer-at-point (&optional arg)
(interactive "p")
(operate-integer-at-point '/ arg))
(defun increment-integer-at-point (&optional arg)
(interactive "p")
(operate-integer-at-point '+ arg))
(defun decrement-integer-at-point (&optional arg)
(interactive "p")
(let ((increment (or arg 1)))
(increment-integer-at-point (- 0 increment))))
(global-set-key (kbd "<kp-add>") 'increment-integer-at-point)
(global-set-key (kbd "<kp-subtract>") 'decrement-integer-at-point)
(global-set-key (kbd "<kp-multiply>") 'multiply-integer-at-point)
(global-set-key (kbd "<kp-divide>") 'divide-integer-at-point)