My Mini Neovim Config

2024-09-21 ⏳9.0 min(3.6k words) 🕸️

None native English speaker. Forgive my Plastic English.🙏

Hello, Guys. Today, I want to introduce you my minimized config of Neovim. I have used Vim/Neovim for almost 10 years. In the beginning, I tended to add more and more configuration and plugins. However, after some years, I have realized that, the more experiences about Vim/Neovim I have, the less configuration or plugins I need. As a result, I try to drop all the unnecessary configuration and plugins as far as I can. So here is my mini Neovim config.

In the my early days of using Vim, I have to add some routine configs. For example, I have to manual enable filetype/syntax/autoindent in the vimrc. Thanks to Neovim, many of these routine options have been setted by default, so I can drop them from the vimrc. This leads my first principle that use and adapt to the default configuration as far as possible.

And I am always following another principle that only add configuration that you understand.

Now let me show you my basic configuration in the ~/.config/nvim/init.vim.

Basic configs

No Mouse

First and foremost, disable the mouse.

set mouse=

The default value of mouse is nvi, which means that Neovim enables mouse in the modes of Normal, Visual, and Insert. In my opinion, if you want to sharpen your Vim sense, you should avoid using mouse as much as possible. On the other hand, enabling mouse do have some annoying byproduct effects. For example, in the default configuration, you can not select text in the Normal mode because vim will enter the Visual mode when you select by mouse.

So close it and forget it.

Visual options

Then I need tweak several visual options.

set linebreak
set cursorline
set laststatus=3
set colorcolumn=80
set termguicolors
set foldmethod=expr
set foldexpr=v:lua.vim.treesitter.foldexpr()

I open the linebreak, so that the whole line of text can be showed in the narrow window with soft wrap. I open the cursorline to highlight the line under cursor. Setting the laststatus to 3 let vim show the status line below all window. And the colorcolumn=80 will indicate me that my code or text may be too wide. And finally, the termguicolors will enable the support of true color, which may let your vim more colorful. Almost all mainstream terminal emulator support the 24-bit true color mode. However, this is not the case of the builtin Terminal app of macOS. Ashamed.

By the way, I am using the WezTerm.

Then are options for folding. Combining both the foldmethod and foldexpr, Vim will fold lines according to the response of lua.vim.treesitter.foldexpr(). The Treesitter need additional config that located in a dedicate plugin. And I will explain more in detail later in this article.

The last problem of visual effects is color scheme. There are tones of color schemes across the Internet. The last one I use, which supports the 24-bit true color, is tender1. It is beautiful and delightful. The default color scheme of vim used to be unpleasant. However, the Neovim has fixed it2. So I just migrate to the new default color scheme as soon as possible.

But not every corner of the default color scheme suits my taste. I do not like it’s background, so I dropped it. And the color of status line is a little to bright, I changed it.

highlight Normal guibg=none
highlight StatusLine guibg=#303030 guifg=#adadad

Searching

So these are all my options for appearance. Now let’s tweak some behaviors of searching.

set ignorecase
set smartcase

By setting the ignorecase, vim will ignore the case of letter for searching, which will be very convenient. However, there are scenarios that you have to search content in the case sensitive way. This is why I enable the smartcase. With this option, if you type keywords contains upper case letter, vim will search in the case sensitive way.

Editing

The final part of basic configuration is for editing.

set formatoptions+=ro
set completeopt+=fuzzy,noinsert,popup

The formatoptions+=ro means appending the options r and o to formatoptions. And it let vim automatically insert the comment leader like // or * if you are editing the comments of programming. The r is for create new line by the Enter key and the o is for creating new line by hit o or O. You can use :h fo-table to get more details. This will be great convenient for programmer.

The completeopt option is for auto completing. Yes, Vim has builtin auto completing function, which can be triggered by <ctrl-x><ctrl-o> in the Insert mode. I append three flags. fuzzy is for fuzzy searching to filter the candidates; noinsert means do not replace the current inputted text using the candidates; popup means show the additional information, like documents of function, in an pop-up window next to the candidates list, instead of the preview window above. All of these flags ares not required, but just for convenience. Just choose according to your preference.

The above content is all about basic configuration. Now let me show you some advanced :)

Remember last position

The only non-basic configuration I want to show is letting vim jump to the last position you exit.

autocmd BufReadPost *
      \ if line("'\"") >= 1 && line("'\"") <= line("$")
      \ |   exe "normal! g`\""
      \ | endif

The autocmd means register callback for certain events. In this case, the event is BufReadPost, which will be triggered after vim has loaded the content of file from disk. And the asterisk * means this callback will be applied to all types of file.

The below lines with prefix of \\ means they are the continuation part of the first line containing the autocmd. With the above configuration, after loading the file content, vim will execute the following if statement.

if line("'\"") >= 1 && line("'\"") <= line("$")
  exe "normal! g`\""
endif

If you want to write all three lines into one line, you have to insert the |.

if line("'\"") >= 1 && line("'\"") <= line("$") | exe "normal! g`\"" | endif

The function call of line("'\"") looks like weird, but its meaning is very simple. Calling the line() function on some position will return the line number of this position. The real argument of line() is '". The single quotation mark ' means retrieve content from some register. Vim has many registers, and every one has a name. The register whose name is double quotation mark " is a special one, because Vim will save the current position of cursor into it automatically. So the call of line("'\"") will get the line number of the last position of cursor. Then the if will compare it with the first line number of 1 and the last line retrieved by line("$"). If the line number of last position is in the middle of the file, it will execute the following directive:

exe "normal! g`\""

The exe means execute, it will let vim execute the following content normal! g`\“, which will emulate in the normal mode striking g, `, and ". As a result, vim will jump the position of the cursor when you last exit.

So far, we have the following content in init.vim:

set mouse=
set linebreak
set cursorline
set colorcolumn=80
set smartcase
set ignorecase
set termguicolors
set formatoptions+=ro
set foldmethod=expr
set foldexpr=v:lua.vim.treesitter.foldexpr()
set completeopt+=fuzzy,noinsert,popup
set laststatus=3

highlight Normal guibg=none
highlight StatusLine guibg=#303030 guifg=#adadad

autocmd BufReadPost *
      \ if line("'\"") >= 1 && line("'\"") <= line("$")
      \ |   exe "normal! g`\""
      \ | endif

Plugins

And now, it is time to introduce the plugins. What Vim is for if you don’t install any plugin? But before our journey, there is one more question to discuss.

Should we use the init.lua instead of init.vim?

init.lua vs init.vim

Many guys have migrated their init.vim into init.lua. But as far as I’m concerned, it seems more rational to use both of them. The lua is more flexible for dynamic configuration, yet the viml is more clear for static one.

Suppose you want to let vim display line number.

By the init.vim, you need to add

set number

By the init.lua, you need to add

vim.opt.number = true

Which one is more clear? If this example is not typical, I can offer another one.

Recall the aforementioned completeopt option:

set completeopt+=fuzzy,noinsert,popup

The lua version of it is:

vim.opt.completeopt:append({'fuzzy', 'noinsert', 'popup'})

Which one is more clear? I prefer the viml one.

The init.vim can be treated as a DSL of init.lua. For static and short configuration, put them into init.vim. For dynamic and complicated ones, organize them into init.lua.

As Neovim will read the init.lua first. So I create the lua file named after vim.lua, and add the following line in the bottom of the init.vim to load it.

runtime vim.lua

OK, we are ready to begin the topic of plugins.

Plugin Managers

If you already have some experiences of vim plugin, you must known the plugins that manage other plugins. In my early days of using vim, plugins like vundle, vim-plug, dein are popular. But now, some lua-based plugins like lazy.nvim or packer.nvim are in the ascendant. Besides, vim also introduce builtin mechanism to manage plugins.

In my opinion, it is enough to use the builtin package feature plus Git. So I choose to use the git submodule to manage my few plugins.

The builtin package feature is very simple. Vim will load all packages located in the pack/*/start path. The asterisk here can be any folder name you like. I choose to use vendor to name it. So my plugins are locating at pack/vendor/start.

Currently there are only 9 plugins are used by me. Four of them are for file management and searching. Two of them are for Git integrating. One is for treesitter, one is for folding, and the last for LSP. Here is the directory structure:

├── init.vim
├── pack/vendor/start
│   ├── ag
│   ├── fzf
│   ├── gitsigns
│   ├── lspconfig
│   ├── mru
│   ├── pretty-fold
│   ├── tree
│   ├── treesitter
│   └── vimagit
└── vim.lua

Git as Manager

Using Git to manage plugins is very simple. Every plugins is a submodule. For example, if you want to install the nvim-treesitter, you can

git submoduel add https://github.com/nvim-treesitter/nvim-treesitter pack/vendor/treesitter

If you want to update one plugin, you can

git submodule update --remote pack/vendor/treesitter

If you want to update all plugins, you can

git submodule update --remote

If you want to show the commit logs of plugins, you can

git diff --submodule=log

If you want to delete one plugin, you can

git rm -rf pack/vendor/start/treesitter

There is no new configuration grammar to learn. It just works. The other popular plugin manager may have a beautiful UI, but they are very complicate as well. I do not need them.

As we have the ability to manage our them, it’s time to introduce my heirloom plugins.

NvimTree

First, we need a file explorer. I used to use the NerdTree, and now migrated to NvimTree. I have written one dedicated post to introduce this migration3. You can read it for more detail.

git submodule https://github.com/nvim-tree/nvim-tree.lua pack/vendor/start/tree

It’s a lua plugin, so we need enable it in the vim.lua:

vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
require("nvim-tree").setup()

The lua package name is nvim-tree, no matter where the directory you save.

In the first two lines, I disable the builtin newrw plugin. And the third line setups the nvim-tree.

Reboot Neovim, you can use :NvimTreeToggle to open or close the explorer. If you want to open the explorer and focus on the current opened file, you can use the :NvimTreeFindFile command. Both of these commands are very frequently used, so I add two keymap to them:

nnoremap <silent> <leader>e :NvimTreeToggle<cr>
nnoremap <silent> <leader>f :NvimTreeFindFile<cr>

Here comes my third principle, use the default keymap as much as possible, and only map your own one when unavoidable. If you want to map your own key, try to use the <leader> key as prefix to avoid overriding the default keymap.

The default key of <leader> is \\. Some one may change it to ,. I don’t recommend you do it, because , and ; is a pair key for inline-searching. You can type fa to find the next a in the current line. Then you can strike ; to jump to the next one, and strike , to the previous one. If you use , as <leader>, the inline-searching will be lame.

Some one may remap <leader> to the space. It looks like a good idea. The space is the easiest key to type, and you hardly ever use it in the Normal mode. It depends on you. But I still prefer the default key map.

ag/fzf/mru

Besides NvimTree, I also use three self-developed tiny plugins:

All the three plugins are developed by myself. They are developed using very short VimScript. None of them need additional configuration, just put them into the pack/vendor/start. All you need is to set one key map to use them:

nnoremap <silent> <leader>r :Mru<cr>
nnoremap <silent> <leader>s :call fzf#Open()<cr>

I choose <leader>r to open the MRU list, r is for recent. And I choose <leader>s to open the fzf search window, s is for search. As for ag, there is no need to set key map, because you can only use it under the command mode like:

:Ag --lua hello

All the three plugins are good examples for learning vim plugin development. I have written three articles to explain how they work:

However, these articles are written in Chinese. You may read them with the help of something like Google Translate.

OK, we have introduced all file related plugins. Now let’s go on for plugins about Git.

Git Integration

I use gitsins7 to show the changing status of Git. And it can also be used to show the diff by :Gitsigns diffthis and show the blame by :Gitsigns blame. It is very fast and the diff view is very nice.

Install:

git submodule add https://github.com/lewis6991/gitsigns.nvim pack/vendor/start/gitsigns

The default configuration is fine for me, so just put one into vim.lua to enable it.

require'gitsigns'.setup()

The vimagit8 is used for interactive staging9, which means you can add only certain parts of you change to staging area. It is very useful to organize you commit history.

vimagit is a classical vim script plugins, there is no need to setup additional configuration for it, just put it int the start directory.

git submodule add https://github.com/jreybert/vimagit pack/vendor/start/vimagit

OK, the Git part has finished. Now let’s begin the Treesitter part.

Treesitter

In the basic part, I use the v:lua.vim.treesitter.foldexpr() to calculate to folding range. It is offered by the Treesitter module of Neovim. In order to support different languages for syntax highlight, folding, we need to install the nvim-treesitter10 plugin. The goal of nvim-treesitter is to provide a simple and easy way to use the interface for tree-sitter in Neovim.

Install nvim-treesitter:

git submodule add https://github.com/nvim-treesitter/nvim-treesitter pack/vendor/start/treesitter

I enable the feature of highlight and indent in the vim.lua:

require'nvim-treesitter.configs'.setup {
    highlight = {
        enable = true,
        additional_vim_regex_highlighting = false,
    },
    indent = {
        enable = true,
    },
}

The folding support has already been setted by the foldexpr option. In the highlight part of configuration, I also disable the regex based highlighting feature to disable duplicated highlight, hoping it will improve the speed to open big file.

Neovim defaults support the treesitter configs of c/lua/viml/vimdoc/markdown. If you need to support other language, you should use the :TSInstall <name> to install it. You can use :TSInstallInfo to show the installed and supported language modules.

Pretty Fold

When it comes to folding, I can’t help but recommend one additional plugin, the pretty-fold. Unfortunately, the original project11 seems to be abandoned. This folk12 is still under maintaining.

The killer feature of pretty-fold is that it puts the folded line number in the tail of current line and retain column of folded code, which is far more clear. So there is no need to install additional tag list plugins.

Install:

git submodule add https://github.com/bbjornstad/pretty-fold.nvim pack/vendor/start

Only one line is needed to enable it in the vim.lua:

require('pretty-fold').setup()

LSP

Finally, we come to the last part of this article, the plugins for the Language Server Protocol (LSP). Neovim has an official repository for configs of common language servers.

Install:

git submodule https://github.com/neovim/nvim-lspconfig pack/vendor/start/lspconfig

Every common programming language may have one ore more language servers. You should choose which one to use and install manually. For me, I use the gopls13 for Golang, and rust_analyzer14 for Rust. How to install these language server is beyond the scope of this post.

The main purpose of the LSP part of this post is to show that there is no need to install additional plugin to use LSP. %he builtin omnifunc can complete items from LSP. All you need is put the following lines into your vim.lua:

local lsp = require'lspconfig'
lsp.gopls.setup({})
lsp.rust_analyzer.setup({})

And it just works for Golang and Rust, if you have installed the corresponding LSP. You can strike <ctrl-x><ctrl-o> to trigger the complete menu, and Neovim will show the candidates coming from LSP.

The latest Neovim will setup many key mappings by default:

Some other programming related options will also be modified by default:

Almost all keymaps about programming will work out of box. However, there are some frequently used methods that are not mapped. We need add our own map in vim.lua:

vim.api.nvim_create_autocmd('LspAttach', {
    callback = function(args)
        local function buf_set_keymap(keys, callback)
            local opts = { noremap=true, silent=true, callback=callback }
            vim.api.nvim_buf_set_keymap(args.buf, 'n', keys, '', opts)
        end

        buf_set_keymap('gD', vim.lsp.buf.declaration)
        buf_set_keymap('gd', vim.lsp.buf.definition)
        buf_set_keymap('gri', vim.lsp.buf.implementation)
    end,
})

In this snippet, I create a callback for the event LspAttach. Every time the Neovim start the LSP client, it will execute the callback function.

In my callback, I try to setup some keymaps. Use the lua method nvim_buf_set_keymap is very powerful, yet very complicated. The first argument is buffer id. The second is mode, which we only map in the Normal mode. The third one is the keys we want to use for mapping. The fourth is the target key. But we want to call lua method, so leave it to empty. And the last one is for options. We set the flags of noremap and silent, and the callback is the target lua method.`

As I said before, the lua method is complicated, but very flexible. In the snippet, I define one helper of buf_set_keymap to wrap nvim_buf_set_keymap, and use it to simplify the mapping process. It’s really nice.

I mapped the gD to jump to the declaration of symbol, and gd to the definition. The gri is mapped to jump to the implementation of interface, which is very frequently used by myself. Feel free to add you own map.

Conclusion

Finally, I have shown you all the minimized config of my Neovim. You can find the whole repo with ten years of commit history from here. I hope this article can bring inspiration to you. If you have any questions or suggestions, feel free to leave message to me. Enjoy 🥂🎊


  1. https://github.com/jacoborus/tender.vim↩︎

  2. https://github.com/neovim/neovim/issues/14790↩︎

  3. https://taoshu.in/vim/migrate-nerdtree-to-nvim-tree.html↩︎

  4. https://github.com/taoso/ag.vim↩︎

  5. https://github.com/taoso/fzf.vim↩︎

  6. https://github.com/taoso/mru.vim↩︎

  7. https://github.com/lewis6991/gitsigns.nvim↩︎

  8. https://github.com/jreybert/vimagit↩︎

  9. https://git-scm.com/book/en/v2/Git-Tools-Interactive-Staging↩︎

  10. https://github.com/nvim-treesitter/nvim-treesitter↩︎

  11. https://github.com/anuvyklack/pretty-fold.nvim↩︎

  12. https://github.com/bbjornstad/pretty-fold.nvim↩︎

  13. https://github.com/golang/tools/tree/master/gopls↩︎

  14. https://rust-analyzer.github.io/↩︎