Accessing Devdocs from Emacs
— Kaushal ModiSpoiled by being able to access in-built docs in Emacs at fingertips,
here’s an attempt to kind-of do that for Nim documentation too,
using devdocs.io
.
- ‼️ This post is outdated. The
index.json
link from devdocs.io referenced later in the post does not exist any more.
Nim lang has good documentation for all its stdlib
functions
online. But the Emacs user in me does not like to switch back and
forth between the Nim code in Emacs buffers and the docs outside in an
external browser.
Well.. a solution to that, that this post is about, still needs one to look up the Nim docs in an external browser.. but the workflow is a bit better — You don’t need to manually launch the doc site, and you don’t need to then manually type in the search query.
If you want to skip the history and code analysis, you can directly jump to the Demo or the Final Code.
JSON Docs #
I tried asking folks on r/nim if there was a good solution for in-editor Nim doc access. /u/PMunch from Reddit gave a wonderful solution—To generate JSON docs for the Nim stdlib, and then parse those to display the docs within Emacs.
I would love that solution!
.. just that I don’t know how to get a nice single .json
for the
whole of Nim documentation.
If someone knows how to do that, please let me know.
Devdocs.io #
So I continued my search online.. I was looking if someone had already implemented a way to access Nim docs from the command line, and that somehow led me to https://devdocs.io/nim/!
And after searching further for “devdocs Emacs”, I found these two Emacs packages:
Christopher Wellon’s devdocs-lookup
#
After reviewing the two packages, I decided to build my solution
further upon the devdocs-lookup
package1 by Christopher
Wellons aka @skeeto from GitHub.
Here’s why —
He first fetches the whole JSON search index (line 5) from docs.devdocs.io for the picked “subject” (which would be “Nim” for this post). So the search index for Nim documentation would be at https://docs.devdocs.io/nim/index.json2.
The retrieved JSON is then parsed using
json-read
(line 13).1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
(defun devdocs-index (subject &optional callback) "Return the devdocs.io index for SUBJECT, optionally async via CALLBACK." (cl-declare (special url-http-end-of-headers)) (let ((index (gethash subject devdocs-index)) (url (format "%s/%s/index.json" devdocs-base-index-url subject))) (cond ((and index callback) (funcall callback index)) ((and index (not callback)) index) ((and (not index) (not callback)) (with-current-buffer (url-retrieve-synchronously url nil t) (goto-char url-http-end-of-headers) (setf (gethash subject devdocs-index) (json-read)))) ((and (not index) callback) (url-retrieve url (lambda (_) (goto-char url-http-end-of-headers) (setf (gethash subject devdocs-index) (json-read)) (funcall callback (gethash subject devdocs-index))))))))
The
name
andpath
properties from the parsed JSON are then stored in an association list on lines 4 and 5.So a JSON entry like this:
{ "name": "os.walkDirRec", "path": "os#walkDirRec.i,string", "type": "os" }
would translate to this Emacs-Lisp alist element:
("os.walkDirRec" . "os#walkDirRec.i,string")
Then the list of all the car’s of such elements is used to create a collection of entries for completion (line 3).
1 2 3 4 5 6 7
(defun devdocs-read-entry (subject) "Interactively ask the user for an entry in SUBJECT." (let ((names (mapcar #'car (devdocs-entries subject))) (hist (intern (format "devdocs--hist-%s" subject)))) (unless (boundp hist) (set hist nil)) (completing-read "Entry: " names nil :match nil hist)))
And finally, for the selected name, the associated path is retrieved from that alist (line 7), and we browse to that path using the Emacs
browse-url
function (line 9). User can of course configure the browser to be used when that function is called.1 2 3 4 5 6 7 8 9 10
(defun devdocs-lookup (subject entry) "Visit the documentation for ENTRY from SUBJECT in a browser." (interactive (let* ((subject (devdocs-read-subject)) (entry (devdocs-read-entry subject))) (list subject entry))) (let ((path (cdr (assoc entry (devdocs-entries subject))))) (when path (browse-url (format "%s/%s/%s" devdocs-base-url subject path)) :found)))
All of that worked beautifully. As I used it a few times though, I felt a need to add a touch of DWIM to that.
Making devdocs-lookup
DWIM #
Here are the 2 things that I wanted to happen automatically:
Auto-select subject based on major-mode
if possible #
In the original code, if I used devdocs-lookup
function, I needed to
manually select the “Nim” subject even when I called that function
from a nim-mode
buffer. At least for my use cases, I would want to
access only Nim docs if I am looking up devdocs while in a Nim code
buffer.
The package has an interesting function called devdocs-setup
which
would generate a function specific to each subject.. so for “Nim”
subject, it would generate a devdocs-lookup-nim
function.
But I wanted to avoid calling devdocs-setup
too.
So below is what I did:
(defun devdocs-lookup (subject entry)
"Visit the documentation for ENTRY from SUBJECT in a browser."
(interactive
(cl-letf (((symbol-function 'string-match-case-insensitive)
(lambda (str1 str2)
(string= (downcase str1) (downcase str2)))))
(let* ((major-mode-str (replace-regexp-in-string "-mode" "" (symbol-name major-mode)))
;; If major mode is `nim-mode', the ("Nim" "nim") element
;; will be auto-picked from `devdocs-subjects'.
(subject-dwim (cadr (assoc major-mode-str devdocs-subjects
#'string-match-case-insensitive)))
(subject (or subject-dwim (devdocs-read-subject)))
(entry (devdocs-read-entry subject)))
(list subject entry))))
(let ((path (cdr (assoc entry (devdocs-entries subject)))))
(when path
(browse-url (format "%s/%s/%s" devdocs-base-url subject path))
:found)))
Auto-filter using the symbol at point #
The second thing that I wanted to work upon was to make Emacs kind of “know” what I was trying to search.
Originally, devdocs-read-entry
would always show the completion-list
pointing at the first entry. I wanted to make that a bit more
intelligent.. If my point were on the walkDirRec
proc identifier
on a line like below,
for filepath in walkDirRec(appPath, yieldFilter={pcFile}):
I wanted the collection to narrow down to only the entries that matched “walkDirRec”.
Below is my modification to do that.
(defun devdocs-read-entry (subject)
"Interactively ask the user for an entry in SUBJECT."
(let ((names (mapcar #'car (devdocs-entries subject)))
(hist (intern (format "devdocs--hist-%s" subject)))
(init (symbol-name (symbol-at-point))))
(unless (boundp hist)
(set hist nil))
(completing-read (format "Entry (%s): " subject) names nil :require-match init hist)))
Above function just pre-sets the filter.. If the user wants to change the search string, they can still do that.
Using devdocs-lookup
#
Finally, I like the key-chord.el
package. So using that, I bind the
??
key-chord to the modified devdocs-lookup
function.
So if I want to look up the docs for walkDirRec
on that line in the
above example, I just move the point there, and hit ??
, and the docs
for that will pop up in my browser..
- No manual launching of the browser.
- No manual typing of the search string.
Demo #
It won’t be fun if I did not end this post without a demo. So here it is —
Code #
You can find the modified devdocs-lookup
code here.
All of the code snippets from Christopher Wellon’s
devdocs-lookup
that now follow in this post are from this commit. ↩︎If you visit that file in Firefox, it will show up in a wonderful formatted form with collapsible drawers, search, etc. ↩︎