‹ jan0sch.de

Using neovim for Scala development - Take 2

2019-03-27

This is an updated version of my previous guide and reflects the latests changes in the scala ecosystem. So if you would like to use neovim for your Scala development work flow then this guide will get you started.

Because ENSIME is dead and gone we will use the metals language server for Scala.

Getting started

First you should install coursier as it will be needed further on. This is as simply as this:

% curl -L -o ~/.local/bin/coursier https://git.io/coursier

The above command will install the coursier application into the .local/bin directory of your home directory which should be available in the $PATH of your shell.

Now you need to bootstrap metals which is done like this:

% coursier bootstrap \
  --java-opt -XX:+UseG1GC \
  --java-opt -XX:+UseStringDeduplication  \
  --java-opt -Xss4m \
  --java-opt -Xms100m \
  --java-opt -Dmetals.client=vim-lsc \
  --java-opt -Dmetals.sbt-script=/usr/local/bin/sbt \
  org.scalameta:metals_2.12:0.4.4 \
  -r bintray:scalacenter/releases \
  -r sonatype:snapshots \
  -o ~/.local/bin/metals-vim -f

This will put the metals binary configured for vim-lsc into the .local/bin directory in your home. The parameter -Dmetals.sbt-script above points to a custom sbt installation on my machine. If you want to use the bundled launcher just omit that line.

Setting up neovim

The minimum you need on the neovim side are just two plugins: vim-lsc and vim-scala.

A minimal configuration using vim-plug looks like this:

Plug 'derekwyatt/vim-scala'
Plug 'natebosch/vim-lsc'

au BufRead,BufNewFile *.sbt set filetype=scala

let g:lsc_enable_autocomplete = v:false
let g:lsc_server_commands = {
  \  'scala': {
  \    'command': 'metals-vim',
  \    'log_level': 'Log'
  \  }
  \}

However for a more complete configuration, here is my current neovim configuration.

" ~./vimrc

" Kompatibilitätsmodus abschalten
set nocompatible
" Backspace (FreeBSD)
set bs=2

" 256 Farben
"set t_Co=256
" True Colour Farben
if (has("termguicolors"))
  set termguicolors
endif

" 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 'docunext/closetag.vim'
Plug 'ctrlpvim/ctrlp.vim'
Plug 'Shougo/deoplete.nvim'
Plug 'mattn/emmet-vim'
Plug 'itchyny/lightline.vim'
Plug 'scrooloose/nerdcommenter'
Plug 'myusuf3/numbers.vim'
Plug 'scrooloose/syntastic'
Plug 'majutsushi/tagbar'
Plug 'SirVer/ultisnips'
Plug 'xolox/vim-easytags'
Plug 'tpope/vim-fugitive'
Plug 'airblade/vim-gitgutter'
Plug 'godlygeek/tabular'
Plug 'xolox/vim-misc'
Plug 'matze/vim-move'
Plug 'vim-pandoc/vim-pandoc'
Plug 'vim-pandoc/vim-pandoc-after'
Plug 'vim-pandoc/vim-markdownfootnotes'
Plug 'vim-pandoc/vim-rmarkdown'
Plug 'vim-pandoc/vim-pandoc-syntax'
Plug 'derekwyatt/vim-scala'
Plug 'natebosch/vim-lsc'
Plug 'rust-lang/rust.vim'
Plug 'racer-rust/vim-racer'
" Colour schemes
Plug 'chriskempson/base16-vim'
Plug 'drewtempelmeyer/palenight.vim'
Plug 'altercation/vim-colors-solarized'
Plug 'whatyouhide/vim-gotham'

call plug#end()

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

let base16colorspace=256
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

" Reconfigure some keyboard shortcuts.
" ------------------------------------
let mapleader="´"
" Execute commands via u umlaut.
nnoremap ü :
" Re-map git diff shortcuts.
nnoremap ßc ]c
" Re-map spell checking shortcuts.
nnoremap ßs ]s
nnoremap c0 z=

" 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>
" Ignoring stderr is a workaround, see https://github.com/scalameta/scalafmt/issues/1236
let g:formatdef_scalafmt = "'scalafmt --stdin 2>/dev/null'"
let g:formatters_scala = ['scalafmt']

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

" 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'
    \ ]
\ }

" Better Search
" -------------
set hlsearch
set incsearch

" Highlight the current line
" --------------------------

se cursorline

" 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

" UltiSnips
" ---------
let g:UltiSnipsExpandTrigger="<C-j>"

" Pandoc
" ------
let g:pandoc#spell#default_langs = [ "de_19", "en_gb" ]

" Rust
" ----

" Settings for autocomplete via rust-racer
set hidden
let g:racer_experimental_completer = 1
"au FileType rust nmap gd <Plug>(rust-def)
"au FileType rust nmap gs <Plug>(rust-def-split)
"au FileType rust nmap gx <Plug>(rust-def-vertical)
"au FileType rust nmap <leader>gd <Plug>(rust-doc)

" Scala
" -----

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

" Map SBT configuration files to scala
au BufRead,BufNewFile *.sbt set filetype=scala

" Configuration for vim-lsc
let g:lsc_enable_autocomplete = v:false
let g:lsc_server_commands = {
  \  'scala': {
  \    'command': 'metals-vim',
  \    'log_level': 'Log'
  \  }
  \}

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

" 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 fullname = expand('%')
    let gitversion = ''
    if fullname =~? 'fugitive://.*/\.git//0/.*'
      let gitversion = ' (INDEX)'
    elseif fullname =~? 'fugitive://.*/\.git//2/.*'
      let gitversion = ' (TARGET)'
    elseif fullname =~? 'fugitive://.*/\.git//3/.*'
      let gitversion = ' (MERGE)'
    elseif &diff == 1
      let gitversion = ' (WORK COPY)'
    endif
    let branch = fugitive#head()
    return branch !=# '' ? ' '.branch.gitversion : ''
  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

Please note the remapping of leader to umlaut keys! You may want to change that.

Now enjoy your new Scala experience on the terminal. :-)