‹ jan0sch.de

Using neovim for Scala development

2018-09-12

If you would like to use neovim for their Scala development workflow then this guide may get you started.

It is possible to simply edit the source files but some configuration and plugins will make your workflow way more productive. This includes using projects like ENSIME, SBT and several plugins for neovim.

Setting up SBT

As SBT is the current default for projects you need to configure it a bit. The ENSIME project provides the necessary pieces to work with text editors.

First modify your global plugins file which is in either ~/.sbt/0.13/plugins/plugins.sbt or ~/.sbt/1.0/plugins/plugins.sbt or both to include ensime-sbt:

addSbtPlugin("org.ensime" % "sbt-ensime" % "2.6.1")

Afterwards it should be configured in your global sbt configuration which is located in ~/.sbt/0.13/global.sbt or ~/.sbt/1.0/global.sbt:

// Custom settings for ENSIME
ensimeJavaFlags in ThisBuild := Seq(
  "-Xss2m",
  "-Xms2g",
  "-Xmx2g",
  "-XX:MaxMetaspaceSize=512m"
)
// You only need this if the scala version of your project does not match
// the scala version of ENSIME.
ensimeIgnoreScalaMismatch in ThisBuild := true
// Prevent Ctrl+C killing sbt.
cancelable in Global := true
// Enable coloured scala repl.
initialize ~= (_ => if (ConsoleLogger.formatEnabled) sys.props("scala.color") = "true")

Setting up neovim

At least the ensime-vim plugin will be needed. But to be more productive I suggest adding some more plugins:

  • ctrlp
  • deoplete
  • rainbow_parentheses
  • vim-scala
  • vim-easytags
  • tagbar

I maintain a complete vim configuration repository which contains configuration files for exctags and neovim. To get you started here is a more compact init.vim file for neovim:

set nocompatible
set bs=2
set t_Co=256

" Install Vim-Plug if missing
" ---------------------------
if empty(glob('~/.local/share/nvim/site/autoload/plug.vim'))
  silent !curl -fLSso ~/.local/share/nvim/site/autoload/plug.vim --create-dirs
    \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
  autocmd VimEnter * PlugInstall --sync | source $MYVIMRC
endif

" Plugins via Vim-Plug
" --------------------
call plug#begin('~/.config/nvim/bundle')

Plug 'mileszs/ack.vim'
Plug 'Chiel92/vim-autoformat'
Plug 'ctrlpvim/ctrlp.vim'
Plug 'Shougo/deoplete.nvim'
Plug 'ensime/ensime-vim'
Plug 'vim-scripts/FuzzyFinder'
Plug 'vim-scripts/L9'
Plug 'itchyny/lightline.vim'
Plug 'scrooloose/nerdcommenter'
Plug 'scrooloose/nerdtree'
Plug 'myusuf3/numbers.vim'
Plug 'junegunn/rainbow_parentheses.vim'
Plug 'scrooloose/syntastic'
Plug 'majutsushi/tagbar'
Plug 'tpope/vim-classpath'
Plug 'kchmck/vim-coffee-script'
Plug 'altercation/vim-colors-solarized'
Plug 'tpope/vim-dispatch'
Plug 'xolox/vim-easytags'
Plug 'tpope/vim-fugitive'
Plug 'airblade/vim-gitgutter'
Plug 'xolox/vim-misc'
Plug 'matze/vim-move'
Plug 'tpope/vim-projectionist'
Plug 'derekwyatt/vim-scala'

call plug#end()

" Some general settings
" ---------------------

let g:solarized_termcolors=256
colorscheme solarized
set background=light
set autoindent
set shiftwidth=2
set showmode
set showmatch
set ruler
set nojoinspaces
set cpo+=$
set whichwrap=""
set modelines=0
set nobackup
set encoding=utf-8
set wildmenu
set laststatus=2
set number
filetype plugin indent on
syntax enable
set hlsearch
set incsearch
se cursorline

" java support
" ------------
autocmd FileType java setlocal expandtab shiftwidth=4 tabstop=4 softtabstop=4

" File Browser
" ------------
" hide some files and remove stupid help
let g:explHideFiles='^\.,.*\.sw[po]$,.*\.pyc$'
let g:explDetailedHelp=0
map  :Explore!<CR>

" Auto-Format
" -----------
noremap <F3> :Autoformat<CR>
let g:formatdef_scalafmt = "'scalafmt --stdin'"
let g:formatters_scala = ['scalafmt']

" Tagbar
" -------
nmap <F8> :TagbarToggle<CR>
let g:tagbar_left = 1

" NerdTree
" --------
nmap <F9> :NERDTreeToggle<CR>

" Tagbar Scala Support
" --------------------
let g:tagbar_type_scala = {
    \ 'ctagstype' : 'Scala',
    \ 'kinds'     : [
        \ 'p:packages:1',
        \ 'V:values',
        \ 'v:variables',
        \ 'T:types',
        \ 't:traits',
        \ 'o:objects',
        \ 'a:aclasses',
        \ 'c:classes',
        \ 'r:cclasses',
        \ 'm:methods'
    \ ]
\ }

" Syntastic
" ---------
let g:syntastic_mode_map = { 'mode': 'passive', 'active_filetypes': ['ruby', 'php', 'python'], 'passive_filetypes': ['scala'] }

" The Silver Searcher (via ack.vim)
" ---------------------------------
if executable('ag')
  let g:ackprg = 'ag --nogroup --nocolor --column --vimgrep'
endif

" Save system files via :w!!
" --------------------------
cmap w!! %!sudo tee > /dev/null %

" Avoid easytags updating too often
" ---------------------------------
let g:easytags_updatetime_min=4000

" Deoplete (NeoComplete for nvim)
" -------------------------------
let g:deoplete#enable_at_startup = 1
autocmd InsertLeave,CompleteDone * if pumvisible() == 0 | pclose | endif

" Lightline configuration
" -----------------------
let g:lightline = {
      \ 'colorscheme': 'solarized',
      \ 'mode_map': { 'c': 'NORMAL' },
      \ 'active': {
      \   'left': [ [ 'mode', 'paste' ], [ 'fugitive', 'filename' ] ]
      \ },
      \ 'component_function': {
      \   'modified': 'LightlineModified',
      \   'readonly': 'LightlineReadonly',
      \   'fugitive': 'LightlineFugitive',
      \   'filename': 'LightlineFilename',
      \   'fileformat': 'LightlineFileformat',
      \   'filetype': 'LightlineFiletype',
      \   'fileencoding': 'LightlineFileencoding',
      \   'mode': 'LightlineMode',
      \ },
      \ 'separator': { 'left': '', 'right': '' },
      \ 'subseparator': { 'left': '', 'right': '' }
      \ }

function! LightlineModified()
  return &ft =~ 'help\|vimfiler\|gundo' ? '' : &modified ? '+' : &modifiable ? '' : '-'
endfunction

function! LightlineReadonly()
  return &ft !~? 'help\|vimfiler\|gundo' && &readonly ? '' : ''
endfunction

function! LightlineFilename()
  let fname = expand('%:t')
  return fname == 'ControlP' && has_key(g:lightline, 'ctrlp_item') ? g:lightline.ctrlp_item :
        \ fname == '__Tagbar__' ? g:lightline.fname :
        \ fname =~ '__Gundo\|NERD_tree' ? '' :
        \ &ft == 'vimfiler' ? vimfiler#get_status_string() :
        \ &ft == 'unite' ? unite#get_status_string() :
        \ &ft == 'vimshell' ? vimshell#get_status_string() :
        \ ('' != LightlineReadonly() ? LightlineReadonly() . ' ' : '') .
        \ ('' != fname ? fname : '[No Name]') .
        \ ('' != LightlineModified() ? ' ' . LightlineModified() : '')
endfunction

function! LightlineFugitive()
  if &ft !~? 'vimfiler\|gundo' && exists("*fugitive#head")
    let branch = fugitive#head()
    return branch !=# '' ? ' '.branch : ''
  endif
  return ''
endfunction

function! LightlineFileformat()
  return winwidth(0) > 70 ? &fileformat : ''
endfunction

function! LightlineFiletype()
  return winwidth(0) > 70 ? (&filetype !=# '' ? &filetype : 'no ft') : ''
endfunction

function! LightlineFileencoding()
  return winwidth(0) > 70 ? (&fenc !=# '' ? &fenc : &enc) : ''
endfunction

function! LightlineMode()
  let fname = expand('%:t')
  return fname == '__Tagbar__' ? 'Tagbar' :
        \ fname == 'ControlP' ? 'CtrlP' :
        \ fname == '__Gundo__' ? 'Gundo' :
        \ fname == '__Gundo_Preview__' ? 'Gundo Preview' :
        \ fname =~ 'NERD_tree' ? 'NERDTree' :
        \ &ft == 'unite' ? 'Unite' :
        \ &ft == 'vimfiler' ? 'VimFiler' :
        \ &ft == 'vimshell' ? 'VimShell' :
        \ winwidth(0) > 60 ? lightline#mode() : ''
endfunction

function! CtrlPMark()
  if expand('%:t') =~ 'ControlP' && has_key(g:lightline, 'ctrlp_item')
    call lightline#link('iR'[g:lightline.ctrlp_regex])
    return lightline#concatenate([g:lightline.ctrlp_prev, g:lightline.ctrlp_item
          \ , g:lightline.ctrlp_next], 0)
  else
    return ''
  endif
endfunction

let g:ctrlp_status_func = {
  \ 'main': 'CtrlPStatusFunc_1',
  \ 'prog': 'CtrlPStatusFunc_2',
  \ }

function! CtrlPStatusFunc_1(focus, byfname, regex, prev, item, next, marked)
  let g:lightline.ctrlp_regex = a:regex
  let g:lightline.ctrlp_prev = a:prev
  let g:lightline.ctrlp_item = a:item
  let g:lightline.ctrlp_next = a:next
  return lightline#statusline(0)
endfunction

function! CtrlPStatusFunc_2(str)
  return lightline#statusline(0)
endfunction

let g:tagbar_status_func = 'TagbarStatusFunc'

function! TagbarStatusFunc(current, sort, fname, ...) abort
    let g:lightline.fname = a:fname
  return lightline#statusline(0)
endfunction

augroup AutoSyntastic
  autocmd!
  autocmd BufWritePost *.c,*.cpp call s:syntastic()
augroup END
function! s:syntastic()
  SyntasticCheck
  call lightline#update()
endfunction

let g:unite_force_overwrite_statusline = 0
let g:vimfiler_force_overwrite_statusline = 0
let g:vimshell_force_overwrite_statusline = 0

" Scala
" -----

" Indenting scaladoc the right way (vim-scala).
let g:scala_scaladoc_indent = 1

" Map some keys for ENSIME
au FileType java,scala nnoremap <leader>t :EnType<CR>
au FileType java,scala xnoremap <leader>t :EnType selection<CR>
au FileType java,scala nnoremap <leader>T :EnTypeCheck<CR>
au FileType java,scala nnoremap <leader>df :EnDeclaration<CR>
au FileType java,scala nnoremap <leader>db :EnDocBrowse<CR>
au FileType java,scala nnoremap <leader>i :EnInspectType<CR>
au FileType java,scala nnoremap <leader>I :EnSuggestImport<CR>
au FileType java,scala nnoremap <leader>r :EnRename<CR>

" Ctrl-P
" ------
let g:ctrlp_map = '<c-p>'
let g:ctrlp_cmd = 'CtrlPMixed'
set wildignore+=*/target/*

When editing a project file you can press <F8> to get a tag browser on the left side. And use <Ctrl+P> to quickly find other files within your project tree. Using <Ctrl+X>,<Ctr+O> should trigger the autocompletion.

Setting up ctags

As neovim (and also vim) use tags files created by excuberant ctags by default all you need is a basic configuration for which should reside in ~/.ctags:

--exclude=_darcs
--exclude=.ensime_cache
--exclude=.git
--exclude=.pijul
--exclude=.svn
--exclude=log
--exclude=project/target
--exclude=public
--exclude=target
--exclude=tmp
--exclude=vendor
--recurse=yes
--tag-relative=yes

--langdef=Clojure
--langmap=Clojure:.clj
--regex-clojure=/\([ \t]*create-ns[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/n,namespace/
--regex-clojure=/\([ \t]*def[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/d,definition/
--regex-clojure=/\([ \t]*defn-?[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/f,function/
--regex-clojure=/\([ \t]*defmacro[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/m,macro/
--regex-clojure=/\([ \t]*definline[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/i,inline/
--regex-clojure=/\([ \t]*defmulti[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/a,multimethod definition/
--regex-clojure=/\([ \t]*defmethod[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/b,multimethod instance/
--regex-clojure=/\([ \t]*defonce[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/c,definition (once)/
--regex-clojure=/\([ \t]*defstruct[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/s,struct/
--regex-clojure=/\([ \t]*intern[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/v,intern/
--regex-clojure=/\([ \t]*ns[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/n,namespace/

--langdef=groovy
--langmap=groovy:.groovy
--regex-groovy=/^[ \t][(private|public|protected) ( \t)][A-Za-z0-9<>]+[ \t]+([A-Za-z0-9]+)[ \t](.)[ \t]{/\1/f,function,functions/
--regex-groovy=/^[ \t]*def[ \t]+([A-Za-z0-9_]+)[ \t]\=[ \t]{/\1/f,function,functions/
--regex-groovy=/^[ \t]*private def[ \t]+([A-Za-z0-9_]+)[ \t]/\1/v,private,private variables/
--regex-groovy=/^[ \t]def[ \t]+([A-Za-z0-9_]+)[ \t]/\1/u,public,public variables/
--regex-groovy=/^[ \t][abstract ( \t)][(private|public) ( \t)]class[ \t]+([A-Za-z0-9_]+)[ \t]/\1/c,class,classes/
--regex-groovy=/^[ \t][abstract ( \t)][(private|public) ( \t)]enum[ \t]+([A-Za-z0-9_]+)[ \t]/\1/c,class,classes/

--langdef=Scala
--langmap=Scala:.scala
--regex-Scala=/^[ \t]*class[ \t]*([a-zA-Z0-9_]+)/\1/c,classes/
--regex-Scala=/^[ \t]*object[ \t]*([a-zA-Z0-9_]+)/\1/o,objects/
--regex-Scala=/^[ \t]*trait[ \t]*([a-zA-Z0-9_]+)/\1/t,traits/
--regex-Scala=/^[ \t]*case[ \t]*class[ \t]*([a-zA-Z0-9_]+)/\1/r,cclasses/
--regex-Scala=/^[ \t]*abstract[ \t]*class[ \t]*([a-zA-Z0-9_]+)/\1/a,aclasses/
--regex-Scala=/^[ \t]*def[ \t]*([a-zA-Z0-9_=]+)[ \t]*.*[:=]/\1/m,methods/
--regex-Scala=/[ \t]*val[ \t]*([a-zA-Z0-9_]+)[ \t]*[:=]/\1/V,values/
--regex-Scala=/[ \t]*var[ \t]*([a-zA-Z0-9_]+)[ \t]*[:=]/\1/v,variables/
--regex-Scala=/^[ \t]*type[ \t]*([a-zA-Z0-9_]+)[ \t]*[\[<>=]/\1/T,types/
--regex-Scala=/^[ \t]*import[ \t]*([a-zA-Z0-9_{}., \t=>]+$)/\1/i,includes/
--regex-Scala=/^[ \t]*package[ \t]*([a-zA-Z0-9_.]+$)/\1/p,packages/

--languages=c,c++,clojure,html,java,lisp,make,ruby,scala,sh,sml,sql

You may want to configure it further but the mappings for scala should be enough to get you started.

To create a tags file in your project root just issue the exctags . command.

Wrapping up and workflow

Now you are ready to work on your scala projects using neovim. :-)

To get started you can use the following sequence of commands from within a project directory:

% exctags .
% sbt clean ensimeConfig test:compile ensimeServerIndex

The ENSIME indexing part might take a while so this is a good opportunity to get some tea or coffee. ;-)

When using shortcuts for the appropriate ENSIME commands you can navigate your project with :EnDeclaration, show documentation with :EnDocBrowse and see typing information via :EnType or typecheck the whole file with :EnTypeCheck.

Using the famous deoplete plugin autocompletion should work with <Ctrl+X>,<Ctrl+O>.

Remember to recompile your project frequently either by hand or by using something like ~compile on the sbt console.