Setup Neovim for Java development

A writeup of how I’ve setup Neovim for Java development

Why not IntelliJ

First of all I want to point out that IntelliJ with the IdeaVim plugin has a great Vim emulation. I’ve been using it for a long time now and when it comes to text editing alone there is nothing I’m really missing.

There are a couple of gripes I have with IntelliJ, but the main reason I looked into using Neovim for Java development is so that I can utilize my main desktop machine also when I’m remote with a less powerful device.

I prefer having a beefy desktop machine because you can get a lot more memory and CPU cores than on a notebook, and for a lower price. I’ve also a Surface Go as a kind of couch device and digital scratchpad, so I was wondering if I could utilize that as a thin client to my desktop to do real work. An option I tried is x2go which kinda works, but there is some more delay and every now and then it acts up and freezes. Connecting via SSH and running Neovim feels a lot smoother.

My main other gripe is that in IntelliJ not everything is a text buffer as it is in Vim. I find this annoying, for example when trying to make sense of the text output of a test run. In Vim I can easily navigate and search the same way as in a regular text buffer, in IntelliJ it is a kind of different window with different rules. The same thing applies for things like git blame integrations.

Features I consider essential

To be able to use Neovim as my main driver for Java development there are a few things that I consider essential. These are:

  • Code navigation features. Find usages of a method. Navigate to the definition of a variable or method. Quickly jump to a specific Symbol.

  • Code code completion based on semantics, not just syntax.

  • Quick actions. For example to automatically add an import to be able to use ArrayList.

  • Many of the 0815 Vim features.

  • Be able to launch all tests in a file, or the test method where my cursor is at.

  • Debugging (?)

Plugin Installation

The way I manage my vim plugin installations may be a bit unusual. I’m using ansible instead of something like vim-plug or Vundle. The reason is simple: Vim already includes native support to load plugins and to setup my development environment I need to install a lot of things outside vim anyway, so why not use a single system for both?

You can find all that stuff in my dotfiles repository. I’ll go through the important bits to install in more detail in a second.

eclipse.jdt.ls

The first important piece of the puzzle is a language server. For Java there is eclipse.jdt.ls. I install it directly from sources. The relevant ansible role is lang-java:

- name: Clone eclipse.jdt.ls
  git:
    repo: https://github.com/eclipse/eclipse.jdt.ls
    dest: ~/dev/eclipse/eclipse.jdt.ls
- name: Build eclipse.jdt.ls
  command: ./mvnw clean install -DskipTests
  args:
    chdir: ~/dev/eclipse/eclipse.jdt.ls
- name: Remove possible incompatible eclipse.jdt.ls state
  file:
    path: ~/.local/share/eclipse
    state: absent

This will clone the repo into ~/dev/eclipse/eclipse.jdt.ls and build the project by calling ./mvn clean install -DskipTests within the project directory.

I also have small script that acts as a wrapper to launch the language server. This is in ~/bin/java-lsp.sh. The environment variable $PATH on my system is extended to include ~/bin/ so that the language server can be launched invoking java-lsp.sh.

LanguageClient-neovim & ncm2

The next piece in the puzzle is to install a Neovim plugin that can utilize the language server. This will provide the code completion functionality and code navigation capabilities.

There is a large selection of language client plugins and recently a PR was merged to Neovim master that provides built-in support for language servers. But it is not fully fleshed out yet and not-quite-yet ready as daily driver. My choice is LanguageClient-neovim for the LSP client and ncm2 to have it automatically show completion suggestions in an asynchronous way.

The ansible role doing the vim plugin installation is vim

The relevant plugins are:

  • autozimu/LanguageClient-neovim
  • ncm2/ncm2
  • roxma/nvim-yarp

There are a couple more extensions to ncm2 so that it also provides completion suggestions from other sources as well:

  • ncm2/ncm2-bufword
  • ncm2/ncm2-path
  • ncm2/ncm2-tagprefix
  • ncm2/ncm2-ultisnips
  • ncm2/ncm2-markdown-subscope
  • ncm2/ncm2-rst-subscope

LanguageClient-neovim is written in Rust and needs a binary executable. This can be fetched by invoking the install.sh within the folder. The relevant piece from my ansible role is:

- name: Install LanguageClient-neovim
  command: bash install.sh
  args:
    chdir: ~/.config/nvim/pack/plugins/start/LanguageClient-neovim

And you may have to update the remote plugins for Neovim:

nvim +UpdateRemotePlugins +qa

The final step to setup these plugins is to configure them in the init.vim:

autocmd BufEnter * call ncm2#enable_for_buffer()

let g:LanguageClient_autoStart = 1
" Use the location list instead of the quickfix list to show linter warnings
let g:LanguageClient_diagnosticsList = "Location"
let g:LanguageClient_rootMarkers = {
    \ 'java': ['.git']
    \ }
let g:LanguageClient_serverCommands = {
    \ 'java': ['java-lsp.sh']
    \ }

I also have setup some mappings to wire up the functionality to some key combinations:

set hidden
nnoremap <buffer> <silent> F5 :call LanguageClient_contextMenu()<CR>
nnoremap <buffer> <silent> K :call LanguageClient_textDocument_hover()<CR>
nnoremap <buffer> <silent> gd :call LanguageClient_textDocument_definition()<CR>
nnoremap <buffer> <silent> gr :call LanguageClient_textDocument_references()<CR>
nnoremap <buffer> <silent> <leader>fs :call LanguageClient_textDocument_documentSymbol()<CR>
nnoremap <buffer> <silent> crr :call LanguageClient_textDocument_rename()<CR>
nnoremap <buffer> <silent> <a-CR> :call LanguageClient_textDocument_codeAction()<CR>

eclipse.jdt.ls works for Maven and (limited) Gradle projects. For Gradle it is required to run the ./gradlew eclipse task manually so that eclipse.jdt.ls knows about your projects layout and dependencies.

Once you’ve done that you should be able to open up a .java file of your project within Neovim and have code navigation capabilities plus code completion support.

vim-test

To be able to run all the tests within a file or the test nearest to the cursor vim-test can be installed.

In our projects we use gradle, so I’ve configured it to use the gradletest runner and a local ./gradlew file if available:

(This is in ftplugin/java.vim)

if filereadable("./gradlew")
    let test#java#runner = 'gradletest'
    let test#java#gradletest#executable = './gradlew test'
endif

And I’ve configured it to use the neovim strategy, which means it will spawn a :terminal to launch the tests.

let test#strategy = "neovim"

To trigger the tests I’ve defined some mappings:

nmap <silent> t<C-n> :TestNearest<CR>
nmap <silent> t<C-f> :TestFile<CR>
nmap <silent> t<C-s> :TestSuite --verbose<CR>
nmap <silent> t<C-l> :TestLast<CR>
nmap <silent> t<C-g> :TestVisit<CR>

fzf

Another plugin that I make heavy use of is [fzf]. It is more of a general purpose plugin, but I mention it here because it also integrates with LanguageClient-neovim.

With the mappings defined in the LanguageClient setup section I can hit alt-. to get a selection of code actions to execute:

code action

Or if you look for the usages of a method you’ll also get the options in the fzf window and you can jump to a given option by selecting it there.

Debugging

This is the feature that I’m missing in Neovim and that I haven’t yet really looked into.

There is a Debug Adapter Protocol and a Visual Studio Code specific component to make it work for Java (also backed by eclipse.jdt.ls). But so far only vimspector implements the protocol for Vim (and only Vim, not for Neovim).

What else is missing

There are a couple of other goodies from IntelliJ that I’m missing so far. Mostly code generation or snippet related. I think many of that can be re-created with one of the snippet managers for Vim, but I haven’t been using Neovim for Java seriously enough to do this.

For the time being IntelliJ still remains my main driver, but eclipse.jdt.ls and the language server integration have matured to a point where it is a real competitor.

Thursday, November 21, 2019