How to get human rights in Neovim without plugins
13 December, 2024
Introduction
This is a rewrite of an article I wrote last year based on my PoC config project: NativeVim.
Plugins have become a natural part of the Neovim community, which has grown explosively starting with v0.5.0 released in 2021.
- lazy.nvim (or packer.nvim or rocks.nvim)
- nvim-lspconfig
- nvim-treesitter
- nvim-cmp (including several cmp related plugins)
- Luasnip (or nvim-snippets)
- mason.nvim
- Comment.nvim
- telescope.nvim (or fzf-lua)
etc
It is pretty common to find more than 10 plugins installed in most minimalistic neovim config.
- Why do I need nvim-lspconfig or nvim-treesitter if Neovim officially supports LSP and tree-sitter?
- Why there are so many plugins needed to just get autocompletion?
- Why should I install weird third-party plugins? I just want to use NEOVIM!
If you have one of these questions, you've come to the right place.
In this article, we will look at how to achieve the same functionality in native neovim without famous plugins, and find out what exactly each plugin does and why it is needed.
Actually you don't need most of them
Most plugins are just 1) abstracted & automated version of common setup, 2) reimplementation of builtin functionalities providing advanced features, 3) implementation of missing features which are now builtin. So they can be easily replaced with manual setup / builtin features.
LSP plugins & setups
Let's start with most basic ones. Plugins known for providing LSP integrations.
nvim-lspconfig
mason.nvim
keymaps/options
Many options are now builtin especially from upcomming v0.11 release.
Completions
While completion is part of LSP features, it can do other other stuffs without LSP. That's why I'm treating Completions in separate section from LSP.
Some surprising facts:
- Vim natively supports completion including the UI. (
:h ins-completion
) - There are user-configurable completion sources for Vim. (
:h 'omnifunc'
) - Neovim provides completion source for LSP (
:h vim.lsp.omnifunc()
) - Actually, it provides more convenice sources like tree-sitter Queries. (
:h vim.treesitter.query.omnifunc()
) - Neovim has
fuzzy
field in'completeopt'
option since v0.11 (:h 'completeopt'
)
I'll compare with nvim-cmp ecosystem because it is most mature and famous completion plugin at this moment. But points below also applies to other common completion plugins like [coq_nvim], [mini.completion], [blink.cmp] and [care.nvim].
cmp-nvim-lsp (LSP completions)
Neovim automatically sets 'omnifunc'
option to vim.lsp.omnifunc()
when Language Server is attached.
Below is how internal code making this possible:
vim.api.nvim_create_autocmd("LspAttach", {
callback = function (ev)
vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc"
end,
})
This auto-command is one of defaults in Neovim since v0.10 so you don't need to set this manually in your config.
Enabling LSP completion
However, we can't say that this alone properly utilizes the autocompletion provided by LSP. Since
'omnifunc'
does not provide features such as snippet or auto-import, support for these must be set separately. Fortunately, starting from v0.11, Neovim added this feature as a built-in API.vim.api.nvim_create_autocmd("LspAttach", { callback = function (ev) vim.lsp.completion.enable(true, ev.data.client_id, ev.buf, { autotrigger = false }) end, })
After
vim.lsp.completion.enable()
is executed, Neovim performs the additional operations provided by the LSP in buffer where user auto-completes with LSP. (e.g. expanding a snippet or modifying part of the code like auto-import)
cmp-path (path completions)
Vim has a built-in filename completion (:h i_CTRL-X_CTRL-F
)
This is based on where
nvim
is executed, not current filepath.
cmp-buffer (buffer text completions)
Vim has a builtin buffer word completion.
:h i_CTRL-X_CTRL-P
:h i_CTRL-X_CTRL-N
:h i_CTRL-P
:h i_CTRL-N
advanced features from builtin completion
- line completion
- legacy omnicompletions
- tree-sitter query completion
Why would I use nvim-cmp then?
While builtin completions are powerful enough, it is quite reasonable to use external completion plugins. External completion plugins not depending on builtin completions usually has better performance and provide way more customizabilty of how it looks and behaves.
Snippets
Lets have the snippet feature without plugins like Luasnip or [nvim-snippet].
Neovim has built-in snippet API in vim.snippet
since v0.10, so most LSP-provided snippets work (since v0.11, if you enable the LSP completion.) But it still don't provide native API to make user-defined snippets that expands on specific keywords or trigger keymaps.
Instead, there is a similar function called Abbreviation
(:h Abbreviations
.) To briefly explain, if you register abbreviation in insert mode with :iab ms Microsoft
, ms<space>
or ms<cr>
will be replaced with Microsoft<space>
and Microsoft<cr>
, respectively. This abbreviation can be triggered with the keymap <c-]>
in addition to special characters such as <space>
, <cr>
or ()[]{}<>'",.
.
By using abbreviation and vim.snippet
, we can implement the following snippet API.
---@param trigger string trigger string for snippet
---@param body string snippet text that will be expanded
---@param opts? vim.keymap.set.Opts
---
---Refer to <https://microsoft.github.io/language-server-protocol/specification/#snippet_syntax>
---for the specification of valid body.
function vim.snippet.add(trigger, body, opts)
vim.keymap.set("ia", trigger, function()
-- If abbrev is expanded with keys like "(", ")", "<cr>", "<space>",
-- don't expand the snippet. Only accept "<c-]>" as trigger key.
local c = vim.fn.nr2char(vim.fn.getchar(0))
if c ~= "" then
vim.api.nvim_feedkeys(trigger .. c, "i", true)
return
end
vim.snippet.expand(body)
end, opts)
end
With this, you can add custom snippets with vim.snippet.add()
and use <c-]>
keymap to expand the snippets.
Example of some lua function definition snippets:
vim.snippet.add(
"fn",
"function ${1:name}($2)\n\t${3:-- content}\nend",
{ buffer = 0 }
)
vim.snippet.add(
"lfn",
"local function ${1:name}($2)\n\t${3:-- content}\nend",
{ buffer = 0 }
)
put this in after/ftplugin/lua.lua
So when you type fn<c-]>
in lua file, it will be expanded to this:
(|...|
is a cursor)
function |name|()
-- content
end
tree-sitter plugins & setups
nvim-treesitter
Long ago, it was near imposible to use tree-sitter parsers in Neovim without nvim-treesitter because most tree-sitter APIs lived in nvim-treesitter. But nowadays, those APIs are included in Neovim core, so you can use tree-sitter parsers without nvim-treesitter.
You can attach tree-sitter parser to current buffer with vim.treesitter.start()
.
vim.api.nvim_create_autocmd("FileType", {
callback = function(ev)
pcall(vim.treesitter.start, ev.buf)
end
})
We use pcall
here because vim.treesitter.start
may fail if no parser is registered for the filetype of the current buffer.
Install tree-sitter parser manually
:TSInstall
command in nvim-treesitter clones Parser's git repository and built the parser from it. Some tree-sitter parsers include query statements (highlight.scm, fold.scm, etc.) related to the parser in the repository, but some parts may not match Neovim's spec, so the nvim-treesitter plugin includes query statements for all parsers.
This is based on the
master
branch of nvim-treesitter. Newmain
branch will not include those query files.
Fortunately, as of this writing, most of the tree-sitter parsers are available on luarocks.org, so you can easily install the tree-sitter parser package for Neovim using [luarocks]. These luarocks packages (aka. rocks) also include various queries (mostly taken from the nvim-treesitter repository) required to use each parser.
luarocks \
--lua-version=5.1 \
--tree=$HOME/.local/share/nvim/rocks \
install tree-sitter-rust
replace
--tree
option to$HOME/AppData/Local/nvim-data/rocks
if you are on Windows
With this script, you can install tree-sitter-rust parser and its query files in $HOME/.local/share/nvim/rocks/lib/luarocks/rock-5.1/tree-sitter-rust/0.0.27-1
where 0.0.27
is the installed version. You can add this path to 'packpath'
and Neovim will recognize and register the parser with bundled queries.
# create target packpath directory (`treesitter` is arbitrary)
mkdir -p $HOME/.local/share/nvim/site/pack/treesitter/start
# symlink installed rock to packpath
ln -sf $HOME/.local/share/nvim/rocks/lib/luarocks/rocks-5.1/tree-sitter-rust/0.0.27-1 $HOME/.local/share/nvim/site/pack/treesitter/start/tree-sitter-rust
You can use :checkhealth vim.treesitter
to see if installed parser is registered without problem.
Some tree-sitter parsers requires other packages to work properly. For example,
tree-sitter-javascript
andtree-sitter-typescript
depend ontree-sitter-ecma
. You don't need to care about this in installation step as luarocks will handle dependencies automatically but you should manually put those inpackpath
Extra IDE features
Folding
Neovim has builtin folding method using tree-sitter. You can use tree-sitter provided folding with following options.
vim.o.foldenable = true
vim.o.foldlevel = 99
vim.o.foldlevelstart = 99
vim.o.foldmethod = "expr"
vim.o.foldexpr = "v:lua.vim.treesitter.foldexpr()"
For related keymaps, please refer to :h folds
.
Commenting
Neovim includes commenting support since v0.10. (:h commenting
)
File Tree
Yes, you read it correct. Neovim does have builtin file tree view. Well it is not a side bar style file tree, but you can see folders in tree view.
Netrw, which is one of first-party plugins providing ability to open a folder in vim actually has multiple display styles.
Try open a directory with neovim and press i
three times.
Or put this somewhere in your config and open a directory.
vim.g.netrw_list_style = 3
You can press SHIFT_ENTER
to toggle each folders.
Check :h netrw
for more info.
You might already have some plugins
It is extremely rare but some system-wide softwares even ship with vim plugins.
fzf (fuzzy finding plugin)
One example for this is fzf. Which contains vim plugin in its main repository.
homebrew
vim.opt.runtimepath:append("/opt/homebrew/opt/fzf")
AUR
If you installed fzf from AUR, system package managers will automatically setup vim plugin as system-wide config.
manual install from source
-- replace `~/.fzf` to path where you cloned the fzf
vim.opt.runtimepath:append("~/.fzf")
Don't expect Vim to be like VSCode
If you learn a new language, don't expect it to behave like the languages you've used before - theprimeagen
Installing plugins
Ok, maybe not using any plugins at all is too much.
Then how to avoid verbose configuratino
1. Use plugins with less setup steps
There are few plugins that don't require manual setup.
Just install with your favorate plugin manager.
No need to run require("my-plugin").setup()
call in your config.
You don't have to care about lazy-loading because plugins can do that better.
For example, [rest.nvim] ships with lazy-loading and no setup calls. Installing this plugin is as simple as adding single line in your plugin manager config:
require("lazy").setup({
spec = {
"rest-nvim/rest.nvim", -- add this line
},
})
2. Use plugins with rockspec support
Plugin managers like lazy.nvim and rocks.nvim supports rockspec. If the plugin
Conclusion
So far we've looked at how to set up Neovim in its purest form, without relying on any external plugins.
Should I use plugins?
YES.