Run Tests From MacVim via Terminal.app or iTerm.app

For the past several weeks I have been using MacVim as my primary editor for Ruby coding. My workflow has been to edit some code, Command-Tab over to a Terminal window and run a test by either typing the command, or using the up-arrow to run the same test I had already typed in.

This worked pretty well, but it was annoying needing to type in the full path to the test file when running a new test. And I would often Command-Tab just once thinking this would take me back to a Terminal window, but end up looking at a browser forgetting that I had been reading some documentation in between test runs.

I played around with some techniques for running the test file “inside” of MacVim, but did not like how they would lock up my editor, or added new buffer splits to show the output.

To improve my workflow I put together a few Vim functions for running a test file in a specific Terminal or iTerm tab using an AppleScript helper script (executed with the osascript command line tool). When I start working on a project I set a global variable specifying the TTY name of the Terminal tab I want to run the test in. Running the tty command in Terminal or iTerm will print out the value that needs to be set. I use the following mapping as a shortcut for setting the global variable, including a hardcoded default value.

map  :let g:vim_terminal="/dev/ttys000"

Three functions are provided in the following Vimscript. One runs the entire file, one runs the file at a specific line, and the last re-runs a previously run file.

function! RunInTerminal(file)
  if match(a:file, '_spec\.rb') != -1
    let l:command = 'bundle exec rspec'
  elseif match(a:file, '\.feature') != -1
    let l:command = 'bundle exec cucumber'
  elseif match(a:file, '\.rb') != -1
    let l:command = 'ruby'
  endif

  if exists("l:command")
    let g:last_run_in_terminal = a:file
    let l:run_script = "!osascript ~/.vim/tools/run_command.applescript" 
    silent execute ":up"
    silent execute l:run_script . " '" . l:command . " " . a:file . "' " . g:vim_terminal . " &"
    silent execute ":redraw!"
  else
    echo "Couldn't figure out how to run " . a:file 
  end
endfunction

function! RunFileInTerminal()
  if exists("g:vim_terminal")
    call RunInTerminal(expand("%")) 
  else
    echo "You need to set g:vim_terminal to a valid TTY (e.g. /dev/ttys000)"
  end
endfunction

function! RunFileAtLineInTerminal()
  if exists("g:vim_terminal")
    call RunInTerminal(expand("%") . ":" . line("."))
  else
    echo "You need to set g:vim_terminal to a valid TTY (e.g. /dev/ttys000)"
  endif
endfunction

function! ReRunLastFileCommand()
  if exists("g:vim_terminal") && exists("g:last_run_in_terminal")
    call RunInTerminal(g:last_run_in_terminal)
  endif
endfunction

command! RunFileInTerminal call RunFileInTerminal()
command! RunFileAtLineInTerminal call RunFileAtLineInTerminal()
command! ReRunLastFileCommand call ReRunLastFileCommand()

The following AppleScript looks pretty awful, but it seems to work and I just can’t muster up the motivation to spend the time learning the language well enough to make it any better. The Vim functions above expect this script to be reside in ~/.vim/tools/run_command.applescript. It looks through all of the Terminal.app and iTerm.app tabs trying to find one that matches a given TTY name.

on appIsRunning(appName)
  tell application "System Events" to (name of processes) contains appName
end appIsRunning

on execInItermTab(_terminal, _session, _command)
  tell application "iTerm"
    activate
    set current terminal to _terminal
    tell _session
      select _session
      write text _command
    end tell
  end tell
end selectTerminalTab

on execInTerminalTab(_window, _tab, _command)
  tell application "Terminal"
    activate
    set frontmost of _window to true
    set selected of _tab to true
    do script _command in _tab
  end
end execInTerminalTab

on run argv
  set _command to item 1 of argv
  set _foundTab to false
  -- Second argument should be the tty to look for
  if length of argv is 2
    if appIsRunning("iTerm") then
      tell application "iTerm"
        repeat with t in terminals
          tell t
            repeat with s in sessions
              set _tty to (tty of s)
              if _tty = (item 2 of argv) then
                set _foundTab to true
                set _session to s
                set _terminal to t
                exit repeat
              end if
            end repeat
          end tell
          if _foundTab then
            exit repeat
          end if
        end repeat
      end tell
      if _foundTab then
        execInItermTab(_terminal, _session, _command)
      end if
    end if
    if not _foundTab and appIsRunning("Terminal") then
      tell application "Terminal"
        repeat with w in windows
          tell w
            repeat with t in tabs
              set _tty to (tty of t)
              if _tty = (item 2 of argv) then
                set _foundTab to true
                set _window to w
                set _tab to t
                exit repeat
              end if
            end repeat
          end tell
          if _foundTab then
            exit repeat
          end if
        end repeat
      end tell
      if _foundTab then
        execInTerminalTab(_window, _tab, _command)
      end if
    end if
  end if
end run

If there is interest I am thinking about packaging this up as a Vim plugin, but for now you can just download the files and fit them into your Vim configuration however you see fit.

One final note: I tried this out in the native Vim that comes with Mac OS X and it seems to work, but I haven’t really tested it much in anything other than the latest MacVim (7.3).

Conversation
  • I think you should really check out Guard. It makes writing all those vim scripts obsolete.

    • Patrick Bacon Patrick Bacon says:

      Pawel: I’ve been using Guard recently to compile HAML templates into JavaScript files for use in client side templates. But I haven’t tried using it for running tests.

      I will frequently run only a single test within a file, especially when working on system/integration tests that take longer to run. I have always preferred having control over which test file, or even which test within a file, I want to run after making code changes. But perhaps I should give something like guard-rspec just to see how it goes.

      Thanks for the feedback.

  • Comments are closed.