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).
I think you should really check out Guard. It makes writing all those vim scripts obsolete.
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.
@Patric running just a single file from a test is something that Guard excels at, like it was made just for that. But it’s oh so much more powerful…
@Patric But, of course, you cannot make it run a single test :)
Sorry for the spam, but apparently RSpec supports a :focus option added to each spec and with a special config set in spec_herlp it lets you run only the selected spec! See http://railscasts.com/episodes/285-spork :)