5 Comments

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 <leader><C-r> :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).