Prominent Python developer and blogger Hynek Schlawack wrote a blog post about the offline API browser Dash. Dash is a MacOS application that lets you download the API documentation of multiple software packages and search it offline. You start it with a press of a button, enter your search terms and it searches the documentation you downloaded. At the time of writing this, the Dash website states that it has the documentation of over 200+ software packages, for example C++, Pandas, Python and Qt.
The blog post mentions there is a package for Emacs that allows you to download this documentation and search it via Helm, namely helm-dash. I decided to try it out…
At the moment I’ve only installed the documentation for Python 3. helm-dash
allows me to have a browser window at the right location in its official docs
with only a few keystrokes. I’ve always used Google for that, but hardly ever
does it have the official documentation at the top of its search results. For
now, helm-dash
is here to stay.
I am a bit concerned about its performance once I start adding more documentation. Entering the search term in Helm is already sluggish, sometimes (but not always) the UI locks up for a second. This might find its cause in the Windows laptop I used to evaluate ~helm-dash~[fn:1].
Unfortunately, before I was able to use helm-dash
, I encountered a lot of
issues that I had to work around. Most of these issues are Windows-specific and
made me wonder whether I’m its only Windows user ;).
After the installation of helm-dash
, you have to install the documentation for
a specific API (or subject), a so-called docset. To install a docset, you use
function helm-dash-install-docset
. Unfortunately, that function isn’t
available yet as it isn’t autoloaded. As a workaround, if you execute
helm-dash
and immediately abort that function, helm-dash-install-docset
is
loaded and available.
By default, helm-dash
installs your docsets in ~~/.docsets~. If that directory
doesn’t exist, helm-dash-install-docset
asks you whether it should create that
directory. If you answer “yes”, it creates that directory and then stops without
downloading the docset. Fortunately, if you call it again, it does download the
docset.
So I executed helm-dash-install-docset
again and for about a minute, nothing
seems to happen. Then it stops and the *Messages*
buffer contains the
following lines:
Contacting host: api.github.com:443 Contacting host: raw.github.com:443 Wrote c:/Users/<username>/AppData/Local/Temp/Python_3-feed.xml Contacting host: sanfrancisco.kapeli.com:80 Wrote c:/Users/<username>/AppData/Local/Temp/Python_3-docset.tgz helm-M-x-execute-command: Failed to extract c:/Users/<username>/AppData/Local/Temp/Python_3-docset.tgz to c:/Users/<username>/.docsets. Error: 1
Browsing through the stack of ELisp functions called, I found the following
function in dash-docs.el
:
(defun dash-docs-extract-and-get-folder (docset-temp-path)
"Extract DOCSET-TEMP-PATH to DASH-DOCS-DOCSETS-PATH, and return the folder that was newly extracted."
(with-temp-buffer
(let* ((call-process-args (list "tar" nil t nil))
(process-args (list
"xfv" docset-temp-path
"-C" (dash-docs-docsets-path)))
;; On Windows, several elements need to be removed from filenames, see
;; https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#naming-conventions.
;; We replace with underscores on windows. This might lead to broken links.
(windows-args (list "--force-local" "--transform" "s/[<>\":?*^|]/_/g"))
(result (apply #'call-process
(append call-process-args process-args (when (eq system-type 'windows-nt) windows-args)))))
;; ...
Note the additional options when you want to unpack the docset tarball on Windows. It turns out that Windows comes with a version of BSD tar pre-installed and this version doesn’t support these options.
(ews-harvester) C:\Users\<username>>which tar /c/WINDOWS/system32/tar (ews-harvester) C:\Users\<username>>tar --version bsdtar 3.5.2 - libarchive 3.5.2 zlib/1.2.5.f-ipp (ews-harvester) C:\Users\<username>>
On my system I also have GNU tar installed (using MSYS2), so I changed the occurrence of “tar” to use my GNU tar,
(let* ((call-process-args (list "c:/Users/<username>/msys64/usr/bin/tar.exe" nil t nil))
;; ...
and re-evaluated the function. I executed helm-dash-install-docset
, again with
the minute-long wait and this time the command was successful:
Contacting host: api.github.com:443 Contacting host: raw.github.com:443 Wrote c:/Users/<username>/AppData/Local/Temp/Python_3-feed.xml Wrote c:/Users/<username>/AppData/Local/Temp/Python_3-docset.tgz Docset installed. Add "Python 3" to dash-docs-common-docsets or dash-docs-docsets.
So time to try out helm-dash
! I entered the string “pathlib” without the
string and after some seconds delay it showed me the options. I pressed return
and… was greeted with the following message:
helm-M-x-execute-command: ShellExecute failed: The system cannot find the file specified.
Again I browsed through the stack of ELisp functions and ended at
browse-url-default-windows-browser
in browse-url.el
. This function uses
the following construct to open the URL:
(w32-shell-execute "open"
;; w32-shell-execute passes file:// URLs
;; to APIs that expect file names, so we
;; need to unhex any %nn encoded
;; characters in the URL. We don't do
;; that for other URLs; in particular,
;; default Windows mail client barfs on
;; quotes in the MAILTO URLs, so we prefer
;; to leave the URL with its embedded %nn
;; encoding intact.
(if (eq t (compare-strings url nil 7
"file://" nil nil))
(url-unhex-string url)
url)))))
Some debugging showed me that function url-unhex-string
changed the URL from
file:///c:/Users/<username>/.docsets/Python%203.docset/Contents/Resources/Documents/doc/library/os.path.html#//apple_ref/Function/os.path.join
to
file:///c:/Users/<username>/.docsets/Python 3.docset/Contents/Resources/Documents/doc/library/os.path.html#//apple_ref/Function/os.path.join
The replacement of Python%203
by a space splits the file path in two, hence
the “cannot find the file specified” error message. So I modified a copy of the
function to add spaces around the string
(if (eq t (compare-strings url nil 7
"file://" nil nil))
(concat "\"" (url-unhex-string url) "\"")
url)))))
re-evaluated the function and called helm-dash
again. Yes! This time the
default browser opened the documentation.
Everything looked fine and dandy but the browser showed the top of the page: it
didn’t scroll the page to the anchor #//apple_ref/Function/os.path.join
. It
turned out that when you pass a URL to Microsoft Edge, it strips the anchor from
the URL. For example, when I execute the following command in a Windows Command
Prompt:
explorer "file:///c:/Users/<username>/.docsets/Python 3.docset/Contents/Resources/Documents/doc/library/os.path.html\#//apple_ref/Function/os.path.join"
Edge shows me the top of the page. If I use that command with Firefox instead, Firefox shows me the page at the anchor.
I’m testing this on a locked-down Windows laptop provided by my client. On this
laptop, Microsoft Edge is the default browser and I cannot change it. I didn’t
want to spent too much time looking into this issue so I just configured
browse-url
to use Firefox[fn:2]. In the end it turned out to be a nice
workaround as it means I have a dedicated window for my offline API
documentation.
If you install a docset, its installation ends with a message like:
Docset installed. Add "Python 3" to dash-docs-common-docsets or dash-docs-docsets.
If you don’t add it to these variables, these variables are empty after a
restart of Emacs. If you execute helm-dash
after such a restart, it won’t find
anything. You can add it via function helm-dash-activate-docset
or use
add-to-list
in your Emacs init fle.
When I use helm-dash
, the *dash-docs-error*
buffer opens with messages like
this:
cannot open: "''" cannot open: "''" cannot open: "''" cannot open: "''" ---------------- HEY! This is dash-docs (sqlite) error logging. If you want to disable it, set `dash-docs-enable-debugging` to nil ----------------
I’ve encountered this message before and no, setting
dash-docs-enable-debugging
to nil
doesn’t count. If I recall correctly, it
has to do with the way the ELisp code inits the SQLite database. I will look
into this at a later time.
[fn:1] The laptop is provided to me by one of my clients. It’s locked-down with a company proxy and virus scanner, somewhat unstable and definately not a speed daemon.
[fn:2] To do so, use the Emacs Customize interface to set variable
browse-url-browser-function
to Firefox, or set it in code to
'browse-url-firerox
.