After migrating from Vim to Emacs as my primary C++ editor in 2015, I switched from Vim to Neovim for miscellaneous non-C++ tasks as it is more convenient in a terminal. Customizing the editor with a language you are comfortable with is important. I found myself increasingly drawn to Neovim's terminal-based simplicity for various tasks. Recently, I've refined my Neovim setup to the point where I can confidently migrate my entire C++ workflow away from Emacs.
This post explores the key improvements I've made to achieve this transition. My focus is on code navigation.
Key mapping
I've implemented custom functions that simplify key mappings.
1 | local function map(mode, lhs, rhs, opts) |
I've swapped ;
and :
for easier access to
Ex commands, especially since leap.nvim renders ;
less
useful for repeating ftFT
. 1
2map({'n', 'x'}, ':', ';')
map({'n', 'x'}, ';', ':')
Cross references
Like many developers, I spend significantly more time reading code than writing it. Efficiently navigating definitions and references is crucial for productivity.
While the built-in LSP client's C-]
is functional (see
:h lsp-defaults
tagfunc
), I found it less
convenient. Many Emacs and Neovim configurations advocate for
gd
. However, both G and D are placed on the left half of
the QWERTY keyboard, making it slow to press them using the left
hand.
For years, I relied on M-j
to quickly jump to
definitions.
To avoid a conflict with my recent zellij change (I adopted
M-hjkl
for pane navigation), I've reassigned J
to trigger definition jumps. Although I've lost the original
J
(join lines) functionality, vJ
provides a
suitable workaround.
1 | nmap('J', '<cmd>Telescope lsp_definitions<cr>', 'Definitions') |
After making a LSP-based jump, the jump list can quickly fill with
irrelevant entries as I navigate the codebase. Thankfully, Telescope's
LSP functionality sets push_tagstack_on_edit
to push an
entry to the tag stack (see :h tag-stack
). To efficiently
return to my previous position, I've mapped H
to
:pop
and L
to :tag
.
1 | nmap('H', '<cmd>pop<cr>', 'Tag stack backward') |
I've adopted x
as a prefix key for cross-referencing
extensions. dl
provide a suitable alternative for
x
's original functionality.
1 | nmap('xB', '<cmd>CclsBaseHierarchy<cr>') |
I utilize xn
and xp
to find the next or
previous reference. The implementation, copied from from LazyVim, only
works with references within the current file. I want to enable the
xn
map to automatically transition to the next file when
reaching the last reference in the current file.
While using Emacs, I created a hydra with x as the prefix key to cycle through next references. Unfortunately, I haven't been able to replicate this behavior in Neovim.
1 | ;; This does not work. |
Movement
I use leap.nvim to quickly jump to specific identifiers
(s{char1}{char2}
), followed by telescope.nvim to explore
definitions and references. Somtimes, I use the following binding:
1 | nmap('U', function() |
Semantic highlighting
I've implemented rainbow semantic highlighting using ccls. Please refer to ccls and LSP Semantic Tokens for my setup.
Other LSP features
I have configured the CursorHold
event to trigger
textDocument/documentHighlight
. When using Emacs,
lsp-ui-doc automatically requests textDocument/hover
, which
I now lose.
Additionally, the LspAttach
and BufEnter
events trigger textDocument/codeLens
.
Window navigation
While I've been content with the traditional C-w + hjkl
mapping for years, I've recently opted for the more efficient
C-hjkl
approach.
1 | nmap('<C-h>', '<C-w>h') |
The keys mirror my pane navigation preferences in tmux and zellij,
where I utilize M-hjkl
.
1 | # tmux select pane or window |
1 | // zellij M-hjkl |
To accommodate this change, I've shifted my tmux prefix key from
C-l
to C-Space
. Consequently, I've also
adjusted my input method toggling from C-Space
to
C-S-Space
.
Debugging
For C++ debugging, I primarily rely on cgdb. I find it superior to
GDB's single-key mode and significantly more user-friendly than LLDB's
gui
command.
1 | cgdb --args ./a.out args |
I typically arrange Neovim and cgdb side-by-side in tmux or zellij. During single-stepping, when encountering interesting code snippets, I often need to manually input filenames into Neovim. While Telescope aids in this process, automatic file and line updates would be ideal.
Given these considerations, nvim-dap appears to be a promising solution. However, I haven't yet determined the configuration for integrating rr with nvim-dap.
Live grep
I've defined mappings to streamline directory and project-wide searches using Telescope's live grep functionality:
1 | nmap('<leader>sd', '<cmd>lua require("telescope.builtin").live_grep({cwd=vim.fn.expand("%:p:h")})<cr>', 'Search directory') |
Additionally, I've mapped M-n
to insert the word under
the cursor, mimicking Emacs Ivy's
M-n (ivy-next-history-element)
behavior.
Task runner
I use overseer.nvim to
run build commands like ninja -C /tmp/Debug llc llvm-mc
.
This plugin allows me to view build errors directly in Neovim's quickfix
window.
Following LazyVim, I use <leader>oo
to run builds
and <leader>ow
to toggle the overseer window. To
navigate errors, I use trouble.nvim with the ]q
and
[q
keys.
1 | nmap('<leader>oo', '<cmd>OverseerRun<cr>') |
Reducing reliance on terminal multiplexer
As https://rutar.org/writing/from-vim-and-tmux-to-neovim/ nicely summarizes, running Neovim under tmux has some annoyance. I've been experimenting with reducing my reliance on zellij. Instead, I'll utilize more Neovim's terminal functionality.
toggleterm.nvim is a particularly useful plugin that allows me to easily split windows, open terminals, and hide them when not in use.
The default command <C-\><C-n>
(switch to
the Normal mode) is clumsy. I've mapped it to <C-s>
(useless feature pause
transmission, fwd-i-search
in zsh).
1 | nmap('<leader>tf', function() require'toggleterm'.toggle(vim.v.count, nil, MyProject(), 'float', nil) end) |
neovim-remote allows me to open files without starting a nested Neovim process.
I use mini.sessions to manage sessions.
Config switcher
Neovim's NVIM_APPNAME
feature is fantastic for exploring pre-configured distributions to get
inspiration.
Lua
Neovim embraces Lua 5.1 as a preferred scripting language. While
Lua's syntax is lightweight and easy to learn, it doesn't shy away from
convenience features like func 'arg'
and
func {a=42}
.
LuaJIT offers exceptional performance.
LuaJIT with the JIT enabled is much faster than all of the other languages benchmarked, including Wren, because Mike Pall is a robot from the future. -- wren.io
This translates into noticeably smoother editing with LSP, especially for hefty C++ files – a significant advantage over Emacs. With Emacs, I've always felt that editing a large C++ file is slow.
The non-default local
variables and 1-based indexing
(shared with languages like Awk and Julia) are annoyances that I can
live with when using a configuration language. So far, I've only needed
index-sensitive looping in one specific location.
1 | -- For LSP semantic tokens |