VimCharm: Approximating PyCharm on vim

Disclaimer: All I’ve done is write a few config files.

This is for you if:

  1. Your python IDE of choice is PyCharm
  2. You wish you had a command-line replacement for PyCharm on all the places you ssh into
  3. You wish you had access to at least some of the niceties of PyCharm when editing a one-off script, without having to create/import it into a new project.
  4. You are somewhat familiar with vim and can comfortably edit a single file on vim. You also know what a .vimrc is
VimCharm, featuring capreolus

Motivation

PyCharm has worked wonderfully well for me, and the only time where I have to use something else is when I ssh into a server to put together a quick script. That something else tends to be vim, and this is an attempt to get vim as close to PyCharm as possible – especially the shortcuts so that I can work on vim the way I work on PyCharm (well, almost). The end result is still a far cry from PyCharm, but it makes navigating a codebase over ssh significantly less painful (at least for me)

But PyCharm can work over ssh

Yeah, but I don’t use PyCharm for one-off scripts. Besides, it pleases me to know that if I can ssh into the server (from an ipad, a phone, or a washing machine), I have an (approximate) IDE I can work on.

List of working approximations

  • Sorta kinda uses the same colorscheme as PyCharm
  • Toggles a project navigation sidebar (NERDTree) using alt + 1 . Approximates PyCharm’s Ctrl+1
  • Comment/uncomment multiple lines using Ctrl+/, just like PyCharm
  • Autocomplete
  • Navigate to the definition of a method/variable using Ctrl + Left click or Ctrl + b, just like PyCharm
  • Jump to the previous location using Alt + - . Approximates PyCharm’s Ctrl + Alt + Left Arrow
  • Fuzzy search for files using Ctrl + o. Approximates PyCharm’s double shift press
  • Search the entire code base using Alt + f. Approximates PyCharm’s Ctrl + Shift + f
  • Edits made in the search results window are reflected on to the underlying file, just like PyCharm
  • Syntax and linter errors show up as you type, just like PyCharm
  • If you are editing files that are part of a git repository, there are indicators on the gutter to show added, modified and subtracted lines, just like PyCharm
  • Pressing F12 brings up a terminal. Approximates PyCharm’s Alt + F12
  • Code folding using the minus (i.e -) key. Approximates PyCharm’s Ctrl+- and Ctrl + +
  • Automatic file saves, just like PyCharm
  • Rename methods/variables and automatically fix imports e.t.c across files, just like PyCharm

Why not just use python-mode?

I simply could not figure out the shortcuts that python-mode used. I thought it would be easier and more flexible if I install and configure the plugins myself.

Prerequisites

  • vim 8
  • A Python virtualenv (or a conda environment). There’s some pip install involved, though this is optional
  • Patience

TL;DR

Go here for the .vimrc

Let’s start from a blank .vimrc

If you need a project-specific .vimrc, see this. If not, everything goes into your ~/.vimrc

Let us begin by being able to see line numbers and some sort of syntax highlighting everywhere. Put these on your .vimrc:

" Some basic stuff
set number
set showmatch
syntax on
imap jj <Esc>

The set showmatch is for highlighting matching parentheses – that’s useful. The last line maps double pressing the j key in insert mode to <Esc> – no more reaching for that far away Escape key using your pinky!

NERDTree for the sidebar

We’ll start our plugin-hoarding with NERDTree. With Vim 8, we can simply copy over a plugin directory to a certain place and Vim would just “pick it up” – there’s no need to use a plugin manager for achieving VimCharm. Create the necessary directory structure and clone NERDTree:

mkdir ~/.vim/pack/vimcharm/start/ -p
git clone https://github.com/preservim/nerdtree.git ~/.vim/pack/vimcharm/start/nerdtree

Open some file (any file) on vim and type :NERDTreeToggle – you should see the sidebar. Executing the same command again would close the NERDTree. PyCharm by default opens/closes the sidebar using Ctrl + 1 . However, the terminal (and consequently Vim) cannot differentiate between 1 and Ctrl + 1, so we’ll map this to Alt + 1 instead. Before we do that, we need to determine what characters are sent by the terminal when we press the key combo. Simply run cat without any arguments, and press Alt + 1. You should see something like this:

You would also see that 1 and Ctrl+1 produces the same character on cat – as far as I know, there’s no way around this.

We need to map the character sequence for Alt + 1 to :NERDTreeToggle on our .vimrc:

set <A-1>=^[1
nmap <A-1> :NERDTreeToggle<CR>

Take care not to simply copy paste the character sequence on to your .vimrc! That won’t work. You should open your .vimrc on Vim, go to insert mode, and press Ctrl+v – this would put a caret under your cursor – now press Alt + 1 and it should fill in the necessary characters there. Restart vim, and Alt+1 should now open and close NERDTree.

Making it look like PyCharm

Gruvbox looks like the default PyCharm theme, kinda, so let’s get that:

git clone https://github.com/morhetz/gruvbox.git ~/.vim/pack/vimcharm/start/gruvbox

According to the installation page, we need to add this to our .vimrc:

autocmd vimenter * ++nested colorscheme gruvbox

Commenting and Uncommenting lines using Ctrl + /

We are going to use NERDCommenter for this. Clone it into the right directory, just as before:

 git clone https://github.com/preservim/nerdcommenter ~/.vim/pack/vimcharm/start/nerdcommenter

Ctrl+/ cannot be directly mapped on your .vimrc. So just like before, we insert the correct escaped character sequence into our .vimrc by going into insert mode, pressing Ctrl+v, and then pressing the desired keycombo (Ctrl + / in this case).

" The part after "=" in the below line should be inserted using Ctrl+v while in insert mode and then pressing Ctrl+/
set <F13>=^_
noremap <F13> :call NERDComment(0,"toggle")<CR>

" So that NERDCommenter can automatically decide how to comment a particular filetype
filetype plugin on

We are telling Vim to map the character sequences that Ctrl + / produces to the F13 key, which probably does not exist on your keyboard, and then we map F13 to the appropriate command to toggle comments. Restart Vim, open (or create) a python file and try Ctrl + / while in normal mode – it should comment/uncomment the line.

Autocomplete

Autocomplete on Vim does not feel as “fluid” as on PyCharm – for example, I haven’t managed to get it to work on imports – but it is still besser als nichts. Get jedi-vim:

 git clone https://github.com/davidhalter/jedi-vim ~/.vim/pack/vimcharm/start/jedi-vim

Restart vim, and Ctrl + space should already be giving you autocomplete suggestions. IMHO, jedi-vim displays too much information (what even is that bar thing that comes up on top?) – all I wanted was a simple autocomplete prompt. Put this on your .vimrc to Make Autocomplete Great Again:

let jedi#show_call_signatures = 0
let jedi#documentation_command = ""
autocmd FileType python setlocal completeopt-=preview

Go-to definition using Ctrl+click

This is something that jedi-vim already does – all we need are some .vimrc entries:

set mouse=a
let g:jedi#goto_command = "<C-LeftMouse>"
map <C-b> <C-LeftMouse>

The above lines enable mouse support on Vim, sets Ctrl + left click as the combination for jedi’s goto command, and then recursively maps Ctrl+b (which is also what PyCharm uses by default) to Ctrl + left click as a keyboard-friendly alternative.

I also prefer the goto command to open a new tab when navigating to a different file. Here’s how to enable that, along with Shift+j and Shift+k to move between tabs:

let g:jedi#use_tabs_not_buffers = 1
nnoremap J :tabp<CR>
nnoremap K :tabn<CR>

Jump to previous location using Alt + minus

If you end up navigating multiple tabs away using Ctrl+b, you can press Ctrl+o repeatedly to jump back to your original position. Press Ctrl+i to go in the other direction. These would come in handy if you have to quickly gg to the beginning of the file to add an import – you can then press Ctrl+o to go back to the line you were editing. I believe in PyCharm the default mappings for this are Ctrl + Alt + Left arrow and Ctrl + Alt + right arrow respectively. I remapped these to Alt + - and Alt + Shift + - (that would be in fact Alt + _ ):

set <A-->=^[-
noremap <A--> <C-O>
set <A-_>=^[_
noremap <A-_> <C-I>

Remember that copy-pasting these won’t work and you will have to enter insert mode and press Ctrl+v and then Alt + -

Fuzzy file search

Fuzzy file search is what PyCharm does when you press the Shift key twice.

Download CtrlP:

git clone https://github.com/kien/ctrlp.vim.git ~/.vim/pack/vimcharm/start/ctrlp

By default pressing Ctrl+p should bring up the search prompt. We can’t map this to double shift (as in PyCharm) since vim can’t recognize Shift key presses (unless it’s combined with a printable character). I decided to map this to Ctrl + o instead (“o” for open), though this is not any better than the default setting. On your .vimrc:

let g:ctrlp_map='<C-O>'
let g:ctrlp_cmd='CtrlPMixed'

The second line above specifies that the search should be over files, buffers, tags e.t.c – you may omit it if you do not want buffers to show up on your search. ctrl+t on a search result will open it in a new tab.

Search everywhere and replace

One of the most useful features in PyCharm is the “search in project” dialog that Ctrl + Shift + f brings up. For example, if I delete/rename a hard-coded string literal, this is the dialog that I would bring up to look for all occurrences of that string literal so that I can rename/delete all of them – right from the search window.

Instead of using the built-in vimgrep or making an external call to the ubiquitous grep, we are going to use ack because it excludes annoying things like gitignore and binaries from the search results by default.

  1. Somehow get ack on your target system
  2. git clone ack.vim to ~/.vim/pack/vimcharm/start/ack.vim

We are going to map Alt + F to a python-only smart-cased search with ack. Add these to your .vimrc:

set <A-F>=^[f
nnoremap <A-F> :Ack! -S --python 

Remember to Ctr+v and then press Alt+F to get those escaped character sequence right. Also, there is an extra space after the --python. Without it, the search term (eg: “foo”) that you type after pressing Alt+F would end up being “–pythonfoo”.

Restart vim and press Alt+f in a python file, enter your search term, and press enter. The results will be shown in a quick-fix window. Move your cursor to a search result and press enter to jump to that location. Press t to open that location in a new tab. Either of these would shift the focus to the editor. Press ctrl + w twice to shift focus back to the quick-fix window.

I usually use /<pattern> to search within the file, but sometimes it’s useful to do a slightly fancier search. I’ve wired Ctrl+f (the regular PyCharm find-in-this-file) to do a search within the open file:

nnoremap <C-F> :Ack!  %<Left><Left>

By default, you cannot make any changes to the contents of the quick-fix window. In Pycharm, the search results are editable and the changes are reflected on the underlying file. We can pull this off using the quickfix-reflector:

git clone https://github.com/stefandtw/quickfix-reflector.vim.git ~/.vim/pack/vimcharm/start/quickfix-reflector.vim

That’s it! Now your edits on the search results should be reflected on the underlying files.

Spot syntax and linter errors as you type

The most annoying thing about writing Python on Vim, at least for me, was that the silly syntax errors I make won’t be discovered until I actually try to run my script – PyCharm usually catches these as you type. Let’s set this up on vim using ALE:

git clone https://github.com/dense-analysis/ale.git ~/.vim/pack/vimcharm/start/ale

You should also have a linter insalled. I use pylint, and a quick pip install pylint does the trick. Restart vim and open a python file, and it should already be linting it as you type. Since ALE works asynchronously, there will be a slight delay (around a second) between you making a mistake and it being flagged on Vim – but in my opinion this is much better than a synchronous linting which freezes Vim, which is why I chose ALE instead of syntastic. However, the default ALE + Pylint combo is too whiny for my taste – I don’t want warnings about how I’m not writing a docstring for every single method; I have this on my .pylintrc:

[MESSAGES CONTROL]
disable=trailing-whitespace,missing-function-docstring,missing-module-docstring,no-else-return,miss    ing-class-docstring,invalid-name

The above is far from how I would like linter to be configured, but it serves as an initial config. I also do not care for highlighting the offending word in a line – all I want is a non-invasive indication in the gutter. On your .vimrc:

let g:ale_set_highlights = 0

Show lines modified after the previous commit

Put vim-gutter at ~/.vim/pack/vimcharm/start/vim-gutter and restart vim. If you edit a file in a repo (or set up git on your current folder with git init), the modified lines would be marked in the gutter. By default it takes 4 seconds for the appropriate mark to appear on your gutter – let us reduce it by putting this line in the .vimrc:

set updatetime=100

The end result is rather unflattering. ALE and git-gutter does not work well together – git-gutter’s modification marks are drawn over by the linter warnings, and in some-cases ALE ends up marking the wrong line with a warning. This thread suggests that there’s probably a way to get them to work the way I want, but I haven’t invested much time here.

Have a terminal handy

In Vim :term will open a terminal in a split window. Mapping this to F12 is trivial, but we want to hide this terminal (instead of killing it) and bring it back again on pressing F12. I could not get the “hide terminal on F12” part working, but I did figure out how to bring up a hidden terminal if it exists (or create a new terminal if it doesn’t) on pressing F12. Before we write a script for it, let’s go through the motions manually:

  1. Open Vim
  2. Type :term to open a terminal in a split window
  3. Type something on the terminal
  4. Press Ctrl+w and then type :hide to hide our terminal window
  5. To show our hidden terminal, type :sbuffer /bin/bash. This would open in a split window a buffer that has “/bin/bash” in its name. If you use something other than bash, you will have to change this string accordingly.

Here’s a LoadTerminal() Vim script I wrote that would bring up an existing bash buffer if it exists, or create a new one if it doesn’t:

function! LoadTerminal()
    let bnr = bufnr('!/bin/bash')
    if bnr > 0 
        :sbuffer !/bin/bash
    else
        :term
    endif
endfunction

Save it as load_terminal.vim at ~/.vim/pack/vimcharm/start and add the following lines to your .vimrc:

source ~/.vim/pack/vimcharm/start/load_terminal.vim
map <F12> :call LoadTerminal()<CR>

The annoying part here is that we can’t map key presses on the terminal window – so you’ll have to press Ctrl + w and type :hide to hide the terminal. Do let me know if you find a way to map this to a keystroke.

Code folding

When you deal with large files, code folding (those tiny “-” signs that you click on PyCharm to collapse an entire class/method) is a godsend. Fortunately vim supports code folding right out of the box and all we need is this on our .vimrc:

set foldmethod=indent
set foldlevelstart=99
nnoremap - za
map _ zM
map + zR 
        

According to our mappings above, there are no folds (i.e every code block is “open”) when we open a file (this is what foldlevelstart specifies). Shift + - (i.e Shift and the minus key) will collapse all blocks, and Shift + + will open all blocks. Use the minus key (i.e -) to toggle collapsing a single fold. You might also want to check out this answer for a quick overview of what’s supported.

Auto save

PyCharm saves the file as you type, sparing you from the hassle of having to press Ctrl+S across multiple tabs. We can get Vim to do this with vim-auto-save. Clone the repo to ~/.vim/pack/vimcharm/start/vim-autosave and add these to your .vimrc to enable auto-save:

let g:auto_save = 1                                                                                
let g:auto_save_events = ["InsertLeave", "TextChanged"] 

A word of caution before we proceed – auto-saving can get quite annoying if enabled globally. I use project-specific vimrcs and use auto-save along with git – so if I accidentally auto-save something that I shouldn’t have, a git diff is all I need to see what went wrong.

Refactor across files

Jedi-vim can do simple renaming, but I wanted to something more powerful. Enter ropevim. You need to pip install rope, ropemode and ropevim. I have a miniconda environment set up, but you can install the packages to your global scope if you want to. We just need one file from the ropevim repo:

wget -O ~/.vim/pack/vimcharm/start/python_ropevim.vim https://raw.githubusercontent.com/python-rope/ropevim/master/ftplugin/python_ropevim.vim

Now let’s source it in our .vimrc:

 source ~/.vim/pack/vimcharm/start/python_ropevim.vim

Now add these to your .vimrc:

nnoremap <C-z> :RopeUndo<CR>                                                                       
set <A-z>=^[z                                                                                      
map <A-z> :RopeRedo<CR>                                                                            
map <F6> :RopeRename<CR>

We have mapped F6 to the rename operation, and Ctrl+z and Alt + z to undo and redo respectively. Remember not to copy paste the mapping for Alt + z, and press Ctrl+v and then the desired keycombo to enter it in your .vimrc.

Restart Vim, open a python file, and try to rename a variable using F6. You will get prompts to create a new ropevim project – press ‘y’ to create one locally, and then proceed to apply the rename. If you get an import error for ropevim when you start Vim, it’s probably because Vim uses the system python (which is probably a different version than the python in your virtualenv) and you pip-installed rope, ropemode and ropevim to a virtualenv. An alternative would be to do conda install -c conda-forge vim on your anaconda/miniconda env so that the Vim in your env will use the local python (and hence your installed pip packages) instead of the system one.

Final thoughts

If anything this exercise has made be better appreciate the work that the Jetbrains devs have put into their IDEs – all I wanted was a working subset of PyCharm’s basic features and what I got was a rather modest approximation. Do let me know (open an issue on Github?) if you managed to get any closer to PyCharm than this.

4 thoughts on “VimCharm: Approximating PyCharm on vim”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s