Chapter 11. Navigating Source Files

In previous chapters, we’ve learned many different ways to navigate within a single buffer, as well as between open tabs and windows. This chapter will go into detail of different ways to navigate between source files.

11.1. Go To Definition

In my opinion, “Go to Definition” is the most valuable feature language servers have brought us. Major IDEs have supported it for compiled languages for eons, but dynamically typed languages—​such as my beloved Python—​have always been hell for static analysis, and such features were often pretty hit or miss.

As one of the best-named editor features of all time, “Go to Definition” jumps your cursor from whatever keyword it is currently sitting on to the place where that keyword is defined, regardless of what file it is in.

Most often, I use this when I am looking at a call site for a function and want to see the function itself. A simple press of gd (go to definition also has one of the more memorable LazyVim keybindings of all time) will take me there.

Depending on how good the LSP for the language I am editing is, this often even allows me to jump into library files or type declarations for third party modules so I can see what is really going on.

Go to definition is context dependent, but will usually do exactly what you expect. If you are looking at a variable, gd will jump to wherever the variable is initialized. If your cursor is on a class name, it will jump to wherever the class is defined.

Typically, once you’ve jumped to a definition and learned what you need to learn from that file, you’ll immediately want to jump back to where you started. You can do this easily using Control-o, as we discussed in Chapter 3 (and Control-i can move forward in your jump history).

11.2. Go To References

The inverse of the Go to Definition command is “Go to References”. If you are looking at a function, variable, type, etc, and want to see all the places that variable is accessed, use the gr command.

Unlike with a definition or declaration, there will typically be more than one reference to a given word (a variable in isolation is a useless variable, indeed). So when you type gr it usually won’t immediately jump to a location. Instead it will pop up a picker view of all the references to the word that was under your cursor, with all the preview and filtering luxury that Fzf.lua always brings.

It is common to want to perform some action—​such as a rename or adding an argument or what have you—​at every reference. You could keep showing the picker by hitting gr again or by using the <Space>sR keybinding which resumes your previous picker search. However, it is often much more useful to use the Trouble list that we learned about in Chapter 10.

To do that, use gr to show the references in a picker as usual. Then use Alt-a to select all results, possibly deselecting some with <Tab>. Now use Enter to open the selected lines in Quick Fix or Control-t to open them in Trouble. Remember, Control-q is a shortcut to open all matches in Quick Fix.

11.3. Context-specific Help

Most non-modal editors show you some help or “hover” text when you hold your mouse over a word or symbol. The quantity and value of this text varies widely depending on the LSP, but usually includes a function signature and documentation for the word under the cursor.

It’s probably possible to set up Neovim to show help texts on hover, but why would you move your hand to the mouse when LazyVim has such amazing navigation on the keyboard? Instead, use the (shifted) K keybinding. Yeah, K is a pretty stupid mnemonic to remember, but H and ? were already taken.

In fact, the K stands for “keywordprog”, which is a legacy Vim concept that has been superseded by language servers in the modern world. So LazyVim reappropriated the keybinding.

11.4. Listing Symbols

Another handy LSP feature is to search all the symbols in the current file or project. If you are editing a particularly long file and need to jump to a function that is not terribly close to your cursor, you might use the <Space>ss command (mnemonic is “search symbols”). As hinted by the double s, this is expected to be a fairly common action.

The dialog that pops up should be fairly familiar by now, as it’s the usual picker:

telescope symbols dark
Figure 51. Picker Symbols

So you already know how to use it. However, I want to remind you of a couple Picker tips that make it more useful:

Most of the time when I’m using this symbol picker, I only care about functions, or sometimes classes. So the fields and properties scattered in the screenshot above are just a distraction. It is possible to configure the picker to only show certain kinds of symbols, but I prefer a quick trick that allows me to narrow it down to just functions: type (part of) the word function.

Since the picker includes the word “function” in the second column of the results, it merrily filters out all the lines that don’t have that word in them. Handy.

Better yet, I can input a space after the word “function” to inform the picker to perform subsequent searching back in the first column. So “func api” filters all functions that have the word “api” in them.

My second tip is to not forget about the Control-q and Control-t shortcuts to dump picker results into the Quick Fix or Trouble list. It generates a quick and dirty table of contents of whatever symbols you filtered for.

If you want to search all the symbols in your whole project, use the “but bigger” mnemonic. <Space>sS will perform such a search. However, be warned that not all LSPs support workspace symbol search. Some only search in currently open files, and even many of those that fully support workspace symbol search are unusably slow.

11.5. Neo-tree Also has a Symbols Outline

If you like the Neo-tree sidebar for file picking, you may appreciate that it also supports a symbol list for the currently focused file. At the time of writing, Neo-tree claims that the symbol picker is “experimental” in its Readme, so there isn’t a keybinding to display it, at least by default.

Instead, use the command :Neotree document_symbols to render the symbol picker in your Neo-tree sidebar:

neotree symbols dark
Figure 52. Neo-tree Symbols

You can navigate to a symbol in the document either by double clicking it with your mouse or by moving your cursor to the line that contains the symbol you want to jump to and pressing <Enter>. You can also use s or S to open a new view of the buffer at the given symbol in a vertical or horizontal split.

If you find that you just can’t get enough of the Neo-tree symbol picker, you’ll probably want to add a keyboard shortcut for that command. Just add the following configuration to a file in your plugins directory:

Listing 30. Neo-tree Document Symbols Keybinding
return {
  {
    "nvim-neo-tree/neo-tree.nvim",
    keys = {
      {
        "<leader>sO",
        "<cmd>Neotree document_symbols<cr>",
        desc = "Document Symbols (Neo-tree)",
      },
    },
  },
}

Feel free to use a different keymap if <Space>s<Shift-O> doesn’t suit.

This is especially useful if you decide to extend or customize other aspects of the Neo-tree configuration in addition to keymaps.

11.6. …​And So Does Trouble!

You can also open a symbols outline using the Trouble plugin. The keybinding is <Space>cs, which you may have trouble finding because it is in the Code menu rather than the Search menu. Unlike most Trouble windows, it opens in a right sidebar by default. It creates a lovely tree view and you can even collapse and expand the tree nodes using the folds keybindings we discussed in Chapter 9.

trouble symbols dark
Figure 53. Trouble Symbols

You can resize the Trouble window using the same keybindings you usually use for resizing windows (<Space>w< and <Space>w>). As you move the cursor over the Trouble window, the symbol it is over will automatically scroll into view.

The fastest way to use the Trouble window is to use Seek mode. Recall that Seek mode can jump to any currently visible window, which includes Trouble. So if I am currently editing the above file and my cursor is currently somewhere near the end of the file, I can use spub to enter Seek mode and search for the characters “pub”. This will place a label on the publicKeyToken in the Trouble window. If I hit that label, my cursor jumps to the trouble window and my editor window immediately scrolls to the function in question. Now I just have to hit Enter to move the cursor back to the file I’m editing.

11.7. Context

The nvim-treesitter-context extra is a helpful way to know where you are in the current file. It uses treesitter to figure out which functions and types you are in, and then pins the lines that define those types to the top of the editor. Enable it as usual by visiting :LazyExtras and hitting x over the line that contains nvim-treesitter-context.

This plugin keeps track of which class or function your cursor is currently in. If the function or type definition is so long that the signature scrolls off the screen, it will helpfully copy that signature into the first line or lines of the code window, highlighted with a slightly different background colour.

This is easier to describe with a reference image, so consider this screenshot:

context dark
Figure 54. Treesitter Context

In this image, the first two lines, which are slightly shaded, are providing context, rather than being part of the buffer. The first line tells me that I am in the DexieApiClient class and the second line tells me that I’m currently looking at the forceAddMemberToRealm method in that class.

Especially notice the relative line number column. The class DexieApiClient line is 109 lines above my current cursor position, and the async forceAddMemberToRealm line is 27 lines above it. In contrast, the first visible line of the function is only 13 lines above my current cursor position.

The effect is quite subtle, but the definitions that make their way into this context section tend to be exactly what you need while coding. If they fit on one line I can see function signatures and return types. You really don’t notice how often you have to scroll up to see what a variable is named in a function signature until you don’t have to do it anymore! And if you DO need to scroll up to the signature, simply type the relative line number followed by k and you’re there with no searching required.

If you need to disable the context temporarily, use the keybinding <Space>ut. We haven’t seen much of the <Space>u menu yet, where you can toggle various User Interface effects. This is largely because the default user interface is configured well enough that you don’t want to change it often!

11.8. Navigating with (Book)marks

You already know how to navigate through your history with Control-o and Control-i, and to jump around documents effectively using a wide variety of motions.

Vim also includes a “bookmarks” feature, although it’s referred to as “marks” I assume because the m character was still free on the Vim keymaps.

Marks are built-into Vim and LazyVim has (as usual) added a few minor improvements.

Much like the registers that we covered in Chapter 8, marks can be assigned to each letter of the alphabet. Additionally, certain punctuation characters represent special system-set marks that you can jump to, but not set.

To set a mark on a line, precede any letter character with the letter m. So ma will set the mark a on the current line. You can tell that this line is marked with an a because there is an a character in the gutter to the left:

mark a dark
Figure 55. Mark in Sign Column

Now I can jump to the line marked with a from anywhere in the current file by using an apostrophe followed by a.

I don’t use this very often because other tools tend to be more useful for navigating within a file than manually setting a mark. However, if I had marked the line with a capital letter (e.g. mA), I would be able to jump to the mark no matter which file is currently open using 'A.

So essentially, you can have up to 26 local marks within each file you ever open, as well as 26 global marks that you can access from any file.

Conveniently, if I just type a single apostrophe (in Normal mode), LazyVim will pop up a menu of all the marks currently available to jump to:

marks dark
Figure 56. Marks Menu

This list shows the lowercase a mark that I’ve set in this file, several system marks that I can jump to using punctuation (notice the descriptions for each of those marks to the right so you don’t have to memorize them), two global marks I use to jump to my kitty and fish configuration files, and the ten numbered marks.

I find the numbered marks to be kind of useless. They essentially refer to the file and cursor location of the last time you closed Neovim. I don’t close Neovim that often unless I’m editing a commit message or pull request description in a temporary instance, so my numbered marks are mostly just those kinds of temporary files. If I need to get back to where I was previously, the <Space>qs keybinding to restore session is typically more useful than the numbered marks.

The menu that pops up when you press the apostrophe key is usually sufficient to find marks, but you can also use the <Space>sm keybinding to search marks in a picker. I don’t usually have enough marks active for this to be useful, but if you’ve got a lot of global and local marks set and you can’t remember which letter is associated with a given one, it might help to use the picker to search for the contents of the line you have marked.

Once you’ve set a mark, you’ll eventually be dogged by the question “how do I get rid of it?” Deleting marks is probably up there with “how do I quit Vim” for most common queries! There isn’t a keybinding for deleting marks. Instead, you need to use the command :delmarks <mark> to delete the given mark. This can be shortened to :delm <mark>. So to get rid of the a mark in this file, I used the command :delmarks a. You don’t have to be on the marked line to delete the mark.

In Command mode, marks can be used in place of line numbers in ranges. For example, if you want to write the text between mark a and mark b to a file, you could do :'a,'bwrite somefile.txt. If you’ve seen the '<,'> in front of colon lines when you have text selected, that is because '< and '> represent the start and end of the most recent visual selection. So rather than manually setting ma and mb you can visually select the thing you want to write and have those marks pre-filled for you.

You can also use '< and '> to jump to the beginning or end of the most recent selection even if it has since been deselected.

The other symbol mark that I use frequently '. which jumps to the last place I inserted or changed text. This can sometimes be quicker than a series of Control-o keypresses.

11.9. Summary

In this chapter, we learned how to navigate code files using go to definition and references, and various “document symbol” plugins.

We saw how LazyVim gives us context on our current location in the document and how to look up documentation for the symbol under the cursor.

Finally, we covered Vim marks, a more manual process of tracking locations that you may want to jump to.

In the next chapter, we’ll learn all about searching text both in the current file and globally across a project.

Reminder

This book is freely available. It contains a textbook's worth of material. If you are deriving value from this content, I really appreciate any support you can give (both to me, and to those who can't afford to give it) by purchasing a copy of the book or through donation. Click here to see various purchase and donation options.

Copyright © 2024 Dusty Phillips