Setting up VIM as an IDE for Python – 2020 edition

13 minute read

Published:

In 2018, I published a blogpost about my VIM setup. Since I’m continously tweaking my setup, my previous post is quite outdated from my current setup. This means it is time for an update.

Note: I know the general wisdom is that people should create their own vimrc setup file, since you will understand it better and tweak it to your liking. However, I always like to browse others config files for inspiration. Also, for novice users of VIM it is quite a steep curve to start your own setup. But please be aware that, like with every tool, you will get the most out of VIM if you configure it to fit how you work.

setup

Main features of this setup:

  • fuzzy autocomplete (meaning it matches subpatterns)
  • signature hints
  • refactoring: extract method, variables, sort imports
  • floating documentation window
  • fuzzy searching whole project, symbols, buffers, lines
  • fast linter hints, warnings and errors displayed as fake comments

Summary of plugins:

Python/programming specific plugins:

Fuzzy searching:

Colorscheme:

Small, but useful plugins:

Useful, more advanced, plugins I use:

On the bottom of this post you will find my full vimrc file and instructions how to install all the coc.nvim extensions.

coc.nvim

I mainly switched to coc.nvim for the asynchronous completion (via jedi), with support for signature hints. I stayed for the configuration capabilities (see coc-python documentation and coc.nvim documentation). For example, I enabled a nice new feature of neovim called virtual text for my linter errors. This is why in the above screenshot my linter error looks like a comment.

As a linter I use coc-pyright, mainly because it is fast and has reasonable defaults.

coc.nvim has its own configuration file aside from vimrc, this is mine. It removes all the floating windows of coc.nvim and makes them echo in the command line of VIM. You can edit your config by typing :CocConfig in VIM.

{
  "diagnostic.virtualText": true,
  "diagnostic.virtualTextPrefix": " # ",
  "diagnostic.refreshOnInsertMode": false,
  "diagnostic.refreshAfterSave": false,
  "diagnostic.checkCurrentLine": true,
  "diagnostic.messageTarget": "echo",
  "diagnostic.enableSign": false,

  "signature.target": "echo",

  "suggest.floatEnable": false,
  "suggest.timeout": 5000,
  "suggest.maxCompleteItemCount": 3,
  "suggest.localityBonus": true,

  "python.pythonPath": "<Change this to your python3 path>",

  "python.jediEnabled": true,
  "python.jediShortcut": "LS",

  "pyright.disableLanguageServices": true,
}

FZF

FZF is a command-line fuzzy finder which I had to try before I really understood the purpose. For example, it lets you recursively search files in a directory matching sub-patterns of your query (the ‘fuzzy’ part). There is a VIM-plugin to use this tool to fuzzy search all VIM related lists: buffers, marks, lines, tags etc. I highly encourage you to try it, see fzf.vim.

fzf

Example of fuzzy searching files in the working directory

My full .vimrc with comments

syntax on
call plug#begin('~/.vim/plugged')

" awesome languageserver support
Plug 'neoclide/coc.nvim', {'do': 'yarn  install --frozen-lockfile'}  
" fuzzy find files/buffers etc.
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }  
Plug 'junegunn/fzf.vim'
" nice white colortheme
Plug 'jonathanfilip/vim-lucius'  
" more text-objects
Plug 'wellle/targets.vim'  
" git plugin
Plug 'tpope/vim-fugitive'  
" comment-out by gc
Plug 'tpope/vim-commentary'  
" auto shiftwidth
Plug 'tpope/vim-sleuth'  
" handy mapping [l ]l etc.
Plug 'tpope/vim-unimpaired'  
" easier to follow scroll movements
Plug 'cskeeters/vim-smooth-scroll'  
"show registers while pasting
Plug 'junegunn/vim-peekaboo'  
" show tags in a bar (functions etc) for easy browsing
Plug 'majutsushi/tagbar'  
" automatically create tags
Plug 'ludovicchabant/vim-gutentags'  
" color and sort imports
Plug 'tweekmonster/impsort.vim'  
" split function arguments etc.
Plug 'AndrewRadev/splitjoin.vim'  
" better indenting for python
Plug 'Vimjas/vim-python-pep8-indent'  
" auto remove search highlight
Plug 'romainl/vim-cool'  
" sort of scrollbar indicator
Plug 'drzel/vim-line-no-indicator'
" easy add parantheses etc around text objects
Plug 'tpope/vim-surround'
" repeat certain plugin mappings
Plug 'tpope/vim-repeat'
call plug#end()

" Change these to your python executables!
let g:python3_host_prog = '/usr/bin/python3'
let g:python_host_prog = '/usr/bin/python2'

set mouse=a

set relativenumber
set number

set ignorecase
" begin search from top of file when nothing is found anymore
set wrapscan  

set expandtab
set tabstop=4
" remove chars from seperators
set fillchars+=vert:\  
" remember more commands and search histor
set history=1000  
set nobackup
set noswapfile
set nofoldenable
set breakindent

set showbreak=..
set wrap
set textwidth=0
set wrapmargin=0
set scrolloff=4
set lbr

set tabstop=4
set softtabstop=4
set shiftwidth=4
set expandtab
set autoindent

set shortmess+=c

set undodir=~/.vim/undodir
set undofile
" maximum number of changes that can be undone
set undolevels=1000000
set undoreload=1000000

set noshowmode
set noshowcmd
set laststatus=2

set splitright
set splitbelow
set wildmenu

set hlsearch
set incsearch

set lazyredraw
set noerrorbells
set visualbell
set t_vb=

" Use system clipboard
set clipboard=unnamedplus

" Colorscheme options
let g:lucius_style="light"
let g:lucius_contrast="low"
colo lucius
set background=light

" Use <C-t> to open tagbar
map <C-t> :set nosplitright<CR>:TagbarToggle<CR>:set splitright<CR>

set shortmess+=c

" Use <space> as leader key
let mapleader = " "
let g:mapleader = " "
let maplocalleader = "`"
let g:maplocalleader = "`"
nnoremap <SPACE> <Nop>

nmap <leader>w :w<cr>
nmap <leader>q :q<cr>

" easy split movement
nnoremap <C-h> <C-w>h
nnoremap <C-j> <C-w>j
nnoremap <C-k> <C-w>k
nnoremap <C-l> <C-w>l

" Remove all trailing whitespace by pressing C-S
nnoremap <C-S> :let _s=@/<Bar>:%s/\s\+$//e<Bar>:let @/=_s<Bar><CR>

" Fix enter key behavior quickfix window
autocmd BufReadPost quickfix nnoremap <buffer> <CR> <CR>

" Neovim terminal emulator options
au TermOpen * setlocal nonumber norelativenumber

" Make sure Vim returns to the same line when you reopen a file.
augroup line_return
  au!
  au BufReadPost *
    \ if line("'\"") > 0 && line("'\"") <= line("$") |
    \     execute 'normal! g`"zvzz' |
    \ endif
augroup END

vnoremap u <nop>
nnoremap Y y$

" Easier to type, and I never use the default behavior.
noremap H ^
noremap L $
vnoremap L g_

" Quit locationlist if that is the last buffer
autocmd QuitPre * if empty(&bt) | lclose | endif

" Easy breakpoint python
au FileType python map <silent> gb ofrom pdb import set_trace; set_trace()<esc>

" I never use folding and find typing [] awkward, so map z to ] and zz to [:
nmap zz [
nmap z ]
nnoremap ]. z.
omap zz [
omap z ]
xmap zz [
xmap z ]

" Speed
set regexpengine=1

" Show total number of matches while searching
let g:CoolTotalMatches = 1

" Python syntax enable everything
let g:python_highlight_all = 1

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" FZF.vim
nmap <leader>l :BLines<cr>
nmap <leader>;l :BLines<cr><c-P>
nmap <leader>b :Buffers<cr>
nmap <leader>f :Files<cr>

command! -bang -nargs=* Ag
  \ call fzf#vim#ag(<q-args>,
  \                 <bang>0 ? fzf#vim#with_preview('up:60%')
  \                         : fzf#vim#with_preview('right:50%:hidden', '?'),
  \                 <bang>0)

nmap <leader>S :Tags<cr>
nmap <leader>s :BTags<cr>
nmap <leader>p :History<cr>
nmap <leader>/ :Ag!<cr>
nmap <leader>;/ :Ag!<cr><c-P>
nmap <space>r :Rg<CR>

command! -bang -nargs=* Rg
  \ call fzf#vim#grep(
  \   'rg --column --line-number --no-heading --color=always --smart-case '.shellescape(<q-args>), 1,
  \   <bang>0 ? fzf#vim#with_preview('up:60%')
  \           : fzf#vim#with_preview('right:50%:hidden', '?'),
  \   <bang>0)

" In Neovim, you can set up fzf window using a Vim command
let g:fzf_layout = { 'window': 'call FloatingFZF()' }
let $FZF_DEFAULT_OPTS = '--layout=reverse  --margin=1,3'

function! FloatingFZF()
  let buf = nvim_create_buf(v:false, v:true)
  call setbufvar(buf, '&signcolumn', 'no')
  let height = &lines
  let width = float2nr(&columns)
  let col = float2nr((&columns - width * 0.8) / 2)
  let horizontal = float2nr((&columns - width) / 2)

  let opts = {
          \ 'relative': 'editor',
          \ 'row': (height - 8) / 2 - 3,
          \ 'col': (width - 75) / 2,
          \ 'width': 75,
          \ 'height': 8
          \ }
  let win= nvim_open_win(buf, v:true, opts)
  call setwinvar(win, '&winhl', 'Normal:FZFBackground')
endfunction

" Customize fzf colors to match your color scheme
hi FZFHighlight ctermbg=255
hi FZFBackground ctermbg=254
let g:fzf_colors =
  \ { 'fg':      ['fg', 'Normal'],
  \ 'bg':      ['bg', 'FZFBackground'],
  \ 'hl':      ['fg', 'Comment'],
  \ 'fg+':     ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
  \ 'bg+':     ['bg', 'FZFHighlight', 'FZFHighlight'],
  \ 'hl+':     ['fg', 'Statement'],
  \ 'info':    ['fg', 'PreProc'],
  \ 'border':  ['fg', 'Ignore'],
  \ 'prompt':  ['fg', 'Conditional'],
  \ 'pointer': ['fg', 'Exception'],
  \ 'marker':  ['fg', 'Keyword'],
  \ 'spinner': ['fg', 'Label'],
  \ 'header':  ['fg', 'Comment'] }

" Enable per-command history.
" CTRL-N and CTRL-P will be automatically bound to next-history and
" previous-history instead of down and up. If you don't like the change,
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
let g:fzf_history_dir = '~/.local/share/fzf-history'

autocmd! FileType fzf tnoremap <buffer> <esc> <c-c>

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Location list

command!  LToggle call s:LListToggle()
command!  LFillForce call s:FillLList()

function! s:LListToggle() abort
   let buffer_count_before = s:BufferCount()

   " Location list can't be closed if there's cursor in it, so we need
   " to call lclose twice to move cursor to the main pane
   silent! lclose
   silent! lclose

   if s:BufferCount() == buffer_count_before
      execute "silent! lopen 2"
      execute "silent! set nonumber norelativenumber"
   endif
endfunction

function! s:LListForceShow() abort
   execute "call setloclist(0, [], 'r')"
endfunction

function! s:BufferCount() abort
   return len(filter(range(1, bufnr('$')), 'bufwinnr(v:val) != -1'))
endfunction

nnoremap <silent> <space>a  :LToggle<cr>
autocmd BufReadPost *.py :LFillForce

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" move with wrap

" Mapping to make movements operate on 1 screen line in wrap mode
function! ScreenMovement(movement)
  if &wrap
    return "g" . a:movement
  else
    return a:movement
  endif
endfunction

onoremap <silent> <expr> j ScreenMovement("j")
onoremap <silent> <expr> k ScreenMovement("k")
onoremap <silent> <expr> 0 ScreenMovement("0")
onoremap <silent> <expr> ^ ScreenMovement("^")
onoremap <silent> <expr> $ ScreenMovement("$")
nnoremap <silent> <expr> j ScreenMovement("j")
nnoremap <silent> <expr> k ScreenMovement("k")
nnoremap <silent> <expr> 0 ScreenMovement("0")
nnoremap <silent> <expr> ^ ScreenMovement("^")
nnoremap <silent> <expr> $ ScreenMovement("$")

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" coc.nvim settings
" if hidden is not set, TextEdit might fail.
set hidden

" Some servers have issues with backup files, see #649
set nowritebackup

" You will have bad experience for diagnostic messages when it's default 4000.
" set updatetime=1000

" don't give |ins-completion-menu| messages.
set shortmess+=c

" Use tab for trigger completion with characters ahead and navigate.
" Use command ':verbose imap <tab>' to make sure tab is not mapped by other plugin.
inoremap <silent><expr> <TAB>
      \ pumvisible() ? "\<C-n>" :
      \ <SID>check_back_space() ? "\<TAB>" :
      \ coc#refresh()
inoremap <expr><S-TAB> pumvisible() ? "\<C-p>" : "\<C-h>"

" Use <c-space> for trigger completion.
inoremap <silent><expr> <c-space> coc#refresh()

" Or use `complete_info` if your vim support it, like:
inoremap <expr> <cr> complete_info()["selected"] != "-1" ? "\<C-y>" : "\<C-g>u\<CR>"

" Use `[c` and `]c` for navigate diagnostics
nmap <silent> [l <Plug>(coc-diagnostic-prev)
nmap <silent> ]l <Plug>(coc-diagnostic-next)

" Remap keys for gotos
nmap <silent> gd <Plug>(coc-definition)
nmap <silent> gy <Plug>(coc-type-definition)
nmap <silent> gi <Plug>(coc-implementation)
nmap <silent> gr <Plug>(coc-references)

" Use K for show documentation in preview window
nnoremap <silent> K :call <SID>show_documentation()<CR>

function! s:show_documentation()
  if &filetype == 'vim'
    execute 'h '.expand('<cword>')
  else
    call CocAction('doHover')
  endif
endfunction

set updatetime=100

" Remap for rename current word
nmap <leader>rn <Plug>(coc-rename)

augroup mygroup
  autocmd!
  autocmd FileType typescript,json setl formatexpr=CocAction('formatSelected')
  " Update signature help on jump placeholder
  autocmd User CocJumpPlaceholder silent! call CocActionAsync('showSignatureHelp')
  autocmd CursorHold, CursorHoldI * call CocActionAsync('showSignatureHelp')
augroup end

" Create mappings for function text object, requires document symbols feature of languageserver.
xmap if <Plug>(coc-funcobj-i)
xmap af <Plug>(coc-funcobj-a)
omap if <Plug>(coc-funcobj-i)
omap af <Plug>(coc-funcobj-a)

command! -nargs=0 OR   :call     CocAction('runCommand', 'editor.action.organizeImport')
xmap <leader>a  <Plug>(coc-codeaction-selected)

nnoremap <silent> <space>c  :<C-u>CocList commands<cr>
nnoremap <silent> <space>o  :<C-u>CocList outline<cr>
nnoremap <silent> <space>S  :<C-u>CocList -I symbols<cr>
nnoremap <silent> <space>j  :<C-u>CocNext<CR>
nnoremap <silent> <space>k  :<C-u>CocPrev<CR>

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Color tweaks
" Change colorscheme Coc errors/warning/hints
hi Pmenu ctermbg=253 ctermfg=240
hi Search cterm=bold ctermbg=None ctermfg=0
hi IncSearch ctermfg=0 ctermbg=254 cterm=bold
hi VertSplit ctermbg=253

hi default CocSubtle term=None
hi default link CocErrorHighlight CocSubtle
hi default link CocWarningHighlight CocSubtle
hi default link CocInfoHighlight CocSubtle
hi default link CocHintHighlight CocSubtle

hi CocWarningHighlight ctermbg=254
hi CocInfoHighlight ctermbg=254
hi CocErrorHighlight ctermbg=254
hi CocHintHighlight ctermbg=254

hi Function cterm=None ctermfg=34
hi Statement cterm=None ctermfg=32
hi Type cterm=None ctermfg=30

" Impsort option
hi pythonImportedObject ctermfg=126 cterm=None
hi pythonImportedFuncDef ctermfg=126 cterm=None
hi pythonImportedClassDef ctermfg=126 cterm=None

hi LineNR ctermbg=254 
hi CursorLineNR ctermbg=254 
hi Function ctermfg=35
hi Number ctermfg=173
hi String ctermfg=173
hi Constant ctermfg=173
hi Keyword ctermfg=31

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Statusline
function! CocErrors() abort
  let info = get(b:, 'coc_diagnostic_info', {})
  if empty(info) | return '' | endif
  let msgs = []
  if get(info, 'error', 0)
    call add(msgs, ' ' . info['error'] . ' ')
  endif
  return join(msgs, ' ')
endfunction

function! CocWarnings() abort
  let info = get(b:, 'coc_diagnostic_info', {})
  if empty(info) | return '' | endif
  let msgs = []
  if get(info, 'warning', 0)
    call add(msgs, ' ' . info['warning'] . ' ')
  endif
  return join(msgs, ' ')
endfunction

function! CocStatus() abort
  return get(g:, 'coc_status', '')
endfunction

set statusline=
set statusline+=%#ColorColumn#
set statusline+=\ %f 
set statusline+=%m\ 
set statusline+=%#DiffDelete#
set statusline+=%{CocErrors()}
set statusline+=%#DiffChange#
set statusline+=%{CocWarnings()}
set statusline+=%#ColorColumn#
set statusline+=%=
set statusline+=\ 
set statusline+=%#ScrollBarHi#
set statusline+=%{LineNoIndicator()}

Final note, when using coc.nvim do not forget to configure using :CocConfig and installing the coc extension: :CocInstall coc-python coc-pyright.