Display images in Slime from Common Lisp
Sometimes an image is worth more than a thousand words. This is true also for a text editor like Emacs, and in fact, while it’s graphical capabilities are limited, it is pretty easy to insert images in a buffer, and display them. While such capability is used in several modes (org-mode for example, or auctex), I never saw it used in Slime, so I’ll show how can be done with a simple example.
The idea by itself is pretty simple: generate a graphical image in Common Lisp, send it to Emacs and have it displayed as the result of the computation. In order to do so in a swank buffer from Common Lisp, we need to do some preparations. First of all we need to load in Emacs “swank-media” which contains “swank-media-insert-image”. swank-media is part of the slime source, so we just need to require it when we load slime. Then we need to tell Emacs that he can accept commands from the Common Lisp compiler that we are using, by setting slime-enable-evaluate-in-emacs to true (in Emacs)
To send the image to Emacs, I chose a simple method: generate the image, either directly in Common Lisp (for example with zpng) or with an external software, save it to a file and ask Emacs to display the file.
For example let’s try to display a graphic representation of a lisp object using graphviz, like those that appear in books for lisp beginners:
We’ll write a function plot-sexpr that takes as input any lisp expression, and will plot the result on its slime buffer:
(defun plot-sexpr(expr)
(let ((filename (make-graph-png (traverse expr))))
(swank:eval-in-emacs
`(slime-media-insert-image (create-image ,filename)
,filename))
(swank:eval-in-emacs `(delete-file ,filename))))
traverse will walk the given expression returning a dot representation of it, while make-graph-png will call dot with that input and return a filename containing the output image:
(defvar *id* 0)
(defun next-id ()
(incf *id*))
(defun next-label ()
(format nil "node_~a" (next-id)))
(defun get-label (x ids)
(unless (gethash x ids)
(setf (gethash x ids) (next-label)))
(gethash x ids))
(defgeneric traverse-rec (obj seen ids))
(defmethod traverse-rec (x seen ids)
(if (gethash x seen)
""
(progn (setf (gethash x seen) t)
(format nil " ~a [label=\"~a\"];" (get-label x ids) (princ-to-string x)))))
(defmethod traverse-rec ((x cons) seen ids)
(if (gethash x seen)
""
(progn (setf (gethash x seen) t)
(format nil " ~a [label=\" | \"];~% ~a:left -> ~a;~% ~a:right -> ~a;~%~a~%~a~%"
(get-label x ids)
(get-label x ids)
(get-label (car x) ids)
(get-label x ids)
(get-label (cdr x) ids)
(traverse-rec (car x) seen ids)
(traverse-rec (cdr x) seen ids)))))
(defun traverse (list)
(format nil "digraph G {~% node [shape=record];~% nodesep=1.0~% ~a~%}~%"
(traverse-rec list
(make-hash-table :test #'eq)
(make-hash-table :test #'eq))))</code>
The implementation of traverse isn’t exactly straightforward, but the idea is simple: traverse the tree, if we find an object that we haven’t already met, give it a label ask for its graphical representation. traverse-rec is a method so one can customize the behavior for specific objects. Once we get the text, we can finally give it to dot:
(defvar *dot-program* "dot")
(defun temporary-file-name (prefix)
(swank:eval-in-emacs `(make-temp-name (concat temporary-file-directory ,prefix))))
(defun make-graph-png (text)
:documentation "Use the given text to create a graph and return the name of the temporary file with the resulting image"
(with-input-from-string (input text)
(let ((filename (temporary-file-name "dotXXX")))
(external-program:run *dot-program*
`("-Tpng" ,(format nil "-o~a" filename))
:input input)
filename)))
One may notice a problem here: we ask a temporary file name from emacs, and we execute the program from CL. This won’t work if we are using a remote swank session. Unfortunately I don’t know of a portable way to get a temporary file name in CL that works both in Windows and Linux (or some other Unix), and in different implementations. One can either ask Emacs to run the program, on the client side, maybe with shell-command, or convert the local path to a tramp path and feed it to Emacs (never done it, but seems doable).
Let’s try the result:
(let* ((x '(1 2))
(y (cons x x)))
(setf (second x) y)
(plot-sexpr x))
Of course this isn’t very useful, but the same idea can be applied to more pratical scenarios. For example one could use gnuplot or some other plotting library/tool to draw directly into the buffer. This is more pratical than plotting on a different windows, as old plots won’t be replaced but remain in the buffer.