related_files.nvim is a Neovim plugin for jumping between files that follow
project-specific naming conventions.
Examples:
- source file -> test file
- public header -> private implementation
- C source/header -> C++ test
- files in matching namespaces or directory trees
If the related file exists, the plugin opens it. If it does not exist, the plugin can create it.
Many projects encode relationships in file paths:
lua/related_files/init.lua
test/related_files/init_spec.lua
c99/public/example/media/v1/parser/PacketReader.h
test/private/example/media/v1/parser/PacketReader_tests.cpp
Vim's alternate-file behavior only gives you one other file. This plugin uses
numbered relation slots instead, so one file can have several related targets.
By default, <leader>1 through <leader>5 jump to relation slots 1 through 5.
Install it with your plugin manager, then call setup():
require("related_files").setup()Default options:
require("related_files").setup({
nr_keymaps = 5,
enable_default_keymaps = true,
use_default_related_files_info = true,
global_related_files_info = nil,
})If default keymaps are enabled, the plugin creates:
<leader>1 open or create relation slot 1
<leader>2 open or create relation slot 2
<leader>3 open or create relation slot 3
...
It also defines <Plug>RelatedFileGetOrCreate1,
<Plug>RelatedFileGetOrCreate2, and so on, so you can map them yourself.
A related-file setup has two parts:
- pargens: path parsers/generators
- relations: which pargens belong together
A pargen describes one kind of file. It can parse a filename into shared parts and generate a filename again from those parts.
For simple paths, use pargen_from_expression():
local from_expr = require("related_files").pargen_from_expression
from_expr("source", "{parent}/{name}.py")
from_expr("test", "{parent}/{name}_test.py")Both expressions share the same fields:
{
parent = "/path/to/project",
name = "file_a",
}That shared data lets the plugin turn file_a.py into file_a_test.py, and
back again.
Create a .related_files_info.lua file in your project:
local from_expr = require("related_files").pargen_from_expression
return {
pargens = {
from_expr("source", "{parent}/{name}.py"),
from_expr("test", "{parent}/{name}_test.py"),
},
relations = {
{ "source", "test" },
},
}With this setup:
file_a.py -- <leader>2 --> file_a_test.py
file_a_test.py -- <leader>1 --> file_a.py
The position in the relation list is the keymap slot. In
{ "source", "test" }, slot 1 is source and slot 2 is test.
Use {namespace} when files keep the same nested path below different roots:
local from_expr = require("related_files").pargen_from_expression
return {
pargens = {
from_expr("source", "{parent}/src/{namespace}/{name}.py"),
from_expr("test", "{parent}/test/{namespace}/test_{name}.py"),
},
relations = {
{ "source", "test" },
},
}This maps:
src/ns1/ns2/file_a.py
test/ns1/ns2/test_file_a.py
The fixed path segment before {namespace} matters. It tells the expression
parser where the namespace starts.
Relations can contain more than two slots:
return {
relations = {
{ "header", "source", "test" },
},
}Then the same current file can jump to different targets:
<leader>1 header
<leader>2 source
<leader>3 test
Relations can also overlap. For C and C++ projects, one public header may relate to several implementation types:
relations = {
{ "public_h", "private_c" },
{ "public_h", "private_cpp" },
{ "public_hpp", "private_cpp" },
}If more than one existing related file matches, the plugin asks you to choose. If none exists, it offers to create one of the generated candidates.
The plugin ships with a default related-file configuration. It covers simple same-directory C/C++ files, Ruby source/test files, Rust source/test files, and a larger C/C++ layout with access levels and language versions.
For example, the default config understands paths like:
c99/public/example/media/v1/parser/PacketReader.h
c99/public/example/media/v1/parser/PacketReader.c
test/private/example/media/v1/parser/PacketReader_tests.cpp
The .h, .c, and private C++ test are related through slot 3 for tests.
Disable the built-in defaults if you only want project-local rules:
require("related_files").setup({
use_default_related_files_info = false,
})The plugin looks for .related_files_info.lua files in parent directories of
the current file. The closest file is tried first, then higher parent
directories, then optional global config, then built-in defaults.
You can also provide a global config directly:
require("related_files").setup({
global_related_files_info = {
pargens = {
-- your pargens here
},
relations = {
-- your relations here
},
},
})Use the :RFInfo command to print how the plugin understands the current file:
:RFInfoThis is useful when a file matches the wrong parser, no parser, or generates an unexpected related filename.
Run tests with Plenary:
nvim --headless -c "PlenaryBustedFile test/related_files/infos_spec.lua"
nvim --headless -c "PlenaryBustedDirectory test/"