A small, single-command Neovim plugin for yanking the current buffer's path
into a register through a picker-driven menu. Path transformations are
pluggable strategies; the picker auto-detects fzf-lua, snacks.nvim, and
vim.ui.select and uses whichever is available.
- One user command (
:YankPath) that works in normal and visual mode - Eight built-in path strategies selected via single-letter shortcut:
ffilenameaabsolute pathhhome-relative path (uses~prefix)nparent + filename (last two path segments)pproject-root relative (auto-detects.git, configurable markers)rrelative N levels up (interactive prompt for N)trelative to a chosen base directory (interactive prompt)vVCS permalink URL (GitHub/GitLab/Bitbucket blob link)
- Visual-mode range is automatically appended (
path:start-endorpath:line; VCS URLs use#Lfragment style) - Backend-agnostic picker:
fzf-lua→snacks.nvim→vim.ui.select - Configurable destination register (defaults to the system clipboard
+) - Runtime strategy registration API for custom transformations
- Lazy, per-directory cached project-root lookup with user-overridable resolver
- No default keymaps — bind whatever you prefer
- Neovim ≥ 0.11 (the project CI matrix tests on 0.11, 0.12, and nightly)
- Optional:
fzf-luaand/orsnacks.nvimfor a richer picker UX
{
"neumachen/yank-path.nvim",
cmd = "YankPath",
opts = {},
}With overrides:
{
"neumachen/yank-path.nvim",
cmd = "YankPath",
opts = {
register = "+",
picker = "auto",
project = {
markers = { ".git", "Cargo.toml", "go.mod" },
},
},
}use({
"neumachen/yank-path.nvim",
config = function()
require("yank-path").setup()
end,
})Plug 'neumachen/yank-path.nvim'
lua << EOF
require("yank-path").setup()
EOFOpen any file buffer and run:
:YankPathA picker appears with the registered strategies. Pick one and the result is
written to the configured register (default +).
In visual mode, the current selection's line range is appended to the result automatically:
foo/bar.lua:42 " single-line visual selection
foo/bar.lua:10-25 " multi-line visual selection
| Key | Name | Example output |
|---|---|---|
f |
Filename | bar.lua |
a |
Absolute | /home/user/proj/src/bar.lua |
h |
Home | ~/proj/src/bar.lua |
n |
Parent | src/bar.lua |
p |
Project | src/bar.lua (relative to git root) |
r |
Relative | src/bar.lua (prompts for N levels) |
t |
Relative to | src/bar.lua (prompts for base directory) |
v |
VCS URL | https://github.com/user/proj/blob/abc123/src/bar.lua |
For r, the plugin prompts Levels up: and accepts a non-negative integer.
The result keeps the last N + 1 path segments — so N = 0 returns just the
filename and N = 2 returns the file plus its two parent directories.
For t, the plugin prompts Base dir: (default: current working directory)
and renders the buffer path relative to the entered base. ~ is expanded in
the entered base. Falls back to the absolute path when the buffer is not under
the chosen base.
For h, the plugin renders paths under $HOME with a ~ prefix. Paths
outside $HOME fall back to the absolute path (never emits ~/../...).
For n, the plugin returns the parent directory plus the filename (the last
two path segments).
The v (VCS URL) strategy builds a remote permalink for the current buffer
using git. It requires git on your PATH.
Host-aware URL formats:
| Host pattern | URL format |
|---|---|
github.com |
https://github.com/{owner}/{repo}/blob/{ref}/{path} |
gitlab.com / GitLab |
https://{host}/{owner}/{repo}/-/blob/{ref}/{path} |
bitbucket.org |
https://bitbucket.org/{owner}/{repo}/src/{ref}/{path} |
| Other / generic | https://{host}/{owner}/{repo}/blob/{ref}/{path} |
The strategy parses SSH (git@host:owner/repo.git), ssh://, and HTTPS
remote URLs; trailing .git is stripped. Nested GitLab groups are preserved
in the owner segment.
Ref resolution:
- Prefers the commit SHA at
HEAD. - If no SHA is resolvable (e.g. a new repository with no commits yet / unborn
HEAD) and
vcs.branch_fallback = true, the current branch name is used (orvcs.default_branchif the branch cannot be determined). - If no SHA is resolvable and
vcs.branch_fallback = false(the default), the strategy errors with a message suggesting you enablebranch_fallback.
Visual-mode line fragments:
In visual mode, the VCS strategy appends a #L URL fragment instead of the
:line suffix used by other strategies:
https://github.com/user/proj/blob/abc123/src/bar.lua#L42 " single line
https://github.com/user/proj/blob/abc123/src/bar.lua#L10-L25 " range
Bind keymaps to specific strategies via the programmatic API:
vim.keymap.set({ "n", "x" }, "<leader>ya", function()
require("yank-path").yank_with("absolute")
end, { desc = "Yank absolute path" })
vim.keymap.set({ "n", "x" }, "<leader>yp", function()
require("yank-path").yank_with("project")
end, { desc = "Yank project-relative path" })
vim.keymap.set({ "n", "x" }, "<leader>yh", function()
require("yank-path").yank_with("home")
end, { desc = "Yank home-relative path" })
vim.keymap.set({ "n", "x" }, "<leader>yv", function()
require("yank-path").yank_with("vcs")
end, { desc = "Yank VCS permalink URL" })yank_with accepts either the key ("a") or the display name
("absolute", case-insensitive). It also accepts a per-call register
override:
require("yank-path").yank_with("absolute", { register = "*" })require("yank-path").setup({
-- Destination register. Anything vim.fn.setreg accepts.
register = "+",
-- Picker backend selection.
-- "auto" -- default priority
-- "fzf-lua" | "snacks" | "vim.ui.select" -- force a single backend
-- { "fzf-lua", "vim.ui.select" } -- ordered fallback list
--
-- Default priority for "auto": fzf-lua -> snacks -> vim.ui.select.
picker = "auto",
project = {
-- Filenames or directory names that mark a project root.
markers = { ".git" },
-- Optional custom resolver. If set, this function fully owns root
-- lookup; markers and the built-in cache are bypassed.
--
-- find_root = function(bufnr)
-- return vim.fs.root(bufnr, { ".git", "Cargo.toml" })
-- end,
find_root = nil,
-- Cache root lookups per buffer directory. Invalidated on BufFilePost.
cache = true,
},
vcs = {
-- Git remote name to use when building VCS URLs.
remote = "origin",
-- Default branch name used when branch_fallback is enabled and the
-- current branch cannot be determined.
default_branch = "main",
-- When true, use a branch name instead of a SHA when no commit is
-- resolvable (e.g. unborn HEAD). When false (default), the strategy
-- errors with a message suggesting you enable this option.
branch_fallback = false,
},
})All values are optional; calling setup() with no arguments uses the
defaults shown above.
require("yank-path").register_strategy({
key = "u", -- single alphanumeric character
name = "Upper",
desc = "Upper-case the absolute path",
transform = function(absolute, ctx)
return absolute:upper(), nil -- (result, err)
end,
})Strategies are pure functions of the absolute path and a context table.
Return (result, nil) to write result to the register, (nil, err) to
surface an error notification, or (nil, nil) to indicate the strategy is
asynchronous (it must then call ctx.continue(result, err) itself; see
lua/yank-path/strategies/relative.lua for an example).
Strategy spec fields:
| Field | Required | Description |
|---|---|---|
key |
yes | Single alphanumeric character (picker shortcut) |
name |
yes | Display name |
desc |
yes | Description shown in pickers that render previews |
transform |
yes | function(absolute, ctx) -> (result, err) |
range_style |
no | "line" (default) or "url". Controls how visual-mode ranges are appended: "line" appends :42 / :10-25; "url" appends #L42 / #L10-L25 (useful for URL strategies). |
The ctx table contains:
| Field | Description |
|---|---|
bufnr |
Source buffer number |
absolute |
Absolute path of the buffer |
range |
Visual range table or nil when not in visual mode |
config |
Live plugin config |
continue |
Async completion callback (result, err) |
Note: The VCS permalink strategy (key
v) is now a built-in. The registration API was designed to support strategies like this without changing the core — see the VCS URL strategy section for details.
The plugin composes a strict linear pipeline:
path.get(bufnr)
-> transform(absolute, ctx)
-> append_range_if_visual(result, range)
-> register.write(result, register)
Each step is a single-responsibility module under lua/yank-path/:
| File | Responsibility |
|---|---|
init.lua |
Public API and built-in registration |
config.lua |
Config schema, defaults, validation |
pipeline.lua |
Linear pipeline composition |
path.lua |
Buffer → absolute path |
range.lua |
Visual mode detection and range append |
register.lua |
Register write |
util.lua |
Notifications, callable check, root cache |
strategies/init.lua |
Strategy registry |
strategies/{f,a,h,n,p,r,t,v}.lua |
Built-in strategies |
picker/init.lua |
Backend resolver |
picker/{ui_select,fzf_lua,snacks}.lua |
Backend adapters |
The picker adapter interface is is_available() + select(items, on_choice).
Adding a new backend is a single file under lua/yank-path/picker/.
make test # run the plenary spec suite
make lint # luacheck
make format # stylua --in-place
make format-check # stylua --check
make check # lint + format-check + testTests live under tests/, one spec file per module plus
tests/integration_spec.lua for end-to-end flows. Tests must pass on the
full CI matrix (Neovim 0.11, 0.12, nightly).
Release process: RELEASING.md.
Reporting policy: SECURITY.md. Disclose privately via
GitHub Security Advisories.
MIT. See LICENSE.