vim-stargazer

April 16, 2017

I find myself frequently navigating to GitHub repositories for the same dozen or so gems or JavaScript libraries. I thought this problem sounded like a good case for writing a vim plugin. After some brainstorming, here’s what I wanted the plugin to do:

Generating a list of a user’s starred repositories

I decided to use the octokit gem because it made working with GitHub’s paginated API a breeze. I was even able to flesh out the feature using TDD with the following spec as my guide:

require_relative "../spec_helper"
require_relative '../../lib/star_fetcher'

describe StarFetcher do
  it "creates a file with a user's stars" do
    github_username = "anhari"

    expect(StarFetcher.fetch_stars_for_user(user: github_username)).
      to eq %w[
        lokesh/lightbox2
        jonathanslenders/ptpython
        # … deleted for brevity
        junegunn/vim-plug
        thoughtbot/suspenders
        tiimgreen/github-cheat-sheet
        vsouza/awesome-ios
        matteocrippa/awesome-swift
      ]
  end
end

This spec, while not perfect because it should really mock the API request, eventually led to the following implementation:

require "octokit"

class StarFetcher
  def self.fetch_stars_for_user(user:)
    Octokit.auto_paginate = true
    stars = Octokit.starred(user)

    repository_full_names = []

    stars.each do |star|
      repository_full_names << star["full_name"]
    end

    repository_full_names
  end
end

And voilà! I had a class with a singleton method capable of pulling down the full list of a user’s starred repositories. Now I just needed a way to utilize this method from the command-line so that it could be called from within vim. I eventually came up with the following ruby script that takes the user’s GitHub user name and generates a dotfile, ~/.starred_repositories, in the user’s home directory:

require "octokit"
require_relative "./star_fetcher"

github_username = ARGV[0]

starred_repositories = StarFetcher.fetch_stars_for_user(
  user: github_username
)

File.open("#{Dir.home}/.starred_repositories", "w") do |file|
  starred_repositories.each { |star| file.puts star }
end

Luckily, Ruby provides the Dir.home command to return the home directory of the current user (or the named user if one is provided). Next, I simply needed to wrap this functionality in a Vim command:

function! FetchStars(github_username)
  echo 'Fetching your starred repositories...'
  execute 'silent !ruby ~/.vim/bundle/vim-stargazer/lib/fetch_star_list.rb ' . a:github_username

  if !empty(glob("~/.starred_repositories"))
    echo 'Fetching of your starred repositores is complete!'
  else
    echo 'Fetch unsuccessful.'
  endif
endfunction

" ...

command! -nargs=1 FetchStars call FetchStars(<f-args>)

Implementing a fuzzy finder for the list of starred repositories

With the dotfile being generated with a list of the user’s starred repositories, the last step was to build an integration with fzf that would allow a user to fuzzy find a repository to open. Thanks to fzf’s excellent documentation, implementing this wasn’t too bad:

function! OpenRepository(github_user_and_repository)
  if g:StargazerNavigateToREADME
    execute 'silent !open https:\/\/github.com\/' . a:github_user_and_repository . '\#readme'
  else
    execute 'silent !open https:\/\/github.com\/' . a:github_user_and_repository
  endif
endfunction

function! Stargaze()
  if !empty(glob("~/.starred_repositories"))
    call fzf#run({
          \ 'source': 'grep --line-buffered --color=never -hsi --include=.starred_repositories "" * ~/.starred_repositories',
          \ 'down':   '40%',
          \ 'sink':   function('OpenRepository')})
  else
    echo "Run the FetchStars <github_username> command to populate your stars!"
  endif
endfunction

The Stargaze() method passes three options to the fzf#run method in order to build this integration:

Finding a way to deal with multiple README extensions

You may have noticed the global Vim variable StargazerNavigateToREADME in the OpenRepository Vim function shown above. If this option is enabled the browser will automatically navigate to the #readme css selector on the repository’s homepage. I realized that navigating to this css elector was much easier than trying to navigate directly to a README blob with an unpredictable extension. I decided to disable this option by default, and to rename the plugin from vim-readme to vim-stargazer in order to better capture it’s functionality.

Conclusion

I am really happy with how the plugin turned out. The process forced me to try a few things I’d never had before like building a vim plugin that leveraged Ruby and interacting with GitHub’s API. I’m chalking it up as a win.