How to get human rights in Neovim without plugins
17 July, 2024
Introduction
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 TreeSitter?
- 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.
This article is based on my PoC config project: NativeVim
Replacing plugins
LSP stuffs
nvim-lspconfig
Let's start with the most famous one.
The Nvim LSP client does not live here. This is only a collection of LSP configures. -- nvim-lspconfig README
As indicated in the README, nvim-lspconfig, as its name suggests, is a plugin that provides default config values for Language Server.
Even without nvim-lspconfig, you can initiate connect Language Server to Neovim using the vim.lsp.start()
API.
-- start the LSP and get the client id
-- it will re-use the running client if one is found matching name and root_dir
-- see `:h vim.lsp.start()` for more info
vim.lsp.start({
name = "lua-language-server",
cmd = { "lua-language-server" },
root_dir = vim.fs.root(0, { ".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml", "selene.toml", "selene.yml", ".git" }),
})
And you can make an AutoCommand to run each Language Servers in specific filetypes.
---@type table<string, vim.lsp.ClientConfig>
local servers = {
lua_ls = {
name = "lua-language-server",
cmd = { "lua-language-server" },
root_dir = vim.fs.root(0, { ".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml", "selene.toml", "selene.yml", ".git" }),
filetypes = { "lua" },
},
-- add more servers here
}
local group = vim.api.nvim_create_augroup("UserLspStart", { clear = true })
for name, config in pairs(servers) do
vim.api.nvim_create_autocmd("FileType", {
group = group,
pattern = config.filetypes,
callback = function (ev)
vim.lsp.start(servers[name], { bufnr = ev.buf })
end,
})
end
With nvim-lspconfig, you only need to write this line and nvim-lspconfig will set all default configs for you.
require("lspconfig").lua_ls.setup()
mason.nvim
mason.nvim is a package manager inside Neovim which can install tools like Language Servers, Debug Servers, Formatters and Linters.
In other words, if you install the tool manually, mason.nvim is not necessary. For example, typescript-language-server can be installed as follows.
# npm
npm install -g typescript-language-server
(you can use whatever package manager you want instead of npm
of course)
default LSP keymaps/options
Neovim sets various LSP related keymaps and options on LspAttach
event by default. You can see full list with :h lsp-defaults
.
Here is quick overview of default keymaps:
NORMAL MODE
K : hover
grn : rename
gra : code action
grr : references
CTRL-] : definition
CTRL-W_] : definition in new window
CTRL-W_} : definition in preview window
VISUAL MODE
gq : format
INSERT MODE
CTRL-S : signature help
CTRL-X_CTRL-O : completion
Completion
nvim-cmp
Some surprising facts:
- Vim natively supports completion and completion UI. (
:h ins-completion
) - There is user-configurable completion source for Vim. (
:h 'omnifunc'
) - Neovim provides completion source from LSP. (
:h vim.lsp.omnifunc()
) - Neovim provides completion source for TreeSitter Nodes or Queries. (
:h vim.treesitter.query.omnifunc()
) - Neovim has
fuzzy
field in'completeopt'
option since v0.11. (:h 'completeopt'
)
cmp-nvim-lsp
So if you make LspAttach
AutoCommand like below, you can use LSP-provided completion source with i_CTRL-X_CTRL-O
keymap (press <c-x><c-o>
in Insert Mode.)
vim.api.nvim_create_autocmd("LspAttach" {
callback = function (ev)
vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc"
-- ...
end,
})
This AutoCommand is one of defaults in Neovim since v0.10.
However, we can't say that this alone properly utilizes the autocompletion provided by LSP. Since omnifunc does not provide functions 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.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc"
-- ...
end,
})
When 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. modifying part of the code like auto-import, or expanding a snippet)
cmp-path
Vim has a built-in filename completion. (:h i_CTRL-X_CTRL-F
)
[!NOTE] This is based on where
nvim
is executed, not current filepath.
cmp-buffer
Vim has a built-in buffer word completion.
:h i_CTRL-X_CTRL-P
:h i_CTRL-X_CTRL-N
:h i_CTRL-P
:h i_CTRL-N
Why use nvim-cmp then?
- Can gather multiple sources and display them at once
- Supports more diverse autocomplete sources
- Provides more diverse UI customization options
- Provides more sorting options
Snippet
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
Why use nvim-snippets then?
nvim-snippets supports snippets in json format (compatible with VSCode,) and can be registered as an completion source for nvim-cmp.
Why use Luasnip then?
Luasnip supports defining snippets in lua on top of that. So users can make more complex snippets.
TreeSitter
nvim-treesitter
Long ago, it was near imposible to use TreeSitter parsers in Neovim without nvim-treesitter because most TreeSitter APIs lived in nvim-treesitter. But nowadays, those APIs are included in Neovim core, so you can use TreeSitter parsers without nvim-treesitter.
You can attach TreeSitter parser to current buffer with vim.treesitter.start()
.
vim.api.nvim_create_autocmd("FileType", {
callback = function(ev)
pcall(vim.treesitter.start)
end
})
We use pcall
here because vim.treesitter.start
may fail if no parser is registered for the filetype of the current buffer.
Install TreeSitter parser manually
:TSInstall
command in nvim-treesitter clones Parser's git repository and built the parser from it. Some TreeSitter 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.
[!INFO] 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 TreeSitter parsers are available on luarocks.org, so you can easily install the TreeSitter 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 --dev \
tree-sitter-rust
[!NOTE] 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/scm-1
path. You can add this path to 'runtimepath'
and Neovim will recognize the rust parser and needed query files.
vim.opt.runtimepath:append(vim.fs.joinpath(vim.fn.stdpath("data") --[[@as string]], "nvim", "rocks", "lib", "luarocks", "rocks-5.1", "tree-sitter-rust", "scm-1"))
You can use :checkhealth vim.treesitter
to see if installed parser is registered without problem.
[!CAUTION] Some TreeSitter parsers requires other packages to work properly. For example,
tree-sitter-javascript
andtree-sitter-typescript
depend ontree-sitter-ecma
.
Fold
Neovim has built-in folding method using TreeSitter. You can use TreeSitter provided fold 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()"
Comment
Neovim has default comment support since v0.10. (:h commenting
)
Then why should I need those plugins?
Most of them provide better performance or more various features than Neovim's native solutions. Some plugins are valuable just for their abstraction.
- You won't want to write all those configs to setup new Language Server or TreeSitter parser in your config.
- You might want more complex features than Neovim's built-in functionalities provide.
- You might even want features that doesn't exist in Neovim from core.
- Some Neovim's built-in features can be slow.
These are all situations where plugins take place.
If you find better way of doing some abstractions or you made some cool new features, you can make that config as a standalone plugin. That's how Neovim's plugin community has grown.
Conclusion
You can still have basic functionalities needed for code editing only with native Neovim APIs. I won't recommend you using Neovim without plugins, but now you know that it is kinda possible.