Adventures in VimscriptSeptember 19, 2016 -
Last week Vim 8.0 came out. I was pretty excited because, among other things, Vim now supports asynchronously running code in plugins. Until now, if your plugin code took time (like compiling your project or some auto-completion code) it would cause Vim to freeze until it was finished. No typing, no moving the cursor, you could do nothing. This has been one of the things that sucked about Vim compared to the many other good editors that exist. There are projects like neovim which are attempts to make a version of Vim which could handle asynchronous plugins, but the last time I tried this it caused a kernel panic (I'm sure that's fixed by now). Now that Vim supports it out of the box, I was excited to dig in and see if I could live my dream of doing iOS development in Vim.
There are a few things that are necessary for me to enjoy development with a compiled language like Swift.
- Syntax highlighting
- Autocomplete (the Cocoa frameworks are full of long method names)
- Buttons for compiling, testing, and running
The vim-swift plugin from kballard takes care of syntax highlighting, so that was easy. The next I thought would be easy was compiling, testing, and running. I had heard about gfontenot's plugin which reduces compiling, testing, and running your normally-Xcode-based-projects to simple commands like
:XBuild that you could map to a key. A quick browse through the documentation showed that he even added support for supplying your own runner for the command it generated. The announcement for Vim 8.0 even mentioned a plugin that used Vim's new asynchronous abilities to run whatever command you wanted. It seemed like a convenient combination, use AsyncRun as the runner for Xbuild and go, go productivity.
This didn't work and suddenly it was time to go into detective mode.
I have a limited familiarity with vimscript (the language of plugins), so I spent a day breezing through Learn Vim The Hard Way which was well written and wonderfully informative. I didn't follow the instructions, of course, but I got the idea. After browsing through the XBuild code (which was easier than normal because Gordon is cool and pointed out where to start), I started by plugging the
echom command everywhere to print out the value of variables and get a feel for which functions executed in which order. The plugin turned out to be pretty clear once you get into it.
First thing I found is that the plugin relies on some shell scripts to determine some of the parameters needed for the actual compile command. These use the runner also, which doesn't work out so great because the plugin doesn't know to wait for the asynchronous runner to finish. Fortunately the plugin provides commands which allow you to manually supply those values and eliminate the need for the scripts.
Once you skip the shell scripts, the plugin no longer hangs Vim, but you get a new problem: the final build command fails because of "command not found" errors in the shell. I tried asking skywind3000 who made the AsyncRun plugin to make sure commands executed with AsyncRun run as the same user and in the same environment as me, the user. He confirmed that was true, so I knew there had to be some other explanation for why I was getting a "command not found" error. I even tried copying the full compiler command, which looks like this:
set -o pipefail; NSUnbufferedIO=YES xcrun xcodebuild build -project './TheoryDrills.xcodeproj' -scheme 'TheoryDrills' -destination "platform=iOS Simulator,name=iPhone SE" | xcpretty --color
(you can see now whey you'd want a plugin to generate it for you) and running it in the terminal myself. "How could it work fine in the terminal, but not in the runner?" I asked myself. Finally it dawned on me. String escaping! When escaped "properly" the command works just fine when fed to AsyncRun.
In the end, the solution to the problem was a bit of a bummer. AsyncRun couldn't be a runner for Xbuild without modifying Xbuild in ways that would break it for other runners. My solution, for now, was to add a mapping in my vimrc to compile and test my project with AsyncRun directly like so:
nnoremap <leader>b :AsyncRun xcodebuild -scheme TheoryDrils -destination name=iPhone\ SE,platform=iOS\ Simulator \| xcpretty<cr>
nnoremap <leader>u :AsyncRun xcodebuild test -scheme TheoryDrills -destination name=iPhone\ SE,platform=iOS\ Simulator \| xcpretty<cr>
It's not as fancy, but it'll keep me working for now. In theory this could evolve a little bit to work with my client work too. Since the projects I work on are so simple, turning the now-hard-coded "TheoryDrills" part into a variable would work just fine. If I manage to get a working solution for autocomplete, Vim on iOS client work could be a dream achieved. My hope is that plugins will catch up to this fancy asynchronous behavior Vim has now and this will be easier some day.