A good number of years ago, back when I had first started using Emacs, I came across an article describing the joy of customising your development workflow using Emacs:
...I could now create empty lines above with a single keystroke. It took just a couple of minutes, I didn't have to install any plugins, or restart the editor - this is normal everyday business for an emacs user.
I was curious about experiencing this for myself but it was only relatively recently that I took the plunge into Emacs scripting; I was more than happy with the fine dev experience that Spacemacs had given me for Emacs, and I had always assumed that it would be too hard and boring to get to grips with Elisp. But I don't know why I held off for so long - you can get a lot of bang for your back with just a little Elisp, and what's more it's pretty fun too!
In this post I want to do a few things:
- Encourage any Emacs users that haven't used Elisp yet to give it a try
- Highlight some key useful Elisp functions
- Provide some examples & inspiration for creating your own extensions (whether you're an Elisp newbie or not)
I'm not going to explain Elisp completely from scratch, since this post would be far too long otherwise; take a look at the 'Learn X in Y minutes' page for Elisp if you need a primer (or if you know Clojure, my cheat sheet for Clojure devs).
- TL;DR
- Web bookmarks
- Parameterising by prompt
- Parameterising by editor contents
- Fetching web data into your editor
- Running shell scripts
- Wrapping up
TL;DR
There's loads of easy, everyday extensions that you can write; they're quick to write, and what's more these are the sorts of extensions that you can find yourself coming back to again and again. Every so often I write some more complex Elisp, but 90% of the time it's just some simple scripting that can really smooth over some bumps in my workflow.
In my experience, such extensions involve some or all of the following:
- Grabbing some text from the context of your editor
- Performing some simple text transformation
- Sending that text somewhere - e.g. your browser, a shell or a web API
- Displaying results in an Emacs buffer
This shouldn't be too surprising; the main advantage of extending your editor over writing a shell script is that you don't need to leave your editor - or at least, you don't need to copy & paste information to and from it by hand.
The main bits of Elisp I'll show you for this are:
- How to create
interactive
functions - Functions for capturing input, such as
-
read-string
for prompting - the
thingatpt
package for grabbing data from your buffer
-
- The
format
function for formatting strings - Functions for sending formatted strings around, such as
-
browse-url
for opening a link in your default web browser -
the
plz
package for simple web requests -
shell-command-to-string
for running (you guessed it!) shell commands - Some basic buffer-handling functions:
generate-new-buffer
,set-buffer
,insert
&switch-to-buffer-other-window
-
Web bookmarks
One of the simplest things we can do is to create interactive functions that serve as web bookmarks. So simple, the word "extension" seems a bit too fancy to describe these - and yet I find them so useful to make. The advantage of adding them to your Emacs setup is that
- you get to take advantage of fuzzy-searching your bookmarks (using Helm or similar)
- you don't even have to leave your editor to search for them
This is as simple as adding the following to your init.el
:
(defun open-github-spacemacs ()
(interactive)
(browse-url "https://github.com/syl20bnr/spacemacs"))
To eval the function (i.e make it available to run), simply hover your cursor somewhere within it then execute M-x eval-defun
(i.e. hold the alt
& x
keys together, release, then type eval-defun
, then press enter
.)
Once that's done, we can execute M-x open-github-spacemacs
to take us to the Spacemacs GitHub page. But then, using Helm, you don't even need to type all that - something like M-x op git sp RET
would be enough.
The interactive
function requires a little explanation - basically, if your function contains a call to interactive
, then emacs will make it available to be ran interactively (i.e. via M-x
) as opposed to only runnable from other Elisp code.
Parameterising by prompt
So, to start getting a little more interesting - let's start looking at parameterised bookmarks. For example, taking us to a GitHub issue with a particular ID:
(defun open-github-spacemacs-issue ()
(interactive)
(browse-url
(format "https://github.com/syl20bnr/spacemacs/issues/%s"
(read-string "Issue ID: "))))
After executing M-x open-github-spacemacs-issue
, Emacs will prompt us to enter an issue ID - e.g. 1234
; once we press enter, Emacs will open a new tab with that issue ID (assuming it exists).
If you prefer more of a top-to-bottom approach, you can make use of let*
:
(defun open-github-spacemacs-issue ()
(interactive)
(let* ((issue (read-string "Issue ID: "))
(url (format "https://github.com/syl20bnr/spacemacs/issues/%s"
issue)))
(browse-url url)))
Parameterising by editor contents
Getting yet more interesting (and useful): we can parameterise a bit like the above, but instead of being prompted we can make use of the text our cursor happens to be on. We get this functionality from the super-helpful thingatpt
("thing at point") package:
(require 'thingatpt)
(defun open-github-spacemacs-issue-from-pt ()
(interactive)
(browse-url
(format "https://github.com/syl20bnr/spacemacs/issues/%s"
(int-to-string (thing-at-point 'number)))))
Eval both forms, then enter 1234
into a buffer somewhere, and then hover your cursor somewhere over it; execute M-x open-github-spacemacs-issue-from-pt
, then you'll get taken to the corresponding issue in GitHub.
Now we're cooking on gas! This is where we can really start to take advantage of the fact that we're running commands from our editor, rather than from within a shell script.
Where this can be especially useful is when you have some sort of global ID that is used to correlate requests or transactions across multiple systems, especially when those systems have web APIs you can query for said ID. I often find myself opening a log file in Emacs, then simply by hovering over an UUID I can take myself to many different diagnostic systems that care about it.
Speaking of UUIDs, you'll need to use thing-at-point
slightly differently in order to grab one from your buffer:
(defun open-widget-page ()
(interactive)
(browse-url
(format "https://postman-echo.com/get?my-id=%s"
(thing-at-point 'uuid))))
Note that if the "thing at point" isn't a valid UUID, then (thing-at-point 'uuid)
will be nil
.
It's a good idea to combine both the prompting & "thing at point" techniques; that way if your cursor isn't pointing to a valid "thing", you can still enter it in:
(defun open-widget-page ()
(interactive)
(browse-url
(format "https://postman-echo.com/get?my-id=%s"
(or (thing-at-point 'uuid)
(read-string "Enter ID: ")))))
As per the documentation for the thingatpt package, the types of "thing" you can specify include:
symbol
, list
, sexp
, defun
, filename
, url
, email
, uuid
, word
, sentence
, whitespace
, line
, number
, and page
.
If you happen to be working with UUIDs, then grabbing them in your editor in this way is particularly handy since it's a lot easier than selecting them using your mouse; and in most GUIs, double-clicking any part of the UUID generally doesn't select all of it.
(For the rest of this guide I'll use (thing-at-point 'word)
since it's easier to experiment with.)
Fetching web data into your editor
So far so good, but all this switching to and from our browser window is starting to get a little old... Let's improve things by grabbing the data into an editor window:
;; only need to run this the first time
(package-install 'plz')
(require 'plz)
(defun show-widget-data ()
(interactive)
(let* ((id (or (thing-at-point 'word)
(read-string "Enter ID: ")))
(url (format "https://postman-echo.com/get?my-id=%s" id))
;; create the name we'll use for the buffer we'll create.
;; The asterisks aren't required; they're merely there as
;; convention to help indicate that it's a generated buffer
;; not associated with a file
(buffer-name (format "*widget-%s*" id))
;; create a new buffer with the above name
(buf (generate-new-buffer buffer-name)))
;; Tell emacs to set the current buffer - i.e. the
;; buffer that buffer-related operations will work on -
;; to the buffer we've just created
(set-buffer buf)
;; Download the data from the URL and write it into the
;; buffer. We're doing this synchronously; async
;; callbacks are possible, but this simple approach is
;; more than enough most of the time
(insert (plz 'get url :as 'string))
;; An optional step - we've downloading JSON in this example,
;; so we may as well automatically enable the JSON mode
(json-mode)
;; Display the buffer in another window
(switch-to-buffer-other-window buf)))
Similar to before, we can run this interactively via M-x show-widget-data
when hovering over a suitable item of data with our cursor; this time though the data will be opened in a new buffer.
The main functions of note in the above are plz
for fetching the data, and the various buffer-handling functions: generate-new-buffer
, set-buffer
, insert
& switch-to-buffer-other-window
. Working with Emacs buffers in Elisp can feel a bit arcane at first, but thankfully it's fine once you get started. See the elisp docs on buffers if you want to go deeper.
Running shell scripts
We can still take advantage of the context of our editor when running shell commands; we can use parameterised input as we've been doing, run a shell command, then display the results in another buffer:
(defun run-widget ()
(interactive)
(let* ((id (or (thing-at-point 'word)
(read-string "Enter ID: ")))
(buffer-name (format "*widget-%s*" id))
(buf (generate-new-buffer buffer-name)))
(set-buffer buf)
(insert (shell-command-to-string (format "cowsay %s" id)))
(switch-to-buffer-other-window buf)))
This is especially useful if you're new to elisp; you can create a script in your favourite language to do the heavy lifting, and just use the simple glue code above to make use of it seamlessly within emacs. For example, for downloading data it can be easier to handle things like authentication tokens and connection pooling in a more modern scripting language. (As a Clojure dev, I love Babashka for this!)
Wrapping up
Hopefully this has shown you how to get started in the wonderful world of Emacs scripting, and given you plenty of inspiration for how you can make your day-to-day development worklow better with just a little bit of Elisp. And what's more, you'll have experienced just how quickly you can try out these sorts of changes - all without even restarting Emacs!
If you want to go deeper into Elisp, I highly recommend checking out awesome Elisp, which lists many helpful tutorials and libraries (which is where I came across the plz
library I used earlier).
As always, please feel free to ask any questions in the comments.
Top comments (2)
Great post with great tips for productivity, thanks for sharing
Thank you so much for sharing this! I'm into Emacs since 3+ years now and my Elisp skills are still... crap 🙈
Looking forward to more posts like this!