DEV Community

Igor Irianto
Igor Irianto

Posted on • Edited on

How to use tags in Vim to jump to definitions quickly

Follow @learnvim for more Vim tips and tricks!

One useful ability in coding is to jump to any function definitions quickly. It helps you to understand and visualize project codes faster.

Vim uses a tag file to jump to a function definition quickly. In this article, I will show you how tag works and how to manually set up tags. I will also show you how to use a plugin to automate your tag creation.

These are some things I will cover:

Tags overview

If you are reading a large source code and see a function you don't understand, you can jump to its definition to understand it.

If I saw this code snippet and want to know what Account model does:

Models::Account.new(
  account_name: from_json(['whatever']
  ...
Enter fullscreen mode Exit fullscreen mode

With tags, I can put my cursor on "Account", press Ctrl-], and jump to definition.

Vim jumps to Account class inside a file:

module MyModule
  module Users
    module Deserializers
      class Account < Iggy::Core::Deserializer

      def initialize()
        ...
Enter fullscreen mode Exit fullscreen mode

With this, I can read what is being initialized in a new account. I can see at other methods inside this class and understand more what this class can do.

Tags give you a quick project visualization. The more you know about the code, the more you understand the project.

The way tag system works in Vim, is that Vim relies on a tag generator to generate a tag file (normally called tags in root directory) that contains a list of definitions. To see where Vim expects to find a tags file, you can run :set tags?. Depending on what tags generator you use, the content of your tags file may look different. At minimum, a tag file must have either one of these formats:

1.  {tagname} {TAB} {tagfile} {TAB} {tagaddress}
2.  {tagname} {TAB} {tagfile} {TAB} {tagaddress} {term} {field} ..
Enter fullscreen mode Exit fullscreen mode

I will go over the content later.

Setting up tags

Here is how you can get started creating tags. To use Vim tags, we need a tag generator. There are several options:

ctags                   # C only. Available in most unix
exuberant ctags         # Good, supports many file types
etags                   # Emacs. Hmm...
JTags                   # Java
ptags.py                # Python
ptags                   # Perl
gnatxref                # Ada
Enter fullscreen mode Exit fullscreen mode

The popular option is exuberant ctags. It supports 41 programming languages. I personally use that. For this tutorial, we will go with exuberant ctags.

If you have mac and homebrew installed, you can run brew install ctags. Once installed, you can check it with ctags --version.

For this demo, I will use dev.to github project. You can use whatever project you want.

To generate ctags, run:

ctags -R .
Enter fullscreen mode Exit fullscreen mode

By default, ctags generates a tags file in your current directory. The -R is recursive option (which you probably want to do most of the time). If you noticed, your tag file contains a very long list. Mine has over 170000 entries. This is because ctags generate tags from our node_modules too! We need to exclude it. Delete our tag file (rm tags) and run this instead:

 ctags -R --exclude=node_modules .
Enter fullscreen mode Exit fullscreen mode

This time it takes less than a second. I only got around 6900 entries. Much better.

By the way, you can use exclude option multiple times, for example:

ctags -R --exclude=.git--exclude=vendor --exclude=node_modules --exclude=db --exclude=log .
Enter fullscreen mode Exit fullscreen mode

Vim tags in action

Let me show you an example of a tags file:

...

!_TAG_PROGRAM_VERSION   5.8 //
Abstract    config/initializers/sidekiq.rb  /^    module Abstract$/;"   m   class:Rack.Session
Accept  elasticsearch-7.5.2/jdk.app/Contents/Home/include/jdwpTransport.h   /^    jdwpTransportError (JNICALL *Accept)(jdwpTransportEnv* env,$/;"   m   struct:jdwpTransportNativeInterface_
Accept  elasticsearch-7.5.2/jdk.app/Contents/Home/include/jdwpTransport.h   /^    jdwpTransportError Accept(jlong accept_timeout, jlong handshake_timeout) {$/;"    f   struct:_jdwpTransportEnv
ActionController    config/initializers/persistent_csrf_token_cookie.rb /^module ActionController$/;"   m
ActsAsFollowerMigration db/migrate/20170208152018_acts_as_follower_migration.rb /^class ActsAsFollowerMigration < ActiveRecord::Migration[4.2]$/;"  c
...
Enter fullscreen mode Exit fullscreen mode

This file is generated from dev.to repo. It may look like gibberish, but it isn't that bad. The top lines (that start with !_TAG_ are metadata. I will not go over that). The important part is the line that looks like this:

ActionController    config/initializers/persistent_csrf_token_cookie.rb /^module ActionController$/;"   m
Enter fullscreen mode Exit fullscreen mode

ActionController is our definition. Think of it like a key in key-value pair. When we press Ctrl-] on "ActionController" string anywhere in our code, Vim looks at our tags file, searches for "ActionController", and looks for the location to jump to, in this case, it is config/initializers/persistent_csrf_token_cookie.rb.

Once Vim finds the location, it looks for the location using this pattern: /^module ActionController$/. ^ is regex pattern for start of line. $ is regex for end of line.

Using tags

You can get good milage using only Ctrl-]. But let's learn a few more tricks with tags.

We just learned that we could put our cursor on "ActionController" string and jump to definition with Ctrl-]. You can do the same with :tag {name}.

:tag ActionController
Enter fullscreen mode Exit fullscreen mode

You can autocomplete :tag argument. If you do:

:tag A{TAB}
Enter fullscreen mode Exit fullscreen mode

Vim lists all tags that starts with A...

You can have multiple definitions for one keyword. For example, I have several "User" definitions in tags file:

User    app/models/user.rb  /^class User < ApplicationRecord$/;"    c
User    app/services/data_sync/elasticsearch/user.rb    /^    class User < Base$/;" c   class:DataSync.Elasticsearch
User    app/services/search/query_builders/user.rb  /^    class User < QueryBase$/;"    c   class:Search.QueryBuilders
User    app/services/search/user.rb /^  class User < Base$/;"   c   class:Search
Enter fullscreen mode Exit fullscreen mode

By default, ctags always jumps to first definition. To jump to Nth definition, do:

:Ntag User
Enter fullscreen mode Exit fullscreen mode

To jump to 3rd User definition (the one inside query_builders/user.rb), we do :3tag User. In normal mode, we can do 3 Ctrl-] while cursor is on User.

You may ask, "This is nice, but how do I know if there are multiple tags for User?"

You can use :tselect (or g] in normal mode) to see all definitions for that string. If you do :tselect User, you'll see a list of all tags generated for "User". You also can put cursor on User and do g]. From there, you can choose where to jump.

Tag stack

Vim keeps a list of all tags we've jumped to in a stack (max 20). You can see the stack with :tags. It looks something like this:

  # TO tag         FROM line  in file/text
  1  1 banned             64  app/controllers/application_controller.rb
  2  1 submission_template_customized    39  submission_template = @tag.submission_template_customized(@user.name).to_s
>
Enter fullscreen mode Exit fullscreen mode

The upper stacks are older stacks. The lower stacks are newer stacks. I started out with "banned", then I searched for "submission_template_customized".

To "pop" the stack, do Ctrl-T or :pop.

Let's say you're working on a new project and you want to investigate a call graph. You can use tag stack to strategically jump to relevant functions.

func0  --> func1a --> func1b
       --> func2a
       --> func3a
Enter fullscreen mode Exit fullscreen mode

Starting at func0, you observe that it calls func1a, func2a, and func3a. You go to func1a with Ctrl-]. You see func1a calls func1b, so you go to func1b (ctrl-]). Once you understand func1a and func1b, you return (Ctrl-T twice) back at func0. Back at func0, jump to func2a with ctrl-], see what it does, and "pop" back to func0. Finally, jump to func3a to complete the call graph.

Plugins

When your project change, your tags need to change. If you removed User's first definition inside app/models/user.rb, the tags still think it is inside app/models/user.rb. You have to tell them that it has changed.

You can regenerate tags (ctasg -R .), but Vim doesn't do this automatically. Luckily, there are plugins to automate tags generation. I use vim-gutentags and it works right out of the box. Many plugins can do this automatically.

Some alternatives:

Conclusion

I think this is a good place to stop. We learned how tag works. We learned how to set up tags for our project. We learned how to jump, pop back, and view tag stack. Understanding how Vim tags work will boost your productivity.

Thanks for reading. Happy coding!

Resources

Top comments (2)

Collapse
 
ggllcu profile image
ggllcu

Hi! Thanks for this article.
But, with lsp, are tags still usefull?

Collapse
 
twnaing_87 profile image
Tun Win Naing

Here is the discussion on reddit.

TL;DR Yes.