Neovim LSP and DAP using Lua

LSP and DAP for Python, Golang, Rust, and Typescript/Javascript.

Neovim IDE with LSP and DAP

Overview

In my previous article I walked through with you on how to convert existing .vimrc to a Lua based configuration and set up LSP for Lua.

In this article let’s add LSP and DAP support. Particularly I will focus on Python, Typescript, Golang and Rust.

Installation of Language Servers

Let’s get started by installing the language server.

If you are using coc.nvim, normally the language server is installed automatically for you. For Neovim LSP, there are available plugins to automate this process. Here I will go through the manual installation steps.

Python

For Python, let’s install pyright.

$ npm install -g pyright

Golang

For Golang, let’s install gopls.

$ GO111MODULE=on go get golang.org/x/tools/gopls@latest

Rust

For Rust, let’s use rust_analyzer. The easiest way to install is to download the binary, copy to a folder, and add the folder to environment PATH.

Typescript

For Typescript, let’s install typescript-language-server.

$ npm install -g typescript-language-server

Configure LSP

Plugins

Under plugins.lua, install nvim-lspconfig and completion-nvim.

-- LSP and completion
use { 'neovim/nvim-lspconfig' }
use { 'nvim-lua/completion-nvim' }

Run :luafile % and then :PackerInstall.

Configuration

The current configuration already supports Lua LSP. I am going to leverage existing configuration.

  • Under lua folder, create a new folder called lang.
  • Create a new file called init.lua under lang folder.
LSP — init.lua

In the main init.lua, I import the new lang module.

-- LSP
require('lang')
-- DAP
require('dbg')

Now Neovim should have the correct language server configured for Lua, Python, Golang, Rust, and Typescript.

Note: In case language server is not started when you are editing a file, type :LspInfo for troubleshooting.

LSP — Python
LSP — Typescript
LSP — Golang
LSP — Rust

Configure snippets

For a Lua based snippets plugin, you can use snippets.nvim. However, there will not be any ready snippets for use, and you have to manually create the snippets that you need.

I am going to use UltiSnips.

-- Snippets
use { 'honza/vim-snippets' }
use { 'SirVer/ultisnips' }

Run :luafile % and then :PackerInstall.

In completion.lua, add the following line.

local utils = require('utils')utils.opt('o', 'completeopt', 'menuone,noinsert,noselect')
vim.cmd [[set shortmess+=c]]
vim.g.completion_confirm_key = ""
vim.g.completion_matching_strategy_list = {'exact', 'substring', 'fuzzy'}
vim.g.diagnostic_enable_virtual_text = 1
vim.g.completion_enable_snippet = 'UltiSnips'
vim.g.completion_enable_auto_popup = 1

And snippets should work now.

UltiSnips

Configure DAP

Plugins

Under plugins.lua, install nvim-dap, telescope-dap.nvim and nvim-dap-python.

-- DAP
use { 'mfussenegger/nvim-dap' }
use { 'nvim-telescope/telescope-dap.nvim' }
use { 'mfussenegger/nvim-dap-python' } -- Python

Run :luafile % and then :PackerInstall.

Key Mappings

Let’s define the key mappings for debugging. You can change them depending on your preferences.

DAP Key Mappings

Note I also define the key mappings for telescope-dap.

telescope-dap

DAP for Python

For Python debugging, I use nvim-dap-python which requires nvim-treesitter and debugpy. You can refer to my previous article on how to install them.

Create a file called python.lua under dbg folder with the following content. Change the Python path accordingly.

require('dap-python').setup('~/miniconda3/bin/python')

Import the module in dbg/init.lua

require('telescope').load_extension('dap')
require('dbg.python')
.....

Restart Neovim and DAP should work now for Python.

DAP — Python

DAP for Rust

For Rust debugging, make sure you have LLVM installed and have lldb-vscode available.

E.g. For MacOS, I can install llvm using brew.

$ brew install llvm

And lldb-vscode should be available under /usr/local/Cellar/llvm

lldb-vscode

Make sure you add lldb-vscode to your environment path!

Create a file called rust.lua under dbg folder with the following content. Change the program path accordingly.

local dap = require "dap"
dap.adapters.rust = {
type = 'executable',
attach = {
pidProperty = "pid",
pidSelect = "ask"
},
command = 'lldb-vscode', -- my binary was called 'lldb-vscode-11'
env = {
LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY = "YES"
},
name = "lldb"
}
dap.configurations.rust = {
{
type = "rust",
name = "Debug",
request = "launch",
program= "/Users/mengwangk/workspace/development/rust-app/target/debug/myapp1"
}
}

Import the module in dbg/init.lua

require('telescope').load_extension('dap')
require('dbg.rust')
.....

Restart Neovim and DAP should work now for Rust.

DAP — Rust

DAP for Golang

For Golang debugging, make sure you have delve installed and available as part of your environment path.

$ go install github.com/go-delve/delve/cmd/dlv@latest

Create a file called go.lua under dbg folder with the following content.

local dap = require "dap"dap.adapters.go = function(callback, config)
local handle
local pid_or_err
local port = 38697
handle, pid_or_err =
vim.loop.spawn(
"dlv",
{
args = {"dap", "-l", "127.0.0.1:" .. port},
detached = true
},
function(code)
handle:close()
print("Delve exited with exit code: " .. code)
end
)
----should we wait for delve to start???
--vim.defer_fn(
--function()
--dap.repl.open()
--callback({type = "server", host = "127.0.0.1", port = port})
--end,
--100)
dap.repl.open()
callback({type = "server", host = "127.0.0.1", port = port})
end
-- https://github.com/go-delve/delve/blob/master/Documentation/usage/dlv_dap.md
dap.configurations.go = {
{
type = "go",
name = "Debug",
request = "launch",
program = "${file}"
}
}

Import the module in dbg/init.lua

require('telescope').load_extension('dap')
require('dbg.go')
.....

Restart Neovim and DAP should work now for Golang.

In case you hit errors, e.g. “connection refused”, try to start delve in DAP mode manually from you project folder by running the following command before start debugging. Do comment out vim.loop.spawn.

$ dlv dap -l 127.0.0.1:38697 --log --log-output="dap"
Delve DAP Debugging Session
Golang — DAP

Note: Debugging Go application using delve directly is still experimental and is not very stable. You can also check out the option to use vscode-go which I think is more stable.

DAP for Javascript/Node

For Javascript/Node debugging, make sure you have node-debug2 installed as described in the documentation.

Create a file called typescript.lua under dbg folder with the following content. Change the path to node-debug2 accordingly.

local dap = require "dap"dap.adapters.node2 = {
type = 'executable',
command = 'node',
args = {os.getenv('HOME') .. '/.xdg_home/nvim/extensions/vscode-node-debug2/out/src/nodeDebug.js'},
}
dap.configurations.javascript = {
{
type = 'node2',
request = 'launch',
-- program = { os.getenv('HOME') .. '/workspace/development/alpha2phi/python-apps/debugging/hello.js'},
program = '${workspaceFolder}/${file}',
cwd = vim.fn.getcwd(),
sourceMaps = true,
protocol = 'inspector',
console = 'integratedTerminal',
},
}

Import the module in dbg/init.lua

require('telescope').load_extension('dap')
require('dbg.typescript')
.....

Restart Neovim and DAP should work now for Javascript/Node.

Javascript — DAP

DAP configuration for other languages can be found at the documentation. Depending on the language you use, you may need to trials and errors to see if it works for you. You can also check out my repository here.

Another option for debugging is vimspector. You can refer to the following articles on how I set it up for Python, Golang, Rust and Javascript/Typescript debugging.

Startup Screen

This is not related to LSP or DAP but let’s also create a startup screen for the development environment.

A popular option is vim-startify, but let’s try dashboard-nvim here.

Add the following line in plugins.lua and install it.

-- Dashboard
use { 'glepnir/dashboard-nvim' }

Create a file called dashboard.vim under the plugin folder.

let g:dashboard_default_executive ='telescope' 
let g:dashboard_custom_footer = ['']
let g:dashboard_custom_header = [
\ '',
\' /\ /\ /\',
\' /\//\\/\ /\//\\/\ /\//\\/\',
\' /\//\\\///\\/\//\\\///\\/\//\\\///\\/\',
\'//\\\//\/\\///\\\//\/\\///\\\//\/\\///\\',
\'\\//\/ \/\\//',
\' \/ \/',
\' /\ /\',
\'//\\ Neovim IDE with LSP and DAP //\\',
\'\\// \\//',
\' \/ \/',
\' /\ /\',
\'//\\/\ /\//\\',
\'\\///\\/\//\\\///\\/\//\\\///\\/\//\\\//',
\' \/\\///\\\//\/\\///\\\//\/\\///\\\//\/',
\' \/\\//\/ \/\\//\/ \/\\//\/',
\' \/ \/ \/',
\'',
\ '',
\ ]

In telescope.vim under plugin folder, let’s remap the shortcuts and keys.

" Find files using Telescope command-line sugar.
nnoremap <silent> <leader>ff :DashboardFindFile<CR>
nnoremap <silent> <leader>fg :Telescope live_grep<cr>
nnoremap <silent> <leader>fb :Telescope buffers<cr>
nnoremap <silent> <leader>ft :Telescope help_tags<cr>
nnoremap <silent> <leader>fh :DashboardFindHistory<CR>
nnoremap <silent> <leader>fc :DashboardChangeColorscheme<CR>
nnoremap <silent> <leader>fw :DashboardFindWord<CR>
nnoremap <silent> <leader>fm :DashboardJumpMark<CR>
nnoremap <silent> <leader>fn :DashboardNewFile<CR>
nmap <leader>fss :<C-u>SessionSave<CR>
nmap <leader>fsl :<C-u>SessionLoad<CR>
let g:dashboard_custom_shortcut={
\ 'last_session' : '<Leader> fs l',
\ 'find_history' : '<Leader> f h',
\ 'find_file' : '<Leader> f f',
\ 'new_file' : '<Leader> f n',
\ 'change_colorscheme' : '<Leader> f c',
\ 'find_word' : '<Leader> f w',
\ 'book_marks' : '<Leader> f m',
\ }

Restart Neovim and the dashboard should come up now.

Neovim Dashboard

Summary

Configuring LSP for Neovim is relatively easy. However configuring DAP is still quite tedious and challenging. Currently 2 popular options are available — nvim-dap and vimspector and you may need to experiment with both to see which one fits your development workflow.

The dotfiles I used can be found at this repository.

Programmer and occasional blogger.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store