How to set up Neovim 0.5 + Modern plugins (LSP, Treesitter, Fuzzy finder, etc)

IDE-like features based on LSP and Treesitter, fuzzy finder, and status line

How to set up Neovim 0.5 + Modern plugins (LSP, Treesitter, Fuzzy finder, etc)

Hi, it’s Takuya. I use Neovim to develop my app called Inkdrop. Recently, I’ve got some updates for my Neovim setup since I’ve published last year. Neovim 0.5, which is nightly at the moment, comes with cool new improvements like Lua remote plugin host, built-in LSP client (yes!), and Treesitter syntax engine. I found there are already a bunch of great plugins that leverage those new nightly features. I tried them, and already love them! Besides, it works pretty well on my M1 MacBook Air, which is awesome. I’d like to introduce my latest setup with Neovim 0.5 and modern plugins. Here is a quick summary of my set up:

  • vim-plug — A minimalist Vim plugin manager
  • nvim-lspconfig — A collection of configurations for Neovim’s built-in LSP
  • nvim-treesitter — Treesitter configurations and abstraction layer for Neovim
  • completion-nvim — An auto completion framework based on Neovim’s built-in LSP
  • glepnir/lspsaga.nvim — A light-weight LSP plugin based on Neovim built-in LSP with highly a performant UI
  • telescope.nvim — A highly extendable fuzzy finder over lists
  • lualine.nvim — A blazing fast and easy to configure neovim statusline plugin written in pure lua

And here is my dotfiles repository:

craftzdog/dotfiles-public
My personal dotfiles. Contribute to craftzdog/dotfiles-public development by creating an account on GitHub.

Prerequisites — iTerm2 and Patched Nerd Font

iTerm2 is a fast terminal emulator for macOS. Install one of Nerd Fonts for displaying fancy glyphs on your terminal. My current choice is Hack. And use it on your terminal app. For example, on iTerm2:

My color theme is NeoSolarized, which is a true color Solarized theme. The Solarized Dark theme for iTerm2 is here.

Install Nightly Neovim (0.5)

The current stable release is 0.4.x. You have to install it from HEAD. With homebrew on macOS:

brew install --HEAD tree-sitter luajit neovim

If you already have them installed, upgarde them by doing so:

brew upgrade --fetch-HEAD tree-sitter luajit neovim

Directory structure

Neovim conforms XDG Base Directory structure. Here is my config file structure:

Neovim loads $HOME/.config/nvim/init.vim or init.lua first instead of $HOME/.vimrc. Then, my init.vim loads platform-depended config, keymaps, and plugins.

Install plugin manager: vim-plug

Install vim-plug into $HOME/.local/share/nvim/site/autoload/plug.vim:

sh -c 'curl -fLo $HOME/.local/share/nvim/site/autoload/plug.vim --create-dirs \ 
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

Then, you can add vim plugins in ~/.local/share/nvim/plugged by doing so:

call plug#begin(stdpath('data') . '/plugged')
...
call plug#end()

I manage plugins in $HOME/.config/nvim/plug.vim. Then, relaunch Neovim and run :PlugInstall to install plugins.

Set up the built-in LSP client

As I mentioned above, Neovim has the built-in language server protocol client. LSP facilitates useful features like:

  • go-to-definition
  • find-references
  • hover
  • completion
  • rename
  • format
  • refactor

It lets Neovim behave more like IDE. To enable it, install nvim-lspconfig, which is a collection of common configurations for language servers of each language:

Plug 'neovim/nvim-lspconfig'

Note that this plugin is just configurations. You need to set up the language servers for each language you use. Follow CONFIG.md for details.

TypeScript

For example, to set up Typescript language server, install required packages via npm:

npm install -g typescript typescript-language-server

Then, setup with lspconfig in your vim config in lua:

local nvim_lsp = require('lspconfig') 
nvim_lsp.tsserver.setup {}
TypeScript diagnostics

Go-to-definition

You can set keymaps by doing so:

local nvim_lsp = require('lspconfig')
local on_attach = function(client, bufnr) 
  local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
-- Mappings. 
  local opts = { noremap=true, silent=true }
buf_set_keymap('n', 'gD', '<Cmd>lua vim.lsp.buf.declaration()<CR>', opts) 
  buf_set_keymap('n', 'gd', '<Cmd>lua vim.lsp.buf.definition()<CR>', opts) 
  --... 
end
-- TypeScript 
nvim_lsp.tsserver.setup { 
  on_attach = on_attach 
}

Type gd in normal mode, then it jumps to the definition. Check out other keybindings here.

Improve LSP UI with lspsaga.nvim

Neovim’s LSP interface is extensible. lspsaga.nvim provides nice UIs for LSP functions. Install it with vim-plug:

Plug 'glepnir/lspsaga.nvim'

Enable it:

local saga = require 'lspsaga'
```lua 
saga.init_lsp_saga { 
  error_sign = '', 
  warn_sign = '', 
  hint_sign = '', 
  infor_sign = '', 
  border_style = "round", 
}

Hover Doc

" show hover doc 
nnoremap <silent>K :Lspsaga hover_doc<CR>

Signature help

inoremap <silent> <C-k> <Cmd>Lspsaga signature_help<CR>

Async LSP Finder

Find the cursor word definition and reference:

nnoremap <silent> gh <Cmd>Lspsaga lsp_finder<CR>

Diagnostics

Install diagnostic-languageserver:

npm install -g diagnostic-languageserver

And configure Neovim LSP like so:

nvim_lsp.diagnosticls.setup { 
  -- your configuration 
}

Use ESLint and Prettier

Since I often write JavaScript, CSS, LESS, SCSS, and Markdown for my app, I configured diagnosticls to use ESLint and Prettier. To reduce latency when invoking ESLint, I’m gonna use eslint_d, which runs eslint as a daemon process.

Install:npm i -g eslint_d prettier

Config:

nvim_lsp.diagnosticls.setup { 
  on_attach = on_attach, 
  filetypes = { 'javascript', 'javascriptreact', 'json', 'typescript', 'typescriptreact', 'css', 'less', 'scss', 'markdown', 'pandoc' }, 
  init_options = { 
    linters = { 
      eslint = { 
        command = 'eslint_d', 
        rootPatterns = { '.git' }, 
        debounce = 100, 
        args = { '--stdin', '--stdin-filename', '%filepath', '--format', 'json' }, 
        sourceName = 'eslint_d', 
        parseJson = { 
          errorsRoot = '[0].messages', 
          line = 'line', 
          column = 'column', 
          endLine = 'endLine', 
          endColumn = 'endColumn', 
          message = '[eslint] ${message} [${ruleId}]', 
          security = 'severity' 
        }, 
        securities = { 
          [2] = 'error', 
          [1] = 'warning' 
        } 
      }, 
    }, 
    filetypes = { 
      javascript = 'eslint', 
      javascriptreact = 'eslint', 
      typescript = 'eslint', 
      typescriptreact = 'eslint', 
    }, 
    formatters = { 
      eslint_d = { 
        command = 'eslint_d', 
        args = { '--stdin', '--stdin-filename', '%filename', '--fix-to-stdout' }, 
        rootPatterns = { '.git' }, 
      }, 
      prettier = { 
        command = 'prettier', 
        args = { '--stdin-filepath', '%filename' } 
      } 
    }, 
    formatFiletypes = { 
      css = 'prettier', 
      javascript = 'eslint_d', 
      javascriptreact = 'eslint_d', 
      json = 'prettier', 
      scss = 'prettier', 
      less = 'prettier', 
      typescript = 'eslint_d', 
      typescriptreact = 'eslint_d', 
      json = 'prettier', 
      markdown = 'prettier', 
    } 
  } 
}

Set keymaps for jump diagnostic and show diagnostics:

nnoremap <silent> <C-j> :Lspsaga diagnostic_jump_next<CR>

See more keymaps here.

Custom diagnostic signs

For lspconfig:

-- icon 
vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( 
  vim.lsp.diagnostic.on_publish_diagnostics, { 
    underline = true, 
    -- This sets the spacing and the prefix, obviously. 
    virtual_text = { 
      spacing = 4, 
      prefix = '' 
    } 
  } 
)

Let it format on save

Set an autocommand for BufWritePre to run lua vim.lsp.buf.formatting_seq_sync() on save:

-- Add the below lines to `on_attach` 
local on_attach = function(client, bufnr) 
  ... 
   
  if client.resolved_capabilities.document_formatting then 
    vim.api.nvim_command [[augroup Format]] 
    vim.api.nvim_command [[autocmd! * <buffer>]] 
    vim.api.nvim_command [[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]] 
    vim.api.nvim_command [[augroup END]] 
  end 
end

Set up tree-sitter for syntax highlightings

Tree-sitter is an incremental language parsing library. Neovim uses it to build syntax tree, in order to understand source code better. As a result, now Neovim provides better highlight (See gallery. To enable it, install nvim-treesitter:

Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'}

Then, relaunch Neovim. You have to install parsers for each language. For exmample, you can install JavaScript parser with:

:TSInstall javascript

You can also get a list of all available languages and their installation status with :TSInstallInfo. By adding languages to ensure_installed, the plugin automatically installs them on initialization:

require'nvim-treesitter.configs'.setup { 
  highlight = { 
    enable = true, 
    disable = {}, 
  }, 
  indent = { 
    enable = false, 
    disable = {}, 
  }, 
  ensure_installed = { 
    "tsx", 
    "toml", 
    "fish", 
    "php", 
    "json", 
    "yaml", 
    "swift", 
    "html", 
    "scss" 
  }, 
}
local parser_config = require "nvim-treesitter.parsers".get_parser_configs() 
parser_config.tsx.used_by = { "javascript", "typescript.tsx" }

Set up auto-completion

Install completion-nvim:

Plug 'nvim-lua/completion-nvim'

You have to enable it in your on_attach handler:

local on_attach = function(client, bufnr) 
  ... 
  require'completion'.on_attach(client, bufnr) 
  ... 
end

Here is my preference:

set completeopt=menuone,noinsert,noselect
" Use <Tab> and <S-Tab> to navigate through popup menu 
inoremap <expr> <Tab>   pumvisible() ? "\<C-n>" : "\<Tab>" 
inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<S-Tab>"

If you are using an auto closing plugin like me using lexima.vim, you have to set the below keymap to avoid conflicting the behaviors:

let g:completion_confirm_key = "" 
imap <expr> <cr>  pumvisible() ? complete_info()["selected"] != "-1" ? 
                 \ "\<Plug>(completion_confirm_completion)"  : "\<c-e>\<CR>" :  "\<CR>"

Custom completion icons

Then, in your nvim-lspconfig config, add the following lines:

local on_attach = function(client, bufnr) 
  ... 
  protocol.CompletionItemKind = { 
    '', -- Text 
    '', -- Method 
    '', -- Function 
    '', -- Constructor 
    '', -- Field 
    '', -- Variable 
    '', -- Class 
    'ﰮ', -- Interface 
    '', -- Module 
    '', -- Property 
    '', -- Unit 
    '', -- Value 
    '', -- Enum 
    '', -- Keyword 
    '﬌', -- Snippet 
    '', -- Color 
    '', -- File 
    '', -- Reference 
    '', -- Folder 
    '', -- EnumMember 
    '', -- Constant 
    '', -- Struct 
    '', -- Event 
    'ﬦ', -- Operator 
    '', -- TypeParameter 
  } 
end

Set up fuzzy finder using telescope.nvim

telescope.nvim provides an interactive fuzzy finder over lists, built on top of the latest Neovim features.

It’s so useful because you can search files while viewing the content of the files without actually opening them. It supports various sources like Vim, files, Git, LSP, and Treesitter. Check out the showcase of Telescope.

To install with vim-plug:

Plug 'nvim-lua/popup.nvim' 
Plug 'nvim-lua/plenary.nvim' 
Plug 'nvim-telescope/telescope.nvim'

My keybindings are:

nnoremap <silent> ;f <cmd>Telescope find_files<cr> 
nnoremap <silent> ;r <cmd>Telescope live_grep<cr> 
nnoremap <silent> \\ <cmd>Telescope buffers<cr> 
nnoremap <silent> ;; <cmd>Telescope help_tags<cr>

And I also map q key to close the window:

local actions = require('telescope.actions')
require('telescope').setup{ 
  defaults = { 
    mappings = { 
      n = { 
        ["q"] = actions.close 
      }, 
    }, 
  } 
}

File icons

Install nvim-web-devicons, which is a dictionary of file icons:

Plug 'kyazdani42/nvim-web-devicons'

Set up status line with lualine.nvim

Install:

Plug 'hoob3rt/lualine.nvim'

My status line set up is:

local status, lualine = pcall(require, "lualine") 
if (not status) then return end
lualine.setup { 
  options = { 
    icons_enabled = true, 
    theme = 'solarized_dark', 
    section_separators = {'', ''}, 
    component_separators = {'', ''}, 
    disabled_filetypes = {} 
  }, 
  sections = { 
    lualine_a = {'mode'}, 
    lualine_b = {'branch'}, 
    lualine_c = {'filename'}, 
    lualine_x = { 
      { 'diagnostics', sources = {"nvim_lsp"}, symbols = {error = ' ', warn = ' ', info = ' ', hint = ' '} }, 
      'encoding', 
      'filetype' 
    }, 
    lualine_y = {'progress'}, 
    lualine_z = {'location'} 
  }, 
  inactive_sections = { 
    lualine_a = {}, 
    lualine_b = {}, 
    lualine_c = {'filename'}, 
    lualine_x = {'location'}, 
    lualine_y = {}, 
    lualine_z = {} 
  }, 
  tabline = {}, 
  extensions = {'fugitive'} 
}

The result:

Customize tabline

function MyTabLine() 
  let s = '' 
  for i in range(tabpagenr('$')) 
    " select the highlighting 
    if i + 1 == tabpagenr() 
      let s .= '%#TabLineSel#' 
    else 
      let s .= '%#TabLine#' 
    endif
" set the tab page number (for mouse clicks) 
    let s .= '%' . (i + 1) . 'T'
" the label is made by MyTabLabel() 
    let s .= ' %{MyTabLabel(' . (i + 1) . ')} '
if i + 1 == tabpagenr() 
      let s .= '%#TabLineSep#' 
    elseif i + 2 == tabpagenr() 
      let s .= '%#TabLineSep2#' 
    else 
      let s .= '' 
    endif 
  endfor
" after the last tab fill with TabLineFill and reset tab page nr 
  let s .= '%#TabLineFill#%T'
" right-align the label to close the current tab page 
  if tabpagenr('$') > 1 
    let s .= '%=%#TabLine#%999X' 
  endif
return s 
endfunction
function MyTabLabel(n) 
  let buflist = tabpagebuflist(a:n) 
  let winnr = tabpagewinnr(a:n) 
  let name = bufname(buflist[winnr - 1]) 
  let label = fnamemodify(name, ':t') 
  return len(label) == 0 ? '[No Name]' : label 
endfunction
set tabline=%!MyTabLine()

That’s pretty much it!
I hope it’s helpful for improving your vim environment.

Follow me online