Creating My First Vim Plugin
Why would anyone ever...?
While I'm at work or hacking away at home, I spend most of my development time in Vim.
I tend to edit a lot of PHP classes and the associated test files at the same time...but finding the files and manually opening them, sucks.
It takes some mental energy to find the file and open it, which can interrupt your train of thought. I do use the excellent ctrlp.vim plugin, but the codebase I work in contains way too many files, making the autocomplete kind of awkward to use at times.
I've been thinking of mapping some command to open the related file but have heard bad things about it.
Until I came across this: Optimize Your TDD Workflow by Writing Vim Plugins
Plugins can be easy
The article made a script to do exactly what I wanted...except it was based on a Rails convention. Nonetheless, the amount of code there is quite small and being a developer, I figured I could make sense of it.
For my version, I ended up replacing the regex with something more complicated for PHP PSR-0 style naming conventions.
The rails format looked like this:
app/<type>/<model>.rb
spec/<type>/<model>_spec.rb
The PSR-0 / PHPUnit standard looks like this:
src/<vendor>/<namespace>/<class>.php
tests/<vendor>/Tests/<namespace>/<class>Test.php
The PHP one is a bit more complicated.
Here is the crazy regex I came up with:
" Converting from src to tests
substitute(a:file, 'src/\([^/]\+\)/\(\([^/]\+/\)\+\)\?\(.*\).php$', 'tests/\1/Tests/\2\4Test.php', '')
" Converting from tests to src
substitute(a:file, 'tests/\([^/]\+\)/Tests/\(\([^/]\+/\)\+\)\?\(.*\)Test.php$', 'src/\1/\2\4.php', '')
Hmmm...not too pretty to look at but it works.
Testing
Looking at some repos for plugins, I'm not sure how people test them.
The regex was pretty complicated and it wasn't productive to constantly change it and test it on a new file.
I decided to create a RunTests
function and an Equals
function, as a quick solution.
RunTests
function! s:RunTests()
let s:test_number = 0
let s:passes = 0
echo "Test s:GetRelated"
" Not in the proper format
call s:Equals('', s:GetRelated('blah.php'))
" Proper format, source file, no namespace
call s:Equals('tests/Vendor/Tests/ClassTest.php', s:GetRelated('src/Vendor/Class.php'))
"...more tests
echo printf("\nFinished test suite - %d of %d tests passed (%d%%)", s:passes, s:test_number, s:passes*100/s:test_number)
endfunction
Equals
function! s:Equals(expect, actual)
let s:test_number = s:test_number + 1
if a:expect ==# a:actual
echo printf("%10s %s", "Test " . s:test_number . ":", "PASS")
let s:passes = s:passes + 1
else
echo printf("%10s %s", "Test " . s:test_number . ":", "FAIL")
echo printf("%20s %s", "Expected:", a:expect)
echo printf("%20s %s", "Actual:", a:actual)
endif
endfunction
Output
Test s:GetRelated
Test 1: PASS
Test 2: PASS
Test 3: PASS
Test 4: PASS
Test 5: PASS
Test 6: PASS
Test 7: FAIL
Expected: apath/to/src/Vendor/Namespace/A/B/Class.php
Actual: path/to/src/Vendor/Namespace/A/B/Class.php
Test 8: PASS
Test 9: PASS
Finished test suite - 8 of 9 tests passed (88%)
Running the Tests
I exposed a function called RelatedTests
to call the internal RunTests
function.
command! RelatedTests :call <SID>RunTests()
To quickly run the tests, I bound the ;
key to perform the following tasks:
- save the file
- source the script
- run the tests
:map ; :w<cr>:so %<cr>:RelatedTests<cr>
I would make a change, then press ;
to perform all those actions.
The result
A :RelatedFile
vim command that finds the PHP source or test class and opens it in a new vertical split.
To use it, I added the following to my .vimrc
:
" Assuming the use of https://github.com/gmarik/vundle
Bundle 'donaldducky/related.vim'
" Find related file
map <Leader>rf :RelatedFile<CR>
Now, I can open any class or test file and press ,rf
to instantly go to the related file.
After a couple days of using it, I already see the benefits of this plugin and have a few ideas to make it more useful.
It's on github at: https://github.com/donaldducky/related.vim