id: | 8045ba6a-a9f4-4787-b259-ac29d54b3b2a |
tags: | neovim,lua,dotfiles,vim |
Read This First! Errata and Changes
Unfortunately, this large document contains errors and does not reflect my current Neovim configuration. While changes are expected over time (drift), the errors are unfortunate. What's more unfortunate is that I'm not sure where they are since they were fixed long ago and I neglected to update the blog post.
While this post does contain useful information, please take the configurations with a small grain of salt. Additionally, Neovim updates have changed some of the configurations or made them irrelevant (filetype, for example).
If you're interested in my current Neovim configuration, which is working properly, please refer to my dotfiles.
---
I decided to use the weekend (July 1, 2, 3) to rework my Neovim configuration: I would transition entirely from Vimscript to Lua.
♯Table of Contents
- Why Switch to Lua?
- Resources
- Package Manager
- Configuation Structure
- My Approach
- General Configuration
- Adding Key Mappings
- Setup Packer
- Color Schemes
- Digression: Using Packer
- Plugin: Telescope
- Plugin: Touble
- Plugin: vim-floaterm
- Plugin: lualine.nvim
- Plugin: bufferline.nvim
- Plugin: gitsigns.nvim
- Plugin: indent-blankline.nvim
- Plugin: nvim-treesitter
- Plugin: nvim-lspconfig
- Plugin: nvim-cmp
- Plugin: nvim-metals
- Plugin: nnn.nvim
- Plugin: todo-comments.nvim
- Plugin: filetype.nvim
- Example: Zig Support
- Closing Thoughts
♯Why Switch to Lua?
I decided to change from Vimscript to Lua for the following reasons:
- Because I could and it seemed interesting.
- Overhauling my configuration would provide me a chance to review every piece of configuration and make a keep/discard decision.
- I wanted to update my set of plugins anyway. There were new plugins that interested me, and some old (relative to my config) ones that I wanted to re-evaluate.
- I despised my multi-file setup and I'm pretty sure I did it wrong in the first place.
♯Resources
I leveraged the following documentation to learn what I needed to do:
I didn't bother reading about Lua despite never having written it. It's easy enough to pick up on the fly, and if you don't have a chunk of custom functions to port (something I do not have!) you likely won't hit any language related confusion.
♯Package Manager
While I previously used
vim-plug, I decided to switch
to packer.nvim as it's
written in and designed for Lua. I loved
vim-plug
and would use it again for a
Vimscript configuration.
♯Configuration Structure
One of my motivations was reworking my (bad) multi-file setup and adopting something reasonable. Step one was determining what my configuration would look like and what files I would be creating.
As a prerequisite, I recommend reading the Where to put Lua files section in [1].
I decided to leverage the following structure:
- ~/.config/nvim/init.lua would be an aggregator that imports other Lua files.
- My primary configuration would live in 4 files located in the
~/.config/nvim/lua
directory:
- general.lua: All standard Neovim configuration. Nothing for plugins.
- colorscheme.lua: Selection of my color scheme and any configuration required for that color scheme.
- keymap.lua: All key bindings. This includes plugin key bindings - keep them all in one place.
- packer_init.lua: Setup packages: packer configuration and all plugin imports.
- If any plugins needed extra configuration, that configuration would live in the ~/.config/nvim/lua/plugins directory. Each plugin would receive a dedicated file.
♯My Approach
After reading documentation and deciding on a file structure, I decided to use the following iterative approach to migrate from Vimscript to Lua:
- Delete my Vimscript files. Revert to an unmodified Neovim experience.
- Add
init.lua
andlua/general.lua
. Port my general configuration togeneral.lua
piece by piece while drawing on [2] as a reference. - Start Neovim and ensure the configuration appears to work (no errors, show the color column in the correct location).
- Add all non-plugin key mappings to make editing feel more natural.
- Import a color scheme using
packer
to learn how to use packer and wrap up my last remaining non-plugin file. - Add plugins one by one. I'll explain my ordering later.
♯General Configuration
This was an easy port. Essentially existing settings in
init.vim
are remapped to
vim.opt.{setting}
(with a couple of
globals).
I recommend referencing the Neovim Documentation for Options which explains what all of these do.
Minor Issues and Gotchas
- I had to "rename" some fields. Example:
set noswapfile
becameopt.swapfile = false
sincenoswapfile
is not a setting - it just acts upon the setting. colorcolumn
is a string, which confused me.- The
undodir
does not expand~
and will create a~
directory anywhere you save a file if you don't know this and attempt to set it. It took some head banging for me to figure this out. I eliminated the setting.
New Items
I adopted some changes from [2]:
- Enable mouse support. I figured it couldn't hurt to try. Not sure if I will bother keeping it.
- Disabled the intro.
Takeaways
This step helped me understand the available options better and forced me to
learn about the actual options in some cases (e.g. reading about
swapfile
rather than blindly setting
noswapfile
). I feel like my configuration is
more normalized as each setting is a single assignment.
init.lua
Note that all imports in this file are considered relative to the
lua
directory and do not require file
extensions.
require('general')
lua/general.lua
This is (roughly) the initial version that I used. This version adds a setting and fixes a bug.
local g = vim.g -- Global variables
local opt = vim.opt -- Set options (global/buffer/windows-scoped)
-----------------------------------------------------------
-- General
-----------------------------------------------------------
opt.mouse = 'a' -- Enable mouse support
opt.swapfile = false -- Don't use swapfile
opt.modelines = 0 -- Disable modelines
opt.encoding = 'utf-8' -- Set default encoding to UTF-8
-- Note that this setting is important for which-key. Also don't reduce it too
-- low, or the behavior will start getting wonky and always show which-key.
opt.timeoutlen = 500 -- Time in ms to wait for a sequnece to complete
opt.shortmess:remove("F"):append("c")
opt.wildignore = {
'*.class', '*.pyc', '*.swp', '*.o', '*.jar', '*/tmp/*', '*.zip',
'*.tar', '*.gz', '*.bz2', '*.xz', '*/.git/*', '*/.metals/*',
'*/.bloop/*', '*/.bsp/*', '*/node_modules/*'
}
-----------------------------------------------------------
-- Completion
-----------------------------------------------------------
-- menu = use a popup menu to show possible completions
-- menuone = show a menu even if there is only one match
-- noinsert = do not insert text for a match until user selects one
-- noselect = do not select a match from the menu automatically
opt.completeopt = 'menu,menuone,noinsert,noselect'
g.completion_enable_auto_popup = 1 -- Enable completions while typing
-----------------------------------------------------------
-- Neovim UI
-----------------------------------------------------------
opt.number = true -- Show line number
opt.cursorline = true -- Highlight the current line
opt.showcmd = true -- Show incomplete commands at the bottom
opt.ttyfast = true -- Rendering optimizations
opt.showmatch = true -- Highlight matching parenthesis
opt.colorcolumn = '80' -- Line length marker at 80 columns
opt.ruler = true -- Show row and column number
opt.signcolumn = 'yes' -- Always show the sign column
opt.foldmethod = 'marker' -- Enable folding (default 'foldmarker')
opt.splitright = true -- Vertical split to the right
opt.splitbelow = true -- Horizontal split to the bottom
opt.linebreak = true -- Wrap on word boundary
opt.termguicolors = true -- Enable 24-bit RGB colors
opt.laststatus = 3 -- Set global statusline
-----------------------------------------------------------
-- Tabs, indent
-----------------------------------------------------------
opt.tabstop = 4
opt.shiftwidth = 4
opt.softtabstop = 4
opt.expandtab = true
opt.smarttab = true
opt.autoindent = true
-----------------------------------------------------------
-- Memory, CPU
-----------------------------------------------------------
opt.hidden = true -- Enable background buffers
opt.lazyredraw = true -- Faster scrolling
opt.synmaxcol = 240 -- Max column for syntax highlight
opt.updatetime = 500 -- ms to wait for trigger an event
-----------------------------------------------------------
-- History
-----------------------------------------------------------
opt.history = 512
opt.undolevels = 128
opt.undofile = true
-----------------------------------------------------------
-- Search
-----------------------------------------------------------
opt.ignorecase = true
opt.smartcase = true
opt.incsearch = true
opt.showmatch = true
opt.hlsearch = true
-----------------------------------------------------------
-- Startup
-----------------------------------------------------------
-- Disable nvim intro
opt.shortmess:append "sI"
♯Adding Key Mappings
I have a small number of general key mappings, and a larger number for some of the plugins that I use. This section covers the basics of mapping keys and my basic key mappings.
Mapping Keys
The following function defines a key mapping with some default settings. Note
that mode
,
lhs
, and
rhs
are all strings.
local function map(mode, lhs, rhs, opts)
local options = { noremap = true, silent = true }
if opts then
options = vim.tbl_extend('force', options, opts)
end
vim.api.nvim_set_keymap(mode, lhs, rhs, options)
end
An example usage could be saving on
<leader>s
:
map('n', 's', ':up')
The mode is n
, for normal mode. The
lhs
is
<leader>s
-- I want to run the
rhs
when I press my leader key followed by
s
. The rhs
(what I want to do when I press the lhs
) is
the command to save my buffer followed by a carriage return to evaluate that
command.
noremap
Please refer to the
Neovim
map
Documentation (Partially copied):
Map the key sequence {lhs}
to
{rhs}
for the modes where the map command
applies. Disallow mapping of {rhs}
, to avoid
nested and recursive mappings. Often used to redefine a command.
It ensures that the mapping I create is non-recursive.
silent
Please refer to the
Neovim
map
Documentation (Partially copied):
To define a mapping which will not be echoed on the command line, add
"<silent>"
as the first argument.
Minor Issues and Gotchas
I only ran into one issue, and that was with leveraging
<Tab>
. Essentially it will not be
expanded in all contexts.
Essentially this mapping in Vimscript:
inoremap <expr> <Tab> pumvisible() ? "\<C-N>" : "\<Tab>"
Does not translate how you might expect, and will insert
\<Tab>
and
\<C-N>
literally.
If you're interested in a full explanation, this is covered (thankfully!) in
[1].
The short version is that using
vim.keymap.set
eliminates the issue and
automatically expands termcodes. Example:
-- Use Tab to jump to the next option in a popup menu
vim.keymap.set('i', '<Tab>', function()
return vim.fn.pumvisible() == 1 and '<C-N>' or '<Tab>'
end, {expr = true})
Takeaways
Mapping keys is pretty much the same. I like the structure that Lua provides, but I'm not feeling strongly about anything.
init.lua
require('general')
require('keymap')
lua/keymap.lua
Note that this snippet only includes the base key mappings since that is what I added in this step. Plugin key mappings will be added later. Neovim comes with a builtin LSP, so I decided to include those mappings in this section.
-----------------------------------------------------------
-- Define keymaps for Neovim and plugins.
-----------------------------------------------------------
local function map(mode, lhs, rhs, opts)
local options = { noremap = true, silent = true }
if opts then
options = vim.tbl_extend('force', options, opts)
end
vim.api.nvim_set_keymap(mode, lhs, rhs, options)
end
-- Change leader to a comma
vim.g.mapleader = ','
-----------------------------------------------------------
-- General
-----------------------------------------------------------
-- Clear search highlighting
map('n', '<leader>/', ':nohlsearch<CR>')
-- Save current buffer
map('n', '<leader>s', ':up<CR>')
-- Move to previous buffer
map('n', '<leader>bp', ':bp<CR>')
-- Move to next buffer
map('n', '<leader>bn', ':bn<CR>')
-- Delete current buffer
map('n', '<leader>bd', ':bd<CR>')
-- Close the quickfix window
map('n', '<leader>cq', ':ccl<CR>')
-- Use Tab to jump to the next option in a popup menu
vim.keymap.set('i', '<Tab>', function()
return vim.fn.pumvisible() == 1 and '<C-N>' or '<Tab>'
end, {expr = true})
-- Use Shift+Tab to jump to the previous option in a popup menu
vim.keymap.set('i', '<S-Tab>', function()
return vim.fn.pumvisible() == 1 and '<C-P>' or '<Tab>'
end, {expr = true})
-----------------------------------------------------------
-- LSP
-----------------------------------------------------------
map('n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>')
map('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>')
map('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>')
map('n', 'gds', '<cmd>lua vim.lsp.buf.document_symbol()<CR>')
map('n', 'gws', '<cmd>lua vim.lsp.buf.workspace_symbol()<CR>')
map('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>')
map('n', '<leader>cl', '<cmd>lua vim.lsp.codelens.run()<CR>')
map('n', '<leader>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>')
map('n', '<leader>rn', '<cmd>lua vim.lsp.buf.rename()<CR>')
map('n', '<leader>ca', '<cmd>lua vim.lsp.buf.code_action()<CR>')
map('n', '<leader>q', '<cmd>lua vim.lsp.diagnostic.set_loclist()<CR>')
map('n', '<leader>e', '<cmd>lua vim.lsp.diagnostic.open_float()<CR>')
map('n', '[c', '<cmd>lua vim.diagnostic.goto_prev { wrap = false }<CR>')
map('n', ']c', '<cmd>lua vim.diagnostic.goto_next { wrap = false }<CR>')
♯Setup Packer
As a sub-step before installing color schemes I decided to make sure that Packer was working properly. This step is essentially copy/paste from the packer.nvim documentation.
I will discuss the usage of Packer later - this is just concerned with setup.
Takeaways
Installing Packer and creating a base configuration is easy and works as
expected. Not a whole lot of difference from
vim-plug
.
Please note that getting Packer to work like I expected later became challenging because I didn't understand packer and was doing it wrong. For additional detail please read my digression on using packer.
Installation
Copied directly from the documentation (you should check it for the latest version and treat this as a brief reference):
git clone --depth 1 https://github.com/wbthomason/packer.nvim\
~/.local/share/nvim/site/pack/packer/start/packer.nvim
Verification
Open Neovim and run :PackerInstall
and
:PackerStatus
.
init.lua
require('packer_init')
require('general')
require('keymap')
packer_init.lua
Note that in each subsequent section I will only show additions to this file.
-- Automatically install packer
local fn = vim.fn
local install_path = fn.stdpath('data') .. '/site/pack/packer/start/packer.nvim'
if fn.empty(fn.glob(install_path)) > 0 then
packer_bootstrap = fn.system({
'git',
'clone',
'--depth',
'1',
'https://github.com/wbthomason/packer.nvim',
install_path
})
vim.o.runtimepath = vim.fn.stdpath('data') .. '/site/pack/*/start/*,' .. vim.o.runtimepath
end
-- Use a protected call so we don't error out on first use
local status_ok, packer = pcall(require, 'packer')
if not status_ok then
return
end
return require('packer').startup(function(use)
-- Plugin/package manager.
use 'wbthomason/packer.nvim'
end)
♯Color Schemes
Thankfully, this section isn't particularly interesting. Adding color schemes offered no surprises and worked first try.
Gotchas
I said that there were no surprises for me, but there is one thing you need to know that I should call out for this article: setting the color scheme requires running a vim command.
vim.cmd 'colorscheme <my_desired_color_scheme>'
Process
First, make the packer updates. Then open Neovim and run:
:PackerInstall
:PackerSync
Finally, make the remaining file changes and open Neovim to confirm the new color scheme has been applied.
Takeaways
This is another case where Lua just doesn't differ a whole lot from standard
Vimscript. At this point I have encountered my first use of
vim.cmd
and I'm starting to like the
require('x').setup { ... }
pattern and not
needing to bracket it off in Lua heredocs. Although the outcomes are rather
similar, I would say that the Lua development/update process is a bit smoother.
I think that part of it is that Lua just feels more "normal" to me.
init.lua
require('packer_init')
require('general')
require('colorscheme')
require('keymap')
lua/packer_init.lua
I import a few color schemes so that I don't forget the ones I'm not using, and in case I want to swap them around quickly.
use 'sainnhe/gruvbox-material'
use 'rebelot/kanagawa.nvim'
use 'marko-cerovac/material.nvim'
lua/colorscheme.lua
I'm using the material.nvim theme.
-- I use dark terminals and schemes, so use a dark background.
vim.opt.background = 'dark'
-- Settings for gruvbox-material
vim.g.gruvbox_material_palette = 'mix'
vim.g.gruvbox_material_background = 'hard'
vim.g.gruvbox_material_enable_bold = 1
-- Settings for material
vim.g.material_style = "palenight"
require('material').setup({
italics = {
comments = true, -- Enable italic comments
},
plugins = { -- Disable stuff that I just don't need.
neogit = false,
sidebar_nvim = false,
lsp_saga = false,
nvim_dap = false,
nvim_navic = false,
hop = false,
}
})
-- Set the color scheme
vim.cmd 'colorscheme material'
♯Digession: Using Packer
At this point, before addressing each plugin, I want to address Packer. The main call out is that it's important to read the README.
Why? This section of the README (partially copied/pasted here for reference) is key:
-- You must run this or `PackerSync` whenever you make changes to your plugin
-- configuration. Regenerate compiled loader file
:PackerCompile
-- Remove any disabled or unused plugins
:PackerClean
-- Clean, then install missing plugins
:PackerInstall
-- Clean, then update and install plugins
:PackerUpdate
-- Perform `PackerUpdate` and then `PackerCompile`
:PackerSync
These commands are important. I feel stupid now, but I kept running
:PackerInstall
and getting confused when
things didn't work. Remember:
You must run :PackerCompile or PackerSync whenever you make changes to your plugin configuration. Regenerate the compiled loader file.
If you don't recompile, your new plugins aren't going to work. Don't be me and skip this step. My general process for installing new plugins:
- Update
packer_init.lua
- Start Neovim in another window
:PackerInstall
:PackerSync
♯Plugin: Telescope
Telescope is a fuzzy finder over lists with a large number of integrations. I use it for opening files, swapping between buffers, selecting some commands (e.g. Metals), and for other plugin integrations. I should probably use LSP support more.
It's worth mentioning that I use telescope-fzf-native which provides a C fork of fzf (which I love) for Telescope.
Why Install This Plugin First?
Telescope doesn't have anything crazy going on, is easy to test, and is useful for working on a project with a bunch of files... like my Neovim configuration. I really wanted the file and buffer lookups before starting on any other plugins.
Why Telescope?
To answer "why" in general: I love a workflow backed by fuzzy searching. It helps me reduce context switching and I'm used to it. I started using Telescope in particular to try something new and because a lot of new plugins have support for it. Other plugins work great as well.
Testing the Installation
Open Neovim and press Ctrl+P
or
;
- a box should appears with a fuzzy finder
and preview window.
init.lua
Add the following after installing Telescope.
require('plugins/telescope')
lua/packer_init.lua
Telescope, like many plugins, depends onplenary.nvim
.
use {
'nvim-telescope/telescope.nvim',
requires = {
{ 'nvim-lua/plenary.nvim' }
}
}
use {
'nvim-telescope/telescope-fzf-native.nvim',
run = 'cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release && cmake --build build --config Release && cmake --install build --prefix build'
}
lua/plugins/telescope.lua
This file is small and could arguably be compressed into
packer_init.lua
, but for now I've opted to
keep it separate.
require('telescope').setup()
-- This is required to use FZF with Telescope.
require('telescope').load_extension('fzf')
lua/keymap.lua
I have a few custom bindings for Telescope. I use the file and buffer searches constantly.
-- Use ';' to search active buffers
map('n', ';', 'Telescope buffers')
-- Use Ctrl+P to show a list of files
map('n', '', 'Telescope find_files')
-- Live Grep
map('n', 'fg', 'Telescope live_grep')
-- Metals commands
map('n', 'fm', 'Telescope metals commands')
♯Plugin: Trouble
Trouble is:
A pretty list for showing diagnostics, references, telescope results, quickfix and location lists to help you solve all the trouble your code is causing.
It's a relatively recent find and I've found it handy.
Why?
I wanted to try leveraging Trouble and getting more used to using lists of output to streamline how I work. So far I would consider a minor win, but I also keep finding more ways to use it that are easy to pick up, such as listing TODOs.
Testing the Installation
Open Neovim and press ,xx
and Trouble should
open at the bottom. Press ,xx
to toggle it
closed.
lua/packer_init.lua
Trouble depends upon nvim-web-devicons, another common dependency. Keep in mind that you still need a patched font.
use {
'folke/trouble.nvim',
requires = 'kyazdani42/nvim-web-devicons',
config = function()
require('trouble').setup()
end
}
lua/keymap.lua
I primarily toggle Trouble and am still learning to use this plugin effectively.-- Show/hide Trouble
map('n', 'xx', 'TroubleToggle')
-- Show workspace diagnostics
map('n', 'xw', 'TroubleToggle workspace_diagnostics')
-- Show document diagnostics
map('n', 'xd', 'TroubleToggle document_diagnostics')
-- Show quickfix
map('n', 'xq', 'TroubleToggle quickfix')
-- Show local list
map('n', 'xl', 'TroubleToggle loclist')
-- Show LSP references
map('n', 'xr', 'TroubleToggle lsp_references')
♯Plugin: vim-floaterm
vim-floaterm provides a togglable floating terminal, and I find this plugin incredibly useful.
Why?
Embedding the Neovim terminal in a floating window is an easy and fast way to
access a terminal (something I do constantly) with minimal context switching. I
could just open another terminal in i3
and
use a different key combo to switch to it - sometimes I even do that in
conjunction with floaterm
! For quick items
and especially for leveraging multiple terminals I like keeping them
scoped to the project editor.
Testing the Installation
Open Neovim and press ,tt
to toggle open a
floating terminal. Press that sequence again to close the terminal.
init.lua
Add the following after installing Telescope.
require('plugins/floaterm')
lua/packer_init.lua
use 'voldikss/vim-floaterm'
lua/plugins/floaterm.lua
vim.g.floaterm_width = 0.8
vim.g.floaterm_height = 0.8
vim.g.floaterm_gitcommit = 'split'
lua/keymap.lua
I chose t
as my starting letter for
terminal
. Toggling a terminal is the most
common action, so doubling-up on the t
makes
it easy to execute. I highly recommend keeping bindings for creating new
terminals and using prev/next because it can be extremely useful. For example:
you can run tests in one terminal check/manage git in the next, etc.
-- Create a new floating terminal
vim.g.floaterm_keymap_new = 'tc'
-- Move to the previous terminal
vim.g.floaterm_keymap_prev = 'tp'
-- Move to the next terminal
vim.g.floaterm_keymap_next = 'tn'
-- Toggle the visibility of the floating terminal
vim.g.floaterm_keymap_toggle = 'tt'
♯Plugin: lualine.nvim
lualine.nvim is a
statusline plugin written in Lua. I was pleasantly surprised with the ease of
setup, and the correct coloring being provided by the
material
color scheme that I use.
I don't go too crazy with my status line and have a very vanilla setup.
If you aren't familiar with status line plugins, it adds some coloring and aesthetic appeal to the bar at the bottom of the Neovim instance. It also provides additional information like a basic Git status, the Git branch, and other possible options. It's worth reading the documentation to see if you want to customize this plugin for your own needs.
Why?
I find the additional information and visual cues useful, and they don't get in my way.
Testing the Installation
Open Neovim and you should see the status line in all its glory, with the correct colors.
init.lua
Add the following after installing lualine.nvim.
require('plugins/lualine')
lua/packer_init.lua
Another usage of nvim-web-devicons.
use {
'nvim-lualine/lualine.nvim',
requires = 'kyazdani42/nvim-web-devicons'
}
lua/plugins/lualine.lua
Note that this file will be modified later once Metals support is added. This configuration depends on the material theme, so you'll want to change that if you're using something different.
require('lualine').setup {
options = {
theme = 'material'
}
}
♯Plugin: bufferline.nvim
bufferline.nvim is a bufferline plugin for Neovim. It's opinionated and inspired by Doom Emacs.
If you aren't familiar with these plugins, this one essentially adds a bar to the top of your Neovim window that lists out all of the open buffers. It looks nice and has things like an icon indicating the file type and a Git status indicator.
Why?
I went without a bufferline for a long time. Honestly I could go back to it, but I'm not hitting any performance issues and I'm not hitting any screen issues and I'm not hitting any visual noise issues. The tabs aren't annoying, but they help me passively keep context. Sometimes I even look at them directly to force myself to focus on what I have open. It's a good combination with fuzzy search via Telescope.
Testing the Installation
Open Neovim and you should see the buffer line at the top of your Neovim
instance - it will have a single "tab" called
[No Name]
as you're working in an unnamed
buffer.
lua/packer_init.lua
Another usage of nvim-web-devicons.use {
'akinsho/bufferline.nvim',
requires = 'kyazdani42/nvim-web-devicons',
config = function()
require('bufferline').setup()
end
}
♯Plugin: gitsigns.nvim
gitsigns.nvim provides indicators in the sign column based on Git.
This plugin offers a bunch of additional features, but I don't really use them. I like the sign visual component but I prefer interacting with Git purely through the command line.
Why?
I just like the signs. Nice easy visual cue. I could throw away everything else in this plugin... though I have a feeling a lot of people will really enjoy the additional features.
Related Configuration
I use the following in lua/general.lua
to
ensure the sign column is always visible:
opt.signcolumn = 'yes' -- Always show the sign column
Testing the Installation
Open a file in a Git repository (ensure the file you're working on isn't ignored!) and make a change. You should see the sign column updated (e.g. a green bar, a red dash) depending on the type of change you made (add/remove/modify).
lua/packer_init.lua
use {
'lewis6991/gitsigns.nvim',
config = function()
require('gitsigns').setup()
end
}
♯Plugin: indent-blankline.nvim
indent-blankline.nvim is a cool recent find that adds visual guides. I actually find these quite handy even for small functions. The guides are dim and don't add too much distraction.
On a related note, this plugin offers more customization/features that I don't use as they seem to be ways to add more visuals to the screen (e.g. marking blank characters, marking newlines) that I don't care about and add significant noise to the screen.
I personally find the line-based visual guides, and those alone, to hit a nice balance.
Why?
I wanted to try out visual guides. As I mentioned above, I've been finding them useful.
Testing the Installation
Open a source file with indentation or open Neovim and create a new file and set
some file type (e.g. set ft=python
) and write
some arbitrary code/lines with indentation. Try multiple lines per level and
multiple levels. You should see visual guides appear that vertically trace each
level of indentation.
lua/packer_init.lua
use {
'lukas-reineke/indent-blankline.nvim',
config = function()
require('indent_blankline').setup()
end
}
♯Plugin: nvim-teesitter
The goal of nvim-treesitter is both to provide a simple and easy way to use the interface for tree-sitter in Neovim and to provide some basic functionality such as highlighting based on it.
Tree-sitter is a parser generator tool and incremental parsing library. This plugin leverages tree-sitter and exposes it to Neovim, and right now I pretty much just use it for nice highlighting, although the power of tree-sitter goes far beyond this and there exist many plugins with tree-sitter integration via this plugin.
Why?
Better syntax highlighting, powerful integrations and a generally widespread tool. I feel like the value of adopting tree-sitter will only grow over time.
Testing the Installation
Open Neovim and run
:checkhealth treesitter
.
Gotcha: Scala 3 Syntax
Unfortunately Scala 3 is not really supported at this time. It's fine I suppose and some of the stuff still works, but if you're like me and trying out the braceless syntax, get used to minor frustration.
This is not at all a tree-sitter problem: nobody is updating the grammar. Also that's not me bitching about it (nobody is paid to do it, nobody has to do it, and I very much understand not having the time or not finding that specific task to be the most gratifying thing ever), moreso just being clear about the current state of affairs.
With the way my life is right now I'm unlikely to jump in, but I'll be incredibly greatful if and when someone else does.
init.lua
require('plugins/treesitter')
lua/packer_init.lua
use {
'nvim-treesitter/nvim-treesitter',
run = ':TSUpdate'
}
lua/plugins/treesitter.lua
Adjust this according to what languages you use. It's worth reviewing the README and list of supported languages for this plugin. Note that I don't use HTML because I don't follow normal indentation rules (I mostly do not indent).
require'nvim-treesitter.configs'.setup {
-- One of "all", "maintained", or a list of languages
ensure_installed = {
"c", "zig", "bash", "scala", "yaml", "css", "javascript",
"latex", "clojure", "lua", "cpp"
},
-- Install languages synchronously (only applied to `ensure_installed`)
sync_install = false,
-- List of parsers to ignore installing
ignore_install = { },
highlight = {
enable = true,
disable = {},
-- Setting this to true will run `:h syntax` and tree-sitter at the same
-- time. Set this to `true` if you depend on 'syntax' being enabled (like
-- for indentation). Using this option may slow down your editor, and you
-- may see some duplicate highlights. Instead of true it can also be a list
-- of languages
additional_vim_regex_highlighting = false,
},
indent = {
enable = true
}
}
♯Plugin: nvim-lspconfig
Now that all of the other plugins are accounted for, it's time to move on to the Neovim LSP, completion, and related plugins.
Please note that this plugin is not the Neovim LSP, it's just a set of configuration for it!
Why?
I used to use coc.nvim which worked incredibly well, and is still an excellent plugin. There were a couple of factors in my switching to use only the Neovim LSP:
- I wanted to try it. I love that Neovim is going this direction.
- It seemed mature enough to use without too much hassle.
- Metals is moving to the Neovim LSP as the primary target. Since I mostly write Scala at this time, moving now made the most sense.
- The other things I write (e.g. Zig, Janet, whatever I'm fiddling with) all have integrations that I was able to get working easily.
Testing the Installation
Make sure that Neovim starts! To really test the LSP, you'll need to use some LSP configuration. I'll share examples of that later.
init.lua
require('plugins/lsp')
lua/packer_init.lua
use 'neovim/nvim-lspconfig'
lua/plugins/lsp.lua
This will require customization according to what you want to use the LSP for.
I've omitted my language-specific things here, and I also do not have
nvim-cmp
support listed in this snippet.
Relevant sections:
-- Not used yet!
local lsp = require('lspconfig')
-- Not used yet!
-- Use an on_attach function to configure after LSP attaches to buffer
local on_attach = function(client, bufnr)
local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end
buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')
end
♯Plugin: nvim-cmp
nvim-cmp is a completion
engine for Neovim. It will help you with completion of things like words in your
buffer, system paths, and LSP sources (e.g. showing available functions or
variables while you type). We need to configure all of those sources and also
advertise nvim-cmp
to our LSP plugins.
Why?
I needed a completion engine that worked well with the Neovim LSP. I actually
tried out coq.nvim
but I couldn't get it
working and haven't found nvim-cmp
to be
slow. Rather than bang my head against a wall, I decided to use a working
solution and enjoy the decent documentation and examples.
Testing the Installation
Open Neovim and start writing. Example, that I just tested:
Testing testing one two three banana on a bear.
Then start typing on a new line and see what happens. You should get a box that
suggests completions if you type a letter of something that should be impacted.
Let's say your buffer letter cutoff is 3
.
Typing o
should show
one
as an option, but not
on
because
on
is below the threshold.
init.lua
require('plugins/cmp')
lua/packer_init.lua
use {
'hrsh7th/nvim-cmp',
requires = {
'hrsh7th/cmp-nvim-lsp',
'hrsh7th/cmp-path',
'hrsh7th/cmp-buffer'
}
}
lua/plugins/cmp.lua
I currently leverage three completion sources:
- The builtin Neovim LSP
- System paths
- The current buffer, where completion starts after 4 characters.
This configuration also allows for using
(enter key) to select an item from the list of available options and
Tab
and
Shift+Tab
to navigate those lists when they
pop up.
local cmp = require('cmp')
cmp.setup{
sources = {
{ name = 'nvim_lsp' },
{ name = 'path' },
{ name = 'buffer', option = { keyword_length = 4 }, },
},
mapping = {
['<CR>'] = cmp.mapping.confirm({}),
['<Tab>'] = function(fallback)
if cmp.visible() then
cmp.select_next_item()
else
fallback()
end
end,
['<S-Tab>'] = function(fallback)
if cmp.visible() then
cmp.select_prev_item()
else
fallback()
end
end,
},
}
lua/plugins/lsp.lua
Again, these capabilities are not yet used - I'm deviating a little bit from my actual process (since I have my languages and such picked out) to show more minimal examples.
-- Establish a set of capabilities so we can advertise nvim-cmp support.
local capabilities = vim.lsp.protocol.make_client_capabilities()
-- Add nvim-cmp support to the capabilities.
capabilities = require('cmp_nvim_lsp').update_capabilities(capabilities)
♯Plugin: nvim-metals
nvim-metals is a plugin for Neovim which uses Neovim's LSP with the Metals language server. I'm primarily a Scala developer right now, and this is my go-to.
Testing the Installation
Open a Scala project, e.g. nvim build.sbt
.
You should be prompted to import the project.
Gotcha: Filetype Restrictions
At first I tried to do this:
use {
'scalameta/nvim-metals',
requires = { 'nvim-lua/plenary.nvim' },
ft = { 'scala', 'sbt' }
}
and I failed. Using ft
just shredded Metals
and I have no idea why. Since I got it working, I haven't bothered to dig
further. Yes, shame on me, but it works fine as-is and I wanted to get back to
writing code.
Gotcha: Adding New Files
I typically add new files while editing by using
:e <path>
. While syntax highlighting
applies properly, code completion does not. I'm assuming that Metals doesn't
know about the file for some reason. I can run a reimport of the project and the
problem persists, but closing Neovim and opening it fixes it.
As a less heavy-handed approach, I should try closing the buffer and reopening the file in a new buffer. There also might be a good way to create a file that allows Metals to immediately process it.
init.lua
require('plugins/scala')
lua/packer_init.lua
use {
'scalameta/nvim-metals',
requires = { 'nvim-lua/plenary.nvim' }
}
lua/plugins/scala.lua
Some things of note:
- Per my comments,
lualine
needs an update for Metals. - We're advertising the
nvim-cmp
capabilities to Metals.
-- Autocmd that will actually be in charging of starting the whole thing
-- Note that Metals will only be initialized for filetypes selected below.
local nvim_metals_group = vim.api.nvim_create_augroup(
'nvim-metals',
{
clear = true
}
)
vim.api.nvim_create_autocmd('FileType', {
pattern = { 'scala', 'sbt', 'java' },
callback = function()
metals_config = require('metals').bare_config()
-- Capabilities for completion.
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require('cmp_nvim_lsp').update_capabilities(capabilities)
metals_config.settings = {
showImplicitArguments = true,
showInferredType = true
}
-- This is a tricky option and _nothing_ will show up unless...
-- "However, to enable this you _must_ have the metals status shown in your
-- status bar somehow."
metals_config.init_options.statusBarProvider = 'on'
metals_config.capabilities = require('cmp_nvim_lsp').update_capabilities(capabilities)
require('metals').initialize_or_attach(metals_config)
end,
group = nvim_metals_group,
})
lua/plugins/lualine.lua
We need to add Metals support to lualine
since I enabled statusBarProvider
. This is a
nice way to get Metals status updates in my statusline.
local function metals_status_for_lualine()
return vim.g["metals_status"] or ""
end
require('lualine').setup {
options = {
theme = 'material'
},
sections = {
lualine_c = {
'filename',
metals_status_for_lualine
}
}
}
lua/keymap.lua
Finally, let's add Telescope support for browsing the available Metals commands.
-- Metals commands
map('n', 'fm', 'Telescope metals commands')
♯Plugin: nnn.nvim
nnn.nvim is a file manager
plugin for
Neovim based on nnn.
nnn
is a terminal file manager that is
full-featured while being very small and very fast.
Why?
For the most part I've eschewed file manager plugins and have found quick fuzzy
search sufficient for typical use. I've also found the file managers to be
clunky. What nnn
solves is giving me a
file-level view, and I'm not sure whether I'll use that or something like LSP
project views more. I do like that nnn
is
intuitive and responsive. It also stays out of my way unless I ask for it. So
far I'm happy with the plugin as a limited-use utility.
Testing the Installation
Open Neovim and press Ctrl+A
and then either
n
for the explorer or
p
for the picker.
lua/packer_init.lua
use {
'luukvbaal/nnn.nvim',
config = function()
require('nnn').setup()
end
}
lua/keymap.lua
Press Ctrl+A
followed by
n
to open
nnn
explorer in a vertical split. This
is nice for having a persistent view of all files in a project. Press
Ctrl+A
followed by
p
to open the picker as an interactive file
manager alternative to the fuzzy search-based
Ctrl+P
based on Telescope.
-- Toggle the nnn file explorer in a vertical split.
map('n', '<C-A>n', '<cmd>NnnExplorer<CR>')
-- Toggle the nnn file picker in a floating window.
map('n', '<C-A>p', '<cmd>NnnPicker %:p:h<CR>')
♯Plugin: todo-comments.nvim
todo-comments.nvim is a really neat plugin that is another one of my new discoveries. I'm using the following features:
- Keyword highlights. My most common keyword is "TODO"
- Trouble integration.
- Telescope integration.
The integrations in this case are super useful if you routinely use TODOs to leave notes for yourself in comments.
Why?
The visual indicators are helpful since I generally want to draw focus to my TODOs. The additional power of listing/searching saves me time because I used to do this manually and have to find and open files myself. My workflows are just more smooth with this extracted to a plugin.
Testing the Installation
Open Neovim and type TODO: Something
and the
TODO
portion should be highlighted. It should
also update the sign column.
lua/packer_init.lua
use {
'folke/todo-comments.nvim',
requires = 'nvim-lua/plenary.nvim',
config = function()
require('todo-comments').setup()
end
}
lua/keymap.lua
-- Search project TODOs in Telescope
map('n', '<leader>ft', '<cmd>TodoTelescope<CR>')
-- Show project TODOs in Trouble
map('n', '<leader>xt', '<cmd>TodoTrouble<CR>')
♯Plugin: filetype.nvim
filetype.nvim replaces the
default filetype
support loaded on startup.
The README does a good job of describing the problem.
Why?
The default is slow. This is less so. ¯\_(ツ)_/¯
Testing the Installation
Neovim should work. I haven't bothered trying to benchmark it and honestly this isn't something I had considered a problem; my computer is fast enough that I didn't really notice. Regardless, I figured it couldn't hurt.
lua/packer_init.lua
use 'nathom/filetype.nvim'
♯Example: Zig Support
I really like Zig. I think it's a fantastic
effort and I've been following it for years, occasionally poking around with
some small projects. I have Zig LSP support via
zls and that serves as a good
sample case for showing off what adding support for a language could look like
using nvim-lspconfig
.
The first thing to check is whether your language server is supported by this configuration set. It turns out that zls is supported.
lua/plugins/lsp.lua
Remember that capabilities
refers to a set of
capabilities to which we added nvim-cmp
support earlier in this article.
lsp.zls.setup {
on_attach = on_attach,
flags = {
debounce_text_changes = 150
},
capabilities = capabilities
}
Additional Plugin Support
You may want to leverage zig.vim and take advantage of format-on-save, though things like syntax are covered by Zig's maintained tree-sitter implementation.
♯Closing Thoughts
It required drastically more effort to write this article than it did to migrate my configuration from Vimscript to Lua. So far I'm completely happy with my decision:
- I have not lost any functionality.
- I have gained functionality through reevaluating my configuration.
- I'm far happier with my config organization.
- I learned a decent amount in the process.
If you're on the edge of trying this out yourself but not sure if it will be too difficult or take too much time, I would encourage you to give it a try! Remember: you can always create a quick backup of your Vimscript and restore it if things go awry.