Neovim has endless shortcuts and features. Even with a grasp on the basics, it can be difficult to really get a grasp on the hidden power of the editor. Here, my goal is to provide some tips that I believe are game changers when it comes to navigating text efficiently.
This is meant for individuals that are past the initial setup phase. You should have a decent understanding of how the configuration system works, and know about some of the basic shortcuts.
Stop Using hjkl
No, you don't have to completely stop using hjkl
. They have their uses. For example, if you are one line above or below, j
and k
make sense. If you are one character to the left or right, you could use h
or l
. However, in any other situation hjkl
are inefficient.
What Should I Use Instead of h and l?
h and l can be replaced with w
, b
, and e
. These three commands can almost fully replace moving left and right by one character. Traversing a line horizontally is significantly more efficient. Take this example:
const foo: Bar = Bar.init();
// ˘ ⦿
You are at the f
in foo
, and you want to get to the B
in Bar
. Normally, you'd have to type l
eleven times! Instead, using w
, you can get there in 4.
We can do better, though. There exists W
, B
, and E
, which act similar to their respective lowercase keybindings, with one main difference. Instead of qualifying a word as a group of continuous characters not interrupted by symbols or white space, the capital versions treat every character that isn't white space as one word. This means that foo:
is considered one word. Now, we can make it to "Bar" in just three key presses!
Anything beyond this can be a bit unreliable. We could use the f
command followed by B
to jump to the first occurrence of B
, and then press ;
to go to the desired one. This is very useful, but is not really a huge improvement, as it still takes three key presses to get to the target.
Using a Plugin for Ultimate Speed
In the previous example, where the target is on the same line, it makes total sense to use W
or f
. However, this is not always the case. Take this example:
const foo: Bar = Bar.init();
// ˘
defer foo.deinit();
foo.baz();
if (foo.hasBar()) {
foo.doSomethingAwesome(arg1, arg2, arg3);
// ⦿
}
Now we are at the f
in foo
and we want to get to the a
in arg3
. This is offset in two dimensions, making it difficult to determine the best way to get there.
We could just directly search up the argument with /
followed by arg3
. This works, but isn't very precise. If arg3
was used anywhere between the cursor and that target location, it would jump there first.
We can utilize flash.nvim to help us out here. Instead of searching or spamming j
and l
, we can press one keybinding (in my case, zk
), and then press the target character. We then get a jump list next to every occurrence of a
on the screen, giving us a unique key to press to instantly teleport to the target location. This allows us to make it to the target in four key presses, despite being in a completely arbitrary location. It can be three as well, I just ran out of one-letter keybinding space.
How to Replace j
and k
?
Now honestly, j
and k
have a lot more practicality than h
and l
. Jumping up and down by one line is extremely common. So, don't completely throw away the muscle memory...
However, if there are large jumps > 2 lines, maybe there is a better approach. I'd like to tell you I used numbered jumps like 3j
, but that isn't true most of the time. Even though I have relative line numbers enabled, I find that looking to the left at the numbers pulls me out of a productive state. There must be some way to jump large blocks of text without having to look away from the text...
In situations where the target line is not visible, I like to use <C-u>
and <C-d>
. These commands, which go up half a page and down half a page respectively, are extremely useful when I need to rapidly travel up and down a page.
If a target line is known and visible, I'll use a variety of techniques. If the location is near a (
or any unique, easy to press character, I'll just use f
followed by the character. Most of the time this works well. In unique situations, I'll use flash.nvim.
{
and }
are very useful when highlighting or traveling by blocks of code, with some caveats. They travel up and down until the line is white space, which they stop on (and include!). This is great for highlighting entire functions that are visible on the screen. It's not so great for moving through code precisely. If you've worked on any code base with more than one developer, you'd know that spacing is dangerously optional. Some people don't even put spaces between their functions!!! In a code base like this, {
and }
become non-deterministic.
ciw
, caw
, etc.
If you didn't know, there is a family of commands for performing an action in and around a text object in nvim. The general form is "". This is really useful in situations where your cursor is not at the beginning of a word, but you need to modify it entirely. It is even more useful when modifying blocks of code encapsulated in brackets. For example:
fn foo(arg1: i32, arg2: u32, arg3: Bar) void {
// ...
}
If I want to change the contents of the arguments, I can just type cib
to remove everything in the arg list and be in insert mode. You don't even have to be inside the brackets, quotes, etc. Nvim will search for the next occurence of the text object and perform the action on that.
Use Telescope.nvim More!
Telescope.nvim is extremely popular for navigating between files in Neovim. But that's not all that it can do! Telescope.nvim comes with dozens of builtins that can be extremely useful. My two favorites are lsp_document_symbols
and live_grep
.
The first is extremely useful for jumping around a file. If an LSP is setup, it will find functions, type definitions, constants, and lots of other symbols that can then be searched for and jumped to. This is extremely useful, especially in files with over a thousand lines. Jumping to a function becomes trivial, even with so much noise.
The second is very useful when you can remember a snippet of a function, definition, etc., but don't know what file or where in the file. live_grep
allows you to search across files for a specific pattern. For example, I can search "matrix" in a game engine code base to find every instance where I typed "matrix".
Using Marks with Telescope.nvim
Marks are no where near as popular as they should be. I see a lot of beginner tutorials talking about using w
, b
, and e
, and maybe f
and t
, but never marks. When used correctly, they can be absolutely revolutionary to your workflow. Take this example:
const Bar = struct {
item: i32
};
// hundreds of lines of code...
fn something(bar: *Bar) void {
bar.item // ...
}
We are modifying something
, but notice we need to change something with the Bar
struct, which is hundreds of lines away. We can utilize previous shortcuts to jump a half-page up until we reach Bar
, correct our position, and make a modification. Or, we can even using Telescope with lsp_document_symbols
to jump straight to Bar (good idea!). However, neither of these approach are perfectly seamless. Even in the Telescope approach, we have to type in a few characters until we find the bar struct. If this was a longer type like MySuperLongStructName
, it could take even more characters to identify it.
We can use marks to prevent doing this every time we need to jump back and forth. To make a mark, place a cursor where you want the mark to be, and then press m
followed by a mark identifier. This should be a lower-case character.We can put a mark on Bar
with ma
and a mark on something
with ms
. Then, while we're working on the function, we can press 'a
to jump to Bar
, and 's
to jump back. We can even omit the mark on something, and press <C-o>
to jump back to our last location before a jump.
So, we can setup marks quickly with Telescope, and then use the marks for ultimate speed from then on out. It looks like we've maxed out our navigation speed!
Make Custom Snippets
Snippets can massively boost productivity. Most languages have some built-in to the LSP. However, they must be non-intrusive and generic enough that everyone accepts them. Your setup is your setup, so you can make whatever you want. For example, I created a simple snippet for creating a struct in zig:
return {
s("stru", {
t({ "const " }),
i(1, ""),
t({ " = struct {", "\t" }),
i(2, ""),
t({ "", "};" }),
}),
}
This was made for the luasnip plugin, which is most likely what you are using for snippets. It allows me to autocomplete "stru" with a skeleton struct implementation, that gives me convenience points to insert content and tab through.
Create Your Own Plugins/Utilities
Plugins are not mystical pieces of software that take an experienced wizard to create--quite the opposite actually. Neovim uses lua because it enables more people to write a plugin than with the harder to use "VimScript" in vim. You can and should make plugins for your own needs.
No one knows you as well as you do. So, go make the thing you always dreamed of having in an editor!
Top comments (0)