Navigating vertically within a buffer in Neovim
I have had issue navigating within a buffer in Neovim for a long time. I can’t point it out exactly but for a long time I couldn’t move exactly where I wanted in an intuitive way.
I always felt I was missing some navigation “in between” the big jump and the very small jump while navigating vertically or that somehow the jump I would go to was not where my brain was at.
Something between search, j, k, CTRL+D and CTRL+U.
Somehow I never really liked } and { either.
Since then, I finally found something that works well with my mental mapping.
Let’s look into this.
A simple example
Let’s take a simple example from the tensorflow ranking package.
Assume you are trying to read the code and understand it.
Classical navigation
Here are a few native way you can do it.
You can play with CTRL+U and CTRL+D.
As you can see, I really feel like I don't have control over where I'm going, even though it performs as expected. I just can't seem to casually read code like that. Please note that I sometimes use it to quickly scroll through my code, but to be honest, every time I use it, I feel I should have used something else instead (e.g. search).
Alternatively, you can use j and k, and be smarter by using a count before j and k to go exactly where you want. I have to admit, initially I had a hard time using the count argument and for years only used j and k when I wanted to read code line by line. Holding j and k is something I often see in YouTube videos. However, I always felt slow and that I wasn't doing the right thing. I've seen a bunch of "hacks" around this, like people adjusting the repeat speed when you hold down a button so j and k move faster. Again, it didn’t feel like my Vim way to me.
For myself, what I did was suffer through using hardtime.nvim, which literally blocks me if I repeat j three times in a row. I can't tell you how many times I looked stupid in front of my coworkers while sharing my screen and being blocked by my PDE, or how many times I thought about uninstalling it from the frustration of being blocked.
See for yourself:
After all this struggle, I've truly learned to navigate vertically better using the full range of commands from <count>j|k to other motions as well. Still, it somehow doesn't feel natural to me to look at the relative line number and use <count>j|k when I'm reading code.
Please note that while coding, I often use these commands, but there's an in-between phase when I'm reading and analyzing code where they just don't work for me.
I sometimes use }|{ to go to the next paragraph, but I never liked having to track when a paragraph ends.
What I really want is to jump to places that are logical at the code level (eg. class, methods…). If you've been using Neovim for a while, you probably know where I'm going with this. You might be right, but stay with me, I think there might be some tidbit you might not be expecting.
The magic of tree-sitter
If you’ve read tutorial on Neovim or you have a Neovim distribution. There is good chance you already know about nvim-treesitter-textobjects.
So I am not going to bore you too much about the classical tree-sitter navigation.
Namely ]m for @function.outer, and ]] for @class.outer. If you’re not familiar let me quickly show you the configuration and a quick video.
In your plugin if you configure it as follow:
You will now be able to navigate between class going to the new start of the class with ]] and method with ]m. You can then use previous with [ and capital letters for end of it.
I like this navigation because it feels very intuitive as I navigate through the code. Finally, here is the interesting part (I hope!). It turns out that the text object navigations I've been using are just the "basic" ones, and I've found quite a few more that are interesting to me. The big one that really solved my mental breakdown is @statement.outer
. I can't really explain it, but there's a very sweet spot where it truly helps me. When I'm within a method, sometimes I like to read through it without going "line by line", and I also don't want to use <count>j|k as mentioned before. It turns out that using square bracket s, which is my shortcut for @statement.outer
, really solves this for me. I don't use it all the time, but it provides a middle ground between j|k and }|{, while letting me know exactly where it will go. It aligns with how my brain works, as, in the end, what I am really doing is reading "statement by statement".
Let's try to show this with a specific example:
You can see in the screenshot that when I use ]s on the config dictionary creation, it goes straight to the next statement. This allows me to avoid using 5j|k, which, although possible, requires a bit more time for me to think through. Is that all? Definitely not. I have been slowly integrating more navigation patterns with square bracket. Right now, I am learning to use i for @conditional.outer
and l for @loop.outer
. The idea is the same: sometimes I know, without having to think, that I want to go back or forward to my previous loop or if statement. Let's show a small example using some different code:
You can see here that this becomes quite convenient and aligns more closely with how my brain works (go to this loop, go to this if…).
A few more things before concluding this article. First, I am not covering other ways of navigating with square bracket. You can see the list I currently use here:
I would give a notable mention to r for last|next references which I often use. I also haven’t mentioned other shortcut like going to definition when your cursor is on a variable and so on which are often quite handy when reading code overall.
The second thing is that I am not covering another aspect, which is yanking/selecting using nvim-treesitter-textobjects. Another neat plugin for this is mini.ai. I haven't had much time to test it yet, but it adds quite a few useful features. For example, using q for quotes allows you to select any type of quote (",`
or '
) without having to specify it. You can also use a count, so if you have ( *a (bb) )
and do v2a(, it will select all of it. Finally, you can combine it with n
and l
to do it on the next one or last one.
Conclusion
I hope you enjoyed exploring some of the beauty of Neovim. Throughout this article, I did my best to show how I use vertical navigation to go through my code. I mentioned the value of using tree-sitter and emphasized that you should really dig into the different text objects available, as they can make a big difference.
Please note that what works for me might not work for you.
Finally, learning navigation in Neovim is a never-ending task, and I encourage you to learn things slowly, introducing one thing at a time. Continuous improvement is the way forward.