Posted on :: Min Read :: Tags: , :: Source Code

Motivation

Ever since TJ said “Personalized Development Environment,” the phrase latched onto me like a cobweb in a mineshaft. A Personalized Development Environment (PDE) describes an ideal setup that is tailored to your needs and preferences – it lies between a bare-bone text editor and a full-fledged IDE. It is a place where you can be productive, efficient, and comfortable. It is a place that suits you and only you.

I started using Vim in my sophomore year of high school when my computer couldn’t run Visual Studio Code and Windows simultaneously, so the terminal (and Linux) became my home. I still remember my early days as a Linux user — filled with frustration, confusion, and much googling. I remember my computer being unable to connect to my printer because Linux didn’t have a patch yet. I remember the first time my bootloader was corrupted due to a Windows update, and I was praying I could still boot into my precious setup. I remember when I decided to delete Windows for good, but I forgot that I had mounted the partition and ended up nuking my hard drive. Going down this path means headaches, but it also means growth. It means learning how to fix your own problems and learning how to be self-sufficient.

I cannot, in good faith, teach you how to debug every issue you will encounter (those errors will structure your ideal PDE), but I can inspire you and your setup. I am now going to be a junior in College, so I have had a few years to refine my setup, and I hope you do the same. I’ll show you the workflow that I have built over the years and how I have optimized it to be as efficient as possible. This is the way of SeniorMars; this is my way!

Dotfiles

I know people will ask, so here they are: SeniorMars’ Dotfiles. Moreover, I will be assuming you are using Neovim 0.10!

Improving the (Neo)Vim Experience

Although, I stated this was not for beginners, I still need to point out the fundamentals. Without these, I would not consider neovim to even approach the basics of a PDE. If you think this is too basic, then you can skip this section – I promise you will learn something.

Search and Replace

Let’s first start with something simple, so you get a taste on what kind of tips I will be giving you. Let’s start with search and replace.

vim.opt.ignorecase = true -- search case insensitive
vim.opt.smartcase = true -- search matters if capital letter
vim.opt.inccommand = "split" -- "for incsearch while sub

The first two options are self-explanatory. ignorecase makes search case-insensitive, and smartcase makes search case-sensitive if you use a capital letter. The third option is where the magic happens. Ever since, I first learned of incsearch, I have been in love. incsearch highlights the search pattern as you type it, giving you a real-time preview of where your pattern matches. The magic doesn’t stop at highlighting, though; by setting inccommand to split, we have a preview of what our substitution will look like.

Now, let’s move on to keybindings.

vim.g.mapleader = ","
local keyset = vim.keymap.set
keyset("n", "<leader>sr", ":%s/<<C-r><C-w>>//g<Left><Left>")

Moreover, when I know I want to rename a variable, I do the following:

  1. Place my cursor on the variable I would like to rename.
  2. Use the keybinding * in normal mode to highlight all instances of the variable.
  3. Do cgn to change the word under the cursor and move to the next instance.
  4. Press . to repeat the change on the next instance.

Now, for some plugins.

-- I use lazy.nvim to load plugins 
{"mhinz/vim-grepper"}

vim.g.mapleader = ","
local keyset = vim.keymap.set
keyset("n", "<leader>fs", ':GrepperRg "" .<Left><Left><Left>')
keyset("n", "<leader>fS", ":Rg<space>")
keyset("n", "<leader>*", ":Grepper -tool rg -cword -noprompt<cr>")
keyset("n", "gs", "<Plug>(GrepperOperator)")
keyset("x", "gs", "<Plug>(GrepperOperator)")

This allows me to use rg to search for a word under the cursor, search for a word in the current director, and search for a word in the current directory. Grepper Operator allows me to use other vim motions to select text and search for it.

Now, to do something similar to other commands like :norm, I use:

{
    "smjonas/live-command.nvim", -- live command
    config = function()
        require("live-command").setup({commands = {Norm = {cmd = "norm"}}})
    end
}

Ex Commands and g command

If you have read the vim help docs, you may know some of these, but these are the little things that make a big difference.

vim.opt.wildmode = "list:longest,list:full" -- for : stuff
vim.opt.wildignore:append({".javac", "node_modules", "*.pyc"})
vim.opt.wildignore:append({".aux", ".out", ".toc"}) -- LaTeX
vim.opt.wildignore:append({
    ".o", ".obj", ".dll", ".exe", ".so", ".a", ".lib", ".pyc", ".pyo", ".pyd",
    ".swp", ".swo", ".class", ".DS_Store", ".git", ".hg", ".orig"
})
vim.opt.suffixesadd:append({".java", ".rs"}) -- search for suffexes using gf

The first option is for the : command. It allows you to use the tab completion to find the longest common match and then show the full list. The second option is for the wildignore option. It allows you to ignore certain files when using tab completion. The third option is for the suffixesadd option. It allows you to use gf to open files with the given suffixes. You can customize these to your liking, but adding this allows you to filter out the noise and focus on what you want to see.

Additionally, this is one of the must-have plugins:

--- lazy.nvim
{"gelguy/wilder.nvim", build = ":UpdateRemotePlugins"}, -- : autocomplete
-- wilder
local wilder = require("wilder")
wilder.setup({modes = {":", "/", "?"}})
wilder.set_option("pipeline", {
    wilder.branch(wilder.python_file_finder_pipeline({
        file_command = function(_, arg)
            if string.find(arg, ".") ~= nil then
                return {"fd", "-tf", "-H"}
            else
                return {"fd", "-tf"}
            end
        end,
        dir_command = {"fd", "-td"},
        filters = {"fuzzy_filter", "difflib_sorter"}
    }), wilder.cmdline_pipeline(), wilder.python_search_pipeline())
})

wilder.set_option("renderer", wilder.popupmenu_renderer({
    highlighter = wilder.basic_highlighter(),
    left = {" "},
    right = {" ", wilder.popupmenu_scrollbar({thumb_char = " "})},
    highlights = {default = "WilderMenu", accent = "WilderAccent"}
}))

Wilder is a fuzzy finder for Ex commands, search history, and command history. It is a must-have plugin for me because it allows me to search for commands and history in a more efficient way.

Wilder

Undo and Redo

Undo and Redo are essential to any text editor. However, in Vim, you can make this more powerful! First,

vim.opt.undofile = true -- save undo history
local keyset = vim.keymap.set
keyset("i", ",", ",<C-g>U")
keyset("i", ".", ".<C-g>U")
keyset("i", "!", "!<C-g>U")
keyset("i", "?", "?<C-g>U")

The first option allows you to save your undo history. The next options add more power to undo as this updates undo when you use other vim operations. Now, we add:

{"mbbill/undotree", lazy = true, cmd = "UndotreeToggle"}, -- see undo tree

keyset("n", "<leader>u", ":UndotreeToggle<cr>")

Undo Tree

This allows you to see your undo tree and navigate it with diffs – extremely useful when you want to see what you have done and what you can undo.

Combine this with AutoSession, I can open a project and have all my windows, buffers, and undo history saved.

{
    "rmagatti/auto-session", -- auto save session
    config = function()
        require("auto-session").setup({
            log_level = "error",
            auto_session_suppress_dirs = {
                "~/", "~/Downloads", "~/Documents"
            },
            auto_session_use_git_branch = true,
            auto_save_enabled = true
        })
    end
}

Movement

Movement is essential to any text editor. However, in Vim, you can make this more powerful! First,

vim.opt.splitbelow = true -- split windows below
vim.opt.splitright = true -- split windows right

-- Movement
local keyset = vim.keymap.set
keyset("i", "jk", "<esc>")

keyset("v", "J", ":m '>+1<cr>gv=gv")
keyset("v", "K", ":m '<-2<cr>gv=gv")
keyset("n", "<space>h", "<c-w>h")
keyset("n", "<space>j", "<c-w>j")
keyset("n", "<space>k", "<c-w>k")
keyset("n", "<space>l", "<c-w>l")
keyset("n", "<leader>wh", "<c-w>t<c-h>H")
keyset("n", "<leader>wk", "<c-w>t<c-h>K")
keyset("n", "<down>", ":resize +2<cr>")
keyset("n", "<up>", ":resize -2<cr>")
keyset("n", "<right>", ":vertical resize +2<cr>")
keyset("n", "<left>", ":vertical resize -2<cr>")
keyset("n", "j", "(v:count ? 'j' : 'gj')", {expr = true})
keyset("n", "k", "(v:count ? 'k' : 'gk')", {expr = true})

This allows us to move line around in visual mode, move windows, and reside windows. Moreover, we can go up and down in long lines!

Furthermore, I have some auto commands that allow me to move between windows and resize them:

local autocmd = vim.api.nvim_create_autocmd
vim.api.nvim_create_augroup("Random", {clear = true})

autocmd("VimResized", {
    group = "Random",
    desc = "Keep windows equally resized",
    command = "tabdo wincmd ="
})

autocmd("TermOpen", {
    group = "Random",
    command = "setlocal nonumber norelativenumber signcolumn=no"
})

This allows me to keep my windows equally resized and to remove the number column when I open a terminal.

Finally, I have some plugins that help me with movement and manipulating text:

{"tpope/vim-repeat"}, -- repeat
{
    "kylechui/nvim-surround", -- surround objects
    config = function() require("nvim-surround").setup({}) end
},
{"windwp/nvim-autopairs"}, -- autopairs 
{
    'andymass/vim-matchup',
    config = function()
        vim.api.nvim_set_hl(0, "OffScreenPopup",
                            {fg = "#fe8019", bg = "#3c3836", italic = true})
        vim.g.matchup_matchparen_offscreen = {
            method = "popup",
            highlight = "OffScreenPopup"
        }
    end
},
{"wellle/targets.vim"}, -- adds more targets like [ or ,

--- setup
local rule = require("nvim-autopairs.rule")
local cond = require("nvim-autopairs.conds")
local autopairs = require("nvim-autopairs")

autopairs.add_rules({
    rule("$", "$", {"tex", "latex"}):with_cr(cond.none())
})

autopairs.get_rules("`")[1].not_filetypes = {"tex", "latex"}
autopairs.get_rules("'")[1].not_filetypes = {"tex", "latex", "rust"}

This allows me to repeat commands, add more targets like [], and auto complete pairs like (). Moreover, with nvim-surround, I can surround text objects with different characters like quotes, brackets, and braces. Finally with vim-matchup, I can see the matching pair of brackets, braces, and quotes. If the code is very long it also shows the top matching pair in a popup window.

Matchup

Better quickfix

If are not taking advantage of the quickfix list, you are missing out. The quickfix list is a powerful tool that allows you to navigate through errors, warnings, and search results – anything as long as you know how to use it. However, I like to use a plugin to make it more powerful.

{"kevinhwang91/nvim-bqf"}, -- better quickfix
--- later in the config
keyset("n", "<leader>cn", ":cnext<cr>")
keyset("n", "<leader>cp", ":cprevious<cr>")

This allows me to navigate through the quickfix list with ease. In fact, this is the setup I used to showoff grepper.

I integrated the quickfix list with other tools I’ll get to later.

Spell Check

Vim has a built-in spell checker, but of course, we need to make it better.

function SpellToggle()
    if vim.opt.spell:get() then
        vim.opt_local.spell = false
        vim.opt_local.spelllang = "en"
    else
        vim.opt_local.spell = true
        vim.opt_local.spelllang = {"en_us"}
    end
end

keyset("n", "<leader>5", ":lua SpellToggle()<cr>")
keyset("n", "<leader>z", "[s1z=``")

This allows me to toggle spell check and fix spelling mistakes with <leader>z while not moving my cursor! In addition to this I use a language server (LSP) to check my spelling through language tool: ltex.

Status Line

At one point in your vim carer, you would have downloaded a status line plugin. After a while, though, you realize that you don’t need a status line plugin. You can make your own status line! Here is mine (with cache implmented):

local cache = {}

local function refresh_cache(key)
    if cache[key] then cache[key].value = cache[key].fn() end
end

local function cache_get(key, compute_fn)
    local cached = cache[key]
    if cached then return cached.value end
    local value = compute_fn()
    cache[key] = {value = value, fn = compute_fn}
    return value
end

function SpellToggle()
    if vim.opt.spell:get() then
        vim.opt_local.spell = false
        vim.opt_local.spelllang = "en"
    else
        vim.opt_local.spell = true
        vim.opt_local.spelllang = {"en_us"}
    end
end

local function spell_status()
    local spellLang = vim.opt_local.spelllang:get()
    if type(spellLang) == "table" then
        spellLang = table.concat(spellLang, ", ")
    end
    return string.upper(spellLang)
end

local function git_branch()
    return cache_get("git_branch", function()
        if vim.g.loaded_fugitive then
            local branch = vim.fn.FugitiveHead()
            if branch ~= "" then
                if vim.api.nvim_win_get_width(0) <= 80 then
                    return " " .. string.upper(branch:sub(1, 2))
                end
                return " " .. string.upper(branch)
            end
        end
        return ""
    end)
end

local function update_status_for_file(file_path)
    -- Get number of lines added and deleted using git diff --numstat
    local diff_stats = vim.fn.system("git diff --numstat " ..
                                         vim.fn.shellescape(file_path))
    if vim.v.shell_error ~= 0 or diff_stats == "" then return "" end

    local added, deleted = diff_stats:match("(%d+)%s+(%d+)%s+%S+")
    added, deleted = tonumber(added), tonumber(deleted)
    local delta = math.min(added, deleted)

    local status = {
        changed = delta,
        added = added - delta,
        removed = deleted - delta
    }

    -- Format the status for display
    local status_txt = {}
    if status.added > 0 then table.insert(status_txt, "+" .. status.added) end
    if status.changed > 0 then
        table.insert(status_txt, "~" .. status.changed)
    end
    if status.removed > 0 then
        table.insert(status_txt, "-" .. status.removed)
    end

    if #status_txt > 1 then
        for i = 2, #status_txt do status_txt[i] = "," .. status_txt[i] end
    end

    local formatted_status = ""
    if #status_txt > 0 then
        formatted_status = string.format("[%s]", table.concat(status_txt))
    else
        formatted_status = ""
    end

    return formatted_status
end

local function status_for_file()
    return cache_get("file_status", function()
        local file_path = vim.api.nvim_buf_get_name(0)

        if file_path == "" then return "" end
        return update_status_for_file(file_path)
    end)
end

local function human_file_size()
    return cache_get("file_size", function()
        local file = vim.api.nvim_buf_get_name(0)
        if file == "" then return "" end

        local size = vim.fn.getfsize(file)
        local suffixes = {"B", "KB", "MB", "GB"}
        local i = 1
        while size > 1024 do
            size = size / 1024
            i = i + 1
        end

        return size <= 0 and "" or string.format("[%.0f%s]", size, suffixes[i])
    end)
end

local function smart_file_path()
    return cache_get("file_path", function()
        local buf_name = vim.api.nvim_buf_get_name(0)
        local is_wide = vim.api.nvim_win_get_width(0) > 80
        if buf_name == "" then return "[No Name]" end

        local file_dir = buf_name:sub(1, 5):find("term") and vim.env.PWD or
                             vim.fs.dirname(buf_name)
        file_dir = file_dir:gsub(vim.env.HOME, "~", 1)

        if not is_wide then file_dir = vim.fn.pathshorten(file_dir) end

        if buf_name:sub(1, 5):find("term") then
            return file_dir .. " "
        else
            return string.format("%s/%s ", file_dir, vim.fs.basename(buf_name))
        end
    end)
end

local function word_count()
    local words = vim.fn.wordcount()
    if words.visual_words ~= nil then
        return string.format("[%s]", words.visual_words)
    else
        return string.format("[%s]", words.words)
    end
end

local modes = setmetatable({
    ["n"] = {"NORMAL", "N"},
    ["no"] = {"N·OPERATOR", "N·P"},
    ["v"] = {"VISUAL", "V"},
    ["V"] = {"V·LINE", "V·L"},
    [""] = {"V·BLOCK", "V·B"},
    [""] = {"V·BLOCK", "V·B"},
    ["s"] = {"SELECT", "S"},
    ["S"] = {"S·LINE", "S·L"},
    [""] = {"S·BLOCK", "S·B"},
    ["i"] = {"INSERT", "I"},
    ["ic"] = {"INSERT", "I"},
    ["R"] = {"REPLACE", "R"},
    ["Rv"] = {"V·REPLACE", "V·R"},
    ["c"] = {"COMMAND", "C"},
    ["cv"] = {"VIM·EX", "V·E"},
    ["ce"] = {"EX", "E"},
    ["r"] = {"PROMPT", "P"},
    ["rm"] = {"MORE", "M"},
    ["r?"] = {"CONFIRM", "C"},
    ["!"] = {"SHELL", "S"},
    ["t"] = {"TERMINAL", "T"}
}, {
    __index = function()
        return {"UNKNOWN", "U"} -- handle edge cases
    end
})

local function get_current_mode()
    local mode = modes[vim.api.nvim_get_mode().mode]
    if vim.api.nvim_win_get_width(0) <= 80 then
        return string.format("%s ", mode[2]) -- short name
    else
        return string.format("%s ", mode[1]) -- long name
    end
end

local function file_type()
    return cache_get("file_type", function()
        local buf_name = vim.api.nvim_buf_get_name(0)
        local width = vim.api.nvim_win_get_width(0)

        local ft = vim.bo.filetype
        if ft == "" then
            return "[None]"
        else
            if width > 80 then
                return string.format("[%s]", ft)
            else
                local ext = vim.fn.fnamemodify(buf_name, ":e")
                local shorter = (string.len(ft) < string.len(ext)) and ft or ext
                return string.format("[%s]", shorter)
            end
        end
    end)
end

---@diagnostic disable-next-line: lowercase-global
function status_line()
    return table.concat({
        get_current_mode(), -- get current mode
        spell_status(), -- display language and if spell is on
        git_branch(), -- branch name
        " %<", -- spacing
        smart_file_path(), -- smart full path filename
        "%h%m%r%w", -- help flag, modified, readonly, and preview
        "%=", -- right align
        status_for_file(), -- git status for file
        word_count(), -- word count
        "[%-3.(%l|%c]", -- line number, column number
        human_file_size(), -- file size
        file_type() -- file type
    })
end

vim.api.nvim_create_augroup("StatusLineCache", {})
vim.api.nvim_create_autocmd({"BufEnter"}, {
    pattern = "*",
    group = "StatusLineCache",
    callback = function()
        refresh_cache("git_branch") -- this should be another event
        refresh_cache("file_status")
        refresh_cache("file_size")
        refresh_cache("file_path")
        refresh_cache("file_type")
    end
})

vim.api.nvim_create_autocmd({"BufWritePost"}, {
    pattern = "*",
    group = "StatusLineCache",
    callback = function()
        refresh_cache("file_status")
        refresh_cache("file_size")
        refresh_cache("file_path")
    end
})

vim.api.nvim_create_autocmd({"WinResized"}, {
    pattern = "*",
    group = "StatusLineCache",
    callback = function()
        refresh_cache("git_branch")
        refresh_cache("file_path")
        refresh_cache("file_type")
    end
})

vim.opt.statusline = "%!luaeval('status_line()')"

vim.wo.fillchars = "eob:~" -- fillchars of windows

This is my status line. It shows the current mode, language, branch name, file path, git status, word count, line number, column number, file size, and file type. It is a lot, but I hope my documentation helps you understand what each part does. It is extremely minimum but provides me with all the information I need depending on my current state.

Status Line

Status Line

Status Line

Formatting

Formatting is essential to any text editor:

vim.api.nvim_create_user_command("FixWhitespace", [[%s/\s\+$//e]], {})
keyset("n", "<leader>3", ":retab<cr>:FixWhitespace<cr>")

However, I use a plugin to make it more powerful:

{"sbdchd/neoformat"}, -- format
vim.g.neoformat_try_formatprg = 1

This allows me to format my code with neoformat and remove trailing whitespace with FixWhitespace. Moreover, I can still use the class = and gq to format my code. Very useful!

Better netrw

I know, I know, netrw is not the best file explorer, but I don’t use it enough to care that much. However, I do have some settings to make it more bearable:

vim.g.netrw_banner = 0
vim.g.netrw_browse_split = 4
vim.g.netrw_liststyle = 3
vim.g.netrw_winsize = -28
vim.g.netrw_browsex_viewer = "open -a firefox"

These settings remove the banner, open the file explorer in a vertical split, use the tree style, and set the size of the file explorer. Moreover, I can open files in the browser with gx.

Netrw

Git Integration

Now, I made a video about this (here), so I won’t go into too much detail. However, I will show you how I set it up:

{"tpope/vim-fugitive"}, -- Git control for vim

-- fugitive
keyset("n", "<leader>gg", ":Git<cr>", {silent = true})
keyset("n", "<leader>ga", ":Git add %:p<cr><cr>", {silent = true})
keyset("n", "<leader>gd", ":Gdiff<cr>", {silent = true})
keyset("n", "<leader>ge", ":Gedit<cr>", {silent = true})
keyset("n", "<leader>gw", ":Gwrite<cr>", {silent = true})
keyset("n", "<leader>gf", ":Commits<cr>", {silent = true})

Honestly, although this pretty simple, it is extremely powerful. I can see the git status, add files, diff files, edit files, write files, and see commits. Together with gh the github cli, I can do everything I need to do with git (though I do have more, which I will talk about in a later section)!

Finally, I set Neovim to be my merge tool and diff tool (through ~/gitconfig):

[user]
	email = seniormars@rice.edu
	name = SeniorMars
	username = SeniorMars
	signingkey = 7C668A6D13D5729989FB126B183357B41320BB2B
[core]
	pager = delta
	editor = nvim
	excludesfile = /Users/charlie/.config/git/gitignore_global
[credential]
	helper = cache
[init]
	defaultBranch = main
[diff]
	algorithm = patience
	compactionHeuristic = true
	tool = nvimdiff
[difftool "nvimdiff"]
	cmd = nvim -d \"$LOCAL\" \"$REMOTE\" -c \"wincmd w\" -c \"wincmd L\"
[merge]
	tool = nvimdiff4
	prompt = false
[mergetool "nvimdiff4"]
	cmd = nvim -d $LOCAL $BASE $REMOTE $MERGED -c '$wincmd w' -c 'wincmd J' -c 'set diffopt&' -c 'set diffopt+=algorithm:patience'
	keepBackup = false

I then use diffget and diffput to merge my files with git mergetool

vim.opt.diffopt:append("linematch:50")

Fugitive

With this, we have completed all the fundamentals!

Getting the full power out of Neovim

In the last section, I spent a lot of time buffing vim features. However, Neovim has been progressing and has added many new features that I have been taking advantage of. In this section, I will show you how I use these features to make my workflow more efficient.

Treesitter

Treesitter is a parser library that is used in real-time editing contexts, where it aims to fill the gap to manipulate and understand your code through the Abstract syntax tree (AST). Take, for instance, syntax highlighting. Originally, syntax highlighting was achieved through regular expressions that would match the tokens of the language you used. However, for large files that was a large tax. Now imagine if instead of matching the literal text to then syntax highlight, your text editor matches the nodes in the AST (and context) to highlight your code. Because treesitter operates within context, it is much more accurate to read your code and provides “better” syntax highlighting. For instance, what is the difference between a macro and a function — should that be highlighted different to notate that it has different behavior? Treesitter says yes, and I agree.

However, if this were the only benefit, I would just shrug off the tool. It’s a lot of development to support a feature like this. It turns out however that treesitter updates its tree while you are typing in real time, which provides additional benefits than just more stable syntax highlighting. Yet, what catches my eyes is the extensibility of treesitter. Here we have a tool that can manipulate the AST – what can you with that? Well, you decide, but here are some of my uses: better code folding, swap function parameters, refactoring entire functions in a way a LSP can’t, and provide integration across multiple programming files. Note the last one, which, I believe, is a feature that should impress you because if treesitter can work on a file with multiple languages, it can tell you what how comments work in that language, what indentation is ideal, what.a function block is in a language. That’s compelling, and what you can do with treesitter is up to you: you have full control of your text editor and AST.

Treesitter is a tool for a programmer to do more with their code editor and I believe it brings more power to editing, reviewing, and writing code. Let’s take a look at how I write these blogs; I often have Rust, Markdown, and LaTeX:

Treesitter

Notice, that treesitter can highlight the syntax of all three languages in the same file!

Here is how I set up treesitter:

--- I use lazy.nvim to load plugins 
{"numToStr/Comment.nvim"}, -- comment
{"nvim-treesitter/nvim-treesitter", build = ":TSUpdate"}, -- :TSInstallFromGrammar
{"nvim-treesitter/nvim-treesitter-textobjects", event = "InsertEnter"}, -- TS objects
{"JoosepAlviste/nvim-ts-context-commentstring"}, -- use TS for comment.nvim
{"danymat/neogen", config = function() require("neogen").setup({}) end}, 

This setups treesitter, treesitter text objects, treesitter context, and treesitter commentstring. I also use neogen to generate doc comments for me. Additionally, I have the following settings:

keyset("n", "<leader>t", ":lua require('neogen').generate()<CR>", {silent = true})
vim.opt.foldmethod = "expr" -- treesiter time
vim.opt.foldexpr = "nvim_treesitter#foldexpr()" -- treesiter

vim.g.skip_ts_context_commentstring_module = true

require("Comment").setup({
    pre_hook = function()
        return
            require("ts_context_commentstring.internal").calculate_commentstring()
    end
})

local ts_repeat_move = require("nvim-treesitter.textobjects.repeatable_move")
keyset({"n", "x", "o"}, ";", ts_repeat_move.repeat_last_move_next)
keyset({"n", "x", "o"}, ",", ts_repeat_move.repeat_last_move_previous)
keyset({"n", "x", "o"}, "f", ts_repeat_move.builtin_f)
keyset({"n", "x", "o"}, "F", ts_repeat_move.builtin_F)
keyset({"n", "x", "o"}, "t", ts_repeat_move.builtin_t)
keyset({"n", "x", "o"}, "T", ts_repeat_move.builtin_T)

require("nvim-treesitter.configs").setup({
    highlight = {enable = true, disable = {"latex"}},
    indent = {enable = true, disable = {"python"}},
    textobjects = {
        move = {
            enable = true,
            set_jumps = true, -- whether to set jumps in the jumplist
            goto_next_start = {
                ["]m"] = "@function.outer",
                ["]]"] = {query = "@class.outer", desc = "Next class start"},
                ["]s"] = {
                    query = "@scope",
                    query_group = "locals",
                    desc = "Next scope"
                }
            },
            goto_previous_start = {
                ["[m"] = "@function.outer",
                ["[["] = "@class.outer"
            },
            goto_next_end = {
                ["]M"] = "@function.outer",
                ["]["] = "@class.outer"
            },
            goto_previous_end = {
                ["[M"] = "@function.outer",
                ["[]"] = "@class.outer"
            },
            goto_next = {["]d"] = "@conditional.outer"},
            goto_previous = {["[d"] = "@conditional.outer"}
        },
        swap = {
            enable = true,
            swap_next = {["<leader>a"] = "@parameter.inner"},
            swap_previous = {["<leader>A"] = "@parameter.inner"}
        },
        select = {
            enable = true,
            lookahead = true,
            keymaps = {
                ["af"] = "@function.outer",
                ["if"] = "@function.inner",
                ["ac"] = "@class.outer",
                ["ic"] = "@class.inner"
            },
            selection_modes = {
                ["@parameter.outer"] = "v", -- charwise
                ["@function.outer"] = "V", -- linewise
                ["@class.outer"] = "<c-v>" -- blockwise
            }
        }
    },
    incremental_selection = {
        enable = true,
        keymaps = {
            init_selection = "<space>i",
            scope_incremental = "<space>i",
            node_incremental = "<space>n",
            node_decremental = "<space>p"
        }
    }
})

Together, I am able to swap function parameters, select text objects, and move between text objects. More important, I can generate doc comments and use treesitter to provide context for my code.

Better terminal

Neovim has :Term, but I prefer a terminal that persists, can open other TUI apps, and is customizable.

{"akinsho/toggleterm.nvim"}, -- for smart terminal

require("toggleterm").setup({
    shade_terminals = false,
    highlights = {
        StatusLine = {guifg = "#ffffff", guibg = "#0E1018"},
        StatusLineNC = {guifg = "#ffffff", guibg = "#0E1018"}
    }
})

vim.opt.laststatus = 3

Combined, this allows me to focus on one window and have a terminal that is always available:

Toggleterm

In fact, I can combine this with LazyGit to have a terminal that can run lazygit and nvim at the same time. Let’s have a look:

This allows me to work with git extremely efficiently. Here is the config I use:

keyset("n", "<space><space>", ":ToggleTerm size=15<cr>", {silent = true})
keyset("n", "<space>t", ":ToggleTerm size=70 direction=vertical<cr>", {silent = true})

local Terminal = require("toggleterm.terminal").Terminal

local lg_cmd = "lazygit -w $PWD"
if vim.v.servername ~= nil then
    lg_cmd = string.format(
                 "NVIM_SERVER=%s lazygit -ucf ~/.config/nvim/lazygit.toml -w $PWD",
                 vim.v.servername)
end

vim.env.GIT_EDITOR = "nvr -cc split --remote-wait +'set bufhidden=wipe'"

local lazygit = Terminal:new({
    cmd = lg_cmd,
    count = 5,
    direction = "float",
    float_opts = {
        border = "double",
        width = function() return vim.o.columns end,
        height = function() return vim.o.lines end
    },
    -- function to run on opening the terminal
    on_open = function(term)
        vim.cmd("startinsert!")
        vim.api.nvim_buf_set_keymap(term.bufnr, "n", "q", "<cmd>close<CR>",
                                    {noremap = true, silent = true})
    end
})

function Edit(fn, line_number)
    local edit_cmd = string.format(":e %s", fn)
    if line_number ~= nil then
        edit_cmd = string.format(":e +%d %s", line_number, fn)
    end
    vim.cmd(edit_cmd)
end

function Lazygit_toggle() lazygit:toggle() end

keyset("n", "<leader>lg", "<cmd>lua Lazygit_toggle()<CR>", {silent = true})

And here is the lazygit.toml file I use:

os:
  edit: 'nvim --server $NVIM_SERVER'
  editAtLine: "{{editor}} --remote-send '<C-\\><C-n>:5ToggleTerm<CR>:lua Edit({{filename}}, {{line}})<CR>'"
git:
  branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium --oneline {{branchName}} --"
  commitPrefixes:
    my_project:
      pattern: "^\\w+\\/(\\w+-\\w+).*"
      replace: '[$1] '
  paging:
    colorArg: always
    pager: delta --dark --paging=never
  commit:
    signOff: false
  merging:
    manualCommit: false
    args: ''
  log:
    order: 'topo-order'
    showGraph: 'when-maximised'
    showWholeGraph: false
  skipHookPrefix: WIP
  autoFetch: true
  autoRefresh: true
  allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative  --pretty=medium'
  overrideGpg: false 
  disableForcePushing: false
  parseEmoji: false
  diffContextSize: 3 # how many lines of context are shown around a change in diffs

This all works through nvr, the neovim remote plugin, so make sure you have that installed.

if not vim.fn.executable("nvr") then
    vim.api.nvim_command("!pip3 install --user neovim-remote")
end

Telescope

Telescope is one of those Neovim plugins that transform how you interact with virtually everything in your development environment—from files, code, documentation, to even running tasks and previewing media. Its extensible and customizable nature makes it a powerhouse for creating a truly personalized development environment. For instance, this is telescope in action with man pages:

Telescope

Now, let’s set up telescope:

{
    "nvim-telescope/telescope.nvim",
    dependencies = {"nvim-lua/plenary.nvim", "nvim-lua/popup.nvim"}
}, {
    "nvim-telescope/telescope-frecency.nvim",
    config = function()
        require("telescope").load_extension "frecency"
    end
}

This sets up telescope and telescope-frecency. Frequency is a plugin that allows you to search for files based on how frequently you use them. Here is how I map telescope:

-- Telescope
keyset("n", "<leader><leader>f", ":Telescope git_files<cr>")
keyset("n", "<leader>fl", ":Telescope live_grep<cr>")
keyset("n", "<leader>ff", ":Telescope frecency workspace=CWD theme=ivy layout_config={height=0.4} path_display={'shorten'}<cr>")
keyset("n", "<leader>fb", ":Telescope buffers<cr>")
keyset("n", "<leader>fm", ":Telescope man_pages<cr>")
keyset("n", "<leader>ft", ":Telescope treesitter<cr>")
keyset("n", "<leader>fk", ":Telescope keymaps<cr>")
keyset("n", "<leader>fh", ":Telescope help_tags<cr>")

Finally, let’s get to the good parts. Customizing telescope:

-- https://github-wiki-see.page/m/nvim-telescope/telescope.nvim/wiki/Configuration-Recipes
local actions = require("telescope.actions")
local previewers = require("telescope.previewers")
local Job = require("plenary.job")
local _bad = {".*%.csv"} -- Put all filetypes that slow you down in this array
local bad_files = function(filepath)
    for _, v in ipairs(_bad) do if filepath:match(v) then return false end end
    return true
end

---@diagnostic disable-next-line: redefined-local
local new_maker = function(filepath, bufnr, opts)
    opts = opts or {}
    if opts.use_ft_detect == nil then opts.use_ft_detect = true end
    opts.use_ft_detect = opts.use_ft_detect == false and false or
                             bad_files(filepath)
    filepath = vim.fn.expand(filepath)

    Job:new({
        command = "file",
        args = {"--mime-type", "-b", filepath},
        on_exit = function(j)
            local mime_type = vim.split(j:result()[1], "/")[1]
            if mime_type == "text" then
                vim.loop.fs_stat(filepath, function(_, stat)
                    if not stat then return end
                    if stat.size > 100000 then
                        vim.schedule(function()
                            vim.api.nvim_buf_set_lines(bufnr, 0, -1, false,
                                                       {"FILE TOO LARGE"})
                        end)
                    else
                        previewers.buffer_previewer_maker(filepath, bufnr, opts)
                    end
                end)
            else
                -- maybe we want to write something to the buffer here
                vim.schedule(function()
                    vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {"BINARY"})
                end)
            end
        end
    }):sync()
end

Since I have some large files, I have to check if the file is too large to preview. This is what the new_maker function dos and applies it to the previewer:

require("telescope").setup({
    defaults = {
        file_sorter = require("telescope.sorters").get_fzy_sorter,
        buffer_previewer_maker = new_maker,
        layout_config = {prompt_position = "bottom"},
        mappings = {
            i = {
                ["<Esc>"] = actions.close,
                ["<C-q>"] = actions.send_to_qflist,
                ["<C-k>"] = actions.move_selection_previous,
                ["<C-j>"] = actions.move_selection_next,
                ["<C-d>"] = actions.delete_buffer + actions.move_to_top
            }
        }
    },
    pickers = {
        find_files = {theme = "ivy", layout_config = {height = 0.4}},
        git_files = {theme = "ivy", layout_config = {height = 0.4}},
        live_grep = {theme = "ivy", layout_config = {height = 0.4}},
        buffers = {theme = "ivy", layout_config = {height = 0.4}},
        keymaps = {theme = "ivy", layout_config = {height = 0.4}},
        file_browser = {theme = "ivy", layout_config = {height = 0.4}},
        treesitter = {theme = "ivy", layout_config = {height = 0.4}},
        help_tags = {theme = "ivy", layout_config = {height = 0.5}},
        man_pages = {
            sections = {"1", "2", "3"},
            theme = "ivy",
            layout_config = {height = 0.4}
        }
    },
    extensions = {
        frecency = {
            auto_validate = false,
            matcher = "fuzzy",
            path_display = {"shorten"}
        }
    }
})

Together, I can move around my codebase with ease and preview files without slowing down my editor.

Note: I wish I could set a default theme for telescope, but I can’t. I have to set it for each picker.

Completion

Just a fair warning, I still use coc.nvim, but it works well for me!

Copilot

First, I use copilot to complete test cases and boilercode – I don’t trust it for logic, but it has its uses. Specifically, I use the lua version as I find it more configurable:

{
    "zbirenbaum/copilot.lua", -- Copilot but lua
    cmd = "Copilot",
    event = "InsertEnter"
}

require("copilot").setup({
    panel = {
        enabled = true,
        auto_refresh = false,
        keymap = {
            jump_prev = "[[",
            jump_next = "]]",
            accept = "<CR>",
            refresh = "gr",
            open = "<leader>ck"
        },
        layout = { position = "bottom", ratio = 0.4 }
    },
    suggestion = {
        enabled = true,
        auto_trigger = true,
        debounce = 75,
        keymap = {
            accept = "<C-v>",
            accept_word = false,
            accept_line = "<C-q>",
            next = false,
            prev = false,
            dismiss = "<C-]>"
        }
    },
    copilot_node_command = "node",
})

keyset("n", "<leader>ck", '<cmd>lua require("copilot.suggestion").toggle_auto_trigger()<cr>')

This allows me to use copilot to generate boilerplate code and test cases. Moreover, I can toggle the auto trigger with <leader>ck. This allows me to use copilot when I need it and not when I don’t.

Coc.nvim

I use coc.nvim for completion, but I have customized almost every single thing:

{"neoclide/coc.nvim", branch = "release", build = ":CocUpdate"}, -- auto complete
{"honza/vim-snippets"}, -- Snippets are separated from the engine so coc can use

This sets up coc.nvim and vim-snippets. Here is how I set up coc.nvim:

vim.api.nvim_create_user_command("Format", "call CocAction('format')", {})

function _G.check_back_space()
    local col = vim.api.nvim_win_get_cursor(0)[2]
    local has_backspace = vim.api.nvim_get_current_line():sub(col, col):match(
                              "%s") ~= nil
    return col == 0 or has_backspace
end


local opts = {silent = true, noremap = true, expr = true}
vim.api.nvim_set_keymap("i", "<TAB>",
                        'coc#pum#visible() ? coc#pum#next(1) : v:lua.check_back_space() ? "<TAB>" : coc#refresh()',
                        opts)
vim.api.nvim_set_keymap("i", "<S-TAB>",
                        [[coc#pum#visible() ? coc#pum#prev(1) : "\<C-h>"]], opts)
vim.api.nvim_set_keymap("i", "<cr>",
                        [[coc#pum#visible() ? coc#pum#confirm() : "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>"]],
                        opts)
keyset("i", "<c-j>", "<Plug>(coc-snippets-expand-jump)")
keyset("i", "<c-space>", "coc#refresh()", {silent = true, expr = true})

--- more keymaps 

keyset("n", "K", function()
    local cw = vim.fn.expand("<cword>")
    if vim.fn.index({"vim", "help"}, vim.bo.filetype) >= 0 then
        vim.api.nvim_command("h " .. cw)
    elseif vim.api.nvim_eval("coc#rpc#ready()") then
        vim.fn.CocActionAsync("doHover")
    else
        vim.api.nvim_command(string.format("!%s %s", vim.o.keywordprg, cw))
    end
end, {silent = true})

I know that nvim-lsp is the new hotness, but coc.nvim just has everything I need. I can use it to format my code, check diagnostics, and use snippets. Moreover, I can use it to check the documentation of a function or keyword. I can also use it to check the definition of a function or keyword. Finally, I can use it to check the references of a function or keyword. I can do all of this with a single plugin.

Moreover, I am able to combine it with the quickfix list:

function _G.diagnostic()
    vim.fn.CocActionAsync("diagnosticList", "", function(err, res)
        if err == vim.NIL then
            if vim.tbl_isempty(res) then return end
            local items = {}
            for _, d in ipairs(res) do
                local text = ""
                local type = d.severity
                local msg = d.message:match("([^\n]+)\n*")
                local code = d.code
                if code == vim.NIL or code == nil or code == "NIL" then
                    text = ("[%s] %s"):format(type, msg)
                else
                    text = ("[%s|%s] %s"):format(type, code, msg)
                end

                local item = {
                    filename = d.file,
                    lnum = d.lnum,
                    end_lnum = d.end_lnum,
                    col = d.col,
                    end_col = d.end_col,
                    text = text
                }
                table.insert(items, item)
            end
            vim.fn.setqflist({}, " ",
                             {title = "CocDiagnosticList", items = items})
            vim.cmd("bo cope")
        end
    end)
end

vim.api.nvim_create_augroup("CocGroup", {})

autocmd("User", {
    group = "CocGroup",
    pattern = "CocLocationsChange",
    desc = "Update location list on locations change",
    callback = function()
        local locs = vim.g.coc_jump_locations
        vim.fn.setloclist(0, {}, " ", {title = "CocLocationList", items = locs})
        local winid = vim.fn.getloclist(0, {winid = 0}).winid
        if winid == 0 then
            vim.cmd("bel lw")
        else
            vim.api.nvim_set_current_win(winid)
        end
    end
})

autocmd("User", {
    group = "CocGroup",
    pattern = "CocJumpPlaceholder",
    command = "call CocActionAsync('showSignatureHelp')",
    desc = "Update signature help on jump placeholder"
})

autocmd("CursorHold", {
    group = "CocGroup",
    command = "silent call CocActionAsync('highlight')",
    desc = "Highlight symbol under cursor on CursorHold"
})

For instance, Coc makes it easy to use random LSPs like ltex, which I use for grammar checking in LaTeX:

Finally, here is my config:

{
  "suggest.noselect": true,
  "coc.preferences.jumpCommand": "drop",
  "coc.preferences.messageLevel": "warning",
  "coc.preferences.maxFileSize": "1MB",
  "coc.preferences.enableMarkdown": true,
  "ltex.language": "en-US",
  "ltex.additionalRules.motherTongue": "en-US",
  "ltex.completionEnabled": true,
  "ltex.sentenceCacheSize": 6000,
  "ltex.languageToolHttpServerUri": "https://api.languagetoolplus.com",
  "ltex.checkFrequency": "save",
  "ltex.enabled": [
    "bibtex",
    "context",
    "context.tex",
    "html",
    "latex",
    "markdown",
    "org",
    "restructuredtext",
    "rsweave",
    "mail",
    "norg",
    "help",
  ],
  "ltex.diagnosticSeverity": {
    "CONFUSED_WORDS": "warning",
    "UPPERCASE_SENTENCE_START": "warning",
    "DATE_WEEKDAY": "warning",
    "MORFOLOGIK_RULE_EN_US": "error",
    "EN_CONTRACTION_SPELLING": "error",
    "EN_A_VS_AN": "error",
    "IN_A_X_MANNER": "hint",
    "PASSIVE_VOICE": "hint",
    "EN_SPECIFIC_CASE": "hint",
    "APOS_AR": "hint",
    "DOUBLE_HYPHEN": "hint",
    "AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA": "hint",
    "SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA": "hint",
    "default": "information"
  },
  "ltex.configurationTarget": {
    "dictionary": "userExternalFile",
    "disabledRules": "userExternalFile",
    "hiddenFalsePositives": "userExternalFile",
  },
  "ltex.additionalRules.enablePickyRules": true,
  "diagnostic.hintSign": "✹",
  "diagnostic.errorSign": "✘",
  "diagnostic.warningSign": "",
  "diagnostic.infoSign": "",
  "codeLens.enable": false,
  "codeLens.separator": " ",
  "codeLens.subseparator": " | ",
  "notification.disabledProgressSources": ["*"],
  "diagnostic.virtualText": true,
  "diagnostic.virtualTextCurrentLineOnly": false,
  "diagnostic.virtualTextLineSeparator": ". ",
  "diagnostic.virtualTextPrefix": " ",
  "diagnostic.checkCurrentLine": true,
  "diagnostic.messageTarget": "float",
  "semanticTokens.enable": true,
  "semanticTokens.highlightPriority": 4096,
  "sumneko-lua.enableNvimLuaDev": true,
  "Lua.semantic.enable": true,
  "Lua.hint.enable": true,
  "Lua.hint.arrayIndex": "Disable",
  "Lua.hint.paramName": "Literal",
  "Lua.hint.setType": true,
  "coc.source.word.filetypes": [
    "norg",
    "text",
    "mail"
  ],
  "suggest.snippetIndicator": " \ue796",
  "suggest.completionItemKindLabels": {
    "keyword": "\uf1de",
    "variable": "\ue79b",
    "value": "\uf89f",
    "operator": "\u03a8",
    "function": "\u0192",
    "reference": "\ufa46",
    "constant": "\uf8fe",
    "method": "\uf09a",
    "struct": "\ufb44",
    "class": "\uf0e8",
    "interface": "\uf417",
    "text": "\ue612",
    "enum": "\uf435",
    "enumMember": "\uf02b",
    "module": "\uf40d",
    "color": "\ue22b",
    "property": "\ue624",
    "field": "\uf9be",
    "unit": "\uf475",
    "event": "\ufacd",
    "file": "\uf723",
    "folder": "\uf114",
    "snippet": "\ue60b",
    "typeParameter": "\uf728",
    "default": "\uf29c"
  },
  "snippets.ultisnips.enable": true,
  "snippets.ultisnips.pythonPrompt": false,
}

Latex

Finally, as a Math major, I use LaTeX a lot. I use the following plugins to make my LaTeX experience better:

{"lervag/vimtex"}, -- LaTeX
-- Vimtex config
vim.g.vimtex_quickfix_mode = 2
vim.g.vimtex_compiler_latexmk_engines = {["_"] = "-lualatex -shell-escape"}
vim.g.vimtex_indent_on_ampersands = 0
vim.g.vimtex_view_method = 'sioyek'
vim.g.matchup_override_vimtex = 1

-- Other settings
vim.g.latexindent_opt = "-m" -- for neoformat, I use latexindent

Together with sioyek, I can compile my LaTeX documents and view them in a pdf viewer automatically. Moreover, I can use latexindent to format my LaTeX documents.

Conclusions

I know that was a lot, but I hope you learned something new. I have been using Neovim for a while now, and I have to say that it has been a game changer. I have been able to customize my text editor to fit my needs and make me more productive. I hope that you can do the same.

Please let me know if you have any questions or comments. I would love to hear from you.

SeniorMars