Making my Nvim Feel More Like Helix with Mini.nvim
I've been rewriting my nvim config to align with the features I am enjoying in Helix: simple TS-aware movements and uniform LSP-actions.
I haven't messed with my nvim config for quite a while… the last time I wrote about a plugin change was five years ago!
Changes
Basically four objectives:
- Update and replace plugins with TreeSitter and LSP aware versions.
- Change keybindings to match Helix where it doesn't break Vim's paradigm.
- Find some LSP-aware text-objects and motions.
- Simplify wherever I can.
I wound up removing everything, and slowly re-adding plugins and bindings as I missed functionality.
I was in the middle of this, when @folke pointed out mini.nvim. Mini provides a slew of useful, well written, TS and LSP aware lua modules that are really straightforward to configure.
This madlad wants vim to work like I want it to work. 👏
I wound up replacing a lot with mini.nvim modules:
- EasyAlign -> mini.align
- flash.nvim -> mini.jump
- lualine -> mini.statusline
- nvim-autopairs -> mini.pairs
- nvim-web-devicons -> mini.icons
- vim-sandwich -> mini.surround
- vim-unimpaired -> mini.bracketed
It's wild to me that I've taken unimpaired out of my nvim config. It's been there since nearly the very beginning… but the reason is Treesitter. Because @echasnovski is prioritizing Treesitter, I get consistent, useful Treesitter motions that go with all the others.
I removed the following plugins because they weren't working, deprecated… of I just don't use them anymore.
- completion-nvim
- git-messenger-vim
- nvim-ts-autotag
- plenary-nvim
- popup-nvim (noice.nvim)
- trouble-nvim
- vim-abolish
- vim-elixir
- vim-fetch
- vim-gist
- vim-projectionist
- vim-repeat
- webapi-vim
I really like mini.nvim's style. You don't have to go all-in like I did, they are so well documented and built out… you probably will.
I also spent some time rewriting my lsp configuration such the the bindings are nearly the same as I have become used to in Helix, and instrumenting those bindings using mini.clue.
Walkthrough
I know that not everyone who reads this is going to want to use nix to manage their dotfiles, so I'll pull it out of nix just to highlight some things.
Here's the final config file (after nix is done with it and with heavy commentary!)
At the top I load my vimrc which has my main settings that are backwards compatible, then I load all the neovim specific options and changes in an init.lua.
-- Load .vimrc
vim.cmd([[runtime .vimrc]])
-- Neovim specific settings
vim.o.icm = 'split'
vim.opt.foldtext = "v:lua.vim.treesitter.foldtext()"
vim.o.showmode = false
-- Use rg
vim.o.grepprg = [[rg --glob "!.git" --no-heading --vimgrep --follow $*]]
vim.opt.grepformat = vim.opt.grepformat ^ { "%f:%l:%c:%m" }
-- Set up pretty unicode diagnostic signs
vim.fn.sign_define("DiagnosticSignError", {text = "", hl = "DiagnosticSignError", texthl = "DiagnosticSignError", culhl = "DiagnosticSignErrorLine"})
vim.fn.sign_define("DiagnosticSignWarn", {text = "", hl = "DiagnosticSignWarn", texthl = "DiagnosticSignWarn", culhl = "DiagnosticSignWarnLine"})
vim.fn.sign_define("DiagnosticSignInfo", {text = "", hl = "DiagnosticSignInfo", texthl = "DiagnosticSignInfo", culhl = "DiagnosticSignInfoLine"})
vim.fn.sign_define("DiagnosticSignHint", {text = "", hl = "DiagnosticSignHint", texthl = "DiagnosticSignHint", culhl = "DiagnosticSignHintLine"})
Then I set some keymaps that I've been using for years that aren't related to any plugins.
-- Make <Tab> work for snippets
vim.keymap.set({ 'i', 's' }, '<Tab>', function()
if vim.snippet.active({ direction = 1 }) then
return '<cmd>lua vim.snippet.jump(1)<cr>'
else
return '<Tab>'
end
end, { expr = true })
-- Covenience macros
-- fix ellipsis: "…" -> "…"
vim.keymap.set('n',
'<leader>fe',
"mc:%s,\\.\\.\\.,…,g<CR>:nohlsearch<CR>`c",
{noremap = true, silent = true, desc = "… -> …"})
-- fix spelling: just an easier finger roll on 40% keyboard
vim.keymap.set('n',
'<leader>fs',
'1z=',
{noremap = true, silent = true, desc = "Fix spelling under cursor"})
The plugins are all loaded through nix, but we'll go through the configured ones here. Nix puts some in vim's paths that don't show up in the config.
First, my trusty zenbones color scheme.
vim.g.zenbones = {
solid_line_nr = true,
solid_vert_split = true,
}
vim.cmd [[color zenbones]]
Then I have a plugin previously that follows the OS light/dark scheme and works across operating systems:
require('auto-dark-mode').setup({
update_interval = 1000,
set_dark_mode = function()
vim.api.nvim_set_option('background', 'dark')
end,
set_light_mode = function()
vim.api.nvim_set_option('background', 'light')
end,
})
A plugin that uses virtualtext to show colors in CSS files:
require'nvim-highlight-colors'.setup({
render = 'virtual',
enable_tailwind = true
})
Oil.nvim is a re-think of vinegar.vim which sits on top of netrw to make interacting with files a bit easier.
require("oil").setup({
view_options = {
show_hidden = true
}
})
vim.keymap.set("n", "-", require("oil").open, { desc = "Open parent directory" })
Display git status in signcolumn:
require('gitsigns').setup()
Highlight TODO style comments and bindings to throw them into the :quickfix list.
require("todo-comments").setup()
A custom function for a minimal writing environment using zen-mode that emulates pencil.vim, but isn't broken. 🤷
-- I write prose in markdown, all the following is to help with that.
function _G.toggleProse()
require("zen-mode").toggle({
window = {
backdrop = 1,
width = 80
},
plugins = {
gitsigns = { enabled = true },
tmux = { enabled = true },
kitty = {
enabled = true,
},
},
on_open = function()
if (vim.bo.filetype == "markdown" or vim.bo.filetype == "telekasten") then
vim.cmd 'set so=999'
vim.cmd 'set nornu nonu'
vim.cmd 'set wrap'
vim.cmd 'set linebreak'
vim.cmd 'set colorcolumn=0'
vim.keymap.set('n', 'j', 'gj', {noremap = true})
vim.keymap.set('n', 'k', 'gk', {noremap = true})
end
end,
on_close = function()
vim.cmd 'set so=3'
vim.cmd 'set rnu'
if (vim.bo.filetype == "markdown" or vim.bo.filetype == "telekasten") then
vim.cmd 'set nowrap'
vim.cmd 'set nolinebreak'
vim.cmd 'set colorcolumn=80'
end
vim.keymap.set('n', 'j', 'j', {noremap = true})
vim.keymap.set('n', 'k', 'k', {noremap = true})
end
})
end
vim.keymap.set(
'n',
'<localleader>m',
':lua _G.toggleProse()<cr>',
{noremap = true, silent = true, desc = "Toggle Writing Mode"}
)
This plugin renders markdown in a sort of styled way in the terminal… which as many doc sources are in markdown makes some of the LSP popups have some style for free!
require("render-markdown").setup()
Telescope for finding files and picking stuff:
vim.keymap.set('n', '<space>/', "<cmd>lua require('telescope.builtin').live_grep()<cr>", {noremap = true, silent = true, desc = "Live Grep"})
vim.keymap.set('n', '<space>f', ":lua require('telescope.builtin').find_files({ find_command = {'rg', '--files', '--hidden', '-g', '!.git' }})<cr>", {noremap = true, silent = true, desc = "Live Grep"})
vim.keymap.set('n', '<space>b', ":lua require('telescope.builtin').buffers()<cr>", {noremap = true, silent = true, desc = "Buffers"})
vim.keymap.set('n', '<space>z', ":lua require('telescope.builtin').find_files({prompt_title = 'Search ZK', shorten_path = false, cwd = '~/src/wiki'})<cr>", {noremap = true, silent = true, desc = "Wiki"})
Treesitter for all my grammars and syntax files.
require'nvim-treesitter.configs'.setup {
highlight = { enable = true, },
indent = { enable = true },
}
Tree-pairs makes % respect Treesitter nodes, which is great for languages with do -> end style syntax (looking at you ruby and elixir!)
require('tree-pairs').setup()
Treesitter Incremental selection lets me emulate most of Helix's feature to select up to the next tree-sitter scope. Highly recommended. I'm using Helix-like bindings. Most people use <cr> or v.
require'nvim-treesitter.configs'.setup {
incremental_selection = {
enable = true,
keymaps = {
init_selection = "<M-o>",
scope_incremental = "<M-O>",
node_incremental = "<M-o>",
node_decremental = "<M-i>",
},
},
}
And then mini.nvim is doing a ton of heavy lifting.
require('mini.ai').setup() -- a/i textobjects
require('mini.align').setup() -- aligning
require('mini.bracketed').setup() -- unimpaired bindings with TS
require('mini.comment').setup() -- TS-wise comments
require('mini.icons').setup() -- minimal icons
require('mini.jump').setup() -- fFtT work past a line
require('mini.pairs').setup() -- pair brackets
require('mini.statusline').setup() -- minimal statusline
require('mini.surround').setup({ -- surround
custom_surroundings = {
['l'] = { output = { left = '[', right = ']()'}}
}
})
local miniclue = require('mini.clue')
miniclue.setup({ -- cute prompts about bindings
triggers = {
{ mode = 'n', keys = '<Leader>' },
{ mode = 'x', keys = '<Leader>' },
{ mode = 'n', keys = '<space>' },
{ mode = 'x', keys = '<space>' },
-- Built-in completion
{ mode = 'i', keys = '<C-x>' },
-- `g` key
{ mode = 'n', keys = 'g' },
{ mode = 'x', keys = 'g' },
-- Marks
{ mode = 'n', keys = "'" },
{ mode = 'n', keys = '`' },
{ mode = 'x', keys = "'" },
{ mode = 'x', keys = '`' },
-- Registers
{ mode = 'n', keys = '"' },
{ mode = 'x', keys = '"' },
{ mode = 'i', keys = '<C-r>' },
{ mode = 'c', keys = '<C-r>' },
-- Window commands
{ mode = 'n', keys = '<C-w>' },
-- `z` key
{ mode = 'n', keys = 'z' },
{ mode = 'x', keys = 'z' },
-- Bracketed
{ mode = 'n', keys = '[' },
{ mode = 'n', keys = ']' },
},
clues = {
miniclue.gen_clues.builtin_completion(),
miniclue.gen_clues.g(),
miniclue.gen_clues.marks(),
miniclue.gen_clues.registers(),
miniclue.gen_clues.windows(),
miniclue.gen_clues.z(),
},
})
lsp_config is setup to give me a consistent interface, even the same leader keys and actions as Helix.
vim.keymap.set('n', '<space>d', vim.diagnostic.setloclist, {desc = "Add buffer diagnostics to the location list."})
-- Use LspAttach autocommand to only map the following keys
-- after the language server attaches to the current buffer
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('UserLspConfig', {}),
callback = function(ev)
-- Enable completion triggered by <c-x><c-o>
vim.bo[ev.buf].omnifunc = 'v:lua.vim.lsp.omnifunc'
-- Buffer local mappings.
-- See `:help vim.lsp.*` for documentation on any of the below functions
local opts = function(str)
return { buffer = ev.buf, desc = str }
end
vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts("Declaration"))
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts("Definition"))
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts("Implementation"))
vim.keymap.set('n', '<M-k>', vim.lsp.buf.signature_help, opts("Signature Help"))
vim.keymap.set('i', '<M-k>', vim.lsp.buf.signature_help, opts("Signature Help"))
vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, opts("Add Workspace Folder"))
vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, opts("Remove Workspace Folder"))
vim.keymap.set('n', '<space>wl', function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, opts("List Workspace Folders"))
vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, opts("Type Definition"))
vim.keymap.set('n', '<space>r', vim.lsp.buf.rename, opts("Rename Symbol"))
vim.keymap.set({ 'n', 'v' }, '<space>a', vim.lsp.buf.code_action, opts("Code Action"))
vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts("Buffer References"))
vim.keymap.set('n', '<localleader>f', function()
vim.lsp.buf.format { async = true }
end, opts("Format Buffer"))
end,
})
local lspconfig = require('lspconfig')
lspconfig.elixirls.setup { cmd = { "elixir-ls" } }
lspconfig.solargraph.setup({
settings = {
solargraph = {
diagnostics = true,
useBundler = true
}
}
})
lspconfig.nixd.setup {}
lspconfig.lua_ls.setup {
settings = {
Lua = {
runtime = {
version = 'LuaJIT',
path = vim.split(package.path, ';'),
},
diagnostics = { globals = {'vim', 'hs'}, },
workspace = {
library = {
[vim.fn.expand('$VIMRUNTIME/lua')] = true,
[vim.fn.expand('$VIMRUNTIME/lua/vim/lsp')] = true,
[vim.fn.expand('/Applications/Hammerspoon.app/Contents/Resources/extensions/hs/')] = true
},
},
},
},
}
lspconfig.markdown_oxide.setup{}
And that's it! 271, down from 441.
…
I've been bouncing back and forth, and so far I'm very pleased. Having all my plugins be Treesitter and lsp aware makes for a much more consistent interface, and mini.clue's little prompts reminds me that I have a lot of textobjects and actions that I often forget about.
It's also a bit snappier. It's much easier to see on my older intel mac, but less plugins == speed.
Thanks to mini.clue and some refactoring of bindings, the way I interact with the LSP is roughly the same muscle memory on Helix or nvim, so I have a choice whether to use Helix's interesting new modal model, or nvim's tried and true ecosystem.
Changelog
-
2024-09-17 18:59:12 -0500Add missing point
-
2024-09-17 16:48:36 -0500Change title
-
2024-09-17 15:59:58 -0500Mention speed
-
2024-09-17 15:59:19 -0500Article