It has been a while since I last shared my Neovim configuration. Over time, I've made several adjustments to enhance my workflow. Recently, I decided to streamline my setup by removing some plugins and replacing them with built-in alternatives. In this post, I'll walk you through the changes I've made and my current Neovim configuration.
Until now, I have used lazy.nvim
to manage my plugins. While lazy.nvim
is an
excellent plugin manager, I wanted to simplify my setup by using the
built-in plugin manager.
Adding plugins with the built-in plugin manager is straightforward. We can do
this by calling vim.pack.add()
. For example, to add my favorite color scheme,
catppuccin
, we can simply include the following lines in our init.lua
file:
vim.pack.add({
{ src = "https://github.com/catppuccin/nvim", name = "catppuccin" },
}, { load = true })
require("catppuccin").setup({...})
vim.cmd.colorscheme("catppuccin")
While exploring the built-in plugin manager, I found this
Reddit post
about implementing lazy loading. For instance, I'm using CopilotChat.nvim
for
AI assistance. To load this plugin only when needed, we can use the following
configuration:
vim.keymap.set("n", "<leader>co", "<cmd>CopilotChatOpen<cr>")
vim.api.nvim_create_autocmd("CmdUndefined", {
group = vim.api.nvim_create_augroup( "lazy-load-copilotchat", { clear = true }),
pattern = { "CopilotChat*" },
callback = function()
vim.pack.add({
{ src = "https://github.com/nvim-lua/plenary.nvim", },
{ src = "https://github.com/CopilotC-Nvim/CopilotChat.nvim", name = "CopilotChat", },
}, { load = true })
require("CopilotChat").setup({...})
end,
once = true,
})
This will load the plugin only when we use the :CopilotChatOpen
command or any
other command provided by the plugin.
Since its release, I've been using the
snacks.nvim
explorer.
I primarily use it to switch between files in the same directory and to copy,
move, rename, and delete files. I accomplish these tasks with the following
keymaps:
vim.keymap.set("n", "<leader>ee", function() local dir = vim.fn.expand("%:.:h") if dir == "." or dir == "" then return ":edit " end return ":edit " .. vim.fn.expand("%:.:h") .. "/" end, { expr = true })
vim.keymap.set("n", "<leader>ec", function() return ":!cp " .. vim.fn.expand("%:.") .. " " .. vim.fn.expand("%:.") end, { expr = true })
vim.keymap.set("n", "<leader>em", function() return ":!mv " .. vim.fn.expand("%:.") .. " " .. vim.fn.expand("%:.") end, { expr = true })
vim.keymap.set("n", "<leader>er", function() return ":!rm " .. vim.fn.expand("%:.") end, { expr = true })
vim.keymap.set("n", "<leader>ey", function() vim.fn.setreg("+", vim.fn.expand("%:.")) end)
My go-to plugin for opening files has been the
snacks.nvim
picker.
However, after reading this
Reddit post,
I wanted to explore the built-in capabilities of Neovim for this purpose.
With the Neovim nightly version, we can use the findfunc
option to define a
custom function for finding files, the wildtrigger()
function to initiate
wildcard expansion in the command line, and the matchfuzzy()
function to
enable fuzzy matching.
To use fd
for finding files with the :find
command and to fuzzy match these files with the provided argument, we can add
the following code to our init.lua
file:
if vim.fn.executable("fd") == 1 then
function _G.fd_find_files(cmdarg, _)
local fnames = vim.fn.systemlist("fd --full-path --hidden --color never --type f --exclude .git")
if #cmdarg == 0 then
return fnames
else
return vim.fn.matchfuzzy(fnames, cmdarg)
end
end
vim.opt.findfunc = "v:lua.fd_find_files"
end
To automatically enable autocompletion when typing the :find
command, we can
add the following autocmd
:
vim.api.nvim_create_autocmd({ "CmdlineChanged", "CmdlineLeave" }, {
pattern = { "*" },
group = vim.api.nvim_create_augroup( "cmdline-autocompletion", { clear = true }),
callback = function(ev)
local function should_enable_autocomplete()
local cmdline_cmd = vim.fn.split(vim.fn.getcmdline(), " ")[1]
return cmdline_cmd == "find"
end
if ev.event == "CmdlineChanged" and should_enable_autocomplete() then
vim.opt.wildmode = "noselect:lastused,full"
vim.fn.wildtrigger()
end
if ev.event == "CmdlineLeave" then
vim.opt.wildmode = "full"
end
end,
})
Other features I utilized from the snacks.nvim
picker include finding recently
used files, sending found files to the quickfix list, locating buffers, and
identifying marks. I achieved these functionalities with custom functions and
keymaps, which can be found in my
dotfiles repository.
The last feature I used from snacks.nvim
was searching through files. To
replace this functionality, I decided to use the built-in :grep
command along
with rg
. To set this up, we can add
the following configuration to our init.lua
file:
if vim.fn.executable("rg") == 1 then
vim.opt.grepprg = "rg --vimgrep --smart-case --hidden --color=never --glob='!.git'"
vim.opt.grepformat = "%f:%l:%c:%m"
end
Together with the following keymaps, I replicated the search functionality I had
with snacks.nvim
and added some useful search commands. These include
searching in the directory of the current file, searching in the current file,
searching for the word under the cursor, searching for visually selected text,
and searching for todo comments like TODO
, FIXME
, and BUG
. The last keymap
also replaces the
todo-comments.nvim
plugin for
me.
vim.keymap.set("n", "<leader>ss", ":silent grep!<space>")
vim.keymap.set("n", "<leader>sc", function() return ":silent grep! --glob='" .. vim.fn.expand("%:.:h") .. "/**' " end, { expr = true })
vim.keymap.set("n", "<leader>s/", function() return ":silent grep! --glob='" .. vim.fn.expand("%:.") .. "' " end, { expr = true })
vim.keymap.set("n", "<leader>sw", ":silent grep!<space><c-r><c-w>")
vim.keymap.set("v", "<leader>sv", 'y:silent grep!<space><c-r>"')
vim.keymap.set("n", "<leader>st", ":silent grep! -e='todo:' -e='warn:' -e='info:' -e='xxx:' -e='bug:' -e='fixme:' -e='fixit:' -e='bug:' -e='issue:'<cr>")
With the recently added support for
inline completion in Neovim's
built-in LSP client, I decided to remove the
copilot.lua
plugin and use the
@github/copilot-language-server
instead. Along with the following autocmd
, I can now utilize the inline
completion feature provided by GitHub Copilot. I also set up a keymap to accept
suggestions with <c-cr>
.
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(event)
local client = vim.lsp.get_client_by_id(event.data.client_id)
local buffer = event.buf
if client then
if client:supports_method( vim.lsp.protocol.Methods.textDocument_inlineCompletion) then
vim.lsp.inline_completion.enable(true)
vim.keymap.set("i", "<c-cr>", function()
if not vim.lsp.inline_completion.get() then
return "<c-cr>"
end
end, {
expr = true,
replace_keycodes = true,
})
end
end
end,
})
I also added the efm-langserver to
replace nvim-lint
and
conform.nvim
for linting and
formatting. You can find the configuration for efm-langserver
in my
dotfiles.
While replacing conform.nvim
, I also came across this
blog post
on implementing formatting using autocmd
. I tried this for golangci-lint
,
but then I decided to use efm-langserver
and golangci_lint_ls
language
server instead.
local function run_golangcilint()
vim.fn.jobstart({
"golangci-lint",
"run",
"--output.json.path=stdout",
"--output.text.path=",
"--output.tab.path=",
"--output.html.path=",
"--output.checkstyle.path=",
"--output.code-climate.path=",
"--output.junit-xml.path=",
"--output.teamcity.path=",
"--output.sarif.path=",
"--issues-exit-code=0",
"--show-stats=false",
}, {
stdout_buffered = true,
on_stdout = function(_, data)
local output = vim.trim(table.concat(data, "\n"))
local ns = vim.api.nvim_create_namespace("golangcilint")
vim.diagnostic.reset(ns)
if output == "" then
return
end
local decoded = vim.json.decode(output)
if decoded["Issues"] == nil or type(decoded["Issues"]) == "userdata" then
return
end
local severities = {
error = vim.diagnostic.severity.ERROR,
warning = vim.diagnostic.severity.WARN,
refactor = vim.diagnostic.severity.INFO,
convention = vim.diagnostic.severity.HINT,
}
local diagnostics = {}
for _, item in ipairs(decoded["Issues"]) do
if vim.fn.expand("%") == item.Pos.Filename then
local sv = severities[item.Severity] or severities.warning
table.insert(diagnostics, {
lnum = item.Pos.Line > 0 and item.Pos.Line - 1 or 0,
col = item.Pos.Column > 0 and item.Pos.Column - 1 or 0,
end_lnum = item.Pos.Line > 0 and item.Pos.Line - 1 or 0,
end_col = item.Pos.Column > 0 and item.Pos.Column - 1 or 0,
severity = sv,
source = item.FromLinter,
message = item.Text,
})
end
vim.diagnostic.set(ns, 0, diagnostics, {})
vim.diagnostic.show()
end
end,
})
end
vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost" }, {
group = vim.api.nvim_create_augroup("linting-go", { clear = true }),
callback = function(opts)
if vim.bo[opts.buf].filetype == "go" then
run_golangcilint()
end
end,
})
Until now, I've been using blink.cmp
.
While it is a great plugin, I wanted to explore Neovim's
built-in completion capabilities.
To enable the built-in completion, we can add the following configuration to our
LspAttach
autocmd:
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(event)
local client = vim.lsp.get_client_by_id(event.data.client_id)
local buffer = event.buf
if client then
if client:supports_method(vim.lsp.protocol.Methods.textDocument_completion) then
vim.lsp.completion.enable(true, client.id, buffer, { autotrigger = true })
vim.keymap.set("i", "<c-space>", function() vim.lsp.completion.get() end)
end
end
end,
})
This will request completions when a trigger character is typed or when Ctrl
+
Space
is pressed. For me, this is more than enough since I primarily use
autocompletion results from the LSP, rather than the other sources I had with
blink.cmp
, such as snippets, buffer, or path completions.
For reviewing pull requests, I used to rely on
diffview.nvim
. However, since I
already had the ability to view changes in the current file with
gitsigns.nvim
, using another
plugin solely for pull request reviews seemed unnecessary. Therefore, I created
my own command, :GitDiff
, to utilize gitsigns.nvim
for this purpose.
vim.api.nvim_create_user_command("GitDiff", function(opts)
if #vim.fn.split(opts.args, " ") ~= 2 then
return
end
local base = vim.fn.split(opts.args, " ")[1]
local head = vim.fn.split(opts.args, " ")[2]
local result = vim.system({ "git", "merge-base", base, head }):wait()
if result.code ~= 0 then
return
end
local commit = vim.fn.trim(result.stdout)
local gitsigns = require("gitsigns")
gitsigns.change_base(commit, true)
gitsigns.setqflist("all")
end, { nargs = "*" })
The :GitDiff
command requires two arguments: the base branch and the pull
request branch. It identifies the common ancestor commit of both branches and
uses this commit as the base for gitsigns.nvim
. The command then populates the
quickfix list with all changes between the two branches.
I'm actively using gh-dash and have added a custom keymap to automatically trigger the command when opening a pull request.
keybindings:
prs:
- name: diff (nvim)
key: D
command: |
cd {{.RepoPath}} && git checkout {{.BaseRefName}} && git pull && gh pr checkout {{.PrNumber}} && nvim -c ":GitDiff {{.BaseRefName}} {{.HeadRefName}}"
With the current Neovim nightly version, I have successfully replaced several plugins with built-in alternatives. My setup is now more minimal, and I can accomplish everything I need without relying on as many plugins. The plugins I am still using are:
catppuccin
nvim-treesitter
helm-ls.nvim
lualine.nvim
gitsigns.nvim
multicursor.nvim
CopilotChat.nvim
I hope you enjoyed the blog post. You can find my complete Neovim configuration in my dotfiles repository and the updated Vim cheatsheet is also available here 🙂.