IEx’s h macro is great but it lacks a pager, so I built a small tool that caches documentation and lets you fuzzy search through it in your OS shell.
It follows the Unix philosophy - Internally pipes to fzf and your default $PAGER. The cache lives at ~/.cache/exh and builds up over time as you workon different projects (no duplicates).
It compiles your project, loads all the available modules, and encodes the documentation using ETF.
Performance-wise, it caches a vanilla Phoenix project in about 10 seconds on first run, 3 seconds on subsequent runs. Tested on my old T420.
The formatting of the docs matches IEx’s `h` macro since I borrowed some of that code.
Example usage:
$ exh fetch
$ exh
Originally tried making it IEx-native using Ports (check the less-in-iex branch), but, despite redirecting the group leader and waiting for the Port’s exit message, couldn’t find a way to make the BEAM fully yield terminal control - resulted in race conditions between the BEAM and the TUI. If anyone has insights on this, I’m all ears!
E.g., the help for a certain module/ function may change depending on the current branch/commit, in particular for “in development” state, not between tagged releases.
Good question, I haven’t thought of that until now. The currently supported solution would be to exh clean and exh fetch.
To make it more efficient, my first thought is to create a key-value store with the module names and a checksum for each module. This way, the caching algorithm can check the validity of each existing cached module when re-caching and clean up stale cache.
Also, validating against .tool-versions and mix.lock could be great.
How useful do you think each approach would be to you? Is cleaning + fetching fine for your use-case?
The improvement comes from not having to perform disk IO for modules that have already been cached previously.
It will always iterate over every available module to the project: erlang/elixir installation, dependencies, and source code.
I find myself consuming docs most of the time from hexdocs.pm or source code. When I do use h inside IEx, I’m typically inside tmux and can “scroll” back up with the keyboard.
So, speaking hypothetically, if exh clean nukes the whole cache, it would be too aggressive if I just want to refresh docs for a particular project. A refetch or fetch --force would be more targeted.
I still wonder how are you structuring the cache. Do you shard by package version? How do you treat Elixir and Erlang documentation?
This idea of centrally storing a deduplicated cache of packages reminds me of the Go Module Cache. In the Go ecosystem published modules have an immutable cryptographic checksum, and the multiple versions of a package are cached read-only in a single place. Separately, there’s also a build cache. How I’d translate that idea to Elixir is that published Hex packages could be cached by version, while “current project” needs special treatment as it can change anytime without a version change.
A refetch or fetch --force would be more targeted.
This sound very reasonable, I am going to add it.
I still wonder how are you structuring the cache. Do you shard by package version? How do you treat Elixir and Erlang documentation?
The cache structure is just a flat dir. There is a “tags” file that works as the index for fzf, then each cached module has its own file. Maybe it is a bit naive, if the re-caching process proves cumbersome I will think about something more complex.
Have you seen the new mix help task in Elixir 1.19?
Interesting, I wasn’t aware.
I still see some value in exh, people working in older versions can still browse docs on the terminal. Maybe I’ll add an option to just cache an index to use fzf with mix help in the future
Hey! Make sure to pull and install the new changes:
exh fetch now updates stale cache. A module is considered stale when the documentation found in the current project/context is different than the documentation for the same module stored in cache.
exh fetch --prune will delete modules that are not found in the current context.
CRUD statistics on cached modules will be displayed after caching.
Bug fix: exh will correctly display functions and macros that have no @spec