Lambda on the Move

A (non-)personal blog about programming, Symbian, and little else.
newest · all · tags · dates · titles · feedRSS feed · author

older · newer

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)
Written on Sunday, 6 July 2014, 17:57:57 UTC by Tero Hasu.
Edited on Tuesday, 13 October 2015, 20:23:11 UTC.
You may send comments by e-mail.
Tagged as Emacs, IDE, Lisp, Racket.
Related posts: Dictionary-Enabled Racket Support for Emacs, Times Are Hard for Racketeers, too, Rascal Mode for Emacs Released.