# NeoVim IDE setup from scratch
This article describes how to get basic functions work in [NeoVim](https://github.com/neovim/neovim) `nvim` for software development, with Rust and Python code example.
## Prerequisite
- Ubuntu `16.04` +
- NeoVim `v0.7.0` +
- Rust (`rustc`, [`rust-anaylzer`](https://github.com/rust-lang/rust-analyzer)) `v1.75.0` +
- ensure the path to these executables is added to environmental variable `$PATH`
- Python `3.9.6` +
- for some plug-ins like [`puremourning/vimspector`](https://github.com/puremourning/vimspector)
---
## Steps for Common Function
### Directory Structure
Be sure to have following files in `$HOME/.config/nvim`, so the changes to all the files make sense :
```shell
├── init.lua
├── lua
│ ├── keys.lua
│ ├── opts.lua
│ ├── plug.lua
│ └── vars.lua
└── plugin
└── packer_compiled.lua
```
Add the following script to `$HOME/.config/nvim/init.lua`, to load other sub-modules next time when you open `nvim`
```lua=
require('vars') -- Variables
require('opts') -- Options
require('keys') -- Keymaps
require('plug') -- Plugins
```
### Plug-in Management
I choose a NeoVim plug-in [Packer](https://github.com/wbthomason/packer.nvim) for plug-in management. It is essential to manually install `Packer` by cloning the codebase to following default local workspace :
```shell
git clone --depth 1 https://github.com/wbthomason/packer.nvim \
$HOME/.local/share/nvim/site/pack/packer/start
```
Add the following script in `$HOME/.config/nvim/lua/plug.lua` :
```lua=
return require('packer').startup(
function()
-- Packer can manage itself, this plug-in
-- should be always kept.
use 'wbthomason/packer.nvim'
-- add more plug-ins by specifying names of
-- their Github codebase.
end
)
```
#### How to Install New Plug-ins
Whenever the script `lua/plug.lua` is saved :
- reload the same script by the `nvim` command `:luafile %`
- install new plug-ins or uninstall absent plug-ins, using extra `nvim` commands like `:PackerInstall` or `:PackerUpdate`.
- all plug-ins managed by `Packer` will be installed under the path `$HOME/.local/share/nvim/site/pack/packer/start`.
### Language Server Protocol (LSP)
It is a [framework](https://microsoft.github.io/language-server-protocol/) that provides language-specific information and facilitates other features in development tools. It is typically designed as client-server architecture in which NeoVim `nvim` acts as client side and [`rust-analyzer`](https://github.com/rust-lang/rust-analyzer/tree/master/lib/lsp-server) or [`python-lsp-server`](https://github.com/python-lsp/python-lsp-server) act as server side.
#### LSP server setup
Depend on the language to use.
|Language|tool name|setup steps|
|---|---|---|
|Rust|[`rust-analyzer`](https://github.com/rust-lang/rust-analyzer/tree/master/lib/lsp-server)|provided as you [install basic Rust tool](https://doc.rust-lang.org/book/ch01-01-installation.html) (since v1.64.0) |
|Python|[`python-lsp-server`](https://github.com/python-lsp/python-lsp-server)|It is suggested to install it within a virtual environment. (tips: gather all 3rd-party packages required by `nvim` to the same `venv`)|
|Python| [`pyright`](https://github.com/microsoft/pyright) |TODO|
||||
#### LSP client setup in NeoVim
[Install](#How-to-Install-New-Plug-ins) the following plug-ins :
- [`neovim/nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig/) : base configuration.
- [`simrat39/rust-tools.nvim`](https://github.com/simrat39/rust-tools.nvim) wraps `lspconfig`, provides more configuration parameters and interacts with Rust language server (in this case, `rust-analyzer`).
Add configuration object for `rust-tools` in `init.lua` :
- the example below enables [features](https://github.com/simrat39/rust-tools.nvim#usage) like **hover actions** or **expand macros**.
- enable more features accroding to your requirement.
```lua=
local rt = require("rust-tools")
rt.setup({
server = {
on_attach = function(_, bufnr)
-- Hover actions, move text cursor to any symbol.
-- Press your custom key combo to show the inner popup
-- which describes the brief definition.
-- Switch text cursor to the inner popup by pressing
-- the same key combo again
vim.keymap.set("n", "<C-space>",
rt.hover_actions.hover_actions,
{ buffer = bufnr })
-- expand macro, show the definition in another
-- split window of the same tab
vim.keymap.set("n", "mac",
rt.expand_macro.expand_macro,
{ buffer = bufnr })
end, -- `on_attach` callback is invoked when attaching
-- a buffer to lanugage server.
settings = {
['rust-analyzer'] = {
-- cargo.sysroot defaults to `discover`, which means the plug-in client
-- or rust-analyzer will try to find it
cargo = {
sysroot = '/path/to/installed',
-- sysrootSrc = '/path/to/installed/lib/rustlib/src/rust/library'
},
procMacro = {
enable = true,
-- note `rust-analyzer-proc-macro-srv` should come with rust toolchain
server = '/path/to/installed/libexec/rust-analyzer-proc-macro-srv',
},
}
}
},
}) -- end of rust-tools setup
```
Enable Python language server (in this case I use `python-lsp-server`) by adding the script below :
```lua=
local default_lsp = require("lspconfig")
default_lsp.pylsp.setup({
settings = {
pylsp = {
plugins = {
maxLineLength = 250,
jedi_completion = {
include_class_objects = true,
include_function_objects = true
},
jedi = {
environment = os.getenv("VENV_PATH_PYLSP")
}
-- note each python application may require different
-- virtual environment, users need to add
-- path to specific venv when typing command
-- `nvim /path/to/file.py`
}}}
})
```
Refresh the configuration by restarting `nvim`
|hover actions|expand macros|
|-|-|
|||
## Auto Completion
Edit the script below to `lua/plug.lua`, then [Install the plug-in](#How-to-Install-New-Plug-ins) [`nvim-cmp`](https://github.com/hrsh7th/nvim-cmp) and its extensions.
```lua=
return require('packer').startup(
function()
-- other plug-ins --
use 'hrsh7th/nvim-cmp' -- Completion framework
use 'hrsh7th/cmp-nvim-lsp' -- LSP completion source:
-- Useful completion sources:
use 'hrsh7th/cmp-nvim-lua'
use 'hrsh7th/cmp-nvim-lsp-signature-help'
use 'hrsh7th/cmp-vsnip'
use 'hrsh7th/cmp-path'
use 'hrsh7th/cmp-buffer'
use 'hrsh7th/vim-vsnip'
-- other plug-ins --
end
)
```
Add configuration object for `cmp` plug-in in `init.lua`, see [reference](#Reference) for detail :
```lua=
-- Completion Plugin Setup
local cmpobj = require'cmp'
cmpobj.setup({
snippet = { ... }, -- Enable LSP snippets
mapping = { ... }, -- keyboard shortcuts
sources = { ... }, -- installed source
window = { ... }, -- menu layout, symbol for categories
formatting = { ... }
}) -- end of `cmp` setup
```
Configure [insert-mode completion](https://neovim.io/doc/user/options.html#'completeopt') in `lua/opts.lua` :
```lua=
vim.opt.completeopt = {'menuone', 'noselect', 'noinsert'}
```
Restart the `nvim` to enable auto-completion. When you are typing NeoVim prompts a popup menu listing all possible text options, some of the options have documentation, see Rust example below.

### Key Mappings to Functions
You can navigate the popup menu by adding keyboard shortcuts which perform some `cmp` functions in the `mapping` field, where :
- `<C-Up>` means `ctrl` key plus `up` arrow key
- `<S-Up>` means `shift` key plus `up` arrow key
|key combo|command|
|---------|-------|
|`<C-Up>`|`cmpobj.mapping.select_prev_item()`|
|`<C-Down>`|`cmpobj.mapping.select_next_item()`|
|`<S-Up>`|`cmpobj.mapping.scroll_docs(-3)`|
|`<S-Down>`|`cmpobj.mapping.scroll_docs(3)`|
|`<C-cmp>`|`cmp.mapping.complete()`|
|`<C-e>`|`cmp.mapping.close()`|
|||
### Sources for Syntax Lookup
The sources may come from :
- code files of current application project (e.g. `cargo` in Rust)
- installed software libraries.
Each source in `sources` field contains :
- name of the source
- minimum number of characters in your keyword to launch syntax lookup
The table below shows a valid setting
|name|num-chars-keyword|
|---------|-------|
|`path`|N/A|
|`nvim_lsp`|`1`|
|`nvim_lua`|`2`|
> **Side Note** :
> NeoVim is unable to provide more precise information for each text option of a completion popup without language servers and adapters like `rust-tools`, the plug-in `cmp` itself still works.
---
## Go-to Definition
### Prerequisites
- [Install plug-ins](#How-to-Install-New-Plug-ins) `nvim-lspconfig`
- ensure the language server you use also supports go-to definition.
### Configuration
Add new key mapping in the Lua script :
```lua=
vim.api.nvim_buf_set_keymap(0, 'n', '<C-]>',
'<cmd>lua vim.lsp.buf.definition()<CR>',
{ noremap = true, silent = true })
```
Note the module `vim.lsp` in NeoVim built-in Lua provides a generic function [`vim.lsp.buf.definition()`](https://neovim.io/doc/user/lsp.html#lsp-buf), which requests a go-to definition to language server.
A typical use case looks like this :
- when you move the text cursor to a symbol in NeoVim, a keyboard shortcut `ctrl + ]` will invoke `vim.lsp.buf.definition()`, which sends request to a language server (in this article, `rust-analyzer`)
- whenever the language server responds with useful information, NeoVim jumps to another code file which contains the symbol definition.
- To get back from the definition, you simply press the key `ctrl + o` or `ctrl + t` (default setting)
### Restriction for Rust Tools
Use go-to definition ONLY under the scope of current `cargo` project, if you go beyond it (e.g. to other 3rd-party crates or standard library) , NeoVim will launch new language server (another `rust-analyzer` process) for each different crate , **allocate more memory** and **seem to never release them**.
---
## Debugger
This section demonstrates how to set up debugger with NeoVim, [C/C++ extension for VScode](https://github.com/microsoft/vscode-cpptools) and [GDB](https://en.wikipedia.org/wiki/GNU_Debugger).
### Prerequisites
- [Install the plug-in](#How-to-Install-New-Plug-ins) [`puremourning/vimspector`](https://github.com/puremourning/vimspector)
```lua=
return require('packer').startup(
function()
-- other plug-ins --
use 'puremourning/vimspector'
-- other plug-ins --
end
)
```
- `vimspector` is implemented in **Python**
- [create a virtual environment](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments) using `pip3 -m venv /PATH/TO/PY-WORK-FOR-NVIM` and activate it.
- install the python package [`pynvim`](https://github.com/neovim/pynvim) in the virtual environment.
- the Python virtual environment should have been activated as long as your NeoVim editor is open for debugging, otherwise NeoVim will report the following error :
```
Vimspector unavailable: Requires Vim compiled with +python3
```
### Configuration for Debugger
#### Keyboard Shortcuts
Add key mappings for debug session control, breakpoint, watchpoint, or line-by-line step control ...etc, to `lua/keys.lua`
```lua=
vim.cmd([[
nmap <F5> <cmd>call vimspector#Launch()<cr>
nmap <F8> <cmd>call vimspector#Reset()<cr>
nmap <F9> <cmd>call vimspector#Continue()<cr>
nmap <F10> <cmd>call vimspector#StepOver()<cr>
]])
vim.api.nvim_set_keymap('n', "bp",
":call vimspector#ToggleBreakpoint()<cr>")
```
|key|function|meaning|
|-|-|-|
|`<F5>`|`vimspector#Launch()`|start a new debug session in a new tab, note you can't do this if current session hasn't terminated yet.|
|`<F8>`|`vimspector#Reset()`|terminate current debug session and close the tab.|
|`<F9>`|`vimspector#Continue()`|continue the program execution from current breakpoint|
|`<F10>`|`vimspector#StepOver()`|run single line of code|
|`b + p`|`vimspector#ToggleBreakpoint()`|create / delete a breakpoint on specific line of code file.|
Restart your `nvim` after the configuration above is saved, now you should be able to toggle breakpoints on some lines of your code, check the list of breakpoints by a dedicated `nvim` command `:VimspectorBreakpoints`.

#### Window Layout
Change [window size](https://github.com/puremourning/vimspector#changing-the-default-window-sizes) by adding the script below to `lua/opts.lua`
```lua=
vim.cmd([[
let g:vimspector_sidebar_width = 64
let g:vimspector_bottombar_height = 15
let g:vimspector_terminal_maxwidth = 60
let g:vimspector_terminal_minwidth = 35
]])
```
#### [Debug Profile Configuration](https://puremourning.github.io/vimspector/configuration.html#debug-profile-configuration)
It is a JSON object describing debugger behaviour. By default `vimspector` automatically extracts debug configurations from the file `.vimspector.json`.
- the value of the `adapter` field in a debug configuration has to be the name of [a gadget properly installed](#Vimspector-Gadget-Installation) in `vimspector`
In Rust you can simply place the file on top of `cargo` project. See [the example configuration](https://github.com/metalalive/EnvToolSetupJunkBox/blob/master/code_snippet/lua/nvim/custom_ide/example.vimspector.json).
### Vimspector Gadget Installation
It is necessary to [install some gadgets](https://github.com/puremourning/vimspector#install-some-gadgets) the first time you launch debugger with `vimspector`. Restart `nvim` with the environment variable [`SSL_CERT_DIR`](https://www.openssl.org/docs/man3.1/man3/SSL_CTX_set_default_verify_dir.html) , which will be referenced in [`ssl.get_default_verify_paths()`](https://docs.python.org/3/library/ssl.html#ssl.get_default_verify_paths) when `vimspector` is downloading gadgets from remote sites.
```!
SSL_CERT_DIR="/path/to/ssl/certs/" nvim /path/to/your/code-file
```
`vimspector` provides extra `nvim` commands `VimspectorInstall` for gadget installation, in this section I'll use [C/C++ extension for VScode](https://github.com/microsoft/vscode-cpptools) :
```!
:VimspectorInstall vscode-cpptools
```
Here's the result for successful installation :
```
Vimspector gadget installation complete!
```
> **Note**
>
> Without `SSL_CERT_DIR`, `vimspector` will encouter download failure because Python does not know the path to appropriate CA certificates by default.
> ```!
> Installing vscode-cpptools@1.14.5...
> - FAILED installing vscode-cpptools: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer ce rtificate (_ssl.c:1129)>
> Installing CodeLLDB@v1.9.0...
> - FAILED installing CodeLLDB: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certifica te (_ssl.c:1129)>
> Failed to install adapters:
> * vscode-cpptools
> * CodeLLDB
> Re-run with --verbose for more info on failures
> ```
### Check Debugger Layout
After you've done [the configuration](#Configuration-for-Debugger) above, you may :
- launch a debug session by pressing key `<F5>`
- control program execution by :
- adding / deleting breakpoints with key `b + p`
- continue to next breakpoint, or run line by line
- monitor current program state, by checking the sub-windows `vimspector.Variables` or `vimspector.StackTrace`.

> In this section, a debug session will trigger a `gdb` process and `OpenDebugAD7` process for the gadget `vscode-cpptools`, they should be present in active process list of your operating system.
---
## TODO
- explore other plug-ins that provide similar functions.
- [explore project structure](https://github.com/nvim-tree/nvim-tree.lua), requires `nvim >= 0.8`
- figure out root cause of [this issue](#Restriction-for-Rust-Tools) about duplicate Rust LSP servers in go-to definition
- `simrat39/rust-tools.nvim` has been archived since Jan. 2024 and does not seem to maintain in foreseeable future, I would switch to another plug-in when I have time for the update.
- find a way to prevent `rust-analyzer` from using a lot of memory space when there are more 3rd-party crates to apply to a `cargo` project, more crates mean more syntax trees to load (from these 3rd-party crates).
## Reference
#### NeoVim Lua Configuration
Check out the detail script at [here](https://github.com/metalalive/EnvToolSetupJunkBox/blob/master/code_snippet/lua/nvim/custom_ide).
#### Relevant articles
- [Rust and Neovim - A Thorough Guide and Walkthrough](https://rsdlt.github.io/posts/rust-nvim-ide-guide-walkthrough-development-debug)
- [Rust analyzer manual](https://rust-analyzer.github.io/manual.html)
- [Debugging in Vim with Vimspector and Node.js example - DEV community](https://dev.to/iggredible/debugging-in-vim-with-vimspector-4n0m)
- [Neovim from scratch - Github Repo](https://github.com/LunarVim/Neovim-from-scratch)
###### tags: `Dev-tool` `Rust` `Python`