Navigate between related files in a Ruby on Rails project. From the current
file, the plugin detects its Rails entity (model, controller, view, or RSpec
spec) and opens a snacks.nvim picker
with the files related to it. Ruby files that match none of those patterns
fall back to generic app/ ↔ spec/ path mirroring.
| Current file | Picker shows (in order) |
|---|---|
Model (app/models/user.rb) |
model spec → controllers → views |
Controller (app/controllers/users_controller.rb) |
controller/request/system/feature specs → model → views |
Model spec (spec/models/user_spec.rb) |
the model |
Controller-ish spec (spec/controllers/…, spec/requests/…, spec/system/…, spec/features/…) |
the controller |
View (app/views/users/index.html.erb) |
specs → controller → model |
View spec (spec/views/users/…) |
the resource's views |
Other app/**.rb file (app/services/payment_processor.rb) |
its mirrored spec (spec/services/payment_processor_spec.rb) |
Other spec/**_spec.rb file |
its mirrored source under app/ |
Notes:
- RSpec structure only (
spec/, not Minitest'stest/). - Ruby files that match none of the specific patterns above (services,
helpers, jobs, mailers, concerns, …) fall back to path mirroring:
app/abc/xyz.rb↔spec/abc/xyz_spec.rb. The counterpart is searched at or below the mirrored directory, so a spec that lives shallower than its source is not matched. - Controller tests match
spec/controllers/<plural>_controller_spec.rbplus<plural>_spec.rb/<plural>_controller_spec.rbunderspec/requests/,spec/system/, andspec/features/. - Related files are found by searching the filesystem with
ripgrep (
rg --files), so namespaced resources (app/controllers/admin/users_controller.rb,app/views/admin/users/…) are found naturally; all matches are shown, with the ones inside the current file's namespace listed first. Hidden files and directories are always excluded (e.g..keepplaceholders underapp/views/<plural>/don't appear as views), and.gitignoreis honored inside git repos. - The searches run asynchronously (on plenary.nvim), so the UI never blocks; ripgrep failures are reported instead of showing an empty result.
- View partials (
_form.html.erb) and all formats in the resource's view directory are included.
- Neovim >= 0.10
- folke/snacks.nvim (the picker)
- nvim-lua/plenary.nvim (async file searches)
- ripgrep (
rgonPATH; used to find the related files)
With LazyVim (or any
lazy.nvim setup): create a plugin spec
file, e.g. ~/.config/nvim/lua/plugins/rails_navigation.lua, with the content
below. lazy.nvim fetches the plugin from GitHub
(thachck/rails_navigation.nvim) automatically on the next start.
return {
"thachck/rails_navigation.nvim",
dependencies = { "folke/snacks.nvim", "nvim-lua/plenary.nvim" },
cmd = "RailsNavigation",
keys = {
{ "<leader>rl", "<cmd>RailsNavigation<cr>", desc = "Rails: related files" },
},
opts = {},
}Defaults (calling setup() is optional):
require("rails_navigation").setup({
-- extra singular -> plural inflections for names the built-in
-- inflector gets wrong, e.g. { octopus = "octopi" }
irregulars = {},
-- options merged into the snacks picker call (layout, title, keys, ...)
picker = {},
})The built-in inflector covers regular English pluralization plus common
irregulars. For app-specific names it gets wrong (Rails' own inflector has the
same quirks, e.g. cafe → caves), add an entry to irregulars. Notably,
bases singularizes to basis (ActiveSupport parity) — for a Base model,
set irregulars = { base = "bases" }.
Tooling is managed with mise (mise install). Tests
run with busted under a real Neovim
via nlua. ripgrep must be installed
and on PATH (the finder specs shell out to it; it is intentionally not
managed via mise.toml):
luarocks test --local # first run: installs busted, nlua, and plenary into ~/.luarocks
busted # subsequent runs
selene lua plugin # lint the plugin code
selene --config selene_spec.toml spec # lint the specs (busted globals)
stylua lua plugin spec # formatmise.toml puts ~/.luarocks/bin on PATH and exports the LUA_PATH /
LUA_CPATH needed by nlua.