I recently blogged about why I use Vim instead of Xcode for iOS development (why Vim is the future).
However, one thing that Xcode does really well is code completion. It’s not without a cost – you have to turn on indexing which can consume a lot of memory and can lock up the UI on load (for several minutes on one particularly large project I work on), but the functionality is really nice when it works.
So after moving back to Vim, the first thing I did was to see about getting code completion working on my iOS projects.
Lots of code completion plugins exist for Vim, but the most awesome (IMO) is YouCompleteMe (written by one of my colleagues). Check out the animated GIF at the top of that link to get a feel for it – it’s really cool. It does fuzzy matching and never blocks editing. I highly recommend enabling it as a language-agnostic identifier-based code completer (kind of like i_CTRL-X_CTRL-N on steroids).
YCM also has a Clang-based semantic completion engine which means that it works for iOS code! All you need to do is wire things up so that clang knows what compilation flags to use for your source code (where to find headers etc).
The trick is to generate a clang compilation database – basically a large JSON array stored in a file called compile_commands.json. Each item in the array contains the appropriate clang command (including all flags) for one of your source files:
"directory" : "/path/to/your/src/",
"command" : "clang ..."
"file" : "foo.m"
So how do you generate a clang compilation database from your Xcode project? The best way that I’ve found is to build your project on the command line using xcodebuild and then use oclint-xcodebuild to parse the output (Oclint is an open source static analyser that also makes use of Clang’s compilation database feature).
Step 1: Build your project via xcodebuild and capture the log in a file called xcodebuild.log (I use xcpretty to make STDOUT more pretty):
xcodebuild -workspace $XCODE_WORKSPACE \
-scheme $XCODE_SCHEME \
-sdk iphonesimulator build \
| tee -a xcodebuild.log \
| xcpretty -c
Step 2: Run oclint-xcodebuild:
You can use the resulting compile_commands.json for YCM, Oclint, and any other clang-based tools you like.
Wiring up YCM
YCM looks for a .ycm_extra_conf.py file in the directory of the opened file or in any directory above it. The quickest way to get YCM working is to copy the version that ships with YCM into your project’s top-level directory and set the compilation_database_folder variable in the script to point to the folder containing your generated compile_commands.json file.
Stripping out unwanted flags
There are some flags that need to be excluded from the clang compilation database in order to keep YCM happy. You can bake this logic into .ycm_extra_conf.py, or just use the following Perl one-liners in your build script:
perl -i -ple 's/-fmodules -fmodules\S* //g' compile_commands.json
perl -i -ple 's/--serialize-diagnostics \S* //g' compile_commands.json
perl -i -ple 's/(-MMD |-MT dependencies |-MF \S* |)//g' compile_commands.json
perl -i -ple 's/(-iquote|-I|-F)\s*\S*DerivedData\S* (?<!hmap )//g' compile_commands.json
Unless you do a clean build, xcodebuild.log only contains information about files that were actually built. Each time you run oclint-xcodebuild, it generates a new compile_commands.json, meaning that you’ll be missing information about most files unless you do a clean build every time. That kind of sucks.
Ideally, oclint-xcodebuild would incrementally apply new information it found in your latest xcodebuild log onto your existing clang compilation database (feature request here).
In the meantime, I’ve written a Node.js script that you can use to do the merging yourself:
# Install Node.js:
brew install nodejs
# Download merger script:
wget https://gist.github.com/patspam/11089180 -O clang_compilation_db_merger.js
Update your build script to generate a temporary clang compilation db each time and then merge this information into your main JSON file:
oclint-xcodebuild -o compile_commands-tmp.json xcodebuild.log
node clang_compilation_db_merger.js compile_commands-tmp.json compile_commands.json
With this set up, you only need to do a clean build once, and henceforth your compilation database will be incrementally updated each time you build (once oclint-xcodebuild supports incremental updating, you can remove this manual merge step).
It would be nice if compile_commands.json was completely portable – that way you could share the file between machines, avoid the need to do an initial clean build, and even get Objective-C code completion on linux! However due to things like precompiled headers (.pch) and header maps (.hmap) you’ll probably still have some references to DerivedData hanging around meaning that some files in your project may not work unless you build the project on that machine with xcodebuild. I’ll report back if I get around to figuring out how to solve that..
In the meantime, enjoy iOS code completion inside Vim! I’m using it successfully for projects that mix Objective-C, Objective-C++, C and C++ (basically, anything that Clang supports). Unlike Xcode, there’s no UI-blocking indexing step, and you still get all the sweet IntelliType features you’d expect from a semantic completion engine (jump to definition, compiler warnings in the margins, method signatures…). And of course, as YCM continues to rapidly improve you’ll get to enjoy all the new features that come down the pipeline.