Opening Racket Modules in Emacs
In recent past, I've adopted Greg Hendershott's racket-mode for Emacs, added keyword completion, hover help, documentation lookup, customized syntax highlighting and indentation and such for my personal tastes, but one thing I haven't really looked at so far is code navigation support for Racket. What seemed like an easy place to start was implementing a function for loading a Racket source file by its module path, as would appear within a require
form.
Below is racket-open-module-at-point
, my attempt at such a function. When the cursor is on a quoted string, the function just assumes that the quoted string is a (relative) filename, and loads it directly using find-file
. Otherwise it grabs the (smallest) S-expression at point (if any), and—after a minor sanity check—passes it over to Racket and its resolve-module-path
function, to hopefully receive a fully resolved pathname.
;; Only works when positioned inside the string, and not at the quotes.
(defun bounds-of-quoted-string-at-point ()
(let ((p (nth 8 (syntax-ppss))))
(and p
(cons p (save-excursion
(goto-char (1+ p))
(search-forward "\"")
(point))))))
(defun racket-open-module-at-point ()
(interactive)
(let ((p (bounds-of-quoted-string-at-point)))
(cond
(p
;; Point is inside a quoted string.
(let ((fn (buffer-substring-no-properties
(1+ (car p))
(1- (cdr p)))))
(when (equal "" fn)
(error "point is inside an empty quoted string"))
(message "%s" fn)
(find-file fn)))
(t
(require 'thingatpt)
(let ((mp (thing-at-point 'sexp)))
(cond
((not mp)
(error "no module path at point"))
(t
(set-text-properties 0 (length mp) nil mp) ;; modifies `mp`!
(cond
((string-match "\\`\"\\([^\"]*\\)\"\\'" mp)
;; The module path is a quoted string.
(let ((fn (match-string 1 mp)))
(when (equal "" fn)
(error "point is at an empty quoted string"))
(message "%s" fn)
(find-file fn)))
((string-match "['\\]" mp)
;; The module path contains characters that might cause shell
;; escaping. Could be a module path like '#%kernel, for which
;; we cannot get a source file.
(error "non-resolvable module path: %s" mp))
(t
(let* ((bfn (buffer-file-name))
(cmd (format
"racket -e '(require syntax/modresolve)
(write
(with-handlers ([exn:fail? (lambda (exn) (quote resolution-failed))])
(let ([r (resolve-module-path (quote %s) %s)])
(match r
[(? path?) (path->string r)]
[(? symbol?) (quote symbolic-path)]
[(list (quote submod)
(and (or (? path?) (? symbol?)) sub-r)
rest ...)
(cond
[(path? sub-r) (path->string sub-r)]
[else (quote symbolic-path)])]
[_ (quote unexpected-result)]))))'"
mp
(if bfn (format "%S" bfn) "#f"))))
(let ((out-s (shell-command-to-string cmd)))
(let ((fn (car (read-from-string out-s))))
(unless (stringp fn)
(error "failed to resolve module path: %s: %S"
mp fn))
(unless (file-exists-p fn)
;; Expecting an absolute path.
(error "resolved to non-existent file: %s -> %S" mp fn))
(message "%s" fn)
(find-file fn)))))))))))))
As thing-at-point
doesn't appear to come with predefined support for double-quoted strings, I hacked together a bounds-of-quoted-string-at-point
function for the purpose of determining the start and end positions of any such string at point. I'm not sure exactly what syntax-ppss
(or parse-partial-sexp
) does, or if it's a good idea to use it for this purpose, but it has worked well enough so far; its use was suggested on Stack Overflow.
Note (13 Oct 2015): Using syntax-ppss
turns out to be less than ideal for the above purpose, as it relies on the relevant syntax being recognized. Not all modes care to do so, nor is S-expression recognition even that relevant for e.g. a mode designed to deal with @-expressions.
I'm not particularly familiar with 'thingatpt
, but it looks like the bounds-of-quoted-string-at-point
function may even be usable for defining a thing-at-point
"thing", named 'quoted-string
, say:
(put 'quoted-string 'bounds-of-thing-at-point
'bounds-of-quoted-string-at-point)