Skip to main content

How to Jump to Definitions in Vim with Tags

·1117 words·6 mins
Linux Learning Lab
Author
Linux Learning Lab
Writing about code, tools, and workflows.

What Are Tags?
#

Tags are an index of symbols (functions, structs, macros, variables) in your codebase. When you generate a tags file, Vim can look up any symbol and jump directly to where it’s defined — even if it’s in a different file.

This is how you get “go to definition” in Vim without plugins or an LSP server. It works with C, Python, JavaScript, Ruby, Go, and dozens of other languages.

Generating Tags with ctags
#

Universal Ctags is the modern implementation. Install it:

# Debian/Ubuntu
sudo apt install universal-ctags

# Fedora/RHEL
sudo dnf install ctags

# macOS
brew install universal-ctags

Generate a tags file
#

# Index the current directory recursively
ctags -R .

# Index specific directories
ctags -R src/ include/

# Index only C files
ctags -R --languages=C .

This creates a tags file in your current directory. Vim reads it automatically.

For a C project
#

# Index source and headers
ctags -R --languages=C,C++ --c-kinds=+p src/ include/

# Include system headers (useful for jumping to stdlib definitions)
ctags -R --languages=C /usr/include/ src/

Keep tags updated
#

Regenerate after changes:

# Add to your Makefile
tags:
	ctags -R src/ include/

# Or regenerate on save (in .vimrc)
autocmd BufWritePost *.c,*.h silent! !ctags -R . &

Jumping to Definitions
#

Core commands
#

CommandAction
Ctrl-]Jump to the definition of the symbol under the cursor
Ctrl-tJump back (pop the tag stack)
Ctrl-oJump back (using the jump list — also works)
:tag funcnameJump to the definition of funcname
:tnNext matching tag (if multiple definitions exist)
:tpPrevious matching tag
:tsList all matching tags and let you choose
g]Like Ctrl-] but shows a list if there are multiple matches
g Ctrl-]Jump if one match, show list if multiple

Example in C
#

// main.c
#include "parser.h"

int main() {
    Node *tree = parse_input("data.txt");  // cursor on parse_input
    // Press Ctrl-] → jumps to parser.c where parse_input is defined
    process(tree);
    return 0;
}
// parser.c
#include "parser.h"

Node *parse_input(const char *filename) {   // ← you land here
    // ...
}

Press Ctrl-t to jump back to main.c.

The Tag Stack
#

Every time you jump with Ctrl-], Vim pushes your position onto the tag stack. Think of it like a browser’s back button for code navigation.

" View the tag stack
:tags

" Output:
"   # TO tag      FROM line in file/text
"   1  1 parse_input    5  main.c
"   2  1 read_file      12 parser.c
"   > 

Navigate the stack:

CommandAction
Ctrl-tPop one level (go back)
:tagPush forward again
:tagsShow the full stack

Deep navigation example
#

main.c → parse_input() → read_file() → open_stream()
   Ctrl-]          Ctrl-]          Ctrl-]

open_stream() → read_file() → parse_input() → main.c
   Ctrl-t          Ctrl-t          Ctrl-t

You can dive several levels deep and always get back.

Multiple Matches
#

Sometimes a symbol has multiple definitions (e.g., a function declared in a header and defined in a source file, or overloaded in different modules).

" Jump to tag, show list if multiple matches
g]

" Or explicitly list matches
:ts parse_input

" Output:
"   # pri kind tag            file
"   1 F   p    parse_input    include/parser.h
"   2 F   f    parse_input    src/parser.c
" Type number and <Enter> (empty cancels):
KindMeaning (C)
fFunction definition
pFunction prototype (declaration)
sStruct
tTypedef
dMacro (#define)
eEnum value
mStruct/union member
vVariable
gEnum name

Preview Window
#

Jump to a definition in a preview window without leaving your current position:

" Open definition in preview window
Ctrl-w }

" Or by name
:ptag parse_input

" Close preview window
:pclose
" or
Ctrl-w z

This is useful when you want to glance at a function’s implementation without losing your place.

Configuring Vim for Tags
#

Tell Vim where to find tags
#

" Look in current directory, then walk up parent directories
set tags=./tags,tags;

" Include system tags for C
set tags+=~/systags

The ; at the end tells Vim to search upward through parent directories — useful when you’re editing a file deep in src/utils/ but the tags file is at the project root.

Generate tags from Vim
#

" Map a key to regenerate tags
nnoremap <leader>ct :!ctags -R .<CR>

C Project Workflow
#

Initial setup
#

# In your project root
ctags -R --languages=C,C++ --c-kinds=+pst src/ include/

Daily workflow
#

" Open your project
vim src/main.c

" Cursor on a function call → jump to definition
Ctrl-]

" Read the implementation, then jump back
Ctrl-t

" Look up a struct definition
:tag Node

" Find all references to a function (uses grep + quickfix)
:grep "parse_input" **/*.c **/*.h
:copen

Combining tags with quickfix
#

" Jump to definition
Ctrl-]

" Now find everywhere this function is called
:grep "parse_input" **/*.c
:copen

" Step through all call sites
:cnext
:cnext

Navigating structs and typedefs#

// types.h
typedef struct {
    char *name;
    int value;
    struct Node *next;
} Node;
" Cursor on 'Node' anywhere in your code
Ctrl-]
" Jumps to the typedef in types.h

.ctags Configuration
#

Create ~/.ctags (or .ctags.d/local.ctags) to customize tag generation:

--recurse=yes
--exclude=.git
--exclude=build
--exclude=node_modules
--exclude=vendor

--languages=C,C++
--c-kinds=+p+x
--fields=+iaS
--extras=+q
OptionPurpose
--c-kinds=+pInclude function prototypes
--c-kinds=+xInclude external declarations
--fields=+iaSInclude inheritance, access, and signature
--extras=+qInclude qualified tags (e.g., struct.member)
--excludeSkip directories that bloat the tags file

Tags vs LSP
#

Tags (ctags) and LSP (Language Server Protocol) both provide “go to definition” but differ:

FeatureTags (ctags)LSP
SetupGenerate a file, doneInstall server + plugin
SpeedInstant lookupCan lag on large projects
AccuracyPattern-based (occasional false matches)Semantic (understands types)
Find referencesNo (use grep)Yes
Rename symbolNoYes
Hover documentationNoYes
Works without pluginsYesNo
Works over SSHYesRequires remote server

Use tags when: you want zero dependencies, you’re on a remote server, or your project is C/C++ where ctags is very accurate.

Use LSP when: you want full IDE features, type-aware navigation, and don’t mind the setup.

Both can coexist — Vim tries tags if LSP doesn’t provide a result.

Best Practices
#

  • Add tags to your project’s .gitignore — it’s generated, not source
  • Set tags=./tags,tags; so Vim finds the tags file regardless of which subdirectory you’re editing in
  • Use g] instead of Ctrl-] when you suspect multiple matches — avoids landing on a prototype instead of the definition
  • Regenerate tags after pulling changes or major refactors
  • Use --exclude in your ctags config to skip build directories and dependencies
  • Combine tags for navigation with quickfix + grep for finding usages — together they give you most of what an IDE provides
  • For C projects, include --c-kinds=+p so you can jump to both prototypes and definitions