I Can’t Seem to Quit Vim

Kevin Feng
Nerd For Tech
Published in
61 min readMay 27, 2023

--

Just a month ago, I started configuring Neovim, telling myself that I would commit to making it my primary means of editing code. Little did I know, I would be biting off far more than I was prepared to chew. Even after spending more than a month with Vim, I still haven’t figured out how to quit…

Let’s not beat a dead horse here: Exiting from Vim is as simple as executing :q, but quitting from Vim is a completely different story. After publishing my guide on setting up an Ubuntu VM and configuring Neovim from scratch, I decided to add a few more plugins. I never thought that configuring a text editor in my terminal would be such a cool experience: Adding various plugins that I found on GitHub, tweaking their settings to my own liking, and figuring out which features felt most useful to me. With each plugin that I installed, or each keymap that I added, I told myself “This will make my configuration perfect. I won’t need to add anything else after this.”

Great meme generator

Of course, that is where the trap of Vim configuration lies: Thinking that one day, you will achieve the perfect configuration that needs no additional plugins, keymaps, or colorscheme changes. This day will never come and instead, you find yourself trapped in a fiery Vim hell, where you are forever damned to this antiquated terminal-based text editor for all of eternity even after executing :q.

Okay, that was a bit of a dramatized hyperbole, but it is true that “Once you go Vim, you never go back.” To demonstrate what I mean, let’s take a look at my current Neovim configuration.

Throughout the process of me writing this blog post, I will have assuredly made changes to my configuration, so for the sake of consistency, when referring to my “current” Neovim config, that really just means that I’m talking about this branch of my Neovim configuration (7ec9497). In addition, this blog post won’t be entirely limited to my isolated Neovim configuration, but will also go over some of the other tools that I’ve used to “rice” my Linux workflow (proud Ubuntu user btw).

Here’s my journey with Neovim and Linux so far.

Editor note: Now that I’ve written out this entire article (its estimated read time is ONE HOUR) and I’m perusing it to make edits, I can confirm that my Neovim config has evolved quite a bit…

The Lord of the Vims: The Fellowship of the Emacs

In this section of my Neovim journey, I recount the configuration that I detailed in my previous Neovim blog post.

My plugins.lua file with some of the most essential plugins:

local ensure_packer = function()
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
fn.system({'git', 'clone', '--depth', '1', 'https://github.com/wbthomason/packer.nvim', install_path})
vim.cmd [[packadd packer.nvim]]
return true
end
return false
end

local packer_bootstrap = ensure_packer()

return require('packer').startup(function(use)
use 'wbthomason/packer.nvim'
use 'ellisonleao/gruvbox.nvim'
use 'nvim-tree/nvim-tree.lua'
use 'nvim-tree/nvim-web-devicons'
use 'nvim-lualine/lualine.nvim'
use 'marko-cerovac/material.nvim'
use 'nvim-treesitter/nvim-treesitter'
use {
'nvim-telescope/telescope.nvim',
tag = '0.1.0',
requires = { {'nvim-lua/plenary.nvim'} }
}
use {
'williamboman/mason.nvim',
'williamboman/mason-lspconfig.nvim',
'neovim/nvim-lspconfig',
}
use 'hrsh7th/nvim-cmp'
use 'hrsh7th/cmp-nvim-lsp'
use 'L3MON4D3/LuaSnip'
use 'saadparwaiz1/cmp_luasnip'
use 'rafamadriz/friendly-snippets'

-- My plugins here
-- use 'foo1/bar1.nvim'
-- use 'foo2/bar2.nvim'

-- Automatically set up your configuration after cloning packer.nvim
-- Put this at the end after all plugins
if packer_bootstrap then
require('packer').sync()
end
end)

My Neovim journey began with a series of basic Neovim configuration tutorial videos, to which I blindly followed the options that were being provided. And even though I was in some type of tutorial hell, the didactic techniques of the tutorials still helped me understand quite a bit about how to configure Neovim with plugins, add keymaps, and customize my own options. I didn’t start out with too many customization, but the plugins that I did have were crucial to any good Neovim config:

LSP

Perhaps one of the most important features of any code editor or IDE. Language Server Protocol (LSP) is extremely powerful, warning you about syntax errors, unused local variables, offering code completions, etc. ThePrimeagen’s Neovim from-scratch configuration video even emphasizes going from “0 to LSP,” which assuredly implies that LSP is an absolute must-have for Neovim. Though there are many ways of doing this, I’ve achieved it through Mason, which manages LSP servers, coupled with nvim-lspconfig, which has some quick start configurations for LSP inside of Neovim.

Pretend that says “mega-plugin”

Another straightforward way to get set up with LSP quickly is with LSP Zero, which aims to bundle LSP and autocompletion features into one “mega-plugin.” You can even utilize LSP Zero to configure code snippets, which delves a bit into the next feature…

Code snippets

Although I just mentioned “completions” for LSP, this feature isn’t just limited to autocompleting variable or function names that the language server protocol detects elsewhere in your file. With something like a combination of LuaSnip, cmp_luasnip, and friendly-snippets, you can set up a powerful snippet autocompletion system that generates tons of boring boilerplate content that no one wants to write (think public static void main (String[] args). When I first set up code snippets, I tested it out by creating a dummy HTML file and found that I could quickly generate an entire Emmet-style HTML boilerplate chunk of code with just a few keystrokes, an experience I was all too familiar with when working in VS Code:

I love
code snippets!

When I say “code snippets” for Neovim, it’s not too disparate from the VS Code snippet extensions you can get from the marketplace. In VS Code, if you need React snippets, you can install an extension like Reactjs code snippets, and simply type rafce to create a React component that utilizes an arrow function. The same type of logic applies to Neovim. All you have to do is supply Neovim with the source of code snippets that you want. In my case, I loaded some VS Code-like snippets and enabled them through my LSP config.

completions.lua

local cmp = require('cmp')

require('luasnip.loaders.from_vscode').lazy_load()

cmp.setup({
mapping = cmp.mapping.preset.insert({
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-o>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }),
}),
snippet = {
expand = function(args)
require('luasnip').lsp_expand(args.body)
end,
},
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'luasnip' },
}, {
{ name = 'buffer' },
}),
})

lsp_config.lua

require('mason').setup()
require('mason-lspconfig').setup({
ensure_installed = { 'lua_ls' }
})

local on_attach = function(_, _)
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, {})
vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, {})


vim.keymap.set('n', 'gd', vim.lsp.buf.definition, {})
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, {})
vim.keymap.set('n', 'gr', require('telescope.builtin').lsp_references, {})
vim.keymap.set('n', 'K', vim.lsp.buf.hover, {})
end

local capabilities = require('cmp_nvim_lsp').default_capabilities()

require('lspconfig').lua_ls.setup {
on_attach = on_attach,
capabilities = capabilities,
settings = {
Lua = {
diagnostics = {
-- the line below is necessary to ignore annoying Lua LSP for vim variables
globals = { 'vim' }
}
}
}
}
Jim Carrey moment

And with code snippets, maybe you won’t have to bash your face on the keyboard so much anymore! Let your editor do half of the work (or all of it if you’re using Copilot).

Treesitter

Treesitter is absolutely a necessity when it comes to Neovim. Without Treesitter, your out-of-the-box Neovim syntax highlighting isn’t complete. Even though Neovim does have some basic, default syntax highlighting, it can only do so much. Treesitter provides a blazingly fast abstract syntax tree that parses your code while you edit it to provide beautiful syntax highlighting while you create your software masterpieces.

Here’s a comparison between traditional syntax highlighting (left) and Treesitter based syntax highlighting (right) from the Treesitter GitHub repo:

Souce: Treesitter GitHub

You’ll notice that with traditional syntax highlighting, too many tokens are in plain white text (or in the case of a light theme, it might be black text), which makes it far more difficult to distinguish keywords, functions, variable names, etc.

Telescope

At its core, Telescope is just a fuzzy finder that operates over lists. However, its ability to be extended to other plugins or various list sources means that it can do much more than simply “find files.” This isn’t to say that finding files is an unimportant part of Telescope. In fact, that’s probably the feature of Telescope that I use the most. Similar to switching files in VS Code with CTRL + P, Telescope provides the same functionality and is often bound to the same keymap is most configurations that utilize it. If you know the name of the file that you want, you can get there without reaching your hand for the mouse. The possibilities with Telescope are pretty endless. Just some of the built-in functions include grep, file finding, and finding old files (otherwise known as recently accessed files). I recently stumbled across a new plugin that accesses GitHub’s code search functionality and integrates it into Telescope, which I think speaks volumes about the extensibility of Telescope.

Beautiful…

nvim-tree

Now we’re getting into some plugins that make Neovim feel like a full-fledged “IDE” (if that’s what you want to call VS Code). nvim-tree is a file tree explorer for Neovim that allows you to explore your files alongside your code with a clean and simple interface. When focused on nvim-tree, some of your traditional Vim keymaps are changed into functions that are extremely useful for creating, renaming, and moving files. For example, pressing a in nvim-tree won’t put you into insert mode on the right side of your cursor like it normally would in Vim. Instead, it prompts you to create a file wherever your cursor was focused. Simple type in the name and hit ENTER (or should I say <CR>) to create your file. End the string with a / to make it a folder. r allows you to rename a file, x, will cut a file, which once you paste with p, will cause it to disappear (c will just copy the file without deleting it upon paste). d will delete a file, which you’ll be prompted to confirm with y/n.

Source: nvim-tree GitHub

There’s a ton of default keymaps that nvim-tree comes with, and I found this documentation from NvChad to be particularly useful. Keep in mind that some of the documentation isn’t completely accurate to a clean install of nvim-tree, since NvChad has some of its own unique remaps (the first discrepancy that I discovered was that <C-h> and <C-l> did not jump between nvim-tree and my code buffer. By default, you’ll have to execute <C-w>h and <C-w>l for those keymaps, respectively (<C-w> initiates moving across different buffers). Because of these inconsistencies between NvChad’s nvim-tree and a clean install, you could just check out the default keymaps that nvim-tree comes with through :help commands or the documentation found on the GitHub. I just found that the NvChad documentation to look quite aesthetically pleasing :).

lualine

lualine is a blazingly fast statusline plugin, meaning that it displays various bits of useful information at the bottom of Neovim. It can show the mode that you’re currently in (insert, visual, normal), information about the Git branch you’re on, your operating system, what percentage of the file your cursor is at vertically, and the filename of the current buffer, just to name a few.

lualine is one of those plugins that makes Neovim look more like full-fledged IDEs, without violating any core Vim philosophies. I mention this because there are other plugins like bufferline which have similar design principles in mind, but go against Vim philosophy. In the case of this bufferline plugin, it starts treating buffers like “tabs” in editors like VS Code, and on top of that, it provides more options for viewing multiple files at once. This isn’t to say that having tabs or looking at a vertical split of two different files is inherently bad, but they don’t necessarily align with the philosophy of how Vim was designed. If you enjoy having tabs that look exactly like those from Sublime Text, then that’s more power to you as far as I’m concerned.

Sublime Text style tabs

nvim-web-devicons

nvim-web-devicons is a plugin that makes some of the previously mentioned plugins a bit more “complete.” nvim-web-devicons is a Lua fork of vim-web-devicons, and it pairs really well with plugins like nvim-tree and lualine (notice how the GitHub user that owns this repository is nvim-tree itself) by providing the same icons as the vim-web-devicons plugin as well as colors for those icons. In simple terms, it makes some of your other plugins look real snazzy. Don’t forget to use a Nerd Font!

The power of Nerd Fonts!

material

This plugin comes down to your personal preference, since material is just the colorscheme that I like best. Some colorscheme plugins are supported by Treesitter (including this one), and you can find a list of them at rockerBOO’s collection of awesome Neovim plugins.

(All images are taken from the material.nvim GitHub repo)

  • Oceanic
  • Deep ocean
  • Palenight
  • Lighter
Remember kids, light attracts “bugs”
  • Darker

I’ve been using Material themes ever since I started using VS Code, but if I had to give my second favorite theme for Neovim, it would have to be gruvbox.

Average gruvbox enjoyer

Shoutout to the Treesitter compatible version of gruvbox as well.

gruvbox-baby is compatible with Treesitter

packer

None of these plugins would matter if there wasn’t a package manager to manage all of them. That’s where packer comes in. packer is a use-package style plugin manager for Neovim that allows for declarative plugin specification, support for Luarocks dependencies, as well as lazy-loading options.

The most popular way of installing packer is through the boostrapping method, which makes it very easy to clone your configuration to other machines and have all your desired plugins automatically install.

If there’s one command that you need to know about while using packer as your Neovim plugin manager, it’s :PackerSync, which essentially performs :PackerUpdate and then :PackerCompile . This combination of commands allows you to update all of your plugins and install any new ones that you’ve added to your list of plugins via packer. Be warned: Updating your plugins isn’t always a “good” thing. It’s possible that pulling in changes for a plugin from its GitHub repository may actually break it.

In one instance, my LSP config broke when Mason got some updates that made it so that the sumneko_lua language server would no longer work in my configuration. However, the bug was transitory, and I simply used lua_ls instead.

One way to avoid this is to utilize packer’s snapshot function. If you’re worried that :PackerSync might break a plugin or two, save a snapshot of your Neovim configuration while it’s still working and then run :PackerSync. If plugins do unfortunately break, you can then rollback to the snapshot you took prior to updating.

The Lord of the Vims: The Two Plugin Managers

In this section of my Neovim journey, I install some more epic plugins and I also replace packer with lazy.nvim

Ever since I created my initial Neovim configuration, I’ve been stuck in Vim. I just cannot escape. Just by looking at the history of my repository, neovim-config, I’ve already made hundreds of commits in the span of a month. Although some of them are just simple README edits, they all serve a purpose towards enhancing my configuration. The README edits are particularly useful since I’ve added a to do list at the bottom, which helps me keep track of what plugins I want to try out, if there are any unresolved bugs within my configuration, etc.

I’ve intentionally left my plugins.lua somewhat messy, as the plugins aren’t organized by relevance. Instead, the file is more or less organized in chronological order of plugin installation. In other words, the first plugins that I installed are at the top of the file, while the newest ones are towards the bottom. I find that organizing my plugins in this way helps tell a story of how my configuration has grown. Here’s my plugins.lua file as of now:

local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end

vim.opt.rtp:prepend(lazypath)

local plugins = {
{'nvim-tree/nvim-tree.lua', lazy = true},
{'nvim-tree/nvim-web-devicons', lazy = true},
{'nvim-lualine/lualine.nvim', lazy = true},
{'marko-cerovac/material.nvim', lazy = true},
{'nvim-treesitter/nvim-treesitter', lazy = true},
{
'nvim-telescope/telescope.nvim',
version = '0.1.0',
dependencies = {
'nvim-lua/plenary.nvim'
}, lazy = true
},
{
'williamboman/mason.nvim',
'williamboman/mason-lspconfig.nvim',
'neovim/nvim-lspconfig',
lazy = true
},
{'hrsh7th/nvim-cmp'},
{'hrsh7th/cmp-nvim-lsp'},
{'L3MON4D3/LuaSnip'},
{'saadparwaiz1/cmp_luasnip'},
{'rafamadriz/friendly-snippets'},
{ "akinsho/toggleterm.nvim",
commit = "2a787c426ef00cb3488c11b14f5dcf892bbd0bda",
config = "require('toggleterm')",
lazy = true
},
-- This method works for lazy, but it doubles startup time
-- {
-- "iamcco/markdown-preview.nvim",
-- config = function ()
-- vim.fn["mkdp#util#install"]()
-- end
-- },
{
'iamcco/markdown-preview.nvim',
build = 'cd app && npm install',
-- using npm to install rather than the vim function leads to significantly faster startup time
init = function()
vim.g.mkdp_filetypes = { 'markdown' }
end,
config = function()
vim.keymap.set("n", "<leader>m", "<Plug>MarkdownPreviewToggle", { desc = "Markdown Preview" })
end
},
-- Peek works (assuming deno and JavaScript dependencies are installed), but there is a bug currently where the preview doesn't load
-- {
-- 'toppair/peek.nvim',
-- run = 'deno task --quiet build:fast',
-- config = function()
-- vim.api.nvim_create_user_command('PeekOpen', require('peek').open, {})
-- vim.api.nvim_create_user_command('PeekClose', require('peek').close, {})
-- end,
-- },
{'numToStr/Comment.nvim', lazy = true},
{'JoosepAlviste/nvim-ts-context-commentstring', lazy = true},
{'windwp/nvim-autopairs', lazy = true},
{'windwp/nvim-ts-autotag', lazy = true},
{
'goolord/alpha-nvim',
config = function ()
require'alpha'.setup(require'alpha.themes.dashboard'.config)
end, lazy = true
},
{'luk400/vim-lichess'},
{'dstein64/vim-startuptime'},
{'rhysd/clever-f.vim'},
{
'ggandor/leap.nvim',
config = function()
require('leap').add_default_mappings()
end,
lazy = true
},
{'ThePrimeagen/vim-be-good', lazy = true},
{'alec-gibson/nvim-tetris'},
{'tamton-aquib/zone.nvim', lazy = true, enabled = false},
{
'folke/noice.nvim',
dependencies = {
'MunifTanjim/nui.nvim',
'rcarriga/nvim-notify',
},
lazy = true
},
{'xiyaowong/transparent.nvim'},
{'ThePrimeagen/harpoon', lazy = true},
{'alanfortlink/blackjack.nvim', lazy = true},
{'0x100101/lab.nvim', build = 'cd js && npm ci', dependencies = { 'nvim-lua/plenary.nvim' }, lazy = true}
}

local opts = {}

require("lazy").setup(plugins, opts)

If you’ve read my previous Neovim blog post, you’ll know that I mentioned two key features that were missing from that base configuration: Treesitter and a built-in terminal. I was able to install Treesitter pretty easily by referencing some other Neovim configs on GitHub, so then I thought that installing some type of terminal plugin would make my Neovim configuration complete. I even distinctly remember telling a friend, “Once I have this plugin, I’ll be done.” Of course, that was just the beginning of my descent into madness, as I would proceed to add plugin after plugin, hopelessly waiting for the addition that would make Neovim finally feel perfect. Spoiler: It doesn’t exist.

toggleterm

toggleterm is a plugin that allows for persisting, toggleable terminals in various orientations. Although Neovim does have terminal emulation (which can be ran with the :terminal command), it doesn’t really compare to the built-in terminal of something like VS Code for example, which can be easily toggled, persists (so it continues to run whatever processes you want it to), and above all else, looks really cool.

At the time of installing it, I really thought that toggleterm would be the last plugin that I needed. After all, what else would I want? I already had LSP, some cool code completion snippets, and a file tree explorer. As far as I could tell, I had something fairly close to VS Code, which was essentially what I viewed as the gold standard for editing code (wait, it’s not Doom Emacs?)

Since toggleterm was the first plugin that I was installing without some type of tutorial or guide, I ran into some very basic, or dare I say, “dumb” bugs.

📂 ~/.config/nvim
├── 🌑 init.lua
├── 📂 lua
│ ├── 📂 v9
│ ├── 🌑 keymaps.lua
│ └── 🌑 plugins.lua
│ └── 📂 plugin_config
│ ├── 🌑 colorscheme.lua
│ └── 🌑 lualine.lua
│ └── 🌑 nvim-tree.lua
│ └── 🌑 init.lua

As shown above, my Neovim config has a pretty simple structure. Although not every folder is shown, all the core components are. At the root directory, an init.lua file calls keymaps.lua, which has a bunch of keymaps and basic Vim options,plugins.lua, which includes whatever plugin manager code is being used, and finally plugin_config/, which is a folder which includes all of the specific configurations of plugins in a very modular and organized fashion.

-- this is the root directory's init.lua

require("v9.keymaps")
require("v9.plugins")
require("v9.plugin_config")

Though I was smart enough to include toggleterm.lua in the plugin_config/ directory, I was also not smart enough to require it in plugin_config/init.lua, or the init.lua that is bolded in the directory structure diagram above.

-- this is plugin_config's init.lua

require('v9.plugin_config.colorscheme')
require('v9.plugin_config.lualine')
require('v9.plugin_config.nvim-tree')
require('v9.plugin_config.telescope')
require('v9.plugin_config.treesitter')
require('v9.plugin_config.lsp')
require('v9.plugin_config.completions')

-- I failed to include this line when I first installed toggleterm...
require('v9.plugin_config.toggleterm')
-- F in the chat :(

require('v9.plugin_config.alpha')
require('v9.plugin_config.lichess')
-- require('v9.plugin_config.zone')
require('v9.plugin_config.autopairs')
require('v9.plugin_config.comment-nvim')
require('v9.plugin_config.noice')
require('v9.plugin_config.harpoon')
-- require('v9.plugin_config.peek')
require('v9.plugin_config.blackjack')
require('v9.plugin_config.leap')
require('v9.plugin_config.vim-be-good')
require('v9.plugin_config.lab')

This second init.lua file calls upon all the plugin configuration files, and failing to include the line require('v9.plugin_config.toggleterm') meant that my toggleterm configuration was never called, so any keymaps that I included wouldn’t work. As for why that would be a problem… Well, let’s just look at my toggleterm configuration:

local status_ok, toggleterm = pcall(require, 'toggleterm')
if not status_ok then
return
end

toggleterm.setup {
size = 13,
open_mapping = [[<c-\>]],
shade_filetypes = {},
shade_terminals = true,
shading_factor = '1',
start_in_insert = true,
persist_size = true,
direction = 'horizontal'
}

As you can see, open_mapping sets the keymap which will toggle the terminal, and if this line of code never runs, then this combination of keys won’t work.

Of course, I’m telling you this in hindsight since I now know the silly mistake that I made, but at the time, I was completely lost as to why toggleterm wasn’t working as I had configured.

One cool thing that packer allows you to do is source your files without exiting Neovim with :source % or :so. This will essentially reload the configuration file that you’re on, so while I was still debugging toggleterm, I noticed that I could manually source toggleterm.lua and make the plugin work, but that was unacceptable. I needed the plugin to load on startup and work without any manual commands.

After doing some reading from the official Neovim.io documentation, I found that I could put my code into ~/.config/nvim/plugin to have it automatically compile on startup. If you look at this specific commit, that’s exactly what I did.

And just two days later, I magically came to the revelation of the proper solution for my mistake, and simply reverted back to the original plugin structure while requiring toggleterm.lua in plugin_config/init.lua .

imgflip

This is the greatest leader key of All Time

In pretty much any Neovim from scratch tutorial, the leader key is discussed. What does this term mean?

In Vim, a leader key is a key that provides a namespace for whatever customized shortcuts we want. In other words, after pressing your leader key, Vim will interpret whatever other keys you press and execute the command (if it exists) that is associated with that keymap. What’s really cool about Vim’s leader key is that it gives you a completely blank namespace for customizing your own keymaps. You won’t have to worry about creating any conflicting keymaps, since none exist by default.

Perhaps the most popular leader key (even in Emacs) is <SPC> , which can be defined as such in Lua for Neovim:

vim.g.mapleader = ' '
vim.g.maplocalleader = ' '

If you’re wondering about what the difference between the first and second options is, it’s not huge. The local leader, as its name implies, exists to target specific buffers, which you can take advantage of for specific file types, just as an example. The leader itself is global to Vim, meaning it applies everywhere. In most configs that I’ve seen, both the local leader and leader keys are set to the same thing, so you probably don’t have to worry about distinguishing between the two.

With a leader key set, you can now set up some pretty nifty keymaps like the following:

local keymap = vim.keymap
local opts = { noremap = true, silent = true }

keymap.set('n', '<leader>w', ':wa<CR>', opts)
keymap.set('n', '<leader>q', ':qa<CR>', opts)

The general formula for setting a keymap in Neovim is to specify four parameters:

  1. The mode that the keymap will apply to
  2. The keymap itself
  3. What the keymap should execute
  4. Any options for the keymap (which I’ve consolidated into a local variable called opts (setting noremap to true makes it so that keymaps are non-recursive and setting silent to true makes it so that commands are executed without being written to Vim’s command line)

In the two examples above, both apply to normal mode and are executed with the leader key followed by w or q, respectively. Functionally, they write to all buffers and quit all buffers, respectively.

This makes writing and quitting Neovim far faster and, in my opinion, more satisfying than writing out the command :wqa and hitting ENTER.

The extensibility of leader keymaps goes far beyond built-in Vim functions like writing or quitting, however. For example, with some type of Markdown preview plugin, you could set a keymap to quickly execute the plugin like so:

keymap.set('n', '<leader>m', ':MarkdownPreview<CR>', opts)

But <SPC> is a suboptimal leader key in my opinion. Sure, the spacebar can be accessed by either hand, is quite a large key, and doesn’t interfere with other keymaps in normal mode, but it doesn’t compare to the almighty semicolon.

Why is the semicolon such a good leader key? Unlike the spacebar, it is on the home row, meaning it will reinforce good typing habits by ensuring that your fingers always float above the home row to have access to this amazing key.

On top of that, it’s the same size key as your other alphanumeric keys, so hitting “combos” with it feels far more satisfying and consistent. I’ve found that with the spacebar, the feedback that I get from hitting a combination of keys just feels less “crisp.” Don’t get me wrong, the spacebar itself can be very satisfying, especially on some very high-end mechanical keyboards, but when coupled with other keys, there’s just something inconsistent about the whole sequence.

Now the semicolon has almost no keymap conflicts in normal mode, there is one exception, which can be fixed with a very simple plugin. In normal mode, ; is only used to reiterate an f jump. In other words, when you use f to jump to the next instance of a character, you use ; to jump to the next instance of that same character and , to jump to the previous instance. When the semicolon is set to your leader key, hitting ; during an f jump does not cause your cursor to immediately jump to the next instance of that character. Instead, due to the leader keymap, Vim will wait one second for any other inputs that you want to string together. If you don’t do anything for an entire second, Vim will then jump forwards to the next character.

And of course, that is completely odious. Any time that I don’t receive immediate feedback from an input, whether it is coding, gaming, or just plain typing, I feel like I want to throw up. But as I mentioned, fixing this is very simple. With the clever-f Vim plugin (which works seamlessly with Neovim), f and F become available in place of ; and ,. So instead of using ; to jump ahead characters, you can simply hit f over and over (and the same can applies to F for ,). This also, intuitively, makes sense, especially when comparing to n and N for jumping forwards and backwards through / search sequences.

The plugin that I decided to add next (which I did right after telling my friend that I wouldn’t need any more plugins) was a plugin for previewing Markdown files. When I used VS Code as my daily driver, I was able to easily view Markdown previews within VS Code itself with a simple shortcut of CTRL + SHIFT + V. Since I tend to pour my heart and soul into my READMEs, I decided to find a plugin that would help me see all my hard work.

markdown-preview

iamcco’s markdown-preview plugin is very straightforward. Once installed, you call upon :MarkdownPreview as a Vim command, and the plugin opens up a tab on your default browser with a preview of your Markdown file. As you scroll through your Markdown file, the preview actually follows along; its position is responsive to your cursor.

Some other features of this plugin include:

I found that typing out :MarkdownPreview every time that I wanted to see what my README looked like to be somewhat of a hassle, so using my new leader key, I set ;m to trigger it.

For me, a Markdown preview plugin is pretty cool to have, but I find that most of the time while editing my READMEs, I don’t actually preview the rendered Markdown — I just use it after making lots of edits to see how it has changed over time. But regardless of how you might use such a tool for previewing your Markdown, it’s definitely not as important as the plugin that follows.

Comments

As programmers, we are obligated to write comments. Whether it is a useless series of comments like this one

/* 
This is a function, which has the name "Minion," which takes in a String
which is denoted by the character in the English alphabet referred to as 's,'
and then returns that same String (note that the String is still called 's.'
*/
public static String Minion(String s){ // function header, take in a single parameter, String s
return s; // this is the return statement, in which the String is returned
} // this is a closing bracket
// this is the end of the function which we have referred to as Minion

or comments that actually document the code pragmatically, comments are a core feature of any programmer’s coding experience. Unlike VS Code, Vim doesn’t have a built-in way to toggle comments based on lines selected.

In VS Code, if you highlight 3 lines and then perform CTRL + /, those lines are commented if they weren’t already (and uncommented if they were). If you don’t select anything, VS Code will toggle the comment status of the current line.

To emulate this behavior, we’ll use Comment.nvim. Comment.nvim is another straightforward plugin that doesn’t really need any additional configuration. Install it with your favorite plugin manager and start toggling comments to your heart’s content with gcc for single lines and gc for visual mode selections.

Indenting and outdenting code

Having been a VS Code user prior, I had already gotten used to indenting my code with TAB and outdenting my code with SHIFT + TAB. When I found out that Vim used >> and << for indenting and outdenting code, respectively, I decided that I needed a quicker way to perform these functions. With these amazing remaps, I can now indent and outdent my code blazingly fast:

-- indent and outdent lines quickly
keymap.set('n', '<TAB>', '>>', opts)
keymap.set('n', '<S-TAB>', '<<', opts)

-- indent and outdent lines in visual mode
keymap.set('v', '<TAB>', '<S->>gv', opts)
keymap.set('v', '<S-TAB>', '<S-<>gv', opts)

The first two keymaps are pretty straightforward. In normal mode, they simply make it such that hitting TAB indents the current line, and performing SHIFT + TAB outdents the current line. The cool part about Vim’s indenting and outdenting is that they can be done regardless of where the cursor is.

In VS Code, indenting a line must be performed while the cursor is before the code (or to the left of it), while outdenting can be performed regardless of where the cursor is. Although this kind of makes sense, it’s just a weird inconsistency that comes at the cost of the user’s inconvenience. When was the last time you wanted to indent something, but only starting from a certain character? It makes much more sense to indent an entire line, regardless of cursor position, which VS Code funnily has the functionality for, but only for outdenting code.

The visual mode keymaps aren’t entirely necessary, and elite Vim users would probably argue that such keymaps are the wrong way to go about indenting or outdenting code. Consider the following code:

public static void main(String[] args){
System.out.println("spaghetti");
System.out.println("and");
System.out.println("meatballs");
System.out.println("dunkey");
}

Clearly, the tabs in this code are not correct. Everything from line 2 to line 5 should be indented once, and that can be achieved in Vim by positioning your cursor to line 2, prepending the number 4(the number of lines to be altered with the subsequent action) and then executing >>, or in the case of the first normal mode keymap, TAB. Though this is undoubtedly faster than doing it with a visual mode selection, I find that selecting the text that I want to indent or outdent makes the whole process more clear.

So with the visual mode indent/outdent keymap, I would position my cursor on line 2, perform V (remember that this means SHIFT + V, since Vim keys distinguish between capital and lowercase variants) to begin a visual line selection, then do 3j to jump down three lines, selecting all of the print statements, and then finally hit TAB.

There are two key differences between the normal mode indent/outdent keymap and the visual mode version: The first is the actual process of indenting/outdenting. With the normal mode keymap, an indent is executed via >>, but in visual mode, this is achieved not with >>, but instead with <S->>, or in non-Vim “syntax,” SHIFT + >.

The second difference is that the normal mode keymap doesn’t have anything following the indenting process itself. The visual mode keymap is followed by gv. What does this do?

In Vim, gv selects your last selection, which is why this is included at the end of the visual mode keymaps. Without it, the code that you highlight would simply be indented/outdented and you would exit visual mode. But with it, after the code is indented/outdented, the same selection you just made is reselected (although this process is seamless, you won’t notice the highlight flicker or anything like that).

Autopairs and autotags:

My Neovim configuration was still lacking some core functionalities that I had by default from VS Code, namely autocompletion for pairs and tags.

By “pairs,” consider a scenario in which you type { and VS Code automatically puts in the } for you. The same is done for parentheses, square brackets, single quotes, and double quotes.

For tags, consider a basic, blank HTML file. If you type this:

<html>

You would expect your editor to automatically insert the ending tag like so:

<html></html>

Both of these can be achieved with two plugins alongside an already existing Treesitter configuration:

For the pairs, there’s the nvim-autopairs plugin, and for the tags, there’s the nvim-ts-autotag plugin. Here’s how you can install and properly configure these two plugins.

In your plugins.lua file or wherever you have packer’s bootstrapping code, include this:

-- packer boostrapping code
use {
"windwp/nvim-autopairs",
config = function() require("nvim-autopairs").setup {} end
}
use {
"windwp/nvim-ts-autotag",
config = function() require("nvim-ts-autotag").setup {} end
}

And in your Treesitter config file, make sure to include these options under the setup function:

local status_ok, treesitter = pcall(require, 'nvim-treesitter.configs')
if not status_ok then
return
end

treesitter.setup {
ensure_installed = { "lua", "vim", "python" },

sync_install = false,
auto_install = true,
highlight = {
enable = true,
},
context_commentstring = {
enable = true,
autocmd = false
},
-- these options
autopairs = {
enable = true
},
autotag = {
enable = true
}
-- these options
}

Once you’ve done that, Neovim will automatically complete pairs and tags for you. From what I can tell, the default options on both of these plugins are quite sensible and pretty much mirror what editors like VS Code achieve.

Alpha: The Best Dashboard Plugin:

At this point, my Neovim config had gotten quite robust — it wasn’t really missing too much in terms of functionality. So it was only logical that I would start pursuing unnecessary plugins that were just plain cool.

Introducing alpha-nvim, a greeter plugin that is highly configurable and includes starting templates based off of vim-startify and dashboard-nvim.

dashboard > startify

Personally, I prefer the dashboard theme, since including actions, like opening the file tree explorer, creating a new file, or using grep to search through a directory, seems more useful to me than just accessing recently used files. And besides, if you include an option to toggle Telescope’s built-in recent file function, the dashboard theme can do everything that the startify one can do, plus some.

I’ve spent quite a lot of time tinkering with alpha, and the current config that I have has some pretty nifty features.

My beautiful alpha dashboard

First, the dashboard generates some pretty awesome ASCII art, which it pulls from another file called logos.lua. Essentially, every time that Neovim is launched, alpha selects a random ASCII art and then prints it out to the screen:

Below the ASCII art is a list of useful options:

  1. Find files — Brings up Telescope’s built-in find files
  2. New file — Simply creates a new, empty file
  3. Recently used files — Brings up Telescope’s built-in recent files (called “oldfiles” by Telescope)
  4. Find text — Brings up Telescope’s live grep
  5. Open file tree — Opens and focuses on nvim-tree
  6. Plugins — Navigates to my plugins.lua file, regardless of the directory Neovim was originally opened
  7. Keymaps — Does the same as the option above, but for keymaps.lua

These options can be used either through the keymap that alpha lists (which are distinct from any keymaps that might have been set elsewhere in the configuration), but also through navigating the lines with the j and k keys and hitting ENTER.

Lastly, in the footer of the dashboard, alpha displays the version of Neovim that’s running, the current date and time, and how many plugins were loaded and how quickly that happened.

alpha + lolcat

Now the fact that alpha can choose a random ASCII art out of a given set is pretty cool, but I think we can do better. The idea is to have alpha utilize lolcat, which is a command line tool which makes outputs rainbow colored, and then animate the ASCII art so that it gets a cool rainbow wave effect that perpetually occurs:

Credit to Pytness’ dotfiles for this

Making this awesome RGB effect happen on our ASCII art isn’t as simple as some of the previous plugin configurations, but here’s what it involves:

First, the alpha configuration has to be set up to do most of the work, calling upon a bash script that will actually animate the output from the .cat files that contain the ASCII art itself:

You’ll notice some discrepancies between the code and the dashboard’s options — that’s because the output is so large it’s actually covering some of them. In this case, “find text,” “recently used files,” and “new file” are all actually hidden underneath the ASCII art. That’s just one of the bugs that I’m still trying to work out with this alpha configuration (hence, why it’s commented out in the master branch).

Another issue with this alpha config is that when calling upon alpha within Neovim, the ASCII art fails to render. Although it doesn’t make much sense opening up a dashboard while already in Neovim, it’s just something nice to have for consistency’s sake. Aside from that, this alpha config is fully functional. The dashboard options all work, and most importantly, that animated hydra looks amazing. Getting back to how this actually works…

Alongide the alpha configuration, you’ll need a bash script that animates the output by looping through calls on lolcat:

Lastly, you’ll need the ASCII art itself, which will be contained in .cat files, rather than in raw string representations like shown previously:

Feel free to find your own ASCII art for this RGB animation. I’ve found that art created with dots (like the hydra) can have way more detail than traditional ASCII art that only use alphanumeric characters and punctuation symbols.

lazy.nvim

There comes a time in every man’s life when he realizes that packer is an antiquated and incomplete plugin manager for Neovim. This inevitably leads to the installation of lazy.nvim (I call it “lazy.nvim” to avoid confusion with LazyVim, the preconfigured Neovim setup), a modern plugin manager so powerful that it even caused the creator of packer to switch over.

lazy.nvim is an amazing plugin manager for Neovim that I’ve barely even scratched the surface of through my own usage. Here are just a few of the amazing features that it offers:

  • Manage all of your plugins with a powerful and fast GUI
  • Faster startup times than packer via caching and lazy-loading of plugins, Lua modules, keymaps, colorschemes, etc.
  • No need to manually compile plugins (like with :source %)
  • A Lockfile called lazy-lock.json that keeps track of installed plugins as well as their commits

I think one of the coolest parts of lazy.nvim is the GUI that comes up when you execute :Lazy:

From the lazy.nvim GitHub repository

This GUI has a ton of options, which you can use by typing the corresponding letter (they’re in Vim notation, so yes, they are capital letters). For more information on these options, I would check out the lazy.nvim GitHub repo, or just run :Lazy help to bring up a help menu directly in Neovim.

Similar to how :PackerSync would perform :PackerUpdate and then :PackerCompile, the most powerful option in lazy.nvim is Sync (S), since it performs the install, clean, and update operations all at once.

Something that’s really cool with this GUI is that it will prioritize showing you breaking changes when updating plugins, like this:

This is really useful; in case you notice any breaking changes while updating your plugins, just make sure to keep note of which commits caused it. If the breaking changes did end up affecting your config’s functionality, you can start debugging with a very solid starting point: the source code that caused your config to break!

Of course, this can be avoided altogether by just pinning your plugins to a certain commit, so that any future updates won’t cause them to break. This can be done with packer as well, but lazy.nvim’s lazy-lock.json file makes it particularly easy to keep track of.

But perhaps the coolest thing about lazy.nvim is its lazy-loading feature. Maybe that’s why it’s called lazy.nvim…

Lazy-loading is a process of loading plugins only when they’re required (or when you specify so). For example, you can configure lazy.nvim to load up Neovim with only the plugins you find necessary and to load the rest upon certain events, such as startup, certain keymaps occurring, etc.

By doing this, you can drastically improve your startup time by having Neovim hold off on trying to load every single plugin before startup. Chances are, you don’t need all of your plugins to be ready right away, so you may as well lazy-load those that you end up using, on average, a few seconds or even a few minutes after starting Neovim

Benchmarking Neovim Startup

Though terminal-based editors like Neovim and Vim already have blazingly fast startup times compared to editors like VS Code, we can always improve those speeds. If you’re curious as to how you can benchmark your Neovim startup times, here are a few ways of doing that:

(1) Use the built-in --startuptime launch argument

This first option involves the least hassle, but it does result in the most difficult data to interpret. Simply launch Neovim like this:

nvim --startuptime medium.txt

Where medium.txt is the name of the file that you would like the results printed out to. The output file will look a little something like this:

times in msec
clock self+sourced self: sourced script
clock elapsed: other lines

000.005 000.005: --- NVIM STARTING ---
000.054 000.050: event init
000.140 000.085: early init
000.186 000.046: locale set
000.214 000.028: init first window
000.407 000.193: inits 1
000.417 000.010: window checked
000.419 000.002: parsing arguments
000.738 000.035 000.035: require('vim.shared')
000.813 000.028 000.028: require('vim._meta')
000.814 000.074 000.046: require('vim._editor')
000.815 000.140 000.031: require('vim._init_packages')
000.818 000.258: init lua interpreter
000.868 000.050: expanding arguments
000.889 000.022: inits 2
001.128 000.239: init highlight
001.130 000.002: waiting for UI
001.802 000.672: done waiting for UI
001.812 000.011: clear screen
--
CUT OUT BECAUSE THIS FILE IS OVER 500 LINES LONG
--
101.896 000.030 000.030: require('noice.ui.popupmenu')
102.058 000.059 000.059: require('noice.text.block')
102.166 000.046 000.046: require('noice.message.filter')
102.168 000.236 000.131: require('noice.message')
102.182 000.284 000.048: require('noice.ui.cmdline')
102.364 003.462: VimEnter autocommands
102.903 000.539: UIEnter autocommands
102.942 000.040: before starting main loop
104.132 001.189: first screen update
104.134 000.003: --- NVIM STARTED ---

Though it might look a little daunting at first, this output just consists of three columns: The clock (which starts once you execute the nvim command), the elapsed time (which tracks how long each event takes), and the event associated with the two aforementioned columns.

All times are in milliseconds, and ideally, Neovim should be able to startup in around 100 ms. Anything under 100 ms is very good, and anything past 250 ms or so becomes noticeably slower.

Of course this begs the question: How much does this matter? It really depends on how many times you start Neovim each day or how much you care about efficiency, just for efficiency’s sake.

For me, I was having some incredibly slow startup times on my desktop, sometimes going as high as 600 ms, so my interest in Neovim startup times originated from a practical standpoint.

(2) Use the vim-startuptime plugin

The vim-startuptime plugin is yet another straightforward plugin that needs no special configuration options. Simply install it using your favorite plugin manager (which should be lazy.nvim of course) and then use its :StartupTime command to produce this visual, graph-type representation of your Neovim startup.

Unlike the --startuptime argument passed while launching Neovim, this method allows you to see a visualization of the data and extrapolate the root cause of a potentially inefficient or slow startup.

In the image above, you can very quickly find out that the overall startup time for my Neovim config is just over 100 ms, with the majority of that time being taken by my init.lua file, which calls upon both v9.plugin_config/, v9.plugins, and v9.keymaps. As expected, v9.keymaps doesn’t show up at the top of this list because remapping keys is far less intensive than loading entire LSP configurations or UI elements, like nvim-tree.

If I wanted to speed up my Neovim startups, I would probably first look into disabling the lab plugin, since it constitutes ~20% of the startup alone. In fact, it’s the only plugin config file that’s taking more than 10 ms to load; the LSP is loading in just under 10 ms.

(3) Use the hyperfine tool

hyperfine is a command-line benchmarking tool that can start Neovim multiple times and give you the average startup time, as well as minimums and maximums. Install it with your favorite package manager (I’m on Ubuntu so I’ll use apt):

sudo apt-get install hyperfine

Once it’s installed, you can run something like

hyperfine "nvim --headless +qa" --warmup 5 

to have hyperfine benchmark Neovim startups, with 5 warmup runs beforehand. The output will look like this:

Benchmark 1: nvim --headless +qa
Time (mean ± σ): 80.3 ms ± 5.8 ms [User: 52.7 ms, System: 15.8 ms]
Range (min … max): 71.3 ms … 91.3 ms 32 runs

If you want to store these results in a file, simply pipe the output to a file of your choosing like this:

hyperfine "nvim --headless +qa" --warmup 5 > hyperfineresults.txt

More on lazy.nvim

One of the main reasons people switch from packer to lazy.nvim is due to slow startup times. And although packer does have support for lazy-loading, lazy.nvim is definitely the gold standard for lazy-loading.

For me, I had been noticing more and more configs using lazy.nvim, and while exploring plugins through GitHub, the presence of lazy.nvim alongside packer was something that I couldn’t ignore.

-- Example syntax of installing noice.nvim through lazy.nvim
{
"folke/noice.nvim",
event = "VeryLazy",
opts = {
-- add any options here
},
dependencies = {
-- if you lazy-load any plugin below, make sure to add proper `module="..."` entries
"MunifTanjim/nui.nvim",
-- OPTIONAL:
-- `nvim-notify` is only needed, if you want to use the notification view.
-- If not available, we use `mini` as the fallback
"rcarriga/nvim-notify",
}
}

So lazy.nvim quickly piqued my interest and I started watching some videos about switching from packer to lazy.nvim. Surprisingly, this switching process is nothing arduous — it just involves switching out packer’s bootstrapping code for lazy.nvim’s, changing some packer syntax to lazy.nvim syntax, and for lazy.nvim’s own health, removing packer files, especially packer_compiled.lua.

I’d highly recommend typecraft’s video on this topic if you’re interested in making this switch:

The process of switching from packer to lazy.nvim is very simple, takes little time, and the syntax differences between the two plugin managers is tiny. Due to a combination of these factors, Chris (the author of the aforementioned video) mentioned that once he switched his config over to lazy.nvim, he wasn’t even sure if it worked properly, and I honestly agree on that point. The process is strangely underwhelming, but if it is, that’s a good sign that everything worked properly.

But the reason that I switched to lazy.nvim in the first place was because I noticed my startup times on my desktop were extremely slow (almost VS Code slow).

I have the same VM setup on both of my devices (desktop and laptop), and the same Neovim configuration as well. Knowing that my desktop that I built has far better specs than my laptop (and also considering the fact that it’s a desktop), I was very surprised to find that my desktop was taking up to 6x as long to start Neovim.

After running some benchmarks while on my packer config, I found that my desktop would take anywhere from 500 to 600 ms to start Neovim while my laptop consistently took just a little over 100 ms. If you don’t think that there is a noticeable difference between 100 ms and 500 ms, then you clearly have not wasted your entire life configuring a terminal-based text editor before.

As mentioned earlier, anything around 100 ms startup time feels like Neovim starts “instantly,” while anything beyond 250 ms starts to feel quite sluggish. I would even argue that around 180–200 ms or so, I can begin to observe a noticeable delay. Perhaps this has something to do with my reaction time being very similar.

This is by no means my average reaction time…

But it wasn’t just Neovim that was slow. I noticed that the VM on my desktop as a whole was significantly slower. The most obvious difference was interacting with a browser. On my laptop, it felt the exact same as if I was using Google Chrome on the host OS (Windows 10), but for my desktop, everything was choppy and certain actions were delayed.

Another pretty glaring difference was how quick the terminal was. When I installed packages using apt on my laptop, typing out the command and the subsequent installation process felt entirely “normal.” There was nothing to complain about in terms of feedback and speed. However, on my desktop, I noticed that there was a tiny delay between me typing a character and it showing up in the terminal, and on top of that, the installation process was a bit slower.

Me when I virtualize Ubuntu wrong

So clearly there was something wrong with how my desktop was virtualizing the Ubuntu OS, and fixing that problem would decrease my startup time far more than any plugin optimizations would.

To my surprise, after switching to lazy.nvim, my startup times actually got slower, not faster. Though these startup times weren’t much slower (in the realm of tens of ms), it still wasn’t a good sign. So I immediately looked into lazy-loading plugins.

My first instinct was to lazy-load everything and just see what would happen. So using some Vim macros, I quickly added lazy = true to the end of each plugin and restarted Neovim. As I expected, some plugins broke, but that almost didn’t matter to me when I saw how much faster my startup times were.

After running a couple of benchmarks, my startup time had show down to 250 ms (from 600 ms!), which was definitely a huge improvement after having to wait more than half a second for Neovim to start previously. But reality sank in soon after, and I started to fix my plugins.

As I started to test the functionality of my plugins one by one, I was surprised to find that even some of the simplest plugins were failing. The fact that toggling comments did not work was quite surprising to me, as I noticed that neither gcc nor gc worked.

And so I removed lazy = true from all the plugins that weren’t working and found myself with… a 500 ms startup time again?

It seemed like everything was working again, but why was the startup time so slow now? After a quick check of the --startuptime logs, I identified the culprit: markdown-preview.

For some reason, markdown-preview was taking an egregiously long time to load (something in the realm of 200 ms if I recall correctly), dwarfing all other plugins in terms of load time, and I don’t mean that in a good way.

Interestingly enough, markdown-preview was the one plugin that I had to look up the lazy.nvim installation syntax for, since the method of installation was invoking a vim-function.

In this Reddit thread (the hyperlink goes right to the comment), you’ll find a comment with code that looks like this:

{
"iamcco/markdown-preview.nvim",
config = function()
vim.fn["mkdp#util#install"]()
end
}

And although this code works for installing markdown-preview on lazy.nvim, I found that it made my configuration extremely slow — it was the main reason for my 500 ms startup time after all! So without considering other options for installing this plugin, I decided to look for an alternative.

peek

peek is another Markdown preview plugin for Neovim, although it takes a slightly different approach compared to iamcco’s markdown-preview. Using Deno, the JavaScript runtime, peek loads a preview of the current Markdown file with most of the core features from markdown-preview, but not in your browser. Instead, it creates a new window that’s bound to Neovim (the window title even says “nvim”), and by default, this window is stylized to look like GitHub’s rendered Markdown files.

The coolest, most fundamental feature that peek supports over markdown-preview is Vim-based “scrolling” in either window. In other words, if you hit j in the Markdown file in Neovim, peek’s preview will follow; markdown-preview also does the same thing in the browser window it spawns. However, if you hit j while focused on the browser window created by markdown-preview, nothing happens. But hitting j while focused on peek’s preview does cause Neovim itself to perform a synchronized scroll, which I think is pretty cool.

So after disabling markdown-preview and installing peek, I found myself with a 300 ms startup time, still three times that of what my laptop was experiencing. How could this be? My CPU, memory, motherboard — really everything in my desktop was superior to my laptop. And it wasn’t an issue of underallocating RAM to my Ubuntu virtual machine, since they both were getting 4 GB (I’ve tried allocating 8 GB, it doesn’t really make a difference).

The Forbidden Hypervisor

Source

A hypervisor is a software that creates and runs virtual machines, allowing a single host computer to support multiple guest virtual machines by virtually sharing its resources. Essentially, hypervisors can enable a system to use more of its available resources as well as improve mobility, as the VMs are independent of the host’s hardware.

How does this tie into Neovim? My desktop was utilizing a hypervisor for virtualization, which would affect the virtual machine’s performance, and therefore, affect Neovim’s performance. And though there are multiple methods of disabling Microsoft’s hypervisor, Hyper-V, nothing seemed to influence my desktop’s virtualization behavior. Here are two ways of disabling Hyper-V in Windows:

Disabling Hyper-V through Control Panel:

  1. Open Control Panel
  2. Select Programs and Features
  3. Select Turns Windows features on or off
  4. Expand the Hyper-V folder, then expand Hyper-V Platform, and then disable the Hyper-V Hypervisor

Disabling Hyper-V through Powershell:

  1. Open a Powershell window as an admin
  2. Run this command:
Disable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Hypervisor

After trying both of these options, I was disappointed to find that my desktop’s VM was still performing poorly. Interacting with the browser was sluggish, the terminal didn’t feel snappy, and most importantly, Neovim was not startup up “instantly,” like it was on my laptop.

And that’s when I came across this savior of a video:

I know that this looks like one of those stereotypical solution tutorial videos that only offer one solution (and that one solution never seems to work) that has tons of dislikes, but this video was actually different. It offered various ways of speeding up virtual machines, one of which was disabling the hypervisor with a command that I hadn’t seen before:

bcdedit /set hypervisorlaunchtype off

After running this command with an elevated instance of command prompt, I launched my Ubuntu virtual machine again. When I logged in, I felt like everything was happening slightly faster — the system was more responsive, but only subtly. When I tried to open a terminal with CTRL + ALT + T, there was no doubt in my mind that the hypervisor had been disabled. An instance of GNOME terminal opened up immediately and I ran the hyperfine benchmark on Neovim’s startup time to find a ridiculous startup time of just 73 ms!

BLAZINGLY FAST

I was beyond elated. I opened up Google Chrome to find that it also opened up blazingly fast compared to before. I went to search something up, and…

I was met with the Chrome dinosaur game. That was weird. My desktop had an ethernet connection, which I knew for certain was being used by the VM.

“This must be some kind of mistake,” I thought to myself, but after a few minutes of waiting and trying to ping Google with the command ping 8.8.8.8, I confirmed that my VM’s internet wasn’t working. As they say, fixing one bug always produces another…

At first I thought it would be as simple as restarting the virtual machine. I shut it down, reopened VMWare Workstation, and rebooted Ubuntu. No dice. I tried again just to make sure. Nothing. So I did the only thing I know how to do as a programmer: Consult Stack Overflow (or at least, the computer enthusiast equivalent of it).

Using one of the first search results that I stumbled across, I found a brilliant and simple solution that involves a lot of restarts. It seems pretty redundant, but it worked for me:

  1. Shut down the VM
  2. Remove the network adapter from the VM
  3. Turn the VM on, then turn it off
  4. Open the vmx file of the virtual machine (this one might be weird to find on Windows, since the file explorer doesn’t explicitly denote the file extension) using a text editor and delete any lines that begin with “ethernet0”
  5. Turn the VM on, then turn it off
  6. Add a new network adapter to the VM
  7. Turn the VM on, but this time, don’t turn it off
  8. Open a terminal and execute this command: ip link
  9. Find the network interface (for my Ubuntu VM, it was ens33) and run this command sudo ip link set <INTERFACE> up, replacing <INTERFACE> with the actual network interface
  10. Finally, have the network interface search for an IP address with this command: sudo dhclient <INTERFACE> -v

Without needing another restart at the end of these steps, my virtual machine was connected to the internet again! Neovim was running blazingly fast, and everything felt really fast and responsive. What else could I ask for?

More plugins.

leap

Leap is a motion plugin for Neovim that entirely replaces the functionality of the mouse when navigating through text. You might argue that Vim, on its own, has no need for the mouse, but it doesn’t really do that out of the box.

In order to move your cursor to a desired location in Vim, there are a few trains of thought depending on the scenario:

  1. If the text is visible, then jump up or down using relative line numbers to first get to the line in question. Then use something like f character jumping or w word jumping to get to the exact position in that line.
  2. If the text is not currently visible, then use CTRL + U or CTRL + D (I have these remapped to CTRL + K and CTRL + J, respectively) to jump up and down the file until the first scenario becomes relevant; apply the philosophy of the first scenario.
  3. Use / search to locate an exact sequence of characters and use n and N to jump to the correct instance (this requires more knowledge about the code or text than simply “hunting” for it like in the previous two scenarios).

Out of these three scenarios, Leap only truly addresses the first one, since that is the only scenario in which you would use your mouse to point and click on where you want your cursor to go in an editor like VS Code. This might make it a bit easier to understand:

Let’s imagine you are using a standard code editor like VS Code. In the second scenario, you aren’t actually clicking your mouse on the desired cursor location. Since the text isn’t visible on the screen, you are changing your code editor’s “view” by either scrolling up or down. Whether you are achieving this with keyboard inputs (up and down arrow keys, page up and page down arrow keys, etc.) or mouse inputs (scrollwheel up and scrollwheel down, click and drag on the navigation bar on the side, etc.) , it doesn’t change the fact that you aren’t clicking on a specific location for your cursor to go to.

In the third scenario, you aren’t even using your mouse at all, since you would be performing something like CTRL + F and then just searching for the characters in question.

It is only the first scenario in which you would click somewhere to start editing text. So how does Leap achieve this without replicating already-existing Vim motions?

By default, when you use s, Leap initiates a simple search that filters by two characters. When you type the first letter of your filter sequence, Leap searches through all the visible text on your screen ahead of your cursor and places markers at every single match. To perform a search backwards, that is, for anything behind your cursor, just begin the search with S.

You can then type the next character of your filter sequence, which will cause three things to happen: (1) Filter the markers more specifically (because now Leap is filtering by two characters specified, not one, (2) jump to the first instance of that two character filter, and (3) enable jumping via character markers. If you want to jump to a marker, simply type in the letter shown to do so.

In this way, Leap allows you to get anywhere on your current screen with just two or three inputs. In the case that your two character filter causes your cursor to jump to the first (or only) instance that has those two characters, you’ll only require two inputs. If this is not the case, you’ll have to specify where you want to jump to with one of the generated markers, so that counts as a third input. I know that this explanation isn’t really clear, so here’s an example. Consider this piece of code:

My keymaps.lua file

To make this example more meaningful, let’s try to edit some code with Leap that would actually make sense to do so with Leap, and not necessarily because it would make logical sense in the context of the code. In other words, we should try to get our cursor to somewhere in the code that would be faster to do with Leap rather than with built-in Vim motions.

A bad example would be to edit the keymap that makes CTRL + A select the entire file like so (27 lines down from the current cursor position) like so:

keymap.set(… to vim.keymap.set(...

Although that makes no sense to do from a standpoint of code logic, it also doesn’t make sense too much sense to jump there with Leap, which is what I’m trying to get at. Rather than using Leap, we can just perform 27j to jump down 27 lines, and then type ivim. to enter insert mode and insert the vim. that should precede the word keymap.

Just for reference, this is what Leap’s markers look like. We can type ‘b’ to jump 27 lines down.

Instead, let’s try to edit something that isn’t in the first column of code, like deleting the opts from the keymap 18 lines down from the cursor’s current location. To do this, we’ll want to get to the comma preceding opts and then delete everything up to the closing parenthesis with de or dt). That second option for deleting it might take an additional keystroke, but it might involve less mental overhead depending on how you think in certain scenarios (I would argue that de is the one that takes less mental overhead in this case).

If we were to follow the philosophy of “vertical motion, then horizontal motion,” we would definitely do 18j for the vertical motion, and as for the horizontal motion, we have several options. The two that come to mind are to either use f character jumping or word jumping.

Jumping through the line with f character jumping (clever-f plugin)

For the f character jumping, simply executing f,;; would get your cursor to the , before opts, since f, would jump your cursor to the first , in the line, and ;; would jump to the second (and last) , in the line. With the clever-f plugin, this becomes even simpler as you can just perform f,ff. Then just perform de or dt).

Though the word jumping might sound like a pretty bad option, you have to keep in mind that backwards word jumping is possible. So even if it would take 19 w's and two l’s to get your cursor to the comma, that doesn’t matter. Instead, we can do $bhh, since $ jumps to the end of the line and b jumps through words, but backwards (the hh adjusts the cursor to the comma).

Editor’s note: You can also do something funky like $F, but I’m not sure if the mental overhead for that one is worth it. If you do think like that, then more power to you.

Both of these options don’t seem that bad. One is to perform 18jf,;; and the other involves performing 18j$bhh. Neither requires too much mental overhead, since most of the “thinking” goes into checking the relative number before performing 18j, although the f character jumping could involve more inputs if the line was longer and involved more , characters (which doesn’t increase mental overhead at all, it just increases the number of inputs required).

With Leap, however, we can eliminate this “vertical motion, then horizontal motion” philosophy, which is especially useful when you already know what you want to edit. While keeping our eyes on where we want to jump, we type s to begin a forward-facing Leap search, then type , as our two character filter (that is a comma followed by a <SPC> just to be clear). This is what we’ll see:

After typing out the two character filter sequence

For every spot where there is a green highlight (the color of the highlight will depend on your colorscheme), there is a character to denote a marker. They are all offset by two characters from the filtered sequence so that you can actually see where you’re jumping to.

Since we’ve been keeping our eyes on the opts 18 lines down from the current cursor position, we'll see what character is affiliated with the marker we want o jump to — we type c and our cursor jumps there! Then we simply perform the aforementioned de or dt).

After typing ‘c’ to jump to the desired marker

The Modularity of lazy.nvim

Though I had solved my virtual machine’s sluggish performance by disabling my hypervisor, my startup times were slowly creeping higher and higher again (which I was not surprised to see, considering the amount of plugins that I had amalgamated). So I started looking into lazy-loading plugins a bit further.

However, I immediately ran into the problem that some plugins simply were not lazy-loading. In other words, trying to set the plugin to be lazy-loaded via lazy.nvim’s lazy = true parameter would result in the plugin not working at all. But why? For those written in Vimscript, like the clever-f plugin, I understood that there might be some compatibility issues, but for all the plugins written in Lua (most of them), why wasn’t lazy-loading working?

By taking a closer look at what distinguished these two sets of plugins (those that lazy-loaded successfully and those that did not), I noticed that all of those that failed were using default configurations or configurations coded directly into plugins.lua.

Now it made so much sense! How could a plugin be lazy-loaded unless it had another file to call upon later (later meaning the lazy-loading event itself)? Essentially, the proper way for plugins to be lazy-loaded involves a modular plugin configuration, which you can see by taking a look at how I’ve arranged my own Neovim configuration.

Of course, there are other ways to lazy-load plugins, like having them load on certain keymaps being pressed, but for me, my startup times are fast enough with the time saved from blindly lazy-loading most plugins — not that I’d recommend doing so.

The Lord of the Vims: The Return of the King

We’re finally entering the third arc of my Neovim config, where everything descends into madness on account of my capricious nature. Whether it’s adding plugins that are flat-out games or plugins that slowly transform my text editor closer and closer to an operating system, I’m all for it.

vim-be-good

vim-be-good is a Neovim plugin that is designed to help you get better at Vim motions. Developed by ThePrimeagen, vim-be-good is a simple game that tests your ability to execute Vim motions by timing your performance over several trials. The game offers a few modes:

The starting menu for vim-be-good

You can start vim-be-good with its command, :VimBeGood and then choose both a game mode and a game difficulty. Make sure you have relative line numbers turned on — not only are they required for the relative game mode, but they tend to be more pragmatic than absolute line numbers.

Once you’ve started vim-be-good, you can select options by deleting lines (or dd). Changing your difficulty option by deleting the corresponding line won’t start the game, but deleting one of the game mode lines will. Once you do so, a countdown from 3 will occur and the game will begin.

Every game will display the round count, the instructions for what you need to do, and the actual text that needs to be edited. Once you successfully complete a task (or the invisible timer runs out for the current round), the next round will begin.

In the “words” game mode, you simply have to use w and dw to delete the words that are different in the current line.

The “words” game mode

As you can see in the example round above, the different word is the one that reads “zar” instead of “var.” To delete it all we have to do is perform wwwdw (note that the different word can be directly under your cursor, so if you’re hunting for that different word for too long), just perform dw to delete the current word).

Once you’ve gone through all 10 rounds, you’ll see the ending screen:

The ending screen after playing the “words” game mode

You’ll see how many rounds you completed successfully as well as the average time it took for you to complete a round. You can then return to the menu with 2jdd, replay the same game with 3jdd, or quit with 4jdd (or more efficiently, ZZ).

In the ci{ game mode, you have to replace the content in between the outer brackets (which can either be square brackets or squiggly ones) with “bar.” As the game mode’s title implies, you should utilize ci[ or ci{ to quickly replace the inside content’s of the outermost braces with “bar,” rather than doing something slow like manually selecting the inside of the brackets, deleting it, and then performing O to create a new line above your cursor and then typing in bar.

If you don’t know about ci in Vim, it pretty much stands for “change inside.” So the character that proceeds the ci is what will be changed (meaning it will not only be deleted, but Vim will also put you into insert mode automatically). A very useful command is ciw, which will delete the current word and put you into insert mode.

I won’t go over all the game modes that can be played in vim-be-good — that’s up to you to explore!

Tetris

It’s Tetris. In Neovim. That’s all it is. Start the game with the :Tetris command and clear lines to your heart’s content!

Credit to the tetris.nvim GitHub repository

Blackjack

Similar to the Tetris plugin, this is just a simple game — but this time, it’s blackjack. Start a game with :BlackJackNewGame and gamble to your heart’s content (although you have no risk of losing money here)!

I’m ready to lose my life’s savings

While we’re on the topic of games…

vim-lichess

That’s right. You can play chess online with real people… in Neovim! The vim-lichess plugin is exactly as it sounds: A client that uses Lichess API calls to find games, interact with the board, send resign/draw offers, and even chat with your opponent.

The configuration for vim-lichess can be a little convoluted, although I found the documentation on the GitHub page to be particularly useful.

You will have to install berserk, which is a package for python3. Simply perform pip install berserk to satisfy that requirement. Speaking of which, you’ll also need python3, which you can verify if you have through some :checkhealth commands.

You’ll also need a Lichess account, which will be linked to vim-lichess through your Lichess API token. You’ll actually be prompted with instructions for this when you first perform :LichessFindGame for the first time. There are also some other settings you’ll have to apply before you can start playing:

vim.g.lichess_api_token = 'NOT FOR YOU TO SEE!!!'
vim.g.lichess_time = 10
vim.g.lichess_increment = 0
vim.g.lichess_rated = 0
vim.g.lichess_variant = 'standard'
vim.g.lichess_color = 'random'

vim.g.python_cmd = 'python3'
vim.g.lichess_debug_level = -1

Once everything is set up, do :LichessFindGame again and embrace the power of Magnus Carlsen! Move pieces by first left clicking on their original location and then right clicking on their destination squares. You can also type out your moves like a chess pro with the UCI format. :LichessMakeMoveUCI e2e4 is just an example.

Zone

Zone is a screensaver plugin for Neovim, emulating how operating systems display a screensaver. There are a ton of configuration options, including what animation should play once Neovim is left idle, how long that idle period is defined as (meaning how long Zone should check for a dearth of user inputs before initiating its screensaver), etc.

With this configuration, I have Zone use the DVD animation — the one where you watch the DVD logo in a trance until it hits a corner — so long as Neovim receives no inputs for 30 seconds.

local status_ok, zone = pcall(require, 'zone')
if not status_ok then
return
end

zone.setup({
style = 'dvd',
after = 30, -- Idle timeout
exclude_filetypes = { 'TelescopePrompt', 'NvimTree', 'neo-tree', 'dashboard', 'lazy' },
-- More options to come later

treadmill = {
direction = 'left',
headache = true,
tick_time = 30, -- Lower, the faster
-- Opts for Treadmill style
},
epilepsy = {
stage = 'aura', -- "aura" or "ictal"
tick_time = 100,
},
dvd = {
-- text = {"line1", "line2", "line3", "etc"}
tick_time = 100,
-- Opts for Dvd style
},
-- etc
})

You’ll also notice that you can exclude certain file types from having Zone act on them. This is very sensible since it looks particularly funky when Zone decides to animate the DVD logo bouncing around in your file explorer, which only occupies a small portion on the left side of your screen.

Noice

Noice is a difficult plugin to define. In simple terms, it makes a lot of menus and popups in Neovim look, well, NOICE.

Noice

Using the fairly new vim.ui_attach API, Noice makes a lot of antiquated UI elements of Neovim look modern and sleek. For example, when you type a colon to enter command mode, instead of your command showing up in the bottom left in a very mundane and boring fashion, Noice will have your command displayed in a floating window in the middle of your screen, even updating its title depending on what you type. Here’s an example.

Here, I’ve simply pressed the colon key, creating this beautiful floating window from Noice that reads “Cmdline.”

Look carefully here. The textbox no longer reads “Cmdline,” but instead reads “Help.” Noice responds to me typing :h (that’s a colon followed by an ‘h’ and then followed by a space) in the command by changing the title of the floating window to “Help,” and changing the icon to a question mark.

Noice can also spawn these amazing popup menus everytime you write to a file

which appear in the top right of your terminal by default.

There’s a lot you can do with Noice, and my summary really does the plugin a disservice to how customizable it is — I’d recommend checking out it’s GitHub repository to see all the options that are available.

Transparent

I’m sure you’ve seen a myriad of coding workflows which involve transparency in their code editors. Whether it’s VS Code or Vim, being able to see your wallpaper is always a cool flex in order to aggrandize your status as a programmer. With this transparency plugin, you can achieve exactly that.

Simply install the plugin as you would with your favorite plugin manager and be sure to not lazy-load it (it can cause some aberrant visual effects to Neovim). The loading time is quite fast, so it’s essentially negligible even without lazy-loading.

Once it’s installed, just use :TransparentToggle to toggle transparency on or off. It’s important to note that this transparency is based off of your terminal’s transparency. Ubuntu uses GNOME Terminal by default and you can easily change the transparency through the built-in GUI via its “Preferences” page.

  1. Right click in an open area of the terminal and select “Preferences”
  2. Switch over to the “Colors” tab and adjust the “Use transparent background” slider while having it checked off

I’d recommend setting it to some value between 10% and 25%, although that’s easier said than done with a slider that doesn’t even display the value that it’s set to. In order to adjust the opacity of the GNOME Terminal, do the following:

First, use this command

dconf dump /org/gnome/terminal/legacy/profiles:/

to find the profile of your terminal. You’ll get some output that looks like this:

[:b1dcc9dd-5262-4d8d-a863-c897e6d979b9]
background-color='rgb(33,33,33)'
background-transparency-percent=10
bold-color-same-as-fg=true
bold-is-bright=false
default-size-columns=80
font='Hack Nerd Font 12'
foreground-color='rgb(255,255,255)'
use-system-font=false
use-theme-colors=false
use-theme-transparency=false
use-transparent-background=true
visible-name='kevinfengcs88'

The first part is what you’ll want, minus the square brackets. In my case, it’s :b1dcc9dd-5262-4d8d-a863-c897e6d979b9 . Then execute this command, replacing <profile-id> with your profile ID (which does start with a colon so make sure to include that) and <value> with your desired opacity value (don’t include the angled brackets).

dconf write /org/gnome/terminal/legacy/profiles:/<profile-id>/background-transparency-percent <value>

I’ve found that setting my terminal opacity to 10 results in a good balance between being able to see my wallpaper and also keeping my code quite readable. Setting the opacity to 25 really starts pushing it for me, but it does make my wallpaper really stand out.

With the power of anime waifus from ThePrimagen and a tool like Variety, I’ve set it so that my wallpaper changed every 5 seconds from a specified directory. I’ve filtered out ThePrimeagen’s wallpaper repo for my favorite anime waifus (and maybe even a few bonuses of my own), and you can find those images in my Neovim config’s waifus folder.

Transparent Neovim = waifus galore

This isn’t the only way to make Neovim transparent, however. As I’ve learned from the great ThePrimeagen, you can include something like this in your config:

vim.api.nvim_set_hl(0, "Normal", { bg = "none" })
vim.api.nvim_set_hl(0, "NormalFloat", { bg = "none" })

Although I still prefer the transparent plugin since I can include a keymap which toggles transparency depending on whether I want it or not:

local opts = { noremap = true, silent = true }
local keymap = vim.keymap

keymap.set('n', '<C-t>', ':TransparentToggle<CR>', opts)

Harpoon

Of course the final plugin that we’ll be going over in our adventure is the greatest plugin of them all — created by the greatest Vimmer to ever exist, ThePrimeagen.

Peak masculinity

Harpoon is a navigation plugin that follows Vim philosophies without going so far as something like bufferline.nvim. The core functionality of Harpoon is to pin desired buffers to the Harpoon menu, which you can then access like a normal buffer, delete pinned buffers, or switch to pinned buffers. I know that sounds somewhat convoluted, so I’d recommend watching ThePrimeagen’s 0 to LSP video, starting at this timestamp, where he discusses Harpoon.

In my Harpoon config, I use the defaults and include some useful keymaps:

local harpoon_status_ok, harpoon = pcall(require, 'harpoon')
if not harpoon_status_ok then
return
end

local harpoon_mark_status_ok, harpoon_mark = pcall(require, 'harpoon.mark')
if not harpoon_mark_status_ok then
return
end

local harpoon_ui_status_ok, harpoon_ui = pcall(require, 'harpoon.ui')
if not harpoon_ui_status_ok then
return
end

local opts = { noremap = true, silent = true }
local keymap = vim.keymap

harpoon.setup({
menu = {
width = 60,
},
})

keymap.set('n', '<leader>h', harpoon_mark.add_file, opts)
keymap.set('n', '<C-e>', harpoon_ui.toggle_quick_menu, opts)

keymap.set('n', '<leader>1', function() harpoon_ui.nav_file(1) end, opts)
keymap.set('n', '<leader>2', function() harpoon_ui.nav_file(2) end, opts)
keymap.set('n', '<leader>3', function() harpoon_ui.nav_file(3) end, opts)
keymap.set('n', '<leader>4', function() harpoon_ui.nav_file(4) end, opts)
keymap.set('n', '<leader>5', function() harpoon_ui.nav_file(5) end, opts)
keymap.set('n', '<leader>6', function() harpoon_ui.nav_file(6) end, opts)
keymap.set('n', '<leader>7', function() harpoon_ui.nav_file(7) end, opts)
keymap.set('n', '<leader>8', function() harpoon_ui.nav_file(8) end, opts)
keymap.set('n', '<leader>9', function() harpoon_ui.nav_file(9) end, opts)

I use <leader>h to pin the current buffer I’m on to Harpoon, <C-e> to toggle Harpoon’s menu (this used to be C-h until I reserved that keymap for moving leftwards in buffer splits), as well as <leader>x jumping to the xth buffer in Harpoon’s menu with x being any number between 1 and 9.

I find Harpoon to be particularly useful for projects that I keep coming back to, like my Neovim config. Since I know my README is the first item in Harpoon’s menu, I simply execute <leader>1 and jump to there with literally no mental overhead since it’s muscle memory at this point.

Godlike Keymaps

While we’re talking about our lord and savior ThePrimeagen, I want to mention some amazing keymaps that I’ve learned from his 0 to LSP video, as well as some keymaps that I’ve also stumbled across myself. Here are what I find to be the most powerful keymaps in my keymaps.lua file.


local opts = { noremap = true, silent = true }
local keymap = vim.keymap

-- search movement keeps cursor in middle
keymap.set('n', 'n', 'nzzzv', opts)
keymap.set('n', 'N', 'Nzzzv', opts)

-- vertical movement keeps cursor in middle
keymap.set('n', '<C-j>', '<C-d>zz', opts)
keymap.set('n', '<C-k>', '<C-u>zz', opts)

-- vertical movement keeps cursor in middle (visual mode)
keymap.set('v', '<C-j>', '<C-d>zz', opts)
keymap.set('v', '<C-k>', '<C-u>zz', opts)

-- copy into system clipboard with CTRL + C
keymap.set('v', '<C-c>', '"+y', opts)

-- copy into host system clipboard with <leader>y
keymap.set('v', '<leader>y', '"*y', opts)

-- move lines around
keymap.set('v', 'J', ":m '>+1<CR>gv=gv", opts)
keymap.set('v', 'K', ":m '<-2<CR>gv=gv", opts)

-- the greatest remap ever (Primeagen)
keymap.set('v', '<leader>p', '"_dP', opts)

Let’s start with the first three keymaps which all keep the cursor in the middle of the screen:

-- search movement keeps cursor in middle
keymap.set('n', 'n', 'nzzzv', opts)
keymap.set('n', 'N', 'Nzzzv', opts)

The first keymap simply ensures that your cursor remains centered when hitting n or N while searching for a sequence with / .

-- vertical movement keeps cursor in middle
keymap.set('n', '<C-j>', '<C-d>zz', opts)
keymap.set('n', '<C-k>', '<C-u>zz', opts)

The next keymap achieves two things: First, it remaps the awkward <C-d> and <C-u> to <C-j> and <C-k> , respectively. After all, j and k are already used for down and up movement, so why would jumping by half a page require different keys? Second, it very simply maintains your cursor in the center of the screen with zz.

-- vertical movement keeps cursor in middle (visual mode)
keymap.set('v', '<C-j>', '<C-d>zz', opts)
keymap.set('v', '<C-k>', '<C-u>zz', opts)

This final keymap does the same thing as the previous one but also in visual mode. That’s it.

After that, we have some keymaps which mess with system clipboards a bit — they might not work for you if you’re not using a virtual machine or if you don’t have wl-clipboard installed.

-- copy into system clipboard with CTRL + C
keymap.set('v', '<C-c>', '"+y', opts)

-- copy into host system clipboard with <leader>y
keymap.set('v', '<leader>y', '"*y', opts)

The first one makes it so that I can easily copy a selection into the system (guest OS, or Ubuntu in my case) with the intuitive <C-c>. The same thing can be achieved for the host OS (Windows 10 for me) with <leader>y.

-- move lines around
keymap.set('v', 'J', ":m '>+1<CR>gv=gv", opts)
keymap.set('v', 'K', ":m '<-2<CR>gv=gv", opts)

This next keymap can also be pretty useful, although I would be careful of using K for moving lines up. Although it is very intuitive, it might conflict with some LSP keymaps, which commonly use capital K to jump to definitions or function descriptions. Of course, this is also unlikely at the same time considering this keymap is visual mode, but hey, you can never be too chary.

As the comment implies, J moves a line down a line by effectively swapping the current line the cursor is on with the one below it and K does the same thing, but in the upwards direction.

-- the greatest remap ever (Primeagen)
keymap.set('v', '<leader>p', '"_dP', opts)

The greatest keymap comes last. Have you ever tried to replace something by highlighting it in visual mode, replaced it with your register, only to find that your register was furtively overridden by what you had just deleted?

Okay, I know that that was a horrible explanation. Let’s consider a simple example instead:

System.out.println("Hello world!");
System.out.println("foobar");

Let’s imagine that I want this program to print out “Hello world!” thrice rather than printing out “Hello world!” followed by “foobar.” So what would we do?

First, we should yank the first line into our Vim register with something like yy or even Vy. After that, we perform j to go down to the line that prints out “foobar.” Since we want to replace the entire line, we select the whole line with V, and replace it with the “Hello world!” line with p .

Now our code looks like this:

System.out.println("Hello world!");
System.out.println("Hello world!");

We need just one more “Hello world!” print statement, since we want it to print three times right? So executing p would seem like the easiest way to do that, but that would actually result in this:

System.out.println("Hello world!");
System.out.println("Hello world!");
System.out.println("foobar");

Why is this? Well Vim has the nasty (but quite useful) habit of storing whatever was just deleted in its register, so when we deleted the line that printed “foobar,” we stored that in our register (which is pasted out when we hit p ). To circumvent this, we use the <leader>p keymap that was defined earlier when pasting over the “foobar” line, preserving our original register to be used when pasting with p .

Bonus Terminal Fun Stuff (figlet + lolcat + neofetch)

Just before we conclude, I wanted to include just a few bonus tools that I use to make my terminal even cooler:

With the power of figlet, lolcat, and neofetch, I can make my terminal startup look quite spiffy. Install all of these with your favorite package manager (no setup is really required).

You can test out figlet with something like

figlet "Neovim is cool"

which should output this:

 _   _                 _             _                       _ 
| \ | | ___ _____ _(_)_ __ ___ (_)___ ___ ___ ___ | |
| \| |/ _ \/ _ \ \ / / | '_ ` _ \ | / __| / __/ _ \ / _ \| |
| |\ | __/ (_) \ V /| | | | | | | | \__ \ | (_| (_) | (_) | |
|_| \_|\___|\___/ \_/ |_|_| |_| |_| |_|___/ \___\___/ \___/|_|

You can then pipe that output to lolcat like so

figlet "Neovim is cool" | lolcat

which will output this really cool rainbow ASCII text:

Lastly, you can test neofetch with no parameters to get some information about your system alongside an ASCII rendition of your operating system. Putting it all together with some commands at the top of your .bashrc or .zshrc like this

figlet -f big "Kevin Feng" | lolcat
neofetch

allows us to get this amazing greeting from our terminal every time we open one:

Conclusion

In conclusion, I will never write this lengthy of an article on Neovim ever again. Although it was fun, I ran out of steam halfway through and put off on finishing it for an entire month (just check out the difference in dates between the GitHub repo branch I mentioned at the beginning and the publication date).

Also, I can confidently state that I will probably never escape the configuration of Neovim for the rest of my life. I already have to do some work on Windows for work this summer, so I’m looking forwards to doing this entire thing again, but for Windows…

Until next time, and happy Vimming!

Sources

https://www.reddit.com/r/vim/comments/39jtib/what_is_the_difference_between_mapleader_and/

https://www.vmware.com/topics/glossary/content/hypervisor.html

--

--