Dictionary-Enabled Racket Support for Emacs

For the last month or so I've found Racket programming even more enjoyable than before. The reason for this is a tool named Ractionary (short for Racket Dictionary Generator), which I wrote for extracting information about Racket language names. Said information can easily be used for setting up some Racket language awareness for Emacs.

There is an Emacs tradition of running an external Lisp (or "inferior Lisp") process to allow for dynamic evaluation of foreign (non Emacs Lisp) code, and this kind of a solution could be used to query information known to Racket on demand. Geiser takes this approach to provide more dynamic Racket programming support under Emacs.

I rather like the straightforward, static approach of just extracting a set of information about a language, and generating files that encode said information in Emacs Lisp data structures. Then all that remains is to just point Emacs at said files, together with some instructions on how to make use of them. Coupled with "standard" Emacs machinery this can lead to fairly decent language "awareness" without any sophisticated support for analyzing Racket code on the fly. No overhead due to managing a Racket subprocess, and far less that could go wrong; you are typically just dealing with a list of strings, after all.

I previously wrote about putting together an Emacs mode of some kind for the Rascal language. Back then, too, I wrote a program to query the Rascal language implementation itself for information about the public names in its standard library. At the time, however, there was no API within Rascal (that I could find) for programmatically retrieving any hand-written docstrings associated with Rascal APIs. This meant that while function signatures and such could be displayed, no additional information about the meaning of the signatures could be made quickly accessible from within Emacs. For Racket, I was able to do better.

The Ractionary-generated “racket-exports.el” file is intended to support auto-completion with hover help. The help texts are (where available) extracted from “.rktd” files whose data DrRacket uses to display (in a blue box on the top right corner) context help for identifiers. Thanks to Robby Findler and Matthew Flatt for pointing out the snippets of code to use for extracting said data.

To get auto-completion and hover help set up I suggest using the Auto Complete Mode and pos-tip.el libraries. Once “racket-exports.elc” is on the Emacs load-path, one may define an auto-completion "source" (call it ac-source-racket) for Auto Complete Mode.

(defun ac-source-racket-help (x)
  (format "%s" (if (listp x) (car x) x)))

(defvar ac-source-racket
  '((depends racket-exports)
    (candidates . racket-dictionary-with-help)
    (cache)
    (symbol . "r")
    (document . ac-source-racket-help)))

The sources to use may be specified in scheme-mode-hook, for example.

(when (boundp 'ac-sources)
  ;; Make ac-source-racket preferred, since only it has hover help.
  (setq ac-sources '(ac-source-racket
                     ac-source-dictionary
                     ac-source-words-in-same-mode-buffers)))

With the above setup, one should get auto-completion for a large number of Racket names.

Auto-completion of "call-".

For many of them, a sensible help text should be available.

Hover help for "call-with-values".

When there is a large number of matches in the completion dialog (as is often the case with this size dictionary), it can be helpful to search within it.

Seaching "call-" names relating to continuations.

It can be useful to have a shortcut key for displaying hover help for the last completed symbol, to get help for a symbol after it has already been inserted.

(define-key scheme-mode-map [(control o) (a)]
  'ac-last-quick-help)

When I search for documentation for a specific symbol from within The Racket Reference I find that at least 90% of the time the first documentation search match is the one I'm looking for (even if said symbol has multiple different definitions in various libraries). For this reason I decided to also make use of the Racket documentation build cross-reference information to generate a dictionary that tries to associate each Racket symbol (that has documentation) with the URL of the documentation that one would most often want to look up for said symbol.

With such a dictionary it is straightforward to implement an "I'm feeling lucky" style search that directly opens up documentation for the "best" match. (By "best" I mean as defined by the Ractionary ranking function, which may not be as good as that used by Racket's own documentation search.) To use the dictionary, again, “racket-urls.elc” file should be on load-path. The racket-search-docs function below implements a "regular" free-form search, whereas racket-doc-for-symbol-at-point makes use of the dictionary.

(load "racket-urls")

(defun make-racket-search-query-url (s)
  (let ((url (concat
              "file://"
              ;; 'racket-doc-dir' from generated 'racket-urls' file
              racket-doc-dir
              "/search/index.html?q=" (url-hexify-string s))))
    url))

(defun racket-search-docs (kw)
  (interactive "sSearch keyword: ")
  (let ((url (make-racket-search-query-url kw)))
    (message "%s" url) ;; 'url' may contain '%' escapes
    (browse-url url)))

(defun lookup-racket-symbol-url (s)
  ;; 'racket-url-lookup-table' from generated 'racket-urls' file
  (let ((r (assq s racket-url-lookup-table)))
    (and r (cadr r))))

;; For looking up a symbol at point.
;;
;; With prefix argument (i.e., C-u), does a documentation search even
;; when there is one or more known exact matches.
(defun racket-doc-for-symbol-at-point (pfx)
  (interactive "P")
  (let ((sym (symbol-at-point)))
    (if (and sym (not (equal (symbol-name sym) "")))
        (let ((url (or (and (not pfx) (lookup-racket-symbol-url sym))
                       (make-racket-search-query-url (symbol-name sym)))))
          (message "%s" url)
          (browse-url url))
      (message "no symbol at point"))))

I am quite happy with my current Racket editing setup. Quack offers a good starting point by improving upon the built-in scheme-mode when it comes to syntax highlighting and indenting Racket. Syntax highlighting customization is easy to do with font-lock-add-keywords, and indentation customization in turn can be done with put 'scheme-indent-function. To these facilities add auto-completion, hover help, and quick API documentation lookup, as enabled by Ractionary, and you've got fairly solid, yet lightweight, Racket support within Emacs.