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-ctagsGenerate 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#
| Command | Action |
|---|---|
Ctrl-] | Jump to the definition of the symbol under the cursor |
Ctrl-t | Jump back (pop the tag stack) |
Ctrl-o | Jump back (using the jump list — also works) |
:tag funcname | Jump to the definition of funcname |
:tn | Next matching tag (if multiple definitions exist) |
:tp | Previous matching tag |
:ts | List 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:
| Command | Action |
|---|---|
Ctrl-t | Pop one level (go back) |
:tag | Push forward again |
:tags | Show 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-tYou 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):| Kind | Meaning (C) |
|---|---|
f | Function definition |
p | Function prototype (declaration) |
s | Struct |
t | Typedef |
d | Macro (#define) |
e | Enum value |
m | Struct/union member |
v | Variable |
g | Enum 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 zThis 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+=~/systagsThe ; 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
:copenCombining 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
:cnextNavigating 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| Option | Purpose |
|---|---|
--c-kinds=+p | Include function prototypes |
--c-kinds=+x | Include external declarations |
--fields=+iaS | Include inheritance, access, and signature |
--extras=+q | Include qualified tags (e.g., struct.member) |
--exclude | Skip directories that bloat the tags file |
Tags vs LSP#
Tags (ctags) and LSP (Language Server Protocol) both provide “go to definition” but differ:
| Feature | Tags (ctags) | LSP |
|---|---|---|
| Setup | Generate a file, done | Install server + plugin |
| Speed | Instant lookup | Can lag on large projects |
| Accuracy | Pattern-based (occasional false matches) | Semantic (understands types) |
| Find references | No (use grep) | Yes |
| Rename symbol | No | Yes |
| Hover documentation | No | Yes |
| Works without plugins | Yes | No |
| Works over SSH | Yes | Requires 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
tagsto 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 ofCtrl-]when you suspect multiple matches — avoids landing on a prototype instead of the definition - Regenerate tags after pulling changes or major refactors
- Use
--excludein 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=+pso you can jump to both prototypes and definitions

