Develop Objective-C in neovim


This is long and I still have a lot to add and improve. Leave a note if I’ve made a mistake or if you discover anything that really helped you.

Initial Setup

I’m not going to assume much prior experience with vim. I am going to assume you’re on a Mac, like any intelligent heathen. First, install Homebrew.

Install neovim. vim is legacy. Long live neovim.

brew update
brew tap homebrew/versions
brew install homebrew/versions/llvm36
brew tap neovim/homebrew-neovim
brew install —-HEAD neovim
pip install neovim

Install vim-plug, the simple plugin manager for vim that has no boilerplate code and does asynchronous installs and updates:

curl -fLo ~/.nvim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

neovim stores all of its settings in .nvimrc, which is written in vim script. It’s found at ~/.nvimrc. Add this to it so that you can start your list of plugins to install:

call plug#begin()
" We will put plugins here later
call plug#end()

You’ll be customizing neovim throughout this article. It might be helpful to reference my .nvimrc, which is a complete and working . It will be helpful for looking up syntax or lots of useful things that I don’t cover here.

Code Completion + Inline Errors and Warnings

This is first because it’s possibly most important. Objective-C is not written for succinctness or easy memorization. It’s meant for easy reading. You want YouCompleteMe.

Plug 'Valloric/YouCompleteMe' "code completion

Add to you ~/.nvimrc so that you get semantic code completion similar to that of Xcode:

let g:ycm_semantic_triggers = {
\ 'objc' : ['re!\@"\.*"\s',
\ 're!\@\w+\.*\w*\s',
\ 're!\@\(\w+\.*\w*\)\s',
\ 're!\@\(\s*',
\ 're!\@\[.*\]\s',
\ 're!\@\[\s*',
\ 're!\@\{.*\}\s',
\ 're!\@\{\s*',
\ "re!\@\’.*\’\s",
\ '#ifdef ',
\ 're!:\s*',
\ 're!=\s*',
\ 're!,\s*', ],
\ }

Now you’ll need to fill out the clang compiler flags so that YouCompleteMe can do semantic code completion, as well as inline warnings and errors. This isn’t nearly as hard as it might seem. Basically, just copy this file to the root of your project. Then build your project using xcodebuild. Look at the flags it uses to build one of your source files and transfer those flags in proper format to the flags[] array in that .ycm_extra_conf.py file. You can see an example xcodebuild command lower in this article.

Note: This bug is currently the largest hurdle to full Xcode-like code completions in neovim using YouCompleteMe. I’m hoping to submit a pull request to solve this, but in the mean time you could use clang_complete instead of YouCompleteMe. clang_complete has fewer features than YouCompleteMe and it’s not the future of code completion, but it so happens that its current integration with UltiSnips makes it autocomplete Objective-C code more like Xcode than YouCompleteMe can right now.

Build

Install vim-dispatch

Plug 'tpope/vim-dispatch'   " Asynchronous build

Reload your ~/.nvimrc (or just quit and restart nvim) and then run `:PlugInstall`. Hopefully you’re also using tmux. Why? Because tmux is amazing and does away with a lot of the stupid of using just a plain shell inside a Terminal GUI:

3 tmux panes = 3 independent shells on one small MacBook screen. Hide and show as you please.

So go ahead and try it:

brew install tmux

Then on the command line run tmux:

tmux attach || tmux new

Now that you have vim-dispatch, you can make it run your xcodebuild command in a background tmux pane while you continue to code. So, you cd into your project’s root directory, open `nvim`, and then set the variable makeprg to your xcodebuild command:

:set makeprg=xcodebuild\ -workspace\ LearningApp.xcworkspace\ -scheme\ Memories-iOS\ -configuration\ Debug

If you have a .xcodeproj rather than a .xcworkspace, then use -project rather than -workspace. If you want to specify simulator, use -sdk iphonesimulator, or specify device with -sdk iphoneos.

To make this automatic, I have this set in my .nvimrc:

if ( getcwd() == '/local/dev/memories_app')
:set makeprg=xcodebuild\ -workspace\ LearningApp.xcworkspace\ -scheme\ Memories-iOS\ -configuration\ Debug
endif

So, if I start nvim from the root directory of my memories_app project, then it will set the makeprg variable for me.

With that variable set, now just run `:Make` and vim-dispatch does its magic.

Jump to Declaration/Definition

This comes with YouCompleteMe. To go to the definition of the method your cursor is over :YcmCompleter GoToDefinition. To go to the declaration :YcmCompleter GoToDeclaration. It’s pretty straightforward. It works pretty well. I mapped these to make it faster:

nnoremap <leader>d :YcmCompleter GoToDefinition <cr>
nnoremap <leader>s :YcmCompleter GoToDeclaration <cr>

Now I can press <space>d to go to the definition of the current function or ,s to go to its declaration. Your <leader> key is set in your ~/.nvimrc.

.m/.h Switching

Plug 'eraserhd/vim-ios'    ".h <-> .m switching and project build

After installing this plugin, you can use :A to switch between the currently open .h or .m file. Note that this only works if the .h and .m files have exactly the same name (Ex. MyClass.h corresponds to MyClass.m). While this is the case 99% of the time, it is valid Objective-C to name your .h and .m differently.

Quick Search Bar (Shift+Cmd+O)

First, install ag, a fast file text searcher like grep or ack:

brew install ag

Add this plugin to your .nvimrc:

Plug 'kien/ctrlp.vim'  " quick search bar
" Make ctrlp a lot faster in git repositories
let g:ctrlp_user_command = ‘ag %s -i — nocolor — nogroup — hidden
\ -g ""'
let g:ctrlp_use_caching = 0 “ ag is so fast that caching isn’t necessary
let g:ctrlp_max_files = 10000
let g:ctrlp_working_path_mode = 'r' " Always use the current working directory rather than the location of the current file
let g:ctrlp_by_filename = 1 " Default to filename only search rather than searching the whole path. This is more like Xcode's Shift+Cmd+O

Now when you want to work on a project, cd into the project’s root directory. Start vim, and just press <c-p> to bring up the quick search bar. Type path components and/or the file’s name to find the file you want to start editing and press enter to go to it.

If there are any files showing up in the ctrlp quick search that aren’t important to you, create ~/.agignore and add this to it, along with any patterns that you want to ignore in your projects:

.git*
extern
*.test

Project-wide Text Search

You installed ag earlier. Now you’ll use it for project-wide search. In your ~/.nvimrc:

Plug 'rking/ag.vim'  " ag support for searching file
" Outside your plugin definitions:
:command! -nargs=+ S :Ag! "<args>"
:command! -nargs=+ Sm :Ag! --matlab --ignore=*Test* --ignore=_* "<args>"
:command! -nargs=+ St :Ag! --matlab --ignore=*Test* --ignore=_* --ignore=*Tool* "<args>"

Now you can type `:S myString` to recursively search for “myString” in every file in your current directory and all its subdirectories. That second command up there allows you to do `:Sm myString` to search only inside .m files, ignoring any unit testing files and generated classes.

Finally, out of convenience you can add these key commands to easily go to the next and previous results in the ag search results list:

nnoremap <leader>j :cn<CR>
nnoremap <leader>k :cp<CR>

With these, you can press Control-j to go to the next result and Control-k to go to the previous result.

You can also add a keyboard shortcut to easily hide and show the quickfix and location lists:

" In your plugins
Plug 'Valloric/ListToggle' " Toggle the display of the location and quickfix windows
" Later in your .nvimrc:
let g:lt_location_list_toggle_map = '<leader>l'
let g:lt_quickfix_list_toggle_map = '<leader>q'

Code Snippets

One of my biggest problems with putting code snippets into Xcode was that I had no idea where they were on the filesystem, they weren’t synchronized to my other computers where I also use Xcode, and I always felt like there was a good chance they would disappear if I had to uninstall and reinstall Xcode or if I had to reset Xcode by clearing out all my user info. Indeed, I’ve never managed to keep an Xcode snippet around for very long.

Plug 'SirVer/ultisnips'        "code snippets
" UltiSnips
" The below key bindings are compatible with YouCompletMe integration
let g:UltiSnipsExpandTrigger="<c-j>"
let g:UltiSnipsJumpForwardTrigger="<c-j>"
let g:UltiSnipsJumpBackwardTrigger="<c-k>"

Now, when you’re editing a file, you’ll get code completions from YouCompleteMe that include UltiSnips code snippets at the end of the list:

Two code snippet completions at the bottom

Tab to the one you want, hit <c-j> and you get:

dispatch_once code snippet chosen

Note that if you just installed neovim and you’re setting things up now for the first time, your colors won’t quite look this pretty from the start. In my thoughts on choosing the right terminal for Mac I mention how to setup these color schemes.

To create a snippet, open any file and execute :UltiSnipsEdit. It will open the corresponding <filetype>.snippets file for you to edit. As an example in Objective-C, you can add a dispatch_once snippet by adding this to that file:

snippet dispatch_once
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
$1
});
endsnippet

I keep these snippets synched across machines by having that Ultisnips folder in my Dropbox, and then I have a symlink to it where Ultisnips looks for it:

ln -s ~/Dropbox/config/Ultisnips ~/.nvim/Ultisnips

Bracket Completion

When you write if( it’s pretty obvious you want the bracket to be automatically completed: ) just as it’s done in Xcode. Add this plugin for that:

Plug 'Raimondi/delimitMate'    " Automatically insert closing brackets

Xcode also has a cool feature where it tries to pair brackets retrospectively. For example, I start with this:

[NSFileManager defaultManager]<cursor here>

and then I keep typing:

[NSFileManager defaultManager] removeFileAtPath:MYPATH];

And Xcode is nice enough to put the [ at the beginning of the line. This doesn’t always work in Xcode, but in vim I have a solution that may not be quite as automatic, but it always works:

Plug 'tpope/vim-surround' " Add, remove, and change surrounding characters in pairs

Now, when you are in the situation above, you can press yssr to automatically wrap this line in []. Learn more about the things made possible with vim-surround with `:h surround`.

Highlight Variable Under Cursor

With this plugin, you can highlight all uses of the variable currently under your cursor, just like Xcode:

Plug 'qstrahl/vim-matchmaker'   " Highlight the term under the cursor

Then, at the bottom of your ~/.nvimrc:

" Underline words matched by Matchmaker
function! ToggleMatchmaker()
" Don’t show matches in non-code files
if &ft =~ ‘objc\|objcpp\|python\|cpp’
Matchmaker
else
" Specifically, I want it disabled on help, mkd
Matchmaker!
endif
endfun
autocmd BufEnter * call ToggleMatchmaker()
highlight Matchmaker guibg=NONE
highlight Matchmaker gui=underline
highlight Matchmaker ctermbg=NONE
highlight Matchmaker cterm=underline

Now you can see the variable under the cursor is being underlined:

Auto Indent

With your Hombrew installation of llvm36 above came the clang code formatter `clang-format`. Install a vim plugin that will use it to auto format you code as you write it:

Plug 'Chiel92/vim-autoformat'  " Auto-format code using existing formatters

Now, to autoformat your currently open file, type :Autoformat. I have it set to automatically format on each file save:

" Autoformat 4 seconds after the user’s cursor stops moving in normal mode

autocmd FileType objc,objcpp autocmd InsertLeave <buffer> :silent Autoformat

Auto Save

You shouldn’t have to incessantly :w to save your buffer. So, in your ~/.nvimrc:

autocmd BufUnload,BufLeave,FocusLost,QuitPre,InsertLeave,TextChanged,CursorHold * silent! wall

Auto Session Save/Load

You should be able to leave and enter NeoVim and start where you left off. For that, in your ~/.nvimrc:

Plug 'xolox/vim-session'    " Save and restore vim state
:PlugInstall

Now that it’s installed, configure it to automatically save to the current directory and automatically load:

" vim-session {{{
let g:session_directory = getcwd() " The session directory is always the current directory. This should allow save and restory on a per project basis
let g:session_default_name = ".vim-session"
let g:session_autoload = 'yes'
let g:session_autosave = 'yes'
let g:session_lock_enabled = 0
""" }}}

What You’ve Got Now

At this point, I never open Xcode for any coding or building. It so happens I don’t use it for debugging either because Xcode doesn’t support the situations where I need debugging, so I’m using command line anyway. That’s 95% of what a programmer’s going to be doing. The only time I do open up Xcode is to edit some of Xcode’s junk that is GUI-only, like storyboards, Core Data models, and plists. They’re just easier to edit in GUI format. When Xcode is healthy enough to open up my storyboard and edit it without shitting on me, then life is excellent.

TODO

It would be really great if someone wrote a neovim plugin that uses xcodeproj to automate all of the project-specific settings for build, install, run, test, clean, debug, and analyze.

Syntax Highlighting

The above cocoa.vim that you installed should give vim enough information to correctly highlight various aspects of the code: classes, types, operators, etc. The colors that are chosen for each entity are dictated by your colorscheme.

TODO: Syntax highlighting of user-defined symbols.

TODO: Get a nice colorscheme going.

TODO: Probably better than cocoa.vim, which does not do autogeneration, is to just use easytags.

Analyze

TODO

Run Unit Tests

TODO: See here.

Run in Simulator

TODO: Use simctl.

Run on Device

TODO: Use ios-deploy.

Debugging

Consider using vim-lldb.

TODO: For the simulator:

% lldb
(lldb) process attach -n 'Name Of Your App' --waitfor
<launch your app in the simulator>
(lldb) continue

Console Logging

TODO: For one solution, see here.

Method and Pragma Mark List

Install cocoa.vim plugin:

Plug 'jgoulah/cocoa.vim'           " method list, documentation

Now you can run :ListMethods to get a list of all the pragma marks and methods in the current file. Press enter on a line to jump to that method:

TODO: Right now I find cocoa.vim is extremely performance inefficient, probably due to its syntax highlighting attempts.

The right thing to do here is to use `brew install ctags` with vim-easytags. At the moment it just doesn’t have its config setup for Objective-C so I just need to complete this and it should work very well.

Browse Documentation

Documentation browsing is also provided by the cocoa.vim plugin. Press K to bring up the documentation for the symbol under your cursor.

TODO: Fix the plugin. Right now it doesn’t work correctly.

Why neovim?

This is down here as a footnote because I think it’s least important. At this point it simply seems overwhelmingly obvious to me. I’ve been using neovim ~14 hours of programming every day (Yes often on weekends as well) for 6 months now. In that entire time period, I can recall it crashing a GRAND TOTAL of twice. That’s two whole times. In both instances I filed a bug report against neovim and there was an instant response. Unlike vim, neovim will become essentially impossible to crash even with rogue plugins installed. Oh, what was that? A groan from ugly old Xcode over there in the corner? *pah*

Sure, there’s a learning curve if you’re new to vim. So what. You’re old and you’re getting stupid. Turn that around by learning new things that are just plain better. There’s absolutely no question that I’m a lot happier and more productive in neovim, and I use it daily as a first class IDE for C++, Python, and Objective-C.

Why not emacs? I’m sure emacs can do all the same things. Give it a go if it floats your boat. The neovim decision came down to two things: 1) neovim’s modal key bindings appeal to me over emacs’ key bindings. 2) The project community and authors are fucking stellar. They’re doing all the right things and it’s shaping up to be a stellar code base with stellar features.

Why not vim? *SPEWS WATER ALL OVER THE WALL* Don’t be ABSURD. vim is a nasty pile of shit being maintained by a gremlin. It doesn’t even have an asyncronous plugin model….?????!?!?!?>!>!?!!>!>>!N!IU!$IUG(T It’s 2015, get with the fucking program. neovim is all over the fucking program.