Fuzzy Go Documentation Browser

In this blog post, I will go through very simple steps to create a fuzzy documentation browsing tool for Go stdlib using fzf.

TLDR

If you are too lazy to follow this tutorial, source code is at https://github.com/diwasrimal/fuzzy-go-docs.git

Requirements

We will need fzf and optionally bat for syntax highlight.

View package level documentation

We can list all packages in stdlib using the command

$ go list std
archive/tar
archive/zip
bufio
bytes
cmp
compress/bzip2
compress/flate
compress/gzip
compress/lzw
compress/zlib
...

Now if we pipe this list into fzf, we can fuzzily search a package name. To preview the documentation on the side, we can provide the --preview option for fzf. We can use {} to capture the matched package name. So

$ go list std | fzf --preview 'go doc {}'

will show documentation of the matched go package.

The documentation requires a lot of horizontal space, while the list of packages do not. So we can make the size of preview window larger using.

$ go list std | fzf --preview 'go doc {}' --preview-window 'right:70%'
preview window size increased

The package documentation can get long, so to navigate the preview text effectively, we can bind keys for scrolling by passing --bind option to fzf. We also enable moving to the first match when query (what we type) changes.

$ go list std | fzf --preview 'go doc {}' --preview-window 'right:70%' --bind 'ctrl-j:preview-half-page-down,ctrl-k:preview-half-page-up' --bind "change:first"

Now we can use ctrl-j to scroll down and ctrl-k to scroll up the documentation on right side.

View symbol documentation inside package

Till now we were browsing packages and viewing their docs on right side. Now we want to “select” a package and view the docs for symbols contained inside the selected package. For this we bind tab and shift-tab keys to go inside and come outside of inner package i.e. symbol level docs.

Using Tab to browse package.symbol

When we hit tab we want the following to happen:

  1. fzf’s input should be replaced by package’s documentation
  2. reset the query (fzf’s input)
  3. change the preview command to show `‘go doc pkg.symbol’
  4. move preview window from right to bottom to show more text

To replace input, we use reload(...) action from fzf. We bind tab such that it sets selected package docs as fzf input. Then we can fuzzy find over individual symbols from the package.

To reset the query, use use change-query() and set it to nothing.

To change the preview command, we use change-preview(...). But we have to extract the symbol from the doc line. For this we can utilize an external script.

#!/bin/sh

# This script extracts the symbol from a go doc line
# Example:
# func ServeTLS(l net.Listener, handler Handler, certFile, keyFile string) error - ServeTLS
# type CookieJar interface{ ... }                                                - CookieJar

echo $@ | awk '{ print $2 }' | sed 's/[(\[].*$//'

Save this file as godoc-sym-extractor.sh, put it somewhere in your path and make it executable.

While looking at symbol level docs, we store the currently browsing pkg name in a file, so that it can be used in preview command as go doc <pkg>.{}.

$ go list std \
    | fzf --preview 'go doc {}' \
          --preview-window 'right:70%' \
          --bind 'ctrl-j:preview-half-page-down,ctrl-k:preview-half-page-up' \
          --bind "change:first" \
          --bind "tab:reload(echo {} > browsing_pkg.txt && go doc {} | grep -E '^(var|func|type|const) ')+change-query()+change-preview(go doc \$(cat browsing_pkg.txt).\$(godoc-sym-extractor.sh {}))+change-preview-window(bottom:60%)"

Using Shift Tab to browse packages again

To view browse packages again we can map shift-tab to reload the fzf input again, and set all the options that were set during the initial run.

$ go list std \
    | fzf --preview 'go doc {}' \
          --preview-window 'right:70%' \
          --bind 'ctrl-j:preview-half-page-down,ctrl-k:preview-half-page-up' \
          --bind "change:first" \
          --bind "tab:reload(echo {} > browsing_pkg.txt && go doc {} | grep -E '^(var|func|type|const) ')+change-query()+change-preview(go doc \$(cat browsing_pkg.txt).\$(godoc-sym-extractor.sh {}))+change-preview-window(bottom:60%)" \
          --bind "shift-tab:reload(go list std)+change-query()+change-preview(go doc {})+change-preview-window(right:70%)"

Make it fast, caching docs

Instead of using go doc every time, it would be faster to just read from a file. In this step we make our script run faster by generating the docs we need i.e. caching, and just reading the file later instead of using go doc. For this we use the below script.

#!/bin/sh

# Creates a go stdlib documentation cache

db=${GOPATH:-$HOME/.cache}/go-std-doc-cache

pkgs=$(go list std)
echo "$pkgs" > "$db/_pkgs.txt"

for pkg in $pkgs; do
    mkdir -p $db/$pkg
    go doc $pkg > "$db/$pkg/_doc.txt"
    grep -E '^(var|func|type|const) ' "$db/$pkg/_doc.txt" > "$db/$pkg/_signatures.txt"
    symbols=$(awk '{ print $2 }' "$db/$pkg/_signatures.txt" | sed 's/[(\[].*$//')
    for sym in $symbols; do
       	go doc "$pkg.$sym" > "$db/$pkg/$sym.txt"
    done
echo "written $db/$pkg"
done

After that we just have to modify our previous command to read from the cache, instead of using go doc. We can now use this script.

#!/bin/sh

cat="cat" # or "bat --language go --color always --plain" for syntax highlighting
db=${GOPATH:-$HOME/.cache}/go-std-doc-cache

save_browsing_pkg="echo {} > $db/browsing_pkg.txt"
get_browsing_pkg="cat $db/browsing_pkg.txt"
sym_extractor="godoc-sym-extractor.sh"

cat "$db/_pkgs.txt" \
	| fzf --preview "$cat $db/{}/_doc.txt" \
		  --preview-window "right:70%" \
		  --bind "ctrl-j:preview-half-page-down,ctrl-k:preview-half-page-up" \
		  --bind "change:first" \
		  --bind "tab:reload($save_browsing_pkg && cat $db/{}/_signatures.txt)+change-preview($cat $db/\$($get_browsing_pkg)/\$($sym_extractor {}).txt)+change-query()+change-preview-window(bottom:60%)" \
		  --bind "shift-tab:reload(cat $db/_pkgs.txt)+change-query()+change-preview($cat $db/{}/_doc.txt)+change-preview-window(right:70%)"

Save this file as fzgd (fuzzy go docs) somewhere in your path and make it executable. And just run

$ fzgd

How I am using it

$ tree ~/.local/bin
├── ...
├── fzgd
└── fzgd-extract-sym-from-line

fzgd is a little modified from the one I mentioned above, allowing cache updating using fzgd -u, (see https://github.com/diwasrimal/fuzzy-go-docs).

fzgd-extract-sym-from-line is the same as godoc-sym-extractor.sh I mentioned above.